<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Hi, ziheng</title><description>一个极简的个人博客，使用 Astro 构建，记录技术思考</description><link>https://blog.zihengzhang.com</link><language>zh-CN</language><managingEditor>shadow1746556951@gmail.com (ziheng)</managingEditor><webMaster>shadow1746556951@gmail.com (ziheng)</webMaster><generator>Astro RSS for zzheng-blog</generator><image><url>https://blog.zihengzhang.com//favicon.svg</url><title>Hi, ziheng</title><link>https://blog.zihengzhang.com/</link><description>一个极简的个人博客，使用 Astro 构建，记录技术思考</description></image><follow_challenge><feedId>169427160495711232</feedId><userId>41448350114993152</userId></follow_challenge><item><title>树莓派 + 7.3 寸墨水屏 + 宜家画框，攒一台电子相册</title><link>https://blog.zihengzhang.com/post/eink-photo-frame</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/eink-photo-frame</guid><description>微雪 7.3 寸彩色墨水屏、树莓派 Zero 2 W、宜家 RÖDALM 画框，加上开源的 InkyPi，装成一台电子相册。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;做一块能自己换照片的墨水屏。不发光，断电之后画面还在，摆着像一幅画，而不是一块屏幕。&lt;/p&gt;
&lt;h2&gt;三件硬件&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;件&lt;/th&gt;
&lt;th&gt;型号&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;屏幕&lt;/td&gt;
&lt;td&gt;微雪 7.3 寸彩色墨水屏（&lt;code&gt;epd7in3e&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;800×480，SPI 接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主机&lt;/td&gt;
&lt;td&gt;树莓派 Zero 2 W&lt;/td&gt;
&lt;td&gt;小、便宜、带 Wi-Fi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;画框&lt;/td&gt;
&lt;td&gt;宜家 RÖDALM&lt;/td&gt;
&lt;td&gt;木纹&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/eink-photo-frame/hardware.webp&quot; alt=&quot;屏幕和树莓派&quot; /&gt;&lt;/p&gt;
&lt;p&gt;屏幕背面是一个 40 针母座。树莓派顶上的排针对准了压下去，硬件就接好了——不用焊接，不用杜邦线，也不用想怎么固定。对不想动烙铁的人来说，这一点比参数重要。&lt;/p&gt;
&lt;h2&gt;软件：InkyPi&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/fatihak/InkyPi&quot;&gt;InkyPi&lt;/a&gt; 在树莓派上跑一个网页服务，专门管这块墨水屏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器打开就能用，不用敲命令行&lt;/li&gt;
&lt;li&gt;插件制：相册、天气、日历、时钟、每日漫画、RSS&lt;/li&gt;
&lt;li&gt;一键安装脚本，装完注册成 systemd 服务，开机自启&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;装完之后，平时用浏览器就能管它。另外，配好 SSH 之后，源码和树莓派上的配置都可以让 AI 在本地改好，再推上去——这台机器的安绞、播放列表、插件排版，基本就是这么调出来的。&lt;/p&gt;
&lt;h2&gt;组装&lt;/h2&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;img src=&quot;/images/blog/eink-photo-frame/assembly.webp&quot; alt=&quot;组装&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通电后屏幕会闪几次，然后画面出来。&lt;/p&gt;
&lt;h2&gt;上手&lt;/h2&gt;
&lt;p&gt;浏览器打开树莓派的地址，进 InkyPi 的管理界面。相册插件里选一张图，点 &lt;strong&gt;Update Now&lt;/strong&gt;，墨水屏就开始刷新；想让它自己换，就在播放列表里排几个插件，再设一个间隔时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/eink-photo-frame/webui.webp&quot; alt=&quot;管理界面&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;发现相框挡住了屏幕边缘&lt;/h2&gt;
&lt;p&gt;上面都还算顺利。真正装进画框用起来之后，才发现屏幕边缘被挡住了。&lt;/p&gt;
&lt;p&gt;问题出在相框的卡纸：它的开孔比屏幕小一圈，装进去之后就直接压住了屏幕边缘。这事很难靠买解决——木框的尺寸是固定的，卡纸本身又占掉一圈边，市面上很难找到开孔刚好等于屏幕尺寸的相框。&lt;/p&gt;
&lt;p&gt;被遮掉的量是量出来的：&lt;/p&gt;
&lt;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;各 64 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上&lt;/td&gt;
&lt;td&gt;33 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;下&lt;/td&gt;
&lt;td&gt;31 px&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;800×480 的屏，左右两边一共压掉了 16% 的显示宽度。&lt;/p&gt;
&lt;h2&gt;解决&lt;/h2&gt;
&lt;p&gt;只能从软件这边适配，把内容从被遮住的边缘挪开。&lt;/p&gt;
&lt;p&gt;第一反应是把整幅画面缩小塞进中间——&lt;strong&gt;这样不行&lt;/strong&gt;：背景会跟着一起缩，四周留出一圈白边，看起来比被裁掉还难看。&lt;/p&gt;
&lt;p&gt;改成&lt;strong&gt;内容往中间收、背景照旧铺满&lt;/strong&gt;：每个插件把自己的文字和图形往内缩，背景色仍然铺满整个屏幕，被相框压住的就只是背景的边缘。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/eink-photo-frame/safe-area.webp&quot; alt=&quot;安全边距调整前后&quot; /&gt;&lt;/p&gt;
&lt;p&gt;边距不是估的，是量出来的：把插件渲染出来的图存下来，逐像素看四边留白，改的时候也按量出来的数字改。&lt;/p&gt;
&lt;p&gt;改完之后，装进画框就是完整的一屏：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/eink-photo-frame/result.webp&quot; alt=&quot;装进画框的成品&quot; /&gt;&lt;/p&gt;
</content:encoded><category>树莓派</category><category>微雪</category><category>墨水屏</category><category>InkyPi</category><category>DIY</category></item><item><title>手机 + Tailscale + pi-web 随时随地跑 AI Agent</title><link>https://blog.zihengzhang.com/post/mobile-tailscale-pi-web</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/mobile-tailscale-pi-web</guid><description>让 Clash 内嵌 Tailscale 出站，Android 手机不用二选一，既能走代理，又能随时访问家里 Mac mini 上的 pi-web。</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;pi-web 是 pi coding agent 的浏览器界面，我把它跑在家里的 Mac mini 上，平时的项目文件、会话记录和 API key 都在那台机器上。出门在外的时候，想用手机打开它接着干活，而不是只能等回家。&lt;/p&gt;
&lt;p&gt;家里的机器本来就组了一张 Tailscale 内网（下称 tailnet），Mac mini、NAS 都在里面。手机只要进了这张网，访问 pi-web 就没有问题。实际配置下来，在 Android 上折腾了几步，这里记录一下最终能用的方案。&lt;/p&gt;
&lt;h2&gt;让 Clash 同时扮演两个角色&lt;/h2&gt;
&lt;p&gt;最直接的想法是手机装个 Tailscale 官方 App。但 Android 同一时间只允许启用一个 VPN 应用，而我平时要用 Clash 走代理，两者都要占用这个位置，装上也必须二选一。&lt;/p&gt;
&lt;p&gt;既然不能两个 App 各管一头，那就让一个 App 全包。Mihomo（Clash Meta 使用的内核）自带 &lt;code&gt;tailscale&lt;/code&gt; 类型的出站，在配置里加一个节点即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxies:
  - name: Tailscale
    type: tailscale
    hostname: android-clash
    state-dir: ./tailscale
    ephemeral: false
    udp: true
    accept-routes: true
    interface-name: wlan0
    ip-version: ipv4-prefer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 CMFA 一个 App 同时承担代理和 tailnet 接入，VPN 槽位只被占用一次。它和机场节点是并存的：平时照常选机场节点走代理，只有发往 tailnet 地址（&lt;code&gt;100.64.0.0/10&lt;/code&gt;）的流量会被规则自动交给 Tailscale，不需要手动切换。&lt;/p&gt;
&lt;p&gt;其中两个参数是必须的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;interface-name: wlan0&lt;/code&gt; —— 不指定的话，Android 上内核识别不到默认路由，Tailscale 起不来，日志会刷 &lt;code&gt;magicsock: Rebind IPv4 failed&lt;/code&gt; 和 &lt;code&gt;couldn&apos;t find ip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip-version: ipv4-prefer&lt;/code&gt; —— 手机所处的网络 IPv6 不一定可达，优先 IPv4 可以避开初始化时的卡顿&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外不要给它加 &lt;code&gt;dialer-proxy&lt;/code&gt;。我一开始想让这条出站也走代理，加上之后就是前面那个报错，删掉才恢复正常。&lt;/p&gt;
&lt;h2&gt;补上域名解析&lt;/h2&gt;
&lt;p&gt;节点配好、Tailscale 也连上之后，用主机名访问还是失败，报 &lt;code&gt;couldn&apos;t find ip: mac-mini&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原因是内嵌的 Tailscale 不会把 tailnet 的 MagicDNS 注入 Android 系统，系统不知道这些主机名该解析到哪里。在配置里手工建立对应关系即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hosts:
  mac-mini: 100.x.x.x

dns:
  use-hosts: true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;把 tailnet 流量交给 Tailscale&lt;/h2&gt;
&lt;p&gt;域名能解析了，访问 &lt;code&gt;100.x.x.x&lt;/code&gt; 仍然不通。这次是分流规则的问题：tailnet 的地址段没有被指给 Tailscale 出站，请求被后面的 GEOIP 或兜底规则接走了。&lt;/p&gt;
&lt;p&gt;在 rules 最前面补一条：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rules:
  - IP-CIDR,100.64.0.0/10,Tailscale,no-resolve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;100.64.0.0/10&lt;/code&gt; 是 Tailscale 使用的地址段，&lt;code&gt;no-resolve&lt;/code&gt; 表示直接按 IP 匹配、不做域名解析。这条必须放在所有规则之前。&lt;/p&gt;
&lt;h2&gt;完整配置&lt;/h2&gt;
&lt;p&gt;把前面几处合起来，和 Tailscale 相关的部分是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mixed-port: 7890
mode: rule
log-level: warning

hosts:
  mac-mini: 100.x.x.x

dns:
  enable: true
  ipv6: false
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  use-hosts: true
  nameserver:
    - 223.5.5.5
    - 119.29.29.29

proxies:
  - name: Tailscale
    type: tailscale
    hostname: android-clash
    state-dir: ./tailscale
    ephemeral: false
    udp: true
    accept-routes: true
    interface-name: wlan0
    ip-version: ipv4-prefer

proxy-groups:
  - name: 节点池
    type: select
    proxies:
      # 平时用的机场节点，Tailscale 不需要放进来

rules:
  - IP-CIDR,100.64.0.0/10,Tailscale,no-resolve
  - GEOIP,CN,DIRECT
  - MATCH,节点池
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个容易出错的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要给 Tailscale 出站加 &lt;code&gt;dialer-proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interface-name&lt;/code&gt; 和 &lt;code&gt;ip-version&lt;/code&gt; 不能省&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hosts&lt;/code&gt; 配合 &lt;code&gt;use-hosts&lt;/code&gt; 用来补 MagicDNS&lt;/li&gt;
&lt;li&gt;&lt;code&gt;100.64.0.0/10&lt;/code&gt; 那条规则必须在最前面&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state-dir&lt;/code&gt; 不要随意清除，删掉后需要重新登录 tailnet&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;现在的效果&lt;/h2&gt;
&lt;p&gt;不需要做任何切换，手机照常用原来的机场节点上网；访问家里 tailnet 里的地址时，流量会自动走 Tailscale：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;打开 &lt;code&gt;http://mac-mini:30141&lt;/code&gt; 使用 pi-web（pi-web 监听在所有网卡上），让家里的 Mac mini 接着处理任务&lt;/li&gt;
&lt;li&gt;手机上选一份本地 PDF 上传，家里的 agent 直接读取文件内容&lt;/li&gt;
&lt;li&gt;其它机器上的 Web 服务，通过它们在 tailnet 里的地址同样可以直接打开&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Tailscale</category><category>Clash</category><category>Android</category><category>AI</category><category>pi</category><category>pi-web</category></item><item><title>使用 Playwright MCP 自动生成产品文档</title><link>https://blog.zihengzhang.com/post/ai-browser-automation</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/ai-browser-automation</guid><description>从一条最小流程出发，将产品文档生成拆成需要 AI 判断的 Markdown 规则和可重复执行的 JavaScript 脚本，再由 AI 根据用户意图自主组合。</description><pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;整理 Web 产品的操作文档。&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;能不能把这套东西直接交给 AI 来做 ？&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2. 最小实验&lt;/h2&gt;
&lt;p&gt;完整跑一遍性价比太低。AI 控制浏览器本来就不快，中途遇到问题，排查路径也太长。&lt;/p&gt;
&lt;p&gt;所以我没有让 AI 跑完整流程，而是只挑了最核心的几个动作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;登录 → 进入首页 → 截图、标注、脱敏
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只给入口和目标，看它自己怎么完成。目的不是完美结果，而是&lt;strong&gt;以小见大&lt;/strong&gt;——用最小成本把问题暴露出来。&lt;/p&gt;
&lt;p&gt;跑完这一小段，发现了两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;截图的时候，页面上突然弹出一个提示弹窗，把我们要截的内容挡住了。所以需要判断这个弹窗是不是当前需要截图的对象——如果不是，就该关掉；如果弹窗本身就是业务操作的一部分，就不能关。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标注和脱敏的逻辑其实很&quot;死板&quot;。画方框就是读坐标、画 SVG，脱敏就是找文字节点、加 blur，做完再恢复。既然这么固定，直接封装成 JS 脚本就好，不需要 AI 每次自己琢磨。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一次最小实验，把两种东西分清楚了：&lt;strong&gt;需要现场判断的，和做法固定可重复的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;3. 开始拆分动作&lt;/h2&gt;
&lt;p&gt;根据实验中发现的问题，我把这些动作逐个拆开，发现每个动作都可以抽成两种行为模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Markdown 规则&lt;/strong&gt;：需要结合现场判断的，写成说明文档，AI 会根据当前场景读取对应的规则&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JavaScript 脚本&lt;/strong&gt;：做法固定、输入输出明确的，封装成可调用的工具&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体来看：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;登录&lt;/strong&gt; — 要不要登录、用哪种方式、验证码怎么办 → Markdown；检查登录状态、填写账号、点击登录 → JS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;弹窗处理&lt;/strong&gt; — 弹窗是不是截图对象、能不能关 → Markdown；发现弹窗、匹配按钮、执行关闭 → JS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;脱敏&lt;/strong&gt; — 什么数据必须挡、什么可以保留 → Markdown；加 blur、截图后恢复 → JS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标注&lt;/strong&gt; — 标哪个按钮、编号怎么排 → Markdown；读坐标、画方框、清理图层 → JS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;页面探索&lt;/strong&gt; — 如何遍历菜单、识别页面结构、记录滚动模型 → Markdown；提取菜单树并持久化为 JSON → JS。探索过一次之后，后续任务直接复用这份数据，不用每次重新找菜单。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;打个比方：没必要每次都让 AI 从零造一台电风扇。提前造好，告诉它开关在哪，用的时候它只需要决定&quot;开&quot;还是&quot;关&quot;。标注和脱敏就是这个电风扇——AI 只需要说&quot;把这个按钮标上编号 1&quot;，脚本直接就执行了。&lt;/p&gt;
&lt;p&gt;按这个思路拆完，最终产出了 &lt;strong&gt;7 个 Markdown 规则&lt;/strong&gt;（登录判断、浏览器会话、菜单提取、页面探索、截图标注、脱敏判定、文档写作）和 &lt;strong&gt;5 个 JS 脚本&lt;/strong&gt;（自动登录、截图标注、文字脱敏、弹窗处理、任务管理）&lt;/p&gt;
&lt;h2&gt;4. AI 自主组合，不做流程编排&lt;/h2&gt;
&lt;p&gt;规则和脚本都拆好之后，传统思路是再写一个&quot;编排层&quot;把流程串起来——先做 A、再做 B、然后 C。&lt;/p&gt;
&lt;p&gt;但 AI 本身就有理解和决策的能力，不需要被编排。&lt;/p&gt;
&lt;p&gt;Markdown 规则和 JS 脚本就像一套乐高积木，每个都是独立的行为单元。AI 根据用户的需求，自己决定拿哪些、按什么顺序组合。&lt;/p&gt;
&lt;p&gt;用户说&quot;帮我看看这个产品有什么功能&quot;，AI 自己知道要连接页面、确认登录、探索菜单，不需要截图也不用写文档。&lt;/p&gt;
&lt;p&gt;用户说&quot;把登录页截图标注一下&quot;，AI 自然会跳到截图和标注的规则和脚本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;顶层 Skill 不规定执行顺序，它只做三件事：&lt;/strong&gt; 告诉 AI 有哪些行为可用、每个行为怎么用、有哪些底线不能碰。剩下的组合方式，由 AI 根据用户意图自己决定。&lt;/p&gt;
&lt;p&gt;这意味着同一套工具，不同用户、不同任务、不同问法，会组合出完全不同的执行路径。&lt;/p&gt;
&lt;h2&gt;5. 沉淀下来的方法&lt;/h2&gt;
&lt;p&gt;整个过程可以归纳为几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选一条最小流程让 AI 做一遍，暴露问题&lt;/li&gt;
&lt;li&gt;把反复出现的行为抽象出来，拆成&quot;判断&quot;和&quot;执行&quot;&lt;/li&gt;
&lt;li&gt;判断写成 Markdown 规则，执行写成 JS 脚本&lt;/li&gt;
&lt;li&gt;让 AI 自主组合这些行为，不做流程编排&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>AI</category><category>浏览器自动化</category><category>Skills</category><category>产品文档</category></item><item><title>AI Skills + Tailscale 管理 VPS 和 Mac mini</title><link>https://blog.zihengzhang.com/post/ai-powered-infrastructure</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/ai-powered-infrastructure</guid><description>用自然语言管理 VPS + Mac mini 架构。</description><pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么需要 AI 辅助&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;/post/vps-frp-docker&quot;&gt;上一篇文章&lt;/a&gt;中，我介绍了如何用 VPS + Mac mini + FRP 搭建家用服务器。但实际运维中会遇到很多问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记不住各种配置文件的位置和语法&lt;/li&gt;
&lt;li&gt;端口冲突、SSL 证书和各种配置容易出错&lt;/li&gt;
&lt;li&gt;故障排查需要查日志、测试连接&lt;/li&gt;
&lt;li&gt;新增服务需要修改多处配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI 改变了这个流程。现在我用 Claude Code + Tailscale，可以：&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;h2&gt;架构图&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    subgraph Internet[互联网]
        User[用户访问]
    end

    subgraph DevMachine[开发机]
        direction TB
        AI[Claude Code&amp;lt;br/&amp;gt;自然语言描述]
        Skills[Skills &amp;lt;br/&amp;gt;连接等命令]
        TSClient1[Tailscale 客户端]
        AI --&amp;gt; Skills
        Skills --&amp;gt; TSClient1
    end

    subgraph VPS[阿里云 VPS]
        direction LR
        Nginx[Nginx&amp;lt;br/&amp;gt;端口: 443/80&amp;lt;br/&amp;gt; 域名配置]
        FRPS[FRPS 服务&amp;lt;br/&amp;gt;隧道端口: 7000&amp;lt;br/&amp;gt;HTTP端口: 8080&amp;lt;br/&amp;gt;管理面板: 7500]
        TSClient2[Tailscale 客户端]
    end

    subgraph MacMini[Mac mini 家用服务器]
        direction TB
        FRPC[FRPC 客户端&amp;lt;br/&amp;gt;跟据域名分请求发到不同 docker]
        TSClient3[Tailscale 客户端]

        subgraph Docker[Docker 容器]
            direction LR
            Blog[博客服务&amp;lt;br/&amp;gt;端口: 4321]
            API[API 服务&amp;lt;br/&amp;gt;端口: 4322]
            Dashboard[管理面板&amp;lt;br/&amp;gt;端口: 4323]
        end
    end

    User --&amp;gt;|HTTPS 请求| Nginx
    Nginx --&amp;gt;|转发到 8080| FRPS
    FRPS &amp;lt;--&amp;gt;|隧道通信 7000| FRPC
    FRPC --&amp;gt; Blog
    FRPC --&amp;gt; API
    FRPC --&amp;gt; Dashboard

    TSClient1 -.-&amp;gt;| | TSClient2
    TSClient1 -.-&amp;gt;| | TSClient3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;核心 Tailscale + Skills&lt;/h2&gt;
&lt;p&gt;使用 Tailscale 让 开发机、VPS、Mac mini 组成一个虚拟局域网，无需公网 IP 就能互相访问。&lt;/p&gt;
&lt;p&gt;在 Claude Code 中创建 Skills，通过 SSH 别名快速连接：&lt;/p&gt;
&lt;p&gt;部分代码:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;阿里云 VPS Skill&lt;/strong&gt; (&lt;code&gt;aliyun.md&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
name: aliyun
description: 在管理 aliyun vps 时使用
---

# 连接方式

ssh aliyun

# 常用命令

systemctl status nginx systemctl reload nginx tailscale status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Mac mini Skill&lt;/strong&gt; (&lt;code&gt;macmini.md&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
name: macmini
description: 在管理 macmini 开发机时使用
---

# 连接方式

ssh macmini

# Docker 管理

ssh nas &quot;docker ps&quot; ssh nas &quot;docker logs -f &amp;lt;container&amp;gt;&quot; ssh nas &quot;docker restart &amp;lt;container&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在只需要在 claude code 中输入 &quot;检查 aliyun 的 Nginx 状态&quot;或&quot;查看 nas 的 Docker 容器&quot;&amp;lt;br/&amp;gt; 它就会自动通过 Skill 执行命令。&lt;/p&gt;
&lt;h2&gt;快速上线新服务&lt;/h2&gt;
&lt;p&gt;假设我要部署一个新的监控服务，域名是 &lt;code&gt;monitor.quillbloom.cn&lt;/code&gt;，运行在 Mac mini 的 Docker 容器端口 8085。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统方式需要：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 Mac mini 上修改 FRPC 配置，添加新的 &lt;code&gt;[[proxies]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重启 FRPC 服务&lt;/li&gt;
&lt;li&gt;在 VPS 上配置 Nginx 域名&lt;/li&gt;
&lt;li&gt;测试连接是否正常&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;AI 辅助方式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我直接告诉 Claude：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Mac mini 上配置 frpc，域名 monitor.quillbloom.cn，端口 8085&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude 自动完成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 Skill 连接到 Mac mini&lt;/li&gt;
&lt;li&gt;读取现有 FRPC 配置文件 &lt;code&gt;/usr/local/frp/frpc.toml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;生成新的 &lt;code&gt;[[proxies]]&lt;/code&gt; 配置：&lt;pre&gt;&lt;code&gt;[[proxies]]
name = &quot;monitor&quot;
type = &quot;http&quot;
localIP = &quot;0.0.0.0&quot;
localPort = 8085
customDomains = [&quot;monitor.quillbloom.cn&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;追加到配置文件&lt;/li&gt;
&lt;li&gt;重启 FRPC 服务：&lt;code&gt;sudo launchctl restart com.frpc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;测试连接&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个过程非常快，还没有心智负担。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;AI + Tailscale + Skills 的组合使运维效率提升了非常多而且不再需要记忆大量命令和配置语法&lt;/p&gt;
</content:encoded><category>AI</category><category>AI CLI</category><category>DevOps</category><category>VPS</category><category>Tailscale</category></item><item><title>用自然语言管理多服务器 Docker 部署</title><link>https://blog.zihengzhang.com/post/skills-natural-language-evolution</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/skills-natural-language-evolution</guid><description>传统方式需要记住 SSH、Docker 命令和脚本参数，AI 方式只需一句话描述需求。通过 Claude Code 的 Skills 和 Commands 实现从&quot;手动挡&quot;到&quot;自动挡&quot;的体验升级。</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近需要在多台远程服务器上启动多个 Docker 容器，并分配不同的端口。&lt;/p&gt;
&lt;h2&gt;传统方式&lt;/h2&gt;
&lt;p&gt;打开终端，SSH 连接到服务器，回忆 docker 命令的参数格式，手动启动容器，遇到错误排查日志，重复这一套流程。&lt;/p&gt;
&lt;p&gt;如果经常遇到类似任务，通常会封装成 shell 脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bash deploy.sh --targets &quot;A,B&quot; --ports &quot;5000,5001,5002&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这种方式也有问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;脚本的参数格式是什么？&lt;code&gt;--targets&lt;/code&gt; 还是 &lt;code&gt;--hosts&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;端口列表用逗号还是空格分隔？&lt;/li&gt;
&lt;li&gt;脚本是否还有未发现的 bug？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不管手动执行还是用脚本，本质上都需要&lt;strong&gt;记住特定的命令格式和参数规则&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;AI 方式&lt;/h2&gt;
&lt;p&gt;用 AI 辅助的方式，同样的任务是这样完成的：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;在服务器 A 和 B 上启动容器 A、B、C，端口从 5000 开始
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;AI 自动完成&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理解意图：需要在 2 台服务器上启动 3 个容器&lt;/li&gt;
&lt;li&gt;提取参数：服务器列表 [A, B]，容器列表 [A, B, C]，起始端口 5000&lt;/li&gt;
&lt;li&gt;自动递增：计算端口分配 5000, 5001, 5002&lt;/li&gt;
&lt;li&gt;构建命令：生成 SSH + Docker 的完整命令&lt;/li&gt;
&lt;li&gt;执行并返回结果&lt;/li&gt;
&lt;/ul&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;AI 方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;输入&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bash deploy.sh --targets &quot;A,B&quot; --ports &quot;5000,5001,5002&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;启动 A、B 容器，端口从 5000 开始&lt;/code&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;参数错误直接报错&lt;/td&gt;
&lt;td&gt;AI 理解意图并纠正&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适应性&lt;/td&gt;
&lt;td&gt;脚本修改后需要重新学习&lt;/td&gt;
&lt;td&gt;AI 自动适配变化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调试&lt;/td&gt;
&lt;td&gt;手动排查每个步骤&lt;/td&gt;
&lt;td&gt;AI 自动处理异常&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;本质区别&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;传统方式&lt;/strong&gt;像手动挡汽车：你需要知道什么时候换挡、怎么踩离合。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 方式&lt;/strong&gt;像自动挡汽车：你只需要说&quot;前进&quot;或&quot;后退&quot;，底层细节由系统处理。&lt;/p&gt;
&lt;p&gt;这不是简单的封装，而是&lt;strong&gt;抽象层级的提升&lt;/strong&gt;——从&quot;命令参数&quot;提升到&quot;意图理解&quot;。&lt;/p&gt;
&lt;h2&gt;技术原理&lt;/h2&gt;
&lt;h3&gt;工作流程&lt;/h3&gt;
&lt;p&gt;AI CLI 的工作流程可以分为三个步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户自然语言输入
    ↓
1. 理解意图（需要做什么？）
    ↓
2. 提取参数（目标、端口、容器...）
    ↓
3. 执行命令（SSH → Docker → 返回结果）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户不需要知道命令格式、参数顺序、错误处理细节，只需要描述&lt;strong&gt;想要什么&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Skills 和 Commands 的角色&lt;/h3&gt;
&lt;p&gt;如果有多台服务器需要管理，可以为每台服务器定义一个 &lt;strong&gt;Skill&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
description: 管理 A 服务器的 Docker 容器
---

A 服务器信息：

- 系统：Kali GNU/Linux Rolling
- Docker：27.5.1

连接方式：ssh A检查容器：ssh A &quot;docker ps&quot; 检查端口：ssh A &quot;ss -tlnp | grep :5000&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Skills&lt;/strong&gt;：AI 自动判断用户需要操作哪台服务器 &lt;strong&gt;Commands&lt;/strong&gt;：用户手动触发，执行封装好的操作逻辑&lt;/p&gt;
&lt;p&gt;两者配合，实现完整的自动化工作流。&lt;/p&gt;
&lt;h3&gt;用户体验对比&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;传统方式需要&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记住 SSH 连接方式&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;AI 方式只需要&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;说一句话：&quot;在 A 服务器启动 A、B 容器，端口从 5000 开始&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;从命令行到自然语言，表面上是输入方式的改变，本质上是&lt;strong&gt;抽象层级的提升&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命令行&lt;/strong&gt;：需要记住工具的语法和参数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自然语言&lt;/strong&gt;：只需要表达想要什么&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>AI</category><category>AI CLI</category><category>Skills</category></item><item><title>我想“财富自由”</title><link>https://blog.zihengzhang.com/post/financial-freedom</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/financial-freedom</guid><description>被动收入大于日常的支出</description><pubDate>Mon, 29 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我想在清晨悠闲地喝咖啡、和朋友聊天、健身或游泳；晚上在河边绿道跑步出汗。生活的节奏不再是被动的，而是主动的、自洽的。&lt;/p&gt;
&lt;p&gt;我想在冬天去北方滑雪，夏天去海边冲浪；还想学习各种运动：自由潜水、皮划艇、登山、攀岩、徒步、拳击、瑜伽……从容地热爱这个世界。&lt;/p&gt;
&lt;p&gt;表面上看，这些事情我现在也能做。偶尔我也会旅游、运动、探索。但是每次都伴随着一些代价，不论是金钱还是时间。更难受的是：每次短暂的“自由”之后，回归现实的落差仿佛南柯一梦，留下一地的经济压力和精神的空虚。&lt;/p&gt;
&lt;p&gt;我不想再只是短暂地“逃走”去追逐自由。我想要的是——一种稳定的生活方式。我渴望的不是度假，而是让生活本身就像度假一样自然、流畅，并且拥有选择的余地。&lt;/p&gt;
&lt;p&gt;这或许就是我对财富自由的真正渴望：不是多么奢侈的消费，而是不再需要为选择而受限，不再为热爱而计算代价。&lt;/p&gt;
</content:encoded><category>生活</category></item><item><title>卖掉使用四年的相机</title><link>https://blog.zihengzhang.com/post/sold-camera</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/sold-camera</guid><description>卖出了 2021 年 1 月份购入的二手索尼 α6400</description><pubDate>Sat, 27 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;摄于 2022 年 1 月 27 日，我离开苏州住所的那一天。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;2025 年 9 月 27 日中午，我在京东入口的“爱回收”下了一个 Sony E PZ 18-105mm f/4.0 G OSS 镜头的回收订单。&lt;/p&gt;
&lt;p&gt;我选择的估价信息全是“完美”品相，平台参考价是 1968 元（新机参考价 3019 元）。&lt;/p&gt;
&lt;p&gt;系统提示大约 15:00–17:00 上门。过了一会儿师傅打来电话，说今天非常忙，可能会晚点到。我记不太清具体时间了，应该是 4 点多到的。&lt;/p&gt;
&lt;p&gt;我请师傅进屋里聊。师傅放下装备后自报家门：“我是爱回收的工作人员，工号……”。他穿着工作服，里面的 T 恤上印着“爱回收”，带了一个偏高的方形工具箱。&lt;/p&gt;
&lt;p&gt;我把镜头递给师傅。他从箱子里拿出检验灯检查镜头：“外观这里有划痕，镜片内有一点微尘，这里掉了一块漆……”。最终镜头估价给到一千出头，加上平台优惠券大概是 1350 元左右。&lt;/p&gt;
&lt;p&gt;随后我又把相机给师傅看。相机估价大概在 2100 元左右。我当时买的就是二手机，所以现在成色确实一般。师傅给出的两件合计成交价大约是 3500 元，这和我心理预期有些差距，我没有当场成交。&lt;/p&gt;
&lt;p&gt;师傅问我心里合适的价格，我说 4000 元。其实那一刻我还没做好卖的心理准备，但被问到，也得给个明确的数字。师傅开始在我的手机上叠加各种优惠券，最后把合计凑到了 4000 元。&lt;/p&gt;
&lt;p&gt;我还是没有卖。师傅问原因，我说“还没到预期”，他有点生气：“如果没打算卖，为什么要让我过来？我从西溪那边过来十多公里，后面还有好几个单子。”我向他道歉，说确实耽误了他的时间，但在他上门检验前，我也无法确定自己是否能接受实际价格。&lt;/p&gt;
&lt;h2&gt;反思&lt;/h2&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;/ol&gt;
&lt;p&gt;我重新梳理了“卖”与“不卖”的动机。&lt;/p&gt;
&lt;h2&gt;不卖的动机&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;相机 + 镜头当年合计约 8800 元，现在合计 4000 元，价值缩水。&lt;/li&gt;
&lt;li&gt;如果以后还想重新玩相机，重新启动的成本会比较高。&lt;/li&gt;
&lt;li&gt;有感情因素：陪了我四年，不舍得。即便不常用，偶尔拿出来把玩也不错。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;卖的动机&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;现在几乎不用，&lt;strong&gt;继续放着就是“负资产”，卖了就是“变现”&lt;/strong&gt;，4000 元总比 0 强。&lt;/li&gt;
&lt;li&gt;技术迭代很快，可能会有更小更强的设备，覆盖相机的大部分场景。&lt;/li&gt;
&lt;li&gt;相机 + 镜头重量接近 1 kg，体积也不小，出门携带不便。&lt;/li&gt;
&lt;li&gt;未来可能会换更轻便的微单或运动相机，符合现在的使用习惯。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;决定&lt;/h2&gt;
&lt;p&gt;想清楚前后的逻辑后，我决定还是卖掉相机。我又给师傅打了电话，约在晚上 9:00 后上门交易。还是原先的流程：自报家门、重新验机。最后师傅让我在手机上点击“确认”按钮，交易就完成了。&lt;/p&gt;
&lt;p&gt;确实不好意思，麻烦师傅又跑了一趟，做这行不容易。&lt;/p&gt;
&lt;p&gt;他说，今天跑了 20 多单。&lt;/p&gt;
&lt;p&gt;我下次尽量把事情办得周全，不能因为自己的事情耽误别人。&lt;/p&gt;
</content:encoded><category>生活</category></item><item><title>VPS、Mac mini、Frp 搭建内网穿透家用服务器</title><link>https://blog.zihengzhang.com/post/vps-frp-docker</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/vps-frp-docker</guid><description>通过阿里云 VPS 提供的公网 IP，使用 FRP 实现内网穿透到家里的 Mac mini。</description><pubDate>Thu, 24 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import WrapImage from &apos;@/components/WrapImage/index.astro&apos;
import constructImage from &apos;@/assets/images/blog/vps-frp-docker/construct.webp&apos;
import { cn } from &apos;@/lib/cn&apos;&lt;/p&gt;
&lt;h2&gt;为什么这么设计&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;核心问题：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;家里的 Mac mini 性能强劲，但没有公网 IP，无法直接从外网访问&lt;/li&gt;
&lt;li&gt;阿里云 VPS 有公网 IP，但配置较低（2 核 2G），性能不足以运行复杂服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPS 负责流量转发：利用公网 IP 作为流量入口，通过 Nginx + FRP 将请求转发到内网&lt;/li&gt;
&lt;li&gt;Mac mini 负责实际服务：利用强劲性能运行各种 Docker 服务，处理真正的业务逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样既解决了公网访问问题，又充分利用了现有硬件资源，避免购买高配 VPS 的额外成本。&lt;/p&gt;
&lt;h2&gt;概览&lt;/h2&gt;
&lt;p&gt;&amp;lt;WrapImage
wrapClass={cn(&apos;w-full flex justify-center&apos;)}
class={cn(&apos;w-[60%]&apos;)}
forceDark
loading=&apos;lazy&apos;
src={constructImage}
alt=&apos;阿里云 VPS + Mac mini 低成本搭建家服务器&apos;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;流量流向&lt;/h3&gt;
&lt;p&gt;当用户访问你的服务时，请求的完整路径如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户浏览器 → VPS 公网 IP (443/80)
         ↓
    Nginx 接收并转发
         ↓
    FRPS (8090) 接收 HTTP 流量
         ↓
    通过 FRP 隧道 (7100) 传输
         ↓
    Mac mini FRPC 接收
         ↓
    转发到本地 Docker 容器 (4321/4322/4323...)
         ↓
    容器处理请求并返回响应
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个过程中，VPS 只负责流量中转，真正的业务逻辑都在 Mac mini 的 Docker 容器中处理。&lt;/p&gt;
&lt;h3&gt;VPS&lt;/h3&gt;
&lt;h4&gt;Nginx&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流量转发&lt;/strong&gt;：通过 443/80 端口接收外部流量，并转发至本地 8090 端口（FRPS 服务）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;server {
   listen 443 ssl http2; # 监听 443 端口(HTTPS)并启用 SSL 和 HTTP/2
   ...

   # 代理配置 - 将请求转发到本地 8090 端口
   location / {
      proxy_pass http://localhost:8090;
      ...
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;FRPS&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;一个快速反向代理，帮助你将 NAT 或防火墙后的本地服务器暴露到互联网&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流量桥接&lt;/strong&gt;：接收 8090 端口的 HTTP 流量，通过 7100 端口与客户端建立隧道连接&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;bindPort = 7100  # FRPS（服务端）与 FRPC（客户端）建立隧道通信的主要端口
auth.token = &quot;******&quot;  # 客户端和服务端之间的认证 Token

vhostHTTPPort = 8090  # FRPS 用于对外暴露 HTTP 服务的端口

# 管理面板（Dashboard）
webServer.port = 7500  # 管理面板端口
webServer.user = &quot;admin&quot;
webServer.password = &quot;******&quot;
webServer.addr = &quot;0.0.0.0&quot;  # 允许所有 IP 访问（公网可访问）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mac Mini&lt;/h3&gt;
&lt;h4&gt;FRPC 客户端&lt;/h4&gt;
&lt;p&gt;Mac mini 通过 FRPC 客户端与 VPS 建立隧道，并通过 [[proxies]] 配置实现多域名分流&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隧道连接&lt;/strong&gt;：与 VPS 上的 FRPS 服务建立稳定连接&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多域名代理&lt;/strong&gt;：通过 &lt;code&gt;[[proxies]]&lt;/code&gt; 配置实现不同域名到本地端口的映射&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[[proxies]]
name = &quot;blog&quot;
type = &quot;http&quot;
localPort = 4321 # 个人博客服务
customDomains = [&quot;blog.example.com&quot;]

[[proxies]]
name = &quot;api&quot;
type = &quot;http&quot;
localPort = 4322 # API 服务
customDomains = [&quot;api.example.com&quot;]

[[proxies]]
name = &quot;dashboard&quot;
type = &quot;http&quot;
localPort = 4323 # 管理面板
customDomains = [&quot;admin.example.com&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 localPort 对应的并不一定是 Mac mini 本机的实际端口，大多数情况下是运行在 Docker 容器中的应用服务。&lt;/p&gt;
&lt;p&gt;例如：&amp;lt;br /&amp;gt; 4321 → API 服务容器（如 FastAPI、Node.js）&amp;lt;br /&amp;gt; 4322 → Nginx 容器，用于个人博客或静态站点&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;换句话说，FRPC 就像&quot;公网入口&quot;，而 Docker 是&quot;本地应用商店&quot;：你在 Mac mini 上随时可以用 Docker 启动一个新服务，分配一个本地端口，然后只需在 FRPC 配置中增加一个 [[proxies]]，几秒钟就能让它通过独立域名对外提供访问。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><category>VPS</category><category>MacMini</category><category>Docker</category><category>FRP</category><category>内网穿透</category></item><item><title>AI 使用：种子页面复用、迭代设计与 Figma to html</title><link>https://blog.zihengzhang.com/post/ai-usage</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/ai-usage</guid><description>针对重复页面、独特页面和设计稿三种场景，介绍如何用 AI CLI 提升开发效率。包含种子模板方法、迭代优化流程，以及截图和文档获取等关键技巧。</description><pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;场景一：结构逻辑高度重复的页面&lt;/h2&gt;
&lt;p&gt;针对结构相似的页面，我会采用以下工作流：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;创建种子模板&lt;/strong&gt;：先实现一个功能完整的&quot;种子页面&quot;作为参考基准&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;描述差异点&lt;/strong&gt;：向 AI 提供新页面的具体差异信息：
&lt;ul&gt;
&lt;li&gt;数据表格的字段定义&lt;/li&gt;
&lt;li&gt;搜索条件的配置项&lt;/li&gt;
&lt;li&gt;表单字段的属性&lt;/li&gt;
&lt;li&gt;页面布局的预期效果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参照实现&lt;/strong&gt;：指定以种子页面为模板，让 AI 生成相似结构的新页面&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;场景二：非重复页面&lt;/h2&gt;
&lt;p&gt;对于结构独特的页面，采用迭代设计的方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;需求描述&lt;/strong&gt;：向 AI 说明页面的结构布局和交互逻辑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案生成&lt;/strong&gt;：让 AI 基于需求生成初步实现方案&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;h2&gt;场景三：基于设计稿生成代码&lt;/h2&gt;
&lt;p&gt;当有完整的设计稿时，可以直接利用 MCP (Model Context Protocol) 工具将设计转换为代码。&lt;/p&gt;
&lt;p&gt;推荐工具：&lt;a href=&quot;https://github.com/GLips/Figma-Context-MCP&quot;&gt;Figma MCP&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这种方式适用于设计规范明确、视觉还原度要求高的场景。&lt;/p&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;p&gt;为提升 AI 生成代码的质量，以下技巧值得注意：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提供准确的参考信息&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当有参考页面时，截图提供给 AI 能显著提升理解准确度&lt;/li&gt;
&lt;li&gt;确保依赖库文档的可获取性：
&lt;ul&gt;
&lt;li&gt;对于较新的依赖包，使用 &lt;a href=&quot;https://github.com/upstash/context7&quot;&gt;context7 MCP&lt;/a&gt; 获取最新文档&lt;/li&gt;
&lt;li&gt;对于较旧的依赖包，可将相关文档下载到本地，让 AI 直接读取&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;相关工具：HTML 转 Figma 插件&lt;/h2&gt;
&lt;p&gt;以下插件可以将现有 HTML 页面转换为 Figma 设计稿，方便进行设计迭代或团队协作：&lt;/p&gt;
&lt;h3&gt;1. HTML to Design&lt;/h3&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://chromewebstore.google.com/detail/htmltodesign/ldnheaepmnmbjjjahokphckbpgciiaed?hl=en-US&amp;amp;utm_source=ext_sidebar&quot;&gt;html-to-figma&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;免费额度：每月 2 次&lt;/p&gt;
&lt;h3&gt;2. HTML to Design by Demoway&lt;/h3&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://chromewebstore.google.com/detail/html-to-figma-by-demoway/amcccnldajjnngnaoinemnaloklogjak?hl=en-US&amp;amp;utm_source=ext_sidebar&quot;&gt;html.to.design&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;免费额度：每月 10 次&lt;/p&gt;
</content:encoded><category>AI</category><category>AI CLI</category></item><item><title>View Transition API 实现主题色过渡切换效果。</title><link>https://blog.zihengzhang.com/post/view-transition</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/view-transition</guid><description>View Transition API，可以为界面带来柔和自然的主题色转换，从而显著提升整体视觉体验。</description><pubDate>Thu, 13 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import WrapImage from &apos;@/components/WrapImage/index.astro&apos;
import { cn } from &apos;@/lib/cn&apos;&lt;/p&gt;
&lt;h2&gt;最小实现&lt;/h2&gt;
&lt;p&gt;流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;startViewTransition 函数执行，并记住当前页面的“快照”&lt;/li&gt;
&lt;li&gt;执行回调函数，当“视图更新”完成后，会触发 ::view-transition-old 和 ::view-transition-new 的动画&lt;/li&gt;
&lt;li&gt;动画结束后，视图更新完成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这段代码是 View Transition API 的最小实现。&amp;lt;br/&amp;gt;复制为 html 文件，打开即可看到效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;style&amp;gt;
    @keyframes opacity-fade-out {
      to {
        opacity: 0;
      }
    }

    ::view-transition-old(root) {
      animation: opacity-fade-out 1s ease-out;
    }
  &amp;lt;/style&amp;gt;

  &amp;lt;script&amp;gt;
    window.addEventListener(&apos;DOMContentLoaded&apos;, () =&amp;gt; {
      document.querySelector(&apos;#toggle&apos;).addEventListener(&apos;click&apos;, () =&amp;gt; {
        document.startViewTransition(() =&amp;gt; {
          if (document.documentElement.style.backgroundColor === &apos;black&apos;) {
            document.documentElement.style.backgroundColor = &apos;white&apos;
          } else {
            document.documentElement.style.backgroundColor = &apos;black&apos;
          }
        })
      })
    })
  &amp;lt;/script&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;button id=&quot;toggle&quot;&amp;gt;toggle&amp;lt;/button&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;兼容性&lt;/h2&gt;
&lt;p&gt;&amp;lt;WrapImage
wrapClass={cn(&apos;w-full flex justify-center&apos;)}
alt=&apos;兼容性&apos;
loading=&apos;lazy&apos;
forceDark
src=&apos;/images/blog/view-transition/can-use.webp&apos;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;演示地址&lt;/h2&gt;
&lt;p&gt;github: &lt;a href=&quot;https://shadowzzh.github.io/first-view-transition/&quot;&gt;first-view-transition&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API&quot;&gt;MDN View Transition API&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>ViewTransition</category></item><item><title>如何检测 PDF 文件的是否已加载完成</title><link>https://blog.zihengzhang.com/post/detection-pdf</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/detection-pdf</guid><description>通过 Fetch API 和 Blob 对象实现检测 PDF 是否已加载完成。</description><pubDate>Mon, 14 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在网页中，如何判断 iframe 中的 PDF 是否已经加载完成呢？&lt;/p&gt;
&lt;h2&gt;网页中常规展示 PDF 的方式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;object data=&apos;filename.pdf&apos; type=&apos;application/pdf&apos;&amp;gt;
  &amp;lt;p&amp;gt;您的浏览器不支持嵌入式 PDF&amp;lt;/p&amp;gt;
&amp;lt;/object&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述写法虽然简洁，但存在一个问题 —— 你无法通过代码判断 PDF 是否加载完成。&lt;/p&gt;
&lt;p&gt;尤其是对于较大的 PDF 文件（例如 10MB），加载过程可能需要几秒钟时间，用户体验不佳。&lt;/p&gt;
&lt;h2&gt;改进方案：使用 Fetch 请求 PDF 后再加载&lt;/h2&gt;
&lt;p&gt;为了更好地控制加载流程，我们可以使用 fetch 请求 PDF 文件，再通过 Blob 创建一个临时 URL 来展示 PDF。整体流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 &lt;code&gt;fetch&lt;/code&gt; 请求 PDF 文件；&lt;/li&gt;
&lt;li&gt;将请求到的文件转换为 &lt;code&gt;Blob&lt;/code&gt; 对象；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;createObjectURL&lt;/code&gt; 创建临时本地 URL；&lt;/li&gt;
&lt;li&gt;使用该 URL 来展示 PDF。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样你可以在加载过程中设置 loading 状态，甚至配合进度条或提示信息，显著提升用户体验。&lt;/p&gt;
&lt;p&gt;👉 示例代码 &lt;a href=&quot;https://codesandbox.io/p/sandbox/usepdfurl-vtyvhf&quot;&gt;standbox:usePDFUrl&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { useCallback, useEffect, useRef, useState } from &apos;react&apos;

interface PdfRenderProps {
  url: string // PDF 的远程地址
  params: Record&amp;lt;string, any&amp;gt; // 可选的 PDF 参数，如页码等
}

// 将参数序列化为 URL 查询字符串
const serialization = (obj: Record&amp;lt;string, any&amp;gt;) =&amp;gt; {
  const params = new URLSearchParams()
  for (let key in obj) {
    params.append(key, obj[key])
  }
  return params.toString()
}

/**
 * 自定义 Hook：根据远程 URL 异步加载 PDF 文件，并返回本地 URL
 */
export const usePdfUrl = (props: PdfRenderProps) =&amp;gt; {
  const { url, params } = props

  const [pdfLocalUrl, setPdfLocalUrl] = useState&amp;lt;string | null&amp;gt;(null)
  const [loading, setLoading] = useState(false)
  const abortController = useRef&amp;lt;AbortController | null&amp;gt;(null)

  // 拼接参数（如页码、缩放比例等）
  const wrapperUrl = pdfLocalUrl ? `${pdfLocalUrl}#${serialization(params)}` : null

  const fetchPdf = useCallback(async (requestUrl: string) =&amp;gt; {
    if (abortController.current) {
      abortController.current.abort()
    }

    setLoading(true)

    abortController.current = new AbortController()
    const signal = abortController.current.signal

    try {
      const response = await fetch(requestUrl, { signal })
      abortController.current = null

      if (!response.ok) {
        throw new Error(`HTTP 请求错误，状态码：${response.status}`)
      }

      const blob = await response.blob()
      const pdfBlob = new Blob([blob], { type: &apos;application/pdf&apos; })

      const localUrl = URL.createObjectURL(pdfBlob)
      setPdfLocalUrl(localUrl)
    } catch (error) {
      console.error(&apos;加载 PDF 失败：&apos;, error)
    }

    setLoading(false)
  }, [])

  useEffect(() =&amp;gt; {
    if (url) {
      fetchPdf(url)
    }

    return () =&amp;gt; {
      abortController.current?.abort()
      abortController.current = null
    }
  }, [fetchPdf, url])

  return {
    pdfLocalUrl: wrapperUrl,
    loading,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;通过使用 fetch + Blob + URL.createObjectURL 的方式，我们可以更好地掌控 PDF 的加载过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以监听加载状态（如 loading）；&lt;/li&gt;
&lt;li&gt;支持设置 URL 参数（如指定页码）；&lt;/li&gt;
&lt;li&gt;可扩展性更强，适用于需要精细控制的业务场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方法相比直接嵌入 &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; 更具灵活性。&lt;/p&gt;
</content:encoded><category>PDF</category><category>react</category></item><item><title>博客技术栈介绍</title><link>https://blog.zihengzhang.com/post/introduce</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/introduce</guid><description>介绍博客的技术架构选择与实现理念，一个极简而优雅的个人博客站点。</description><pubDate>Fri, 27 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;欢迎来到我的博客！这是一个使用现代 Web 技术构建的极简个人博客。&lt;/p&gt;
&lt;p&gt;项目名称寓意&quot;羽笔绽放&quot;，象征着文字的生长和创造力的迸发。&lt;/p&gt;
&lt;h2&gt;核心设计理念&lt;/h2&gt;
&lt;p&gt;使用 Astro 作为核心框架，确保网站的快速加载和良好的 SEO 表现。&lt;/p&gt;
&lt;p&gt;秉承&quot;小而美&quot;的设计原则，尽量避免使用 JS；一些特定的交互如果需要使用 JS，则使用内联的 WebComponent 实现。&lt;/p&gt;
&lt;h2&gt;技术栈详解&lt;/h2&gt;
&lt;h3&gt;核心框架&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;&lt;/strong&gt; 5.7+&lt;/p&gt;
&lt;p&gt;现代静态站点生成器，支持多种前端框架。Astro 的核心优势在于零 JavaScript 的默认行为，只在需要交互时才加载 JavaScript，显著提升了页面性能。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;样式与 UI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind CSS&lt;/a&gt;&lt;/strong&gt; 4.0+&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://react.dev/&quot;&gt;React&lt;/a&gt;&lt;/strong&gt; 19&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内容管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内容集合 (Content Collections)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Astro 内置的内容管理方案，使用 Zod 进行类型验证，支持 Markdown 和 MDX 格式，提供类型安全的内容查询。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MDX 支持&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Markdown 中嵌入 React 组件，实现更丰富的内容表达。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图片处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://sharp.pixelplumbing.com/&quot;&gt;Sharp&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;高性能的 Node.js 图像处理库，用于自动生成不同尺寸的图片和 WebP 格式转换。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Markdown 处理插件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rehypejs/rehype-autolink-headings&quot;&gt;rehype-autolink-headings&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为标题自动添加锚点链接。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/rehypejs/rehype-slug&quot;&gt;rehype-slug&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为标题生成唯一的 ID 属性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;开发与构建&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://eslint.org/&quot;&gt;ESLint&lt;/a&gt; &amp;amp; &lt;a href=&quot;https://prettier.io/&quot;&gt;Prettier&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://pnpm.io/&quot;&gt;pnpm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署方案&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://vercel.com/&quot;&gt;Vercel&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Astro</category><category>博客</category></item><item><title>使用快捷指令 + vipsthumbnail 修改图片</title><link>https://blog.zihengzhang.com/post/shortcuts</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/shortcuts</guid><description>厌倦了繁琐的图片压缩过程？本文展示如何利用快捷指令和 Vipsthumbnail 工具，轻松实现高效的图片压缩和格式转换。</description><pubDate>Fri, 20 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;解决方案展示&lt;/h2&gt;
&lt;h3&gt;最终成果快捷指令压缩裁剪转换图片&lt;/h3&gt;
&lt;p&gt;首先来看下效果；下面视频中展示的是使用方法：&lt;/p&gt;
&lt;p&gt;&amp;lt;div&amp;gt;
&amp;lt;video width={500} height={400} controls&amp;gt;
&amp;lt;source src=&apos;/images/blog/shortcuts/procedure.mp4&apos; type=&apos;video/mp4&apos; /&amp;gt;
&amp;lt;/video&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;把四张总计 50 M 大小的小猫照片，压缩裁剪成总计 661 KB 的 1080 分辨率大小的图片。&lt;/p&gt;
&lt;h3&gt;使用方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;右键点击图片 -&amp;gt; 鼠标移动到&lt;strong&gt;快捷操作&lt;/strong&gt; -&amp;gt; 点击&lt;strong&gt;图片大小&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或是，选中图片 -&amp;gt; 点击图片大小&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么需要压缩裁剪图片&lt;/h2&gt;
&lt;h3&gt;图片分享的常见问题&lt;/h3&gt;
&lt;p&gt;我想通过微信把相机照片分享给我的好友，但是我的照片太大了，拍的照片大小都是 10M 以上。这就会导致以下几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果&quot;体积&quot;过大，微信会自动进行压缩，导致图片模糊不清。&lt;/li&gt;
&lt;li&gt;加载时间过长，体验感很差。&lt;/li&gt;
&lt;li&gt;流量消耗过多。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果可以对图片压缩后分享；如果是尺寸特别大的图片，还可以再把图片的尺寸&quot;缩小&quot;至 4k 以内，这样会大大缩小图片的&quot;体积&quot;，就可以避免上述问题。&lt;/p&gt;
&lt;h3&gt;我使用过的方案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://tinify.cn/&quot;&gt;TinyPNG&lt;/a&gt;
&lt;blockquote&gt;
&lt;p&gt;TinyPNG 使用智能有损压缩技术将您的 WebP, PNG and JPEG 图片的文件大小降低。通过选择性的减少图片中的颜色，只需要很少的字节数就能保存数据。对视觉的影响几乎不可见，但是在文件大小上有非常大的差别。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.iloveimg.com/&quot;&gt;Iloveimg&lt;/a&gt;
&lt;blockquote&gt;
&lt;p&gt;通过批量处理，您可以同时转换多张照片。iLoveIMG 支持 PNG、JPG、GIF、WEBP、HEIC、RAW 等多种格式转换，并提供高级功能，每项任务最多可批量转换高达 1GB 的文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Lymphatus/caesium-image-compressor&quot;&gt;Caesium Image Compressor&lt;/a&gt;
&lt;blockquote&gt;
&lt;p&gt;Caesium 是一款图像压缩软件，助您存储、发送和分享数字图片，支持 JPG、PNG、WebP 和 TIFF 格式。您可以快速减小文件大小（并根据需要调整分辨率），同时保持图像的整体质量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;局限性&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;TinyPNG&lt;/strong&gt;、&lt;strong&gt;Iloveimg&lt;/strong&gt; 等在线工具使用不方便。每次使用都要打开浏览器 -&amp;gt; 打开网站 -&amp;gt; 传图片 -&amp;gt; 等待压缩 -&amp;gt; 下载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caesium&lt;/strong&gt; 本地软件也有一定使用成本。其实已经能满足我的所有需要，但我还是觉得不够方便。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我需要记住，压缩裁剪图片需要使用什么软件、压缩视频、Twitter 视频下载需要使用什么软件... 实际上我想；选中视频，右键压缩；选中图片，右键压缩。&lt;/p&gt;
&lt;h2&gt;命令行工具的探索&lt;/h2&gt;
&lt;h3&gt;ImageMagick&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;官方简介：&lt;a href=&quot;https://imagemagick.org&quot;&gt;ImageMagick&lt;/a&gt; 是一款免费、开源的软件套件，用于编辑和处理数字图像。它可以用于创建、编辑、合成或转换位图图像，并支持多种文件格式，包括 JPEG、PNG、GIF、TIFF 和 Ultra HDR。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它提供了 &lt;strong&gt;magick&lt;/strong&gt; CLI 命令行工具，可以使用 shell 脚本调用。下面是使用 magick 最简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;magick picture.jpg -resize 1080 picture-1080.jpgmagick picture.jpg -resize 1080 picture-1080.jpgmagick picture.jpg -resize 1080 picture-1080.jpgmagick picture.jpg -resize 1080 picture-1080.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Vipsthumbnail&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;官方简介：&lt;a href=&quot;https://www.libvips.org/&quot;&gt;libvips&lt;/a&gt; 是一个按需驱动、水平线程化的图像处理库。与类似库相比，libvips 运行速度快且内存占用小。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;libvips&lt;/strong&gt; 附带了一个便捷的命令行 &lt;strong&gt;vipsthumbnail&lt;/strong&gt; CLI 命令行工具，可以使用 shell 脚本调用。下面是一个使用 vipsthumbnail 最简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vipsthumbnail picture.jpg --size 1080 -o picture-1080.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ImageMagick 速度测试&lt;/h3&gt;
&lt;p&gt;测试素材 30 张 jpg 格式的图片，总计 550M 左右。&lt;/p&gt;
&lt;p&gt;测试代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 开始计时
start_time=$(date +%s)

magick &apos;*.JPG&apos; -resize 1080 thumbnail%03d.jpg

# 结束计时
end_time=$(date +%s)

# 计算并打印执行时间
execution_time=$((end_time - start_time))
echo &quot;Execution time: $execution_time seconds&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：压缩时间：15 秒&lt;/p&gt;
&lt;p&gt;压缩后的图片总大小：26.6 MB&lt;/p&gt;
&lt;h3&gt;Vipsthumbnail 速度测试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;测试代码：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 开始计时
start_time=$(date +%s)

vipsthumbnail *.JPG -s 1080

# 结束计时
end_time=$(date +%s)

# 计算并打印执行时间
execution_time=$((end_time - start_time))
echo &quot;Execution time: $execution_time seconds&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：压缩时间：5 秒&lt;/p&gt;
&lt;p&gt;压缩后的图片总大小：7.3 MB&lt;/p&gt;
&lt;h3&gt;ImageMagick 和 Vipsthumbnail 速度和压缩大小对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;vipsthumbnail&lt;/th&gt;
&lt;th&gt;ImageMagick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;压缩时间&lt;/td&gt;
&lt;td&gt;5 秒&lt;/td&gt;
&lt;td&gt;15 秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;压缩后大小&lt;/td&gt;
&lt;td&gt;7.3 MB&lt;/td&gt;
&lt;td&gt;26.6 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded><category>快捷指令</category><category>图片</category><category>libvips</category></item><item><title>这是一个测试 Markdown 语法文档</title><link>https://blog.zihengzhang.com/post/markdown-style-guide</link><guid isPermaLink="true">https://blog.zihengzhang.com/post/markdown-style-guide</guid><description>完整的 Markdown 语法参考指南，涵盖从基础到高级的所有常用格式和技巧，帮助你快速掌握 Markdown 写作。</description><pubDate>Wed, 19 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一个完整的 Markdown 语法测试文档，涵盖了所有常用的格式和语法规则。无论你是初学者还是想要查阅特定语法，这份文档都能为你提供清晰的示例和说明。&lt;/p&gt;
&lt;h2&gt;标题&lt;/h2&gt;
&lt;p&gt;以下 HTML &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;—&lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt; 元素代表六个级别的章节标题。&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 是最高级别的标题，而 &lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt; 是最低级别。&lt;/p&gt;
&lt;h1&gt;一级标题&lt;/h1&gt;
&lt;h2&gt;二级标题&lt;/h2&gt;
&lt;h3&gt;三级标题&lt;/h3&gt;
&lt;h4&gt;四级标题&lt;/h4&gt;
&lt;h5&gt;五级标题&lt;/h5&gt;
&lt;h6&gt;六级标题&lt;/h6&gt;
&lt;h2&gt;段落&lt;/h2&gt;
&lt;p&gt;段落是最基本的文本单元。在 Markdown 中，段落由一个或多个连续的文本行组成。相邻的两行文本，如果中间没有空行，会被视为同一个段落内的文本。&lt;/p&gt;
&lt;p&gt;如果你想创建一个新的段落，只需要在两段文字之间添加一个空行即可。这样可以保证文档的清晰结构和良好的可读性。&lt;/p&gt;
&lt;h2&gt;图片&lt;/h2&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;![替代文本](./完整/或/相对/图片路径)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;p&gt;引用元素用于表示引用自其他来源的内容，可选择性地包含引用源信息，引用源信息必须位于 &lt;code&gt;footer&lt;/code&gt; 或 &lt;code&gt;cite&lt;/code&gt; 元素内，也可以包含内联修改，如注释和缩写。&lt;/p&gt;
&lt;h3&gt;无引用源的引用&lt;/h3&gt;
&lt;h4&gt;语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 代码是写给人看的，只是顺便能在机器上运行。**注意**你可以在引用中使用 _Markdown 语法_。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;效果&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;代码是写给人看的，只是顺便能在机器上运行。&lt;strong&gt;注意&lt;/strong&gt;你可以在引用中使用 &lt;em&gt;Markdown 语法&lt;/em&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;有引用源的引用&lt;/h3&gt;
&lt;h4&gt;语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 不要通过共享内存来通信，而要通过通信来共享内存。&amp;lt;br&amp;gt; — &amp;lt;cite&amp;gt;Rob Pike[^1]&amp;lt;/cite&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;效果&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;不要通过共享内存来通信，而要通过通信来共享内存。&amp;lt;br&amp;gt; — &amp;lt;cite&amp;gt;Rob Pike[^1]&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[^1]: 以上引用摘自 Rob Pike 在 2015 年 11 月 18 日 Gopherfest 期间的&lt;a href=&quot;https://www.youtube.com/watch?v=PAAkCSZUG1c&quot;&gt;演讲&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;表格&lt;/h2&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;| 斜体   | 粗体     | 代码   |
| ------ | -------- | ------ |
| _斜体_ | **粗体** | `代码` |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;斜体&lt;/th&gt;
&lt;th&gt;粗体&lt;/th&gt;
&lt;th&gt;代码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;斜体&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;粗体&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;代码&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;代码块&lt;/h2&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;p&gt;我们可以使用三个反引号 ``` 在新行中开始代码块，编写代码片段，然后在新行中用三个反引号结束。要启用特定语言的语法高亮，请在第一个三反引号后写上语言名称，例如：html、javascript、css、markdown、typescript、txt、bash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```html
&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;zh-CN&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;HTML5 示例文档&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;p&amp;gt;测试内容&amp;lt;/p&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;zh-CN&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;HTML5 示例文档&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;p&amp;gt;测试内容&amp;lt;/p&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;列表类型&lt;/h2&gt;
&lt;h3&gt;有序列表&lt;/h3&gt;
&lt;h4&gt;语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;1. 第一项
2. 第二项
3. 第三项
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;效果&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;第一项&lt;/li&gt;
&lt;li&gt;第二项&lt;/li&gt;
&lt;li&gt;第三项&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;无序列表&lt;/h3&gt;
&lt;h4&gt;语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- 列表项
- 另一个项目
- 再一个项目
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&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;/ul&gt;
&lt;h3&gt;嵌套列表&lt;/h3&gt;
&lt;h4&gt;语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- 水果
  - 苹果
  - 橙子
  - 香蕉
- 乳制品
  - 牛奶
  - 奶酪
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;效果&lt;/h4&gt;
&lt;ul&gt;
&lt;li&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;/li&gt;
&lt;li&gt;乳制品
&lt;ul&gt;
&lt;li&gt;牛奶&lt;/li&gt;
&lt;li&gt;奶酪&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;其他元素 — abbr、sub、sup、kbd、mark&lt;/h2&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;abbr title=&quot;Graphics Interchange Format&quot;&amp;gt;GIF&amp;lt;/abbr&amp;gt; 是一种位图图像格式。

H&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;O

X&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt; + Y&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt; = Z&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt;

按下 &amp;lt;kbd&amp;gt;CTRL&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;ALT&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;Delete&amp;lt;/kbd&amp;gt; 来结束会话。

大多数 &amp;lt;mark&amp;gt;蝾螈&amp;lt;/mark&amp;gt; 都是夜行动物，以昆虫、蠕虫和其他小生物为食。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;p&gt;&amp;lt;abbr title=&quot;Graphics Interchange Format&quot;&amp;gt;GIF&amp;lt;/abbr&amp;gt; 是一种位图图像格式。&lt;/p&gt;
&lt;p&gt;H&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;O&lt;/p&gt;
&lt;p&gt;X&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt; + Y&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt; = Z&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt;&lt;/p&gt;
&lt;p&gt;按下 &amp;lt;kbd&amp;gt;CTRL&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;ALT&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;Delete&amp;lt;/kbd&amp;gt; 来结束会话。&lt;/p&gt;
&lt;p&gt;大多数 &amp;lt;mark&amp;gt;蝾螈&amp;lt;/mark&amp;gt; 都是夜行动物，以昆虫、蠕虫和其他小生物为食。&lt;/p&gt;
&lt;h2&gt;文本格式&lt;/h2&gt;
&lt;h3&gt;基础文本样式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;这是 **粗体文本** 这是 _斜体文本_ 这是 **_粗斜体文本_** 这是 ~~删除线文本~~ 这是 `行内代码`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是 &lt;strong&gt;粗体文本&lt;/strong&gt; 这是 &lt;em&gt;斜体文本&lt;/em&gt; 这是 &lt;strong&gt;&lt;em&gt;粗斜体文本&lt;/em&gt;&lt;/strong&gt; 这是 &lt;s&gt;删除线文本&lt;/s&gt; 这是 &lt;code&gt;行内代码&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;链接&lt;/h2&gt;
&lt;h3&gt;内联链接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;这是一个 [内联链接](https://www.example.com) 这是一个带标题的 [链接](https://www.example.com &apos;链接标题&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是一个 &lt;a href=&quot;https://www.example.com&quot;&gt;内联链接&lt;/a&gt; 这是一个带标题的 &lt;a href=&quot;https://www.example.com&quot; title=&quot;链接标题&quot;&gt;链接&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;引用式链接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;这是一个 [引用式链接][1] 这是另一个 [引用式链接][link-ref]

[1]: https://www.example.com
[link-ref]: https://www.example.com &apos;可选标题&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是一个 &lt;a href=&quot;https://www.example.com&quot;&gt;引用式链接&lt;/a&gt; 这是另一个 &lt;a href=&quot;https://www.example.com&quot; title=&quot;可选标题&quot;&gt;引用式链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;分隔线&lt;/h2&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
---

---
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;hr /&gt;
&lt;h2&gt;任务列表&lt;/h2&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- [x] 已完成的任务
- [ ] 待完成的任务
- [x] ~~已完成并删除的任务~~
- [ ] 另一个待完成任务
  - [x] 子任务已完成
  - [ ] 子任务待完成
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[x] 已完成的任务&lt;/li&gt;
&lt;li&gt;[ ] 待完成的任务&lt;/li&gt;
&lt;li&gt;[x] &lt;s&gt;已完成并删除的任务&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;[ ] 另一个待完成任务
&lt;ul&gt;
&lt;li&gt;[x] 子任务已完成&lt;/li&gt;
&lt;li&gt;[ ] 子任务待完成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;转义字符&lt;/h2&gt;
&lt;p&gt;当你需要显示 Markdown 语法字符本身时，可以使用反斜杠进行转义：&lt;/p&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;\*这不是斜体\* \*\*这不是粗体\*\* \# 这不是标题 \[这不是链接\](http://example.com)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;p&gt;*这不是斜体* **这不是粗体** # 这不是标题 [这不是链接](http://example.com)&lt;/p&gt;
&lt;h2&gt;脚注&lt;/h2&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;这里有一个脚注引用[^note1]。

这里是另一个脚注[^note2]。

[^note1]: 这是第一个脚注的内容。

[^note2]: 这是第二个脚注的内容，可以包含多行。

    甚至可以包含代码块和其他格式。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;p&gt;这里有一个脚注引用&lt;a href=&quot;%E8%BF%99%E6%98%AF%E7%AC%AC%E4%B8%80%E4%B8%AA%E8%84%9A%E6%B3%A8%E7%9A%84%E5%86%85%E5%AE%B9%E3%80%82&quot;&gt;^note1&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这里是另一个脚注&lt;a href=&quot;%E8%BF%99%E6%98%AF%E7%AC%AC%E4%BA%8C%E4%B8%AA%E8%84%9A%E6%B3%A8%E7%9A%84%E5%86%85%E5%AE%B9%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%8C%85%E5%90%AB%E5%A4%9A%E8%A1%8C%E3%80%82&quot;&gt;^note2&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;甚至可以包含代码块和其他格式。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这份文档涵盖了 Markdown 的主要语法特性。掌握这些语法后，你就能够创建结构清晰、格式美观的文档了。记住，Markdown 的哲学是简单易读，所以即使不渲染，原始的 Markdown 文本也应该是易于阅读的。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;最后更新：2024年6月19日&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Markdown</category></item></channel></rss>