<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>开发工具链 on 扎塔-Zata</title><link>https://www.zata.cc/categories/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E9%93%BE/</link><description>Recent content in 开发工具链 on 扎塔-Zata</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>Example Person</copyright><lastBuildDate>Thu, 24 Sep 2026 17:09:06 +0800</lastBuildDate><atom:link href="https://www.zata.cc/categories/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E9%93%BE/index.xml" rel="self" type="application/rss+xml"/><item><title>Blender 详解：奥斯卡和 AI Agent 为什么都选了它</title><link>https://www.zata.cc/p/blender-complete-guide/</link><pubDate>Tue, 15 Sep 2026 10:30:00 +0800</pubDate><guid>https://www.zata.cc/p/blender-complete-guide/</guid><description>&lt;img src="https://www.zata.cc/p/blender-complete-guide/images/index/index.svg" alt="Featured image of post Blender 详解：奥斯卡和 AI Agent 为什么都选了它" />&lt;p>2025 年 3 月，洛杉矶，第 97 届奥斯卡颁奖礼。最佳动画长片颁给了拉脱维亚的《Flow》（猫猫的奇幻漂流）——一个小团队、低成本做出来的电影。有一个技术细节容易被忽略：&lt;strong>它全程在 Blender 里制作，渲染用的是 Blender 自带的实时引擎 EEVEE&lt;/strong>，不是渲染农场里跑几十小时的路径追踪，是游戏级的实时渲染。&lt;/p>
&lt;p>差不多同一时期，AI 圈里另一个和 Blender 有关的东西也在涨：blender-mcp，一个把 Blender 变成 AI 助手可操控工具的开源项目，如今 GitHub 星数超过 2.8 万。&lt;/p>
&lt;p>一部奥斯卡获奖电影，和一个 AI Agent 工具链，选了同一款免费软件。这篇文章回答三个问题：Blender 是什么、它为什么好、以及它为什么恰好站在了 AI 的位置上。先说结论：&lt;strong>它的赢法不是&amp;quot;免费打败收费&amp;quot;，而是三件事的叠加——一个不追求租金的基金会、三次把能力拉平的技术解锁，以及一套恰好为 AI Agent 准备好的架构。&lt;/strong>&lt;/p>
&lt;h2 id="一一个应用就是一整条管线">一、一个应用，就是一整条管线
&lt;/h2>&lt;p>先看主流 3D 制作的工作方式：Maya 管建模动画，ZBrush 管雕刻，Substance 管贴图，After Effects 管合成——一条管线由四五个付费软件拼起来。Blender 走的是相反的路，把全部环节装进同一个应用：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>建模与雕刻&lt;/strong>：多边形建模、雕刻模式（4.x 版本大幅重写）、重拓扑；&lt;/li>
&lt;li>&lt;strong>UV 与纹理&lt;/strong>：UV 编辑、程序化纹理节点；&lt;/li>
&lt;li>&lt;strong>绑定与动画&lt;/strong>：骨骼、约束、驱动器、非线性动画编辑器；&lt;/li>
&lt;li>&lt;strong>物理模拟&lt;/strong>：流体、烟雾、布料、毛发、刚体；&lt;/li>
&lt;li>&lt;strong>渲染&lt;/strong>：Cycles（路径追踪，2011 年起）与 EEVEE（实时渲染，2019 年起）双引擎；&lt;/li>
&lt;li>&lt;strong>后期&lt;/strong>：合成器、视频剪辑、运动跟踪；&lt;/li>
&lt;li>&lt;strong>2D 动画&lt;/strong>：Grease Pencil——正是《蜘蛛侠：纵横宇宙》用来处理线稿的工具。&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.zata.cc/p/blender-complete-guide/images/blender-workspace.png"
width="1920"
height="1080"
srcset="https://www.zata.cc/p/blender-complete-guide/images/blender-workspace_hu11690998140454500120.png 480w, https://www.zata.cc/p/blender-complete-guide/images/blender-workspace_hu11088046971622823315.png 1024w"
loading="lazy"
alt="Blender 工作区"
class="gallery-image"
data-flex-grow="177"
data-flex-basis="426px"
>&lt;/p>
&lt;p>&lt;em>▲ Blender 的工作区：3D 视口、大纲视图、属性面板、时间线同屏。顶部标签栏本身就是一份流程清单——建模、雕刻、渲染、合成、脚本、视频剪辑都在同一个应用里。图：Ogat / Wikimedia Commons（GPL）&lt;/em>&lt;/p>
&lt;p>一体化带来的真正价值是环节之间不丢信息。在 Blender 里改了模型的拓扑，UV 和材质自动跟着走，不需要导出导入、不需要对版本、不需要在四个软件之间来回搬运资产。&lt;/p>
&lt;p>两个渲染器值得单独说。&lt;strong>Cycles 是路径追踪器&lt;/strong>，负责&amp;quot;照片级&amp;quot;，2011 年随 2.61 版本加入；&lt;strong>EEVEE 是实时渲染器&lt;/strong>，负责&amp;quot;秒出&amp;quot;，随 2019 年的 2.8 版本登场，2024 年的 4.2 LTS 把它重写为 EEVEE Next，实时渲染第一次有了可用的全局光照——《Flow》全片用的就是它。&lt;/p>
&lt;h2 id="二为什么免费还能做到这个水平">二、为什么免费还能做到这个水平
&lt;/h2>&lt;p>Blender 的起点是破产。1998 年 Ton Roosendaal 创立 NaN 公司开发 Blender，2002 年公司经营失败进入清算，Blender 差点作为资产被卖掉。Roosendaal 先成立非营利的 Blender Foundation，然后在当年 7 月 18 日发起「Free Blender」行动：向社区募资 10 万欧元，一次性付给债权人，换取源码以 GPL 协议开源。&lt;strong>7 周，钱凑齐了。2002 年 9 月 7 日，Blender 正式开源。&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/blender-complete-guide/images/ton-roosendaal-2018.jpg"
width="663"
height="991"
srcset="https://www.zata.cc/p/blender-complete-guide/images/ton-roosendaal-2018_hu13564319366734350329.jpg 480w, https://www.zata.cc/p/blender-complete-guide/images/ton-roosendaal-2018_hu9976080645179497956.jpg 1024w"
loading="lazy"
alt="Ton Roosendaal"
class="gallery-image"
data-flex-grow="66"
data-flex-basis="160px"
>&lt;/p>
&lt;p>&lt;em>▲ Ton Roosendaal，Blender 创始人与 Blender Foundation 主席，摄于 2018 年。2002 年他发起众筹，用 10 万欧元把 Blender 的源码从破产清算里赎了出来。图：Blender / Wikimedia Commons（CC BY 3.0）&lt;/em>&lt;/p>
&lt;p>这个故事决定了此后二十年的一切。因为 GPL 加非营利基金会，Blender 没有股东要回报、没有订阅要续费、也没有&amp;quot;把基础功能拆出来单独收费&amp;quot;的动机。它的钱来自 &lt;strong>Blender Development Fund&lt;/strong>——一种订阅制资助：个人按月捐，企业按档赞助，NVIDIA、AMD、Intel、Meta、Epic 等公司长期在名单上。维基百科口径下，Blender Institute 的规模是 26 名全职员工加 12 名自由职业者。&lt;/p>
&lt;p>这个体量在大厂眼里不值一提，但它二十年只做一件事：把 3D 全流程做进同一个免费应用。对比之下，Maya 的订阅一年要上千美元，而 Blender 从建模到渲染到剪辑一分钱不花。对个人创作者和中小团队，&amp;ldquo;免费&amp;quot;不是省钱——它决定的是项目能不能启动。&lt;/p>
&lt;p>基金模式还有一个副产品：Blender Studio 用&amp;quot;做电影来磨工具&amp;rdquo;。从 2008 年的《Big Buck Bunny》到 2021 年的《Sprite Fright》，每一部开放电影都是新功能的试验场，成片和工程文件全部开源，反哺社区教程和素材库。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/blender-complete-guide/images/sprite-fright-poster.jpg"
width="1920"
height="2716"
srcset="https://www.zata.cc/p/blender-complete-guide/images/sprite-fright-poster_hu3427986655927459397.jpg 480w, https://www.zata.cc/p/blender-complete-guide/images/sprite-fright-poster_hu9053365260347517335.jpg 1024w"
loading="lazy"
alt="Sprite Fright 海报"
class="gallery-image"
data-flex-grow="70"
data-flex-basis="169px"
>&lt;/p>
&lt;p>&lt;em>▲ Blender Studio 的开放电影《Sprite Fright》（2021）海报。Blender 用真实制作推动引擎和工具开发，成片与资产全部开源。图：Blender Studio / Wikimedia Commons（CC BY 4.0）&lt;/em>&lt;/p>
&lt;h2 id="三三次解锁从能用到好用到工业级">三、三次解锁：从能用，到好用，到工业级
&lt;/h2>&lt;p>免费只是入场券。&amp;ldquo;开源软件难用&amp;quot;的标签，是 Blender 自己花十几年撕掉的。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>时间&lt;/th>
&lt;th>版本&lt;/th>
&lt;th>解锁了什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>2019 年 7 月&lt;/td>
&lt;td>2.80&lt;/td>
&lt;td>界面重做、左键选择、行业兼容键位、EEVEE 实时渲染登场&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2021 年 2 月&lt;/td>
&lt;td>2.92&lt;/td>
&lt;td>Geometry Nodes 几何节点，程序化建模免费化&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2025 年 11 月&lt;/td>
&lt;td>5.0&lt;/td>
&lt;td>ACES 色彩管线、HDR、NanoVDB 体积渲染、材质编译提速 4 倍&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>2019 年的 2.8，把&amp;quot;难用&amp;quot;改掉了。&lt;/strong> 在此之前，Blender 的键位和交互是自成一体的体系，新用户的第一道门槛不是建模而是适应界面。2.8 一次性对齐了行业惯例——左键选择、兼容键位、重新设计的界面，并让实时渲染器 EEVEE 首次登场。这是它从&amp;quot;能用&amp;quot;走向&amp;quot;好用&amp;quot;的起点。&lt;/p>
&lt;p>&lt;strong>2021 年的 Geometry Nodes，把 Houdini 的能力免费化了。&lt;/strong> 2.92 引入的几何节点让程序化建模——散布、阵列、非破坏性修改——变成连节点就能完成的事。这是影视和游戏里最贵的一类技能，此前基本是 Houdini 的专属领地。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/blender-complete-guide/images/geometry-nodes-editor.png"
width="1365"
height="768"
srcset="https://www.zata.cc/p/blender-complete-guide/images/geometry-nodes-editor_hu7927589709464110956.png 480w, https://www.zata.cc/p/blender-complete-guide/images/geometry-nodes-editor_hu16923210020319532050.png 1024w"
loading="lazy"
alt="Geometry Nodes 编辑器"
class="gallery-image"
data-flex-grow="177"
data-flex-basis="426px"
>&lt;/p>
&lt;p>&lt;em>▲ Blender 2.92 的 Geometry Nodes 编辑器，正在演示石子散布（Pebble Scattering）。程序化建模从数千美元一年的软件专属能力，变成了免费功能。图：Simon Thommes / Wikimedia Commons（CC BY 4.0）&lt;/em>&lt;/p>
&lt;p>&lt;strong>2025 年的 5.0，把色彩和渲染追上了工业标准。&lt;/strong> 2025 年 11 月 18 日发布的 &lt;a class="link" href="https://www.blender.org/press/blender-5-0-release/" target="_blank" rel="noopener"
>Blender 5.0&lt;/a> 补齐了影视工业最挑剔的两块短板：原生支持 ACES 色彩管线与 HDR 广色域，以及用 NanoVDB 重构的体积渲染——烟雾、火焰的内存占用大幅下降。同期 EEVEE 的材质编译速度最高提升 4 倍。&lt;/p>
&lt;p>工业界的接受度有一条清晰的证据链：《Next Gen》（2018）由 Tangent Animation 全流程用 Blender 制作；《蜘蛛侠：纵横宇宙》（2023）用 Grease Pencil 处理线稿；《RRR》的 VFX 团队 Makuta VFX 是 Blender 用户；Ubisoft 动画工作室宣布自 2020 年起转向 Blender。到&lt;a class="link" href="https://en.wikipedia.org/wiki/Flow_%282024_film%29" target="_blank" rel="noopener"
>《Flow》&lt;/a>拿下奥斯卡最佳动画长片，这条链条闭了环。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/blender-complete-guide/images/lone-monk-cycles-render.jpg"
width="1920"
height="1920"
srcset="https://www.zata.cc/p/blender-complete-guide/images/lone-monk-cycles-render_hu11827668924084357562.jpg 480w, https://www.zata.cc/p/blender-complete-guide/images/lone-monk-cycles-render_hu15930006302787822357.jpg 1024w"
loading="lazy"
alt="Cycles 渲染"
class="gallery-image"
data-flex-grow="100"
data-flex-basis="240px"
>&lt;/p>
&lt;p>&lt;em>▲ 用 Cycles 渲染器渲染的 Lone Monk 演示场景（Blender 2.92 官方演示文件）。路径追踪让&amp;quot;照片级&amp;quot;渲染跑在了消费级显卡上。图：Carlo Bergonzini / Monorender，PantheraLeo135953 渲染 / Wikimedia Commons（CC BY 4.0）&lt;/em>&lt;/p>
&lt;h2 id="四ai-为什么说它用起来方便">四、AI 为什么说它&amp;quot;用起来方便&amp;rdquo;
&lt;/h2>&lt;p>Blender 在 AI 圈的走红，有它自己独立的逻辑，和它在传统 3D 圈的积累不是一回事。把&amp;quot;AI 用 Blender&amp;quot;拆开看，是四个具体机制在起作用。&lt;/p>
&lt;p>&lt;strong>它把点击任务降级成了代码任务。&lt;/strong> Blender 的 Python API（bpy）覆盖了界面上几乎每一个操作。对语言模型来说这是本质区别：&lt;strong>写 Python 是主场能力，点击 GUI 不是&lt;/strong>。用 Blender，Agent 不需要&amp;quot;看&amp;quot;界面、算坐标、点按钮，直接输出脚本——等于绕开了 computer-use 最难的两个环节：像素观察和坐标点击。&lt;/p>
&lt;p>&lt;strong>观察便宜，验证干净。&lt;/strong> &lt;code>blender --background --python script.py&lt;/code> 全程无 GUI，Agent 的观察通道是 stdout 和 &lt;code>bpy.data&lt;/code> 里的结构化场景数据，不是截图。这带来两个好处：一个脚本可以批量完成几十个操作，一次工具调用搞定；验证变成&amp;quot;读数据&amp;quot;——&lt;code>bpy.data.objects['Cube'].location&lt;/code> 是精确、可复现的验证态，不存在&amp;quot;两张截图看起来一样但其实状态变了&amp;quot;的模糊地带。&lt;/p>
&lt;p>&lt;strong>几乎没有反自动化摩擦。&lt;/strong> 免费、开源、无许可证服务器、无加密狗、可容器化。更关键的是从 3.4 版本起 &lt;a class="link" href="https://wiki.blender.org/wiki/Reference/Release_Notes/3.4/Python_API" target="_blank" rel="noopener"
>&lt;code>pip install bpy&lt;/code>&lt;/a>——Blender 本身就是一个 Python 依赖包，Agent 调它和调 numpy 没有区别。Maya 做不到：需要运行实例加授权激活。&lt;/p>
&lt;p>&lt;strong>语料的正反馈。&lt;/strong> 网上有海量的 Blender Python 代码——教程、插件、问答，模型的先验很强，生成脚本成功率高；用的人越多，语料越多。这个循环闭源商业软件很难复制。&lt;/p>
&lt;p>生态层面，&lt;a class="link" href="https://github.com/ahujasid/blender-mcp" target="_blank" rel="noopener"
>blender-mcp&lt;/a> 把 MCP 协议桥接进了 Blender 侧边栏：装好插件，Claude 这类 AI 助手可以通过自然语言直接建模、改材质、跑渲染。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/blender-complete-guide/images/blender-mcp-panel.png"
width="1727"
height="1058"
srcset="https://www.zata.cc/p/blender-complete-guide/images/blender-mcp-panel_hu6031685292497632017.png 480w, https://www.zata.cc/p/blender-complete-guide/images/blender-mcp-panel_hu14914626936924664415.png 1024w"
loading="lazy"
alt="Blender MCP 面板"
class="gallery-image"
data-flex-grow="163"
data-flex-basis="391px"
>&lt;/p>
&lt;p>&lt;em>▲ Blender 侧边栏里的 BlenderMCP 面板（Port 9876）。启动服务后，AI 助手通过 MCP 协议直接驱动 Blender 执行建模、材质和渲染操作。图：ahujasid/blender-mcp（MIT）&lt;/em>&lt;/p>
&lt;p>2026 年这套模式已经铺开——Unity MCP、UnrealClaude、Cocos Creator MCP 都进入可用状态。Agent 的角色也在变：当下的实践是把整条管线交给 Agent 调度（概念生成 → 3D 重建 → 自动绑定 → Blender 导入清理 → 动画驱动 → 导出引擎格式），Blender 当落地执行的那个节点。&lt;/p>
&lt;p>有一个坑值得单独提醒：&lt;code>bpy.ops&lt;/code> 加 &lt;code>mode_set&lt;/code> 是一套上下文状态机——必须先切到正确模式（比如 POSE）才能操作骨骼，否则 operator 直接失败。这是 Agent 写 Blender 脚本最常见的故障。好的一面是报错是文本，能进自我修复循环；换成 GUI 点击，失败往往是静默的，没法自动修。&lt;/p>
&lt;h2 id="五ai3d-管线里的位置">五、AI+3D 管线里的位置
&lt;/h2>&lt;p>&amp;ldquo;AI 用 Blender&amp;quot;要分成两件事：一是生成式 AI 直接产出 3D 资产（Tripo、Meshy 这类模型），Blender 做精修和落地；二是 Agent 操控 Blender（上一节）。前者在 2026 年出现了一个关键变化。&lt;/p>
&lt;p>&lt;strong>拓扑瓶颈被突破。&lt;/strong> 2026 年 9 月 1 日，VAST 发布 3D 基础模型 &lt;a class="link" href="https://kfqgw.beijing.gov.cn/ywdt/kjcgzhgd/kjqy/202609/t20260909_4856670.html" target="_blank" rel="noopener"
>Tripo P2.0&lt;/a>——据北京经开区官网报道，这是全球首个原生生成四边面拓扑网格的 3D 基座模型，最多 25k 四边面或 50k 三角面，边缘流更合理、大平面更干净、支持自动分件。这件事的分量在于：此前 AI 生成的是三角面汤，重拓扑成本和重新建模差不多，所以只能做静态背景道具；&lt;strong>原生四边面意味着生成结果可以直接进游戏引擎和动画绑定管线&lt;/strong>——AI 3D 从玩具变成生产工具的分界线，就在这里。&lt;/p>
&lt;p>国内大厂已经在跑工业化管线。腾讯光子内部打磨 4 年的 &lt;a class="link" href="https://ol.3dmgame.com/news/202606/42992.html" target="_blank" rel="noopener"
>Light AI&lt;/a> 于 2026 年 6 月对外亮相，把&amp;quot;概念探索 → 2D 三视图 → 3D 白模 → 材质 → 骨骼动作迁移&amp;quot;整合成一套平台；腾讯 &lt;a class="link" href="https://news.17173.com/content/03162026/200403001.shtml" target="_blank" rel="noopener"
>VISVISE&lt;/a> 的动作生成能力已用在 90 多款游戏里（含《和平精英》《王者荣耀》），支持文生动画和&amp;quot;3-5 个关键帧生成 10 秒动画&amp;rdquo;。&lt;/p>
&lt;p>成本侧的数据同样直白：摩根士丹利测算生成式 AI 可削减 3A 游戏开发成本约 44%（投行测算，当量级参考）；国内游戏研发端的 AI 普及率已达 86.36%（音数协口径）。&lt;/p>
&lt;p>Blender 在这条管线里的位置由此清晰：免费让它没有授权摩擦，headless 让它能进服务器批处理，bpy 让它能被脚本和 Agent 驱动。&lt;strong>它就是那个&amp;quot;AI 生成 → 自动清理 → 导出引擎格式&amp;quot;的落地中枢&lt;/strong>——决定这个位置的是它的架构，而不是某个新功能。&lt;/p>
&lt;h2 id="六它不强的部分">六、它不强的部分
&lt;/h2>&lt;p>把话说全，Blender 的短板同样明确：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>大规模角色动画与绑定&lt;/strong>，Maya 仍是行业标准，Blender 在复杂角色管线上的工具链还不够顺；&lt;/li>
&lt;li>&lt;strong>高端雕刻&lt;/strong>，ZBrush 仍是天花板，Blender 的雕刻模式进步很大但没有到取代的程度；&lt;/li>
&lt;li>&lt;strong>CAD 精度&lt;/strong>（NURBS、参数化实体建模）基本没有，这块要交给 Fusion、Rhino、Plasticity；&lt;/li>
&lt;li>&lt;strong>大而全本身也是代价&lt;/strong>——每一项都不是同类最强，专业环节的团队依然会配置专业软件；&lt;/li>
&lt;li>&lt;strong>硬件门槛在提高&lt;/strong>：5.0 起不再支持老 GPU（GeForce 900 系之前、GCN 4 之前、Kaby Lake 之前）。&lt;/li>
&lt;/ul>
&lt;p>还有一个容易被 AI 叙事掩盖的风险：制作端省钱不等于市场端接受。据 &lt;a class="link" href="https://www.jiemian.com/article/14579362.html" target="_blank" rel="noopener"
>Game Oracle 统计&lt;/a>，明确声明使用 AI 的游戏发售首月评论数平均减少 52.6%。AI 管线降低的是生产成本，不是市场风险。&lt;/p>
&lt;h2 id="七我的判断">七、我的判断
&lt;/h2>&lt;p>Blender 火过两次，逻辑是同一件事：&lt;strong>它把 3D 生产的成本结构改了。&lt;/strong> 第一次对内——免费和一体化解决了&amp;quot;能不能做&amp;quot;；第二次对外——它的架构（bpy 全覆盖、headless、pip 可安装、无授权摩擦）恰好是 AI Agent 时代最需要的形态。&lt;/p>
&lt;p>这个判断成立的前提是：Blender 保持基金会治理和 GPL。只要这个前提在，它在&amp;quot;AI 3D 落地点&amp;quot;这个位置上的优势就是结构性的，不是某个模型升级能带走的。真正的变量在 AI 生成质量那一侧——如果生成的模型哪天真的开箱即用，精修环节会缩水；但&amp;quot;谁来批量调度、自动清理、导出管线&amp;quot;这个问题，仍然需要一个 Blender 这样的中枢。&lt;/p>
&lt;h2 id="总结">总结
&lt;/h2>&lt;ul>
&lt;li>Blender 用基金会 + GPL 的模式，把建模到渲染到剪辑的全流程做进了同一个免费应用，这是它二十年最大的结构性优势；&lt;/li>
&lt;li>2019 的 2.8、2021 的 Geometry Nodes、2025 的 5.0 三次解锁，让它从&amp;quot;能用&amp;quot;走到&amp;quot;工业级&amp;quot;，《Flow》拿下奥斯卡是这条链条的闭环时刻；&lt;/li>
&lt;li>对 AI Agent 而言，它的核心价值是 bpy 全覆盖、headless 执行、&lt;code>pip install bpy&lt;/code> 和可读的验证态——把&amp;quot;点击任务&amp;quot;变成了&amp;quot;代码任务&amp;quot;，而后者正是大模型的主场；&lt;/li>
&lt;li>想上手：从 &lt;a class="link" href="https://www.blender.org/download/" target="_blank" rel="noopener"
>Blender 官网&lt;/a> 下载 4.2 LTS 或 5.0，跟着一个甜甜圈教程走完全流程，就能体会&amp;quot;一个应用一条管线&amp;quot;的含义；想玩 AI 驱动，装 blender-mcp 加 Claude，从让它改一个材质开始。&lt;/li>
&lt;/ul></description></item><item><title>Herdr 详解：给 AI Agent 用的终端运行时</title><link>https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/</link><pubDate>Tue, 15 Sep 2026 10:30:00 +0800</pubDate><guid>https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/</guid><description>&lt;img src="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/index.svg" alt="Featured image of post Herdr 详解：给 AI Agent 用的终端运行时" />&lt;p>如果你同时开着三个 Claude Code、两个 Codex，再挂一台远程机器的构建任务，你大概经历过这几件事：某个 Agent 早就停下来等你批准命令，你却在另一个标签页里刷了两分钟；SSH 断了一下，跑了一半的活没了；想知道&amp;quot;现在到底哪个窗口在干活&amp;quot;，只能一个个切过去看。&lt;/p>
&lt;p>Herdr 就是冲这些事来的。它的官方定位是&amp;quot;The runtime your coding agents live on&amp;quot;——&lt;strong>不是替代 Agent，而是给 Agent 提供它们运行于其中的那层终端基础设施&lt;/strong>。用一句话概括：&lt;strong>Herdr 是把 tmux 按 Agent 的工作方式重新设计了一遍。&lt;/strong>&lt;/p>
&lt;p>这篇文章基于我本机装的 &lt;strong>herdr 0.9.0&lt;/strong>（stable 通道，protocol 22），把它的用法、概念模型和配置讲清楚，命令和输出都是实际跑出来的。（9 月 15 日补充：右键转发、复制模式、会话恢复的全景、插件从装到写、手机用法、中文输入法开关。同日二次补充：9.6.7 装了一个真插件的完整实录。）&lt;/p>
&lt;hr>
&lt;h2 id="一它到底是什么和-tmux-差在哪">一、它到底是什么，和 tmux 差在哪
&lt;/h2>&lt;p>Herdr 是 tmux 一脉的 &lt;strong>后台 server + 前端 client&lt;/strong> 架构：client 只是一个&amp;quot;显示器 + 键盘&amp;quot;，真正的 pty、进程、布局都在 server 里。但它在几个地方做了明确的 Agent 化改造：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>维度&lt;/th>
&lt;th>tmux&lt;/th>
&lt;th>Herdr&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>基本目的&lt;/td>
&lt;td>保住终端会话&lt;/td>
&lt;td>托管 AI Agent 的工作区&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>组织单位&lt;/td>
&lt;td>session → window → pane&lt;/td>
&lt;td>&lt;strong>workspace → tab → pane&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>状态感知&lt;/td>
&lt;td>无，pane 里有什么全靠肉眼看&lt;/td>
&lt;td>每个 pane 有语义状态：&lt;code>working&lt;/code> / &lt;code>blocked&lt;/code> / &lt;code>done&lt;/code> / &lt;code>idle&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>总览&lt;/td>
&lt;td>逐个 window 切&lt;/td>
&lt;td>侧边栏跨所有 workspace 汇总 Agent 列表&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>控制接口&lt;/td>
&lt;td>&lt;code>tmux&lt;/code> 命令&lt;/td>
&lt;td>CLI + 本地 socket JSON API，&lt;strong>给 Agent 用的&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>多机&lt;/td>
&lt;td>要自己拼 ssh&lt;/td>
&lt;td>&lt;code>--remote&lt;/code> / 已保存机器，本地与远程同一视图&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>鼠标&lt;/td>
&lt;td>基本可用&lt;/td>
&lt;td>一等公民，点击/拖拽/右键菜单全能&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>原话是&amp;quot;不包装、不替换 Agent，只接管它们的终端&amp;quot;。所以 Claude Code、Codex、Cursor、OpenCode、Grok 这些还是原来那些，只是被放进了 Herdr 管理的工作区里——Herdr 能识别它们、给它们状态打标、允许它们互相说话。&lt;/p>
&lt;p>技术上是 &lt;strong>Rust 单二进制，没有 Electron&lt;/strong>，终端后端用 libghostty（Ghostty 的终端库），Windows 走 ConPTY。装完就是一个约等于 tmux 的东西，但多了 Agent 那一层。&lt;/p>
&lt;p>想先看动起来是什么样，官方 README 里有一段二十来秒的演示（&lt;a class="link" href="https://github.com/user-attachments/assets/043ec09f-4bdd-41d5-aee0-8fda6b83e267" target="_blank" rel="noopener"
>视频直链&lt;/a>）：多个 Agent 在分屏里并行跑，侧边栏实时显示谁 blocked、谁 idle、谁 done，鼠标点着切窗格。下文用到的几张截图就是从这段演示和官方文档里取的。&lt;/p>
&lt;hr>
&lt;h2 id="二安装与第一次启动">二、安装与第一次启动
&lt;/h2>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 首选&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew install herdr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或官方脚本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSL https://herdr.dev/install.sh &lt;span class="p">|&lt;/span> sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或 mise&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mise use -g herdr
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Windows 现在也已 GA：一行 PowerShell 装上（&lt;code>powershell -ExecutionPolicy Bypass -c &amp;quot;irm https://herdr.dev/install.ps1 | iex&amp;quot;&lt;/code>），企业安全软件拦无文件脚本时改用 &lt;code>install.cmd&lt;/code> 版本，Windows ARM64 跑 x86_64 构建（模拟）。官方维护了一份&lt;a class="link" href="https://herdr.dev/zh-cn/docs/windows-beta/" target="_blank" rel="noopener"
>已知限制清单&lt;/a>，不长，装前值得看一眼。补全脚本支持 bash / zsh / fish / elvish / powershell：&lt;code>herdr completion zsh&lt;/code>。&lt;/p>
&lt;p>升级要注意安装来源：直接安装的用 &lt;code>herdr update&lt;/code>；Homebrew、mise、Nix 装的用各自的包管理器（&lt;code>herdr update&lt;/code> 对它们不生效）。直接安装还能切更新通道——&lt;code>herdr channel set preview&lt;/code> 跟进 master 的预发布版（可能回归），&lt;code>herdr channel set stable&lt;/code> 切回，包管理器安装不支持预览通道。&lt;/p>
&lt;p>装完在任意项目目录里敲：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/code/ZataTree
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它&lt;strong>启动或连接默认后台会话&lt;/strong>，不需要你去管 socket 在哪。第一次会走一个 onboarding（也可以 &lt;code>onboarding = false&lt;/code> 跳过）。如果一个 workspace 都没有，它会自动开一个。&lt;/p>
&lt;p>我本机的实际状态：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr --version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">herdr 0.9.0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="gp">$&lt;/span> herdr status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">client:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> version: 0.9.0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> channel: stable
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> protocol: 22
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> endpoint_protocol_generation: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="go">server:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> status: running
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> version: 0.9.0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> endpoint_compatible: yes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> private_protocol: 22
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> private_protocol_compatible: yes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> socket: /Users/zata/.config/herdr/herdr.sock
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="go">update:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> restart_needed: no
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> server_binary_stale: no
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意这一行：&lt;strong>server 是 running，而 client 是我每次敲 &lt;code>herdr&lt;/code> 才起的&lt;/strong>。这是理解 Herdr 的第一把钥匙——server 活着，你的活就活着。&lt;/p>
&lt;hr>
&lt;h2 id="三概念模型workspace--tab--pane--agent">三、概念模型：workspace / tab / pane / agent
&lt;/h2>&lt;p>比 tmux 多了一层&amp;quot;工作区&amp;quot;，而且这层是&lt;strong>按项目&lt;/strong>切而不是按窗口切：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>workspace&lt;/strong> — 项目级容器，对应一个仓库/一个项目。侧边栏按 workspace 分组。&lt;/li>
&lt;li>&lt;strong>tab&lt;/strong> — workspace 内的标签页。&lt;/li>
&lt;li>&lt;strong>pane&lt;/strong> — tab 内可分割的终端格子，每个格子通常跑一个 Agent。&lt;/li>
&lt;li>&lt;strong>agent&lt;/strong> — Herdr 从 pane 里&lt;strong>检测出来的&lt;/strong>编程 Agent 进程，是关注对象而不是容器。&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-architecture.svg"
loading="lazy"
alt="Herdr 架构一张图：client 只是显示器与键盘，server 后台常驻并持有所有 pane 与 Agent 状态；CLI 与 socket API 组成控制平面，Agent 通过 hook 上报状态"
>&lt;/p>
&lt;p>&lt;code>prefix+w&lt;/code> 或侧边栏的 &lt;code>switch&lt;/code> 面板把这三层同时摊开，是这个模型最直观的一屏——左侧边栏分 spaces / tabs / agents 三段，Agent 名字下面是它的状态：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-switch-panel.jpg"
width="942"
height="1180"
srcset="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-switch-panel_hu15828666175290999348.jpg 480w, https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-switch-panel_hu4299887262138499271.jpg 1024w"
loading="lazy"
alt="Herdr 的 switch 面板：spaces 段列出工作区及其 git 分支，agents 段列出各 Agent 的状态（来源：herdr 官方文档）"
class="gallery-image"
data-flex-grow="79"
data-flex-basis="191px"
>&lt;/p>
&lt;p>我本机现在的样子（&lt;code>--json&lt;/code> 输出是我删掉无关字段后的样子）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr workspace list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">{&amp;#34;result&amp;#34;:{&amp;#34;workspaces&amp;#34;:[
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> {&amp;#34;workspace_id&amp;#34;:&amp;#34;w4&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;freshai&amp;#34;,&amp;#34;number&amp;#34;:1,&amp;#34;pane_count&amp;#34;:2,&amp;#34;tab_count&amp;#34;:1},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> {&amp;#34;workspace_id&amp;#34;:&amp;#34;w6&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;zata_code_template&amp;#34;,&amp;#34;number&amp;#34;:2,&amp;#34;pane_count&amp;#34;:1,&amp;#34;tab_count&amp;#34;:1}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">],&amp;#34;type&amp;#34;:&amp;#34;workspace_list&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="gp">$&lt;/span> herdr pane list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">{&amp;#34;result&amp;#34;:{&amp;#34;panes&amp;#34;:[
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> {&amp;#34;pane_id&amp;#34;:&amp;#34;w4:p1&amp;#34;,&amp;#34;workspace_id&amp;#34;:&amp;#34;w4&amp;#34;,&amp;#34;cwd&amp;#34;:&amp;#34;/Users/zata/code/freshai&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> &amp;#34;terminal_title&amp;#34;:&amp;#34;⠸ Complete external skill change review task in worktree&amp;#34;,&amp;#34;agent_status&amp;#34;:&amp;#34;unknown&amp;#34;},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> {&amp;#34;pane_id&amp;#34;:&amp;#34;w4:p3&amp;#34;,&amp;#34;workspace_id&amp;#34;:&amp;#34;w4&amp;#34;,&amp;#34;cwd&amp;#34;:&amp;#34;/Users/zata/code/ZataTree&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> &amp;#34;terminal_title&amp;#34;:&amp;#34;⠧ 询问 herdr 相关信息&amp;#34;,&amp;#34;agent_status&amp;#34;:&amp;#34;unknown&amp;#34;},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> {&amp;#34;pane_id&amp;#34;:&amp;#34;w6:p1&amp;#34;,&amp;#34;workspace_id&amp;#34;:&amp;#34;w6&amp;#34;,&amp;#34;cwd&amp;#34;:&amp;#34;/Users/zata/code/zata_code_template&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> &amp;#34;terminal_title&amp;#34;:&amp;#34;✳ Greeting and session start&amp;#34;,&amp;#34;agent_status&amp;#34;:&amp;#34;unknown&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">],&amp;#34;type&amp;#34;:&amp;#34;pane_list&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>pane ID 的格式是 &lt;code>w4:p1&lt;/code>&lt;/strong>——workspace 短 ID 加 pane 短 ID，稳定且可引用。所有跨 pane 的命令都用它寻址。Agent 列表现在是空的，因为我当前这几个 pane 里跑的不是被识别的 Agent（原因见第八节）。&lt;/p>
&lt;blockquote>
&lt;p>一个实践建议：&lt;strong>每个活跃项目给一个独立 workspace&lt;/strong>。侧边栏的 Agent 汇总只有在 workspace 边界清晰时才读得懂，否则所有项目的 Agent 混成一堆，&amp;ldquo;哪个项目在等我&amp;quot;又变成一个需要推理的问题。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="四核心能力一分离而不停止">四、核心能力一：分离而不停止
&lt;/h2>&lt;p>这是第一个真正让你离不开它的功能。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">#&lt;/span> 分离（客户端退出，server 和 Agent 继续跑）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">prefix + q
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="gp">#&lt;/span> 回来
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">herdr
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>也可以直接关掉终端窗口——效果一样。想真正结束会话和里面的 pane：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr server stop
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Herdr 在这一点上比 tmux 多走了一步：&lt;strong>重启恢复&lt;/strong>。server 重启后，它会恢复保存的布局，并且对&lt;strong>装了官方集成、上报过原生 session 引用的 Agent&lt;/strong>，尝试恢复到原来的对话（&lt;code>[session] resume_agents_on_restore = true&lt;/code>，默认开）。&lt;/p>
&lt;p>但这里有个必须说清楚的边界：&lt;strong>恢复的是 Agent 的原生会话，不是原始进程&lt;/strong>。你的 shell 里跑到一半的 &lt;code>npm run build&lt;/code> 不会自己接着跑，别指望这个。Herdr 官方在文档里也写得很直白。&lt;/p>
&lt;p>&lt;strong>把&amp;quot;恢复&amp;quot;拆成四个问题，答案才清楚。&lt;/strong> 进程还在吗、布局回来吗、屏幕内容回来吗、对话能续上吗——官方文档就是这么拆的，我把它那张表放在这里：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>情形&lt;/th>
&lt;th>进程继续跑&lt;/th>
&lt;th>布局&lt;/th>
&lt;th>最近屏幕内容&lt;/th>
&lt;th>Agent 对话&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>分离再附着&lt;/td>
&lt;td>是&lt;/td>
&lt;td>是&lt;/td>
&lt;td>是（活的终端）&lt;/td>
&lt;td>是（进程从没停）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>server 重启&lt;/td>
&lt;td>否&lt;/td>
&lt;td>是&lt;/td>
&lt;td>仅在开了屏幕历史回放时&lt;/td>
&lt;td>仅在装了原生会话恢复时&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>herdr update&lt;/code>（不带 &lt;code>--handoff&lt;/code>）&lt;/td>
&lt;td>兼容的 server 继续跑&lt;/td>
&lt;td>重启后回来&lt;/td>
&lt;td>仅在开了屏幕历史回放时&lt;/td>
&lt;td>仅在装了原生会话恢复时&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>herdr update --handoff&lt;/code>&lt;/td>
&lt;td>尽力保留&lt;/td>
&lt;td>是&lt;/td>
&lt;td>是（交接成功时来自活终端）&lt;/td>
&lt;td>是（同上）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>两个默认不启用、但值得知道的能力：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>屏幕历史回放&lt;/strong>：&lt;code>[experimental] pane_history = true&lt;/code>，让 server 全量重启后也能把每个 pane 最近的屏幕内容铺回去。默认关的原因很正当——pane 输出里可能有 token、密钥和敏感提示词，开了就要&lt;strong>像对待终端历史一样对待它&lt;/strong>（内容落在会话目录的 &lt;code>session-history.json&lt;/code>）。&lt;/li>
&lt;li>&lt;strong>live handoff（热交接）&lt;/strong>：升级时把活着的 pane &lt;strong>连同进程一起&lt;/strong>从旧 server 交到新 server，跳过&amp;quot;停掉再重建&amp;rdquo;。命令是 &lt;code>herdr update --handoff&lt;/code>，&lt;code>herdr --remote workbox --handoff&lt;/code> 也能用它换掉远端的 server。实验性、需要显式开，而且包管理器安装用不了（升级走 brew/mise/nix）。边界也要说清楚：&lt;strong>交接那一刻，在途的 CLI 请求、&lt;code>agent wait&lt;/code>、事件订阅都会断&lt;/strong>，客户端要自己重连重试。&lt;/li>
&lt;/ul>
&lt;p>顺带一提命名会话，多环境隔离时很有用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr --session work &lt;span class="c1"># 用或建一个命名会话&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr session list &lt;span class="c1"># 列出&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr session attach work &lt;span class="c1"># 附着&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr session stop work &lt;span class="c1"># 停掉&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>socket 落在 &lt;code>~/.config/herdr/sessions/&amp;lt;name&amp;gt;/herdr.sock&lt;/code>，默认会话则是 &lt;code>~/.config/herdr/herdr.sock&lt;/code>。&lt;/p>
&lt;hr>
&lt;h2 id="五核心能力二状态可见working--blocked--done--idle">五、核心能力二：状态可见——&lt;code>working&lt;/code> / &lt;code>blocked&lt;/code> / &lt;code>done&lt;/code> / &lt;code>idle&lt;/code>
&lt;/h2>&lt;p>Herdr 最有价值的一点是&lt;strong>把 Agent 的状态变成了有语义的东西&lt;/strong>，而不是让你去读屏幕。&lt;/p>
&lt;p>每个 pane 有一个状态，四种取值：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>状态&lt;/th>
&lt;th>含义&lt;/th>
&lt;th>你该做什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>working&lt;/code>&lt;/td>
&lt;td>Agent 正在干活&lt;/td>
&lt;td>别打扰&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>blocked&lt;/code>&lt;/td>
&lt;td>&lt;strong>卡住了，等你输入/批准&lt;/strong>&lt;/td>
&lt;td>切过去处理&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>done&lt;/code>&lt;/td>
&lt;td>这一轮任务完成&lt;/td>
&lt;td>验收&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>idle&lt;/code>&lt;/td>
&lt;td>空着，等新指令&lt;/td>
&lt;td>可以派活&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>unknown&lt;/code>&lt;/td>
&lt;td>没有检测到/上报状态&lt;/td>
&lt;td>需要装集成，见第八节&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>侧边栏把&lt;strong>所有 workspace&lt;/strong> 的 Agent 汇总成一个列表，默认按 workspace 分组（&lt;code>ui.agent_panel_sort = &amp;quot;spaces&amp;quot;&lt;/code>），也可以切成 &lt;code>priority&lt;/code>——按&amp;quot;谁在等我&amp;quot;排队的注意力队列。我认为 &lt;code>priority&lt;/code> 在 Agent 多了之后更实用：它本质是一个待办队列。&lt;/p>
&lt;p>状态还会&lt;strong>沿层级向上汇总&lt;/strong>：一个 blocked 的 Agent 会让它所在的 pane、tab、workspace 都显示成 blocked；working 会让 workspace 显示活跃；done 会一直挂着，&lt;strong>直到你真的看过它&lt;/strong>。所以侧边栏能当&amp;quot;哪个项目在等我&amp;quot;的单一入口——不是靠人记住，而是靠这条汇总规则替你维护。&lt;/p>
&lt;p>状态指示符默认是彩色圆点，可以换成形状区分：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">status_indicators&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;symbols&amp;#34;&lt;/span> &lt;span class="c"># 用不同字形区分 blocked/working/done/idle/unknown&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下面这张是同一个 Agent 在 &lt;code>working&lt;/code> 状态下的实拍——标题栏的 &lt;code>tab 1/1&lt;/code>、顶部的工作区名 &lt;code>nav-keybinds&lt;/code>、底部的 &lt;code>Working...&lt;/code> 都是 Herdr 自己画的：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-agent-working.jpg"
width="942"
height="1300"
srcset="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-agent-working_hu2874628268532941709.jpg 480w, https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-agent-working_hu17423323121427161237.jpg 1024w"
loading="lazy"
alt="Herdr 实拍：Agent 处于 working 状态，标题栏显示工作区与 tab 信息（来源：herdr 官方文档）"
class="gallery-image"
data-flex-grow="72"
data-flex-basis="173px"
>&lt;/p>
&lt;p>再加上通知：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toast&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">delivery&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;herdr&amp;#34;&lt;/span> &lt;span class="c"># herdr(应用内) | terminal(外层终端，SSH 场景好用) | system(系统通知) | off&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">delay_seconds&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toast&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">herdr&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">position&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;bottom-right&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sound&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">enabled&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># done_path = &amp;#34;sounds/done.mp3&amp;#34; # 任务完成音&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># request_path = &amp;#34;sounds/request.mp3&amp;#34; # 需要你介入的音&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>阻塞和完成用不同的声音&lt;/strong>，这是我配完之后最满意的一条改动——不用看屏幕，光靠耳朵就知道是&amp;quot;干完了&amp;quot;还是&amp;quot;卡住了要我批准&amp;quot;。macOS 上 &lt;code>delivery = &amp;quot;system&amp;quot;&lt;/code> 会先试 &lt;code>terminal-notifier&lt;/code>，没有则退回 &lt;code>osascript&lt;/code>（注意会显示成 Script Editor，且无法激活终端）；要 &lt;code>brew install terminal-notifier&lt;/code> 才有完整体验。&lt;/p>
&lt;p>Herdr 会&lt;strong>抑制当前活跃 tab 的弹窗&lt;/strong>——你正盯着那个 pane 看，它不会弹一个框告诉你它 blocked 了。这个细节做对了。&lt;/p>
&lt;hr>
&lt;h2 id="六用法鼠标是默认键盘是可选">六、用法：鼠标是默认，键盘是可选
&lt;/h2>&lt;p>Herdr 的入门门槛比 tmux 低，核心原因是&lt;strong>它把鼠标做成了默认路径&lt;/strong>，前缀键只是另一条路。&lt;/p>
&lt;p>&lt;strong>鼠标：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>点击 pane / tab / workspace / agent 行 → 聚焦&lt;/li>
&lt;li>拖动分割边框 → 调整大小&lt;/li>
&lt;li>右键 → 上下文菜单（包含分割窗格、新建标签页）&lt;/li>
&lt;li>拖选文本即复制，双击一个词即复制该词——&lt;strong>不需要 Ctrl+C&lt;/strong>&lt;/li>
&lt;li>Ctrl+点击打开链接（OSC 8 超链接和可见的 &lt;code>http(s)://&lt;/code> URL）。macOS 下鼠标捕获开着时要 Ctrl+点，&lt;code>Cmd+点&lt;/code>需要终端原生绕过路径；也可以用 &lt;code>ui.mouse_capture = false&lt;/code> 整体让出鼠标&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>右键的归属是可以调的。&lt;/strong> 默认右键是 Herdr 的窗格菜单，但 pane 里跑着 Claude Code、Neovim 这类开了鼠标上报的程序时，右键本来是它们的功能——Herdr 曾经统统一口吃掉，后来专门修过（issue #25）。现在&lt;strong>这个归属是按 pane 单独选的&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>右键窗格 → 菜单里的 &lt;strong>&amp;ldquo;Send right-clicks to pane&amp;rdquo;&lt;/strong> 一键切过去；切过去之后再开菜单，这一项会变成 &amp;ldquo;Use Herdr right-click menu&amp;rdquo;，点它切回。&lt;/li>
&lt;li>CLI 等价物：&lt;code>herdr pane input --current --right-click pane|herdr&lt;/code>（要在目标 pane 里执行；从别处操作就换成 &lt;code>--pane w1:p2&lt;/code>）。&lt;code>herdr pane split&lt;/code> 也接受 &lt;code>--right-click pane&lt;/code>，分割时一次定好。&lt;/li>
&lt;li>socket 层是 &lt;code>pane.input.set&lt;/code>，参数 &lt;code>right_click: &amp;quot;pane&amp;quot;&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>注意它和 &lt;code>ui.mouse_capture = false&lt;/code> 不是一回事：那个是把&lt;strong>整个鼠标&lt;/strong>都让给 pane 里的应用，这个只让右键，其余交互仍归 Herdr。&lt;/p>
&lt;p>不想逐个 pane 切的话，还有全局的&lt;strong>修饰键混合模式&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">right_click_passthrough_modifier&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;ctrl&amp;#34;&lt;/span> &lt;span class="c"># ctrl+右键转发给应用，普通右键仍开 Herdr 菜单&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>接受 ctrl / alt / cmd / meta / hyper，可以组合（不能带 shift）。&lt;/p>
&lt;p>最后是必须提前知道的坑：&lt;strong>右键转发出去之后，Herdr 菜单要靠右键窗格的边框唤回&lt;/strong>。不知道这一条的人，会在 pane 里反复右键、以为菜单坏了。&lt;/p>
&lt;p>&lt;strong>键盘：&lt;/strong> 前缀键默认 &lt;code>ctrl+b&lt;/code>，和 tmux 一致，所以从 tmux 迁过来基本零成本。按下 &lt;code>ctrl+b&lt;/code> 进入前缀模式，再按动作键。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>动作&lt;/th>
&lt;th>默认绑定&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>向右分割窗格&lt;/td>
&lt;td>&lt;code>prefix+v&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>向下分割窗格&lt;/td>
&lt;td>&lt;code>prefix+minus&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>新建标签页&lt;/td>
&lt;td>&lt;code>prefix+c&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>下一个 / 上一个标签页&lt;/td>
&lt;td>&lt;code>prefix+n&lt;/code> / &lt;code>prefix+p&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>切到第 N 个标签页&lt;/td>
&lt;td>&lt;code>prefix+1..9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>重命名标签页&lt;/td>
&lt;td>&lt;code>prefix+shift+t&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>工作区选择器&lt;/td>
&lt;td>&lt;code>prefix+w&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>新建工作区&lt;/td>
&lt;td>&lt;code>prefix+shift+n&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>重命名工作区&lt;/td>
&lt;td>&lt;code>prefix+shift+w&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>方向聚焦（vim 式）&lt;/td>
&lt;td>&lt;code>prefix+h/j/k/l&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>窗格循环切换&lt;/td>
&lt;td>&lt;code>prefix+tab&lt;/code> / &lt;code>prefix+shift+tab&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>关闭窗格&lt;/td>
&lt;td>&lt;code>prefix+x&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>缩放窗格&lt;/td>
&lt;td>&lt;code>prefix+z&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>重命名窗格&lt;/td>
&lt;td>&lt;code>prefix+shift+p&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>侧边栏开关&lt;/td>
&lt;td>&lt;code>prefix+b&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>分离客户端&lt;/strong>&lt;/td>
&lt;td>&lt;code>prefix+q&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>进入复制模式（键盘复制）&lt;/td>
&lt;td>&lt;code>prefix+[&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>查看当前全部有效绑定&lt;/strong>&lt;/td>
&lt;td>&lt;code>prefix+?&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>复制模式。&lt;/strong> 鼠标拖选即复制之外，&lt;code>prefix+[&lt;/code> 提供一条完整的键盘路径——没有鼠标的 SSH 场景里，这是唯一的复制方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>移动&lt;/strong>：&lt;code>h/j/k/l&lt;/code>；tmux 风格的单词 &lt;code>w/b/e&lt;/code>、大字词 &lt;code>W/B/E&lt;/code>（按空白分词）；&lt;code>{&lt;/code>/&lt;code>}&lt;/code> 段落；&lt;code>PageUp&lt;/code>/&lt;code>PageDown&lt;/code> 翻页、&lt;code>ctrl+u&lt;/code>/&lt;code>ctrl+d&lt;/code> 半页。&lt;/li>
&lt;li>&lt;strong>搜索&lt;/strong>：&lt;code>/&lt;/code> 向前、&lt;code>?&lt;/code> 向后，&lt;code>n&lt;/code>/&lt;code>N&lt;/code> 顺逆序重复。&lt;strong>查询里没有大写字母就不区分大小写&lt;/strong>，想精确匹配就带上一个大写。&lt;/li>
&lt;li>&lt;strong>选择与复制&lt;/strong>：&lt;code>v&lt;/code> 或空格开始选择（&lt;code>shift+v&lt;/code> 是整行选择），&lt;code>y&lt;/code> 或回车复制，&lt;code>q&lt;/code>/&lt;code>Esc&lt;/code> 放弃退出。&lt;code>Esc&lt;/code> 会先清掉当前的选择或搜索，再按一次才退出。&lt;/li>
&lt;li>&lt;strong>复制模式不暂停进程&lt;/strong>：输出照常产生、视图跟在底部；你滚进历史时画面钉住不动。&lt;/li>
&lt;/ul>
&lt;p>两个容易错过的点：&lt;strong>默认前缀就是 &lt;code>ctrl+b&lt;/code>，所以在复制模式里 &lt;code>ctrl+b&lt;/code> 仍然是&amp;quot;前缀&amp;quot;、不是翻页&lt;/strong>，想用它翻页得先换前缀；另外 &lt;code>prefix+e&lt;/code> 能把回滚内容直接在 &lt;code>$EDITOR&lt;/code> 里打开，&lt;strong>软折行会还原成逻辑行&lt;/strong>——审一段长日志，比在终端里滚舒服得多。&lt;/p>
&lt;p>&lt;strong>给终端起个看得懂的名字。&lt;/strong> 默认窗格标题显示的是运行中程序动态设置的 terminal title——比如 Claude Code 会把它改成当前正在做的事，还带转圈动画，一直变，根本认不出哪个窗格是哪个。三级对象都能改名：&lt;code>prefix+shift+p&lt;/code>（窗格）、&lt;code>prefix+shift+t&lt;/code>（标签页）、&lt;code>prefix+shift+w&lt;/code>（工作区），按下后弹输入框，起个名，侧边栏和窗格边框从此显示固定名字。CLI 也能改，适合脚本或远程操作：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr pane rename w4:p3 &lt;span class="s2">&amp;#34;zata blog&amp;#34;&lt;/span> &lt;span class="c1"># 给指定 pane 起固定名字&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane rename w4:p3 --clear &lt;span class="c1"># 清掉，退回动态标题&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>再配一条：窗格&lt;strong>没有手动命名&lt;/strong>时，在分割边框上显示检测到的 Agent 标签：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">show_agent_labels_on_pane_borders&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两个容易忽略但很实用的点：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>prefix+?&lt;/code> 是帮助面板，显示当前生效的绑定。&lt;/strong> 改过配置之后不用去翻文档。&lt;/li>
&lt;li>&lt;strong>字面 &lt;code>ctrl+b&lt;/code> 用 &lt;code>prefix+ctrl+b&lt;/code> 发送。&lt;/strong> 也就是&amp;quot;再按一次&amp;quot;。终端里有个程序真的需要收到 &lt;code>ctrl+b&lt;/code> 时用这个。&lt;/li>
&lt;/ul>
&lt;p>配置里的绑定语法是显式的：&lt;code>prefix+n&lt;/code> 表示要先按前缀，&lt;code>ctrl+alt+n&lt;/code> 才是终端模式下的直接快捷键。可以直接绑普通按键（比如 &lt;code>n&lt;/code>），但&lt;strong>有风险&lt;/strong>——它会拦截你所有输入，所以除非你确实想要一个无前缀的模式，否则都用 &lt;code>prefix+&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">prefix&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;ctrl+b&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">new_tab&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;prefix+c&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">split_horizontal&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;prefix+minus&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 一个动作可以有多个绑定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">next_tab&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;prefix+n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;ctrl+alt+]&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 不用进 resize 模式，直接调大小&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">resize_pane_right&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;ctrl+shift+alt+right&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>改配置改乱了的救援命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr config reset-keys &lt;span class="c1"># 备份 config.toml，移除自定义 [keys]，回到内置 v2 默认&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr server reload-config &lt;span class="c1"># 热加载（大部分 UI 设置不用重启 pane）&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="七自定义命令与弹出面板">七、自定义命令与弹出面板
&lt;/h2>&lt;p>Herdr 允许把任意命令绑到按键上，这是把日常工具接进工作流的入口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">command&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;prefix+alt+g&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;popup&amp;#34;&lt;/span> &lt;span class="c"># popup | pane | shell | plugin_action&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;lazygit&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">description&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;run lazygit&amp;#34;&lt;/span> &lt;span class="c"># 会显示在 prefix+? 帮助面板里&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">width&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;80%&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">height&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;80%&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>四种类型的语义差别值得记一下：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;code>type&lt;/code>&lt;/th>
&lt;th>行为&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>popup&lt;/code>&lt;/td>
&lt;td>会话级模态弹窗，不改变 tab 布局，会拿到所有输入（含 Escape）直到命令退出&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>pane&lt;/code>&lt;/td>
&lt;td>开一个临时 pane，命令退出就关掉&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>shell&lt;/code>&lt;/td>
&lt;td>后台游离执行&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>plugin_action&lt;/code>&lt;/td>
&lt;td>调用已安装插件的某个 action&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>popup&lt;/code> 最实用。比如随手开一个临时 shell：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">command&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;prefix+t&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;popup&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;exec \&amp;#34;${SHELL:-sh}\&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">description&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;open scratch terminal&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">width&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;80%&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">height&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;80%&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>注意 popup 命令拿不到 &lt;code>HERDR_PANE_ID&lt;/code>&lt;/strong>（它是会话级的，不属于任何 pane），要引用底下那个平铺 pane 得用 &lt;code>HERDR_ACTIVE_PANE_ID&lt;/code>。这个坑不踩一次很难注意到。&lt;/p>
&lt;p>自定义命令能拿到的环境变量里，比较有用的几个：&lt;code>HERDR_SOCKET_PATH&lt;/code>、&lt;code>HERDR_BIN_PATH&lt;/code>、&lt;code>HERDR_ACTIVE_WORKSPACE_ID&lt;/code>、&lt;code>HERDR_ACTIVE_TAB_ID&lt;/code>、&lt;code>HERDR_ACTIVE_PANE_ID&lt;/code>、&lt;code>HERDR_ACTIVE_PANE_CWD&lt;/code>。&lt;/p>
&lt;p>标签栏右侧还能挂状态：主机名、时间、一段自定义脚本的输出。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">tab_bar_position&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;bottom&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">tab_bar_right&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;zoom&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;hostname&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;datetime&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">format&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;%H:%M&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;~/.config/herdr/status.sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">interval_seconds&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">timeout_seconds&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">tab_bar_right_separator&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34; · &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>hostname&lt;/code> / &lt;code>datetime&lt;/code> / &lt;code>command&lt;/code> 都在 &lt;strong>server 端&lt;/strong>求值，所以 &lt;code>herdr --remote&lt;/code> 时显示的是远端机器的值——这点很正确，看远程构建状态时不会误判。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-sidebar-status.jpg"
width="1200"
height="831"
srcset="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-sidebar-status_hu11392512355782293930.jpg 480w, https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-sidebar-status_hu15252206511630996518.jpg 1024w"
loading="lazy"
alt="Herdr 实拍：左侧边栏汇总 Agent 状态（working / idle / done），右侧是正在流式输出的 Agent 窗格（来源：herdr 官方演示视频）"
class="gallery-image"
data-flex-grow="144"
data-flex-basis="346px"
>&lt;/p>
&lt;hr>
&lt;h2 id="八关键一步让状态真正准确集成">八、关键一步：让状态真正准确——集成
&lt;/h2>&lt;p>&lt;strong>如果你什么都不做，&lt;code>agent list&lt;/code> 会是空的，状态全是 &lt;code>unknown&lt;/code>。&lt;/strong> 我上面贴的实测输出就是这样。这不是 bug，是设计：Herdr 需要 Agent 侧有一个 hook 主动上报状态。&lt;/p>
&lt;p>装法是一条命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr integration install claude
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr integration install codex
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr integration status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>支持的一堆：&lt;code>pi&lt;/code>、&lt;code>omp&lt;/code>、&lt;code>claude&lt;/code>、&lt;code>codex&lt;/code>、&lt;code>copilot&lt;/code>、&lt;code>devin&lt;/code>、&lt;code>droid&lt;/code>、&lt;code>kimi&lt;/code>、&lt;code>opencode&lt;/code>、&lt;code>kilo&lt;/code>、&lt;code>hermes&lt;/code>、&lt;code>mastracode&lt;/code>、&lt;code>qodercli&lt;/code>、&lt;code>qwen&lt;/code>、&lt;code>cursor&lt;/code>。&lt;/p>
&lt;p>我本机 &lt;code>herdr integration status&lt;/code> 的实际输出：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="go">pi: current (v8) (/Users/zata/.pi/agent/extensions/herdr-agent-state.ts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">claude: current (v9) (/Users/zata/.claude/hooks/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">codex: current (v8) (/Users/zata/.codex/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">copilot: current (v3) (/Users/zata/.copilot/hooks/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">kimi: current (v7) (/Users/zata/.kimi-code/hooks/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">opencode: current (v11) (/Users/zata/.config/opencode/plugins/herdr-agent-state.js)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">hermes: current (v5) (/Users/zata/.hermes/plugins/herdr-agent-state/__init__.py)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">omp: not installed (/Users/zata/.omp/agent/extensions/herdr-omp-agent-state.ts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">devin: not installed (/Users/zata/.config/devin/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">droid: not installed (/Users/zata/.factory/hooks/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">qwen: not installed (/Users/zata/.qwen/hooks/herdr-agent-session.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">cursor: not installed (/Users/zata/.cursor/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">grok: not installed (/Users/zata/.grok/hooks/herdr-agent-state.sh)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">...
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>有意思的是：它检测到我已经装了 Claude、Codex、Copilot、Kimi、OpenCode、Hermes 的集成，&lt;strong>但一个都没装 pi&lt;/strong>。这正是我前面 &lt;code>agent list&lt;/code> 为空的原因——我当前跑在 pane 里的那些进程，不是这几个装了集成的 Agent。装完集成后必须&lt;strong>在 Herdr 里重启那个 Agent&lt;/strong>，hook 才会生效。&lt;/p>
&lt;p>集成除了报状态，还负责上报&lt;strong>原生会话引用&lt;/strong>，这就是第 4 节&amp;quot;重启后恢复对话&amp;quot;依赖的东西。没装集成的 Agent，重启后只能恢复成一个普通 shell。&lt;/p>
&lt;p>Herdr 还有一份&lt;strong>给你自己的 Agent 看的技能文件&lt;/strong>，装上之后 Agent 就知道怎么用 herdr 控制周边窗格：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx skills add herdrdev/herdr --skill herdr -g
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或直接 &lt;code>herdr --skill&lt;/code> 打印与当前二进制版本匹配的内置副本。技能文件里第一条是护栏：&lt;strong>如果 &lt;code>HERDR_ENV=1&lt;/code> 没设置，Agent 必须停下来&lt;/strong>，并说明自己没跑在 Herdr 管理的 pane 里——防止 Herdr 外部的 Agent 去控制一个不属于它的会话。&lt;/p>
&lt;hr>
&lt;h2 id="九重头戏把-herdr-当-agent-的控制平面">九、重头戏：把 Herdr 当 Agent 的控制平面
&lt;/h2>&lt;p>这才是 Herdr 和 tmux 的分水岭。所有操作都通过&lt;strong>本地 socket 上的 JSON API&lt;/strong>，CLI 只是它的一个包装。&lt;/p>
&lt;h3 id="91-三层接口按需选">9.1 三层接口，按需选
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>层&lt;/th>
&lt;th>用途&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Agent 技能&lt;/td>
&lt;td>教 coding agent 在 pane 内使用 Herdr&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CLI 包装&lt;/strong>&lt;/td>
&lt;td>脚本、简单编排、人工调试 ← 大多数情况从这层开始&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>原始 socket API&lt;/td>
&lt;td>自定义工具、协议客户端、事件订阅&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>三层共用同一个控制平面。先看 CLI 的规模——这是 &lt;code>herdr pane --help&lt;/code> 的命令表：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr pane --help
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">Commands:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> list List panes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> current Show the current pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> get Show a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> layout Show pane layout information
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> process-info Show pane process information
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> neighbor Find a pane neighbor
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> edges Show pane edge information
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> focus Focus a neighboring pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> resize Resize a pane split
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> zoom Toggle or set pane zoom
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> read Read pane terminal output
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> rename Rename a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> input Set pane input routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> split Split a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> swap Swap panes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> move Move a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> close Close a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> send-text Send literal text to a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> send-keys Send key presses to a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> wait-output Wait for matching pane output
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> run Run a command in a pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> report-agent Report pane agent lifecycle state
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> report-agent-session Report pane agent session identity
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> release-agent Release pane agent lifecycle authority
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> report-metadata Report display-only pane metadata
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>关键点：&lt;strong>pane 是你的手，agent 是你的眼。&lt;/strong> 用 &lt;code>pane run&lt;/code> 开东西，用 &lt;code>pane read&lt;/code> / &lt;code>agent wait&lt;/code> 拿结果。&lt;/p>
&lt;h3 id="92-基础编排起活读输出等结果">9.2 基础编排：起活、读输出、等结果
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 建一个工作区，指定目录和标签&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr workspace create --cwd ~/code/project --label api
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 切一个 pane 出来跑测试&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane split w1:p1 --direction right
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane run w1:p2 &lt;span class="s2">&amp;#34;npm test&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 读输出（四种来源见下）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane &lt;span class="nb">read&lt;/span> w1:p2 --source recent --lines &lt;span class="m">50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 等某个字符串出现&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane wait-output w1:p2 --match &lt;span class="s2">&amp;#34;Test Suites:&amp;#34;&lt;/span> --timeout &lt;span class="m">120000&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>pane read --source&lt;/code> 的四种取值语义不一样，选错了会读出一堆噪声：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>source&lt;/th>
&lt;th>含义&lt;/th>
&lt;th>适用场景&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>visible&lt;/code>&lt;/td>
&lt;td>当前渲染的屏幕&lt;/td>
&lt;td>&lt;strong>UI 反馈循环&lt;/strong>——想看用户看到什么&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>recent&lt;/code>&lt;/td>
&lt;td>带终端软折行的最近回滚&lt;/td>
&lt;td>一般读取&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>recent-unwrapped&lt;/code>&lt;/td>
&lt;td>不带软折行的最近回滚&lt;/td>
&lt;td>&lt;strong>日志&lt;/strong>——不想被折行切断&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>detection&lt;/code>&lt;/td>
&lt;td>Agent 屏幕检测用的底部缓冲区快照&lt;/td>
&lt;td>排查检测逻辑&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>我读日志一律用 &lt;code>recent-unwrapped&lt;/code>。用 &lt;code>recent&lt;/code> 读一段长 JSON 或宽表格，折行会把结构切碎。&lt;/p>
&lt;h3 id="93-agent-之间互相指挥">9.3 Agent 之间互相指挥
&lt;/h3>&lt;p>这是最有意思的部分。&lt;code>herdr agent&lt;/code> 提供了一组面向&amp;quot;别的 Agent&amp;quot;的动词：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr agent --help
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> list List agents
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> get Show an agent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> read Read agent terminal output
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> send-keys Send key presses to an agent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> prompt Submit a prompt to an agent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> rename Rename an agent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> focus Focus an agent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> wait Wait until an agent reaches one of the requested states
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> attach Attach directly to an agent terminal
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> start Start a supported interactive agent in an existing pane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> explain Explain agent detection state
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--kind&lt;/code> 支持的 Agent 相当全：&lt;code>pi&lt;/code>、&lt;code>claude&lt;/code>、&lt;code>codex&lt;/code>、&lt;code>gemini&lt;/code>、&lt;code>cursor&lt;/code>、&lt;code>devin&lt;/code>、&lt;code>agy&lt;/code>、&lt;code>cline&lt;/code>、&lt;code>omp&lt;/code>、&lt;code>mastracode&lt;/code>、&lt;code>opencode&lt;/code>、&lt;code>copilot&lt;/code>、&lt;code>kimi&lt;/code>、&lt;code>kiro&lt;/code>、&lt;code>droid&lt;/code>、&lt;code>amp&lt;/code>、&lt;code>grok&lt;/code>、&lt;code>hermes&lt;/code>、&lt;code>kilo&lt;/code>、&lt;code>qodercli&lt;/code>、&lt;code>qwen&lt;/code>、&lt;code>maki&lt;/code>、&lt;code>muse&lt;/code>。&lt;/p>
&lt;p>于是你可以这样写一个&amp;quot;评审&amp;quot;流程——&lt;strong>一个 Agent 等另一个 Agent 干完再接手&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在右窗格起一个 Codex，让它做代码审查&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane split w1:p1 --direction right
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr agent start reviewer --kind codex --pane w1:p2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 给它派活，并直接阻塞到它需要你/干完为止&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr agent prompt reviewer &lt;span class="s2">&amp;#34;审查 src/auth 的变更，只报问题不报风格&amp;#34;&lt;/span> --wait --until blocked --timeout &lt;span class="m">600000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 读它的结论&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr agent &lt;span class="nb">read&lt;/span> reviewer --source recent-unwrapped --lines &lt;span class="m">120&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>agent start&lt;/code> 有个前置条件：目标 pane 必须停在&lt;strong>交互式 shell 提示符&lt;/strong>上——它把 Agent 进程放进现成的 shell 里启动，并确认检测成功才算数。&lt;/p>
&lt;p>看清楚 &lt;code>--wait --until blocked&lt;/code> 的含义：&lt;strong>阻塞直到那个 Agent 真的需要人类介入&lt;/strong>，而不是&amp;quot;命令跑完了&amp;quot;。这个语义差别很重要，下面 9.4 会展开。&lt;/p>
&lt;p>&lt;code>agent prompt&lt;/code> 的帮助里有一段很值得逐字读的行为约定：&lt;/p>
&lt;blockquote>
&lt;p>如果目标 agent 已经处于 blocked 状态，提交会在&lt;strong>发送任何输入之前&lt;/strong>被拒绝，返回 &lt;code>agent_blocked&lt;/code>。当一个被接受的提交从非 working 状态起步时，&lt;code>--wait&lt;/code> 要求在 5000ms 内观察到一个 &lt;code>working&lt;/code> 或 &lt;code>blocked&lt;/code> 状态，否则返回 &lt;code>agent_prompt_stalled&lt;/code>。&lt;/p>
&lt;/blockquote>
&lt;p>也就是说它&lt;strong>主动防止你往一个正在等人类输入的 Agent 里灌东西&lt;/strong>。这个栏杆方向是对的——&lt;code>blocked&lt;/code> 的 Agent 需要的往往是你去回答一个问题，而不是再来一条指令。&lt;/p>
&lt;p>另外几个顺手的动词：&lt;code>herdr agent attach reviewer&lt;/code> 把&lt;strong>当前终端直接接到某个 Agent 的屏幕&lt;/strong>上（不看全景，就看这一个），退出用 &lt;code>ctrl+b q&lt;/code>，想给它发字面 &lt;code>ctrl+b&lt;/code> 就连按两次；已经有别的直连客户端占着输入时用 &lt;code>--takeover&lt;/code> 抢过来。非 Agent 的普通终端有对应的 &lt;code>herdr terminal attach &amp;lt;terminal_id&amp;gt;&lt;/code>。&lt;code>herdr agent rename w1:p1 reviewer&lt;/code> 改的是 Agent 的名字，改完它就是一个能用在命令里的稳定引用，比 pane ID 好记。&lt;/p>
&lt;h3 id="94-为什么-agent-wait-不是等命令跑完">9.4 为什么 &lt;code>agent wait&lt;/code> 不是&amp;quot;等命令跑完&amp;quot;
&lt;/h3>&lt;p>这是我觉得 Herdr 在概念上最清醒的地方。看 help 原文：&lt;/p>
&lt;blockquote>
&lt;p>Agent waits observe &lt;strong>semantic state&lt;/strong>, not completion of arbitrary commands.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;code>agent wait&lt;/code> 观察的是&lt;strong>语义状态&lt;/strong>，不是任意命令的完成。所以在 socket 层它是这么实现的：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>server 持有、事件驱动&lt;/strong>——不是客户端轮询&lt;/li>
&lt;li>&lt;strong>钉在解析出的 pane 占用者上&lt;/strong>——如果那个 Agent 被换成了另一个，&amp;ldquo;等待&amp;quot;不会由新来的假冒满足&lt;/li>
&lt;/ul>
&lt;p>配合 &lt;code>events.subscribe&lt;/code> 可以做真正的编排：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;sub_1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;events.subscribe&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;subscriptions&amp;#34;&lt;/span>&lt;span class="p">:[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;pane.agent_status_changed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;pane_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;w1:p1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;agent_status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;blocked&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>还有一个容易被忽略的竞态。如果你分两步写：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr agent prompt reviewer &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span> &lt;span class="c1"># 先发&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr agent &lt;span class="nb">wait&lt;/span> reviewer --until &lt;span class="k">done&lt;/span> &lt;span class="c1"># 再等&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两步之间 Agent 可能已经干完并进入 &lt;code>done&lt;/code>，你的 &lt;code>wait&lt;/code> 就漏掉了。Herdr 的解法是&lt;strong>把 prompt 和 wait 合成一个请求&lt;/strong>——socket 层的 &lt;code>agent.prompt&lt;/code> 接受内嵌的 &lt;code>wait&lt;/code> 对象：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;agent.prompt&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pane_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;w1:p1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wait&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;until&amp;#34;&lt;/span>&lt;span class="p">:[&lt;/span>&lt;span class="s2">&amp;#34;done&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>&lt;span class="nt">&amp;#34;timeout_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">600000&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一次请求原子地完成&amp;quot;提交 + 开始等待&amp;rdquo;，没有中间态。CLI 侧对应的就是 &lt;code>--wait --until&lt;/code>。&lt;/p>
&lt;h3 id="95-状态上报report-agent-与-report-metadata-的区别">9.5 状态上报：&lt;code>report-agent&lt;/code> 与 &lt;code>report-metadata&lt;/code> 的区别
&lt;/h3>&lt;p>任何工具（hook、插件、自定义脚本）都可以报状态，但&lt;strong>有两个通道，语义完全不同&lt;/strong>：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>命令&lt;/th>
&lt;th>影响&lt;/th>
&lt;th>用途&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>pane report-agent&lt;/code>&lt;/td>
&lt;td>&lt;strong>语义状态&lt;/strong>——影响 wait、通知、汇总&lt;/td>
&lt;td>告诉 Herdr &amp;ldquo;我在干活/我卡住了&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>pane report-metadata&lt;/code>&lt;/td>
&lt;td>&lt;strong>仅展示&lt;/strong>——只进侧边栏&lt;/td>
&lt;td>显示模型名、当前子任务这类信息&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 语义：这个 pane 在干活&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane report-agent w1:p1 --source my-hook --agent docs-bot --state working --message &lt;span class="s2">&amp;#34;building docs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 展示：只给侧边栏看的自定义字段&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr pane report-metadata w1:p1 --source my-hook &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --token &lt;span class="nv">model&lt;/span>&lt;span class="o">=&lt;/span>opus --token &lt;span class="nv">summary&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;reviewing authentication&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>别把该用 metadata 的东西丢进 report-agent。&lt;/strong> 语义状态会驱动 &lt;code>agent wait&lt;/code> 的返回和通知的触发，污染它等于让整个编排逻辑读错信号。这条边界划得很清楚，值得尊重。&lt;/p>
&lt;p>自定义字段在侧边栏里用 &lt;code>$name&lt;/code> 引用，还能配合条件着色：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sidebar&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agents&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">rows&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;state_icon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;$model&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;$summary&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tab&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 或者按数值变色&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># [{ token = &amp;#34;$load&amp;#34;, fg = &amp;#34;#fff&amp;#34;, rules = [{ gt = 80, fg = &amp;#34;#f55&amp;#34;, bold = true }, { gt = 50, fg = &amp;#34;#fc0&amp;#34; }] }]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>侧边栏行布局本身是可编程的——&lt;code>rows&lt;/code> 里放 token，最多 16 行、每行 16 个 token，支持 &lt;code>equals&lt;/code> / &lt;code>contains&lt;/code> / &lt;code>starts_with&lt;/code> / &lt;code>gt&lt;/code> / &lt;code>lt&lt;/code> 做条件着色（只有第一条命中的规则生效，&lt;strong>没有正则、没有模糊匹配、没有脚本&lt;/strong>）。还能给特定 Agent 单独换一套：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sidebar&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agents&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">rows_by_agent&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">claude&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;state_icon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;state_text&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;terminal_title_stripped&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="c"># 直接把 Claude 自己的标题拿来用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tab&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意 key 必须是&lt;strong>规范 agent ID&lt;/strong>（&lt;code>claude&lt;/code>、&lt;code>codex&lt;/code>、&lt;code>pi&lt;/code>），&lt;code>claude-code&lt;/code> 这种检测别名不接受。而且它是&lt;strong>替换&lt;/strong> &lt;code>rows&lt;/code> 而不是追加。&lt;/p>
&lt;h3 id="96-插件宿主给挂载点你自己接行为">9.6 插件：宿主给挂载点，你自己接行为
&lt;/h3>&lt;p>插件是 Herdr 留给&amp;quot;核心之外&amp;quot;的口子。官方的说法是：Herdr 保持精简的办法，是核心只管终端工作区、窗格、Agent 和一套稳定的 CLI/socket API，其余流程做成&lt;strong>可分享的插件&lt;/strong>——一个目录、一份 &lt;code>herdr-plugin.toml&lt;/code> manifest、若干宿主能启动的命令，语言随你（Bash、Node、Lua、Rust 二进制都行）。&lt;/p>
&lt;p>三个定位要点：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>宿主管挂载，插件管实现。&lt;/strong> 安装、manifest 校验、键位、终端窗格、事件、调用上下文、socket 访问由 Herdr 提供；实现语言、依赖、文件、持久状态由插件自己负责。&lt;/li>
&lt;li>&lt;strong>没有插件 SDK，也没有受限命令集。&lt;/strong> 整个 Herdr CLI 就是插件 API——插件里的代码想调什么就调什么。&lt;/li>
&lt;li>&lt;strong>回调写 &lt;code>HERDR_BIN_PATH&lt;/code>，不要硬编码 socket。&lt;/strong> 它指向正在运行的 Herdr 二进制，顺带抹平 Unix socket 和 Windows 命名管道的差异。&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-plugins.svg"
loading="lazy"
alt="Herdr 插件模型：左边是插件目录（manifest &amp;#43; 命令 &amp;#43; 配置/状态目录），中间是宿主提供的五类挂载点（actions / panes / events / link_handlers / startup），右边是插件通过 HERDR_BIN_PATH 回调整个 CLI，底部是五种窗格摆放"
>&lt;/p>
&lt;p>v1 明确不做的事也值得先知道：不能在运行时注册 action、没有原生非终端 UI，也没有 Herdr 托管的存储 API——需要状态就自己存。&lt;/p>
&lt;p>这一段是&amp;quot;官方文档 + 本机 0.9.0 实测&amp;quot;的组合：命令面、参数解析都是我跑出来的；凡是没亲手跑过的我会标出来。&lt;/p>
&lt;p>（9 月 15 日二次补充：原本写着&amp;quot;我本机一个插件都没装&amp;quot;。现在装了，所以有了 &lt;a class="link" href="#967-%e8%af%95%e8%a3%85%e5%ae%9e%e5%bd%95herdr-file-viewer" >9.6.7 试装实录&lt;/a>，前面几处&amp;quot;没跑过&amp;quot;的说法已经按实测结果改掉。）&lt;/p>
&lt;h4 id="961-先看宿主给了什么">9.6.1 先看宿主给了什么
&lt;/h4>&lt;p>一条 &lt;code>--help&lt;/code> 就能把整个插件面看完（输出截断了结尾的 AI 提示段）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr plugin --help
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">Install and run workflow plugins
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="go">Usage: herdr plugin [COMMAND]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="go">Commands:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> install Install a plugin from GitHub
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> uninstall Uninstall a plugin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> link Link a local plugin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> unlink Unlink a local plugin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> enable Enable a plugin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> disable Disable a plugin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> list List installed plugins
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> config-dir Print a plugin config directory
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> action List or invoke plugin actions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> log Inspect plugin command logs [aliases: logs]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> pane Manage plugin-owned panes
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>十一个子命令分三组：&lt;strong>装&lt;/strong>（&lt;code>install&lt;/code> / &lt;code>link&lt;/code> / &lt;code>uninstall&lt;/code> / &lt;code>unlink&lt;/code> / &lt;code>enable&lt;/code> / &lt;code>disable&lt;/code> / &lt;code>list&lt;/code> / &lt;code>config-dir&lt;/code>）、&lt;strong>跑动作&lt;/strong>（&lt;code>action&lt;/code>）、&lt;strong>开窗格和查日志&lt;/strong>（&lt;code>pane&lt;/code> / &lt;code>log&lt;/code>）。&lt;/p>
&lt;h4 id="962-装两条路径">9.6.2 装：两条路径
&lt;/h4>&lt;p>从 GitHub 装，只接受简写，内部用 &lt;code>git&lt;/code> 拉取：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr plugin install &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&lt;span class="o">[&lt;/span>/subdir...&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--ref REF&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--yes&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>交互式终端里它会先给一份&lt;strong>信任预览&lt;/strong>——源码位置、以及将要执行的命令——你确认之后才跑 manifest 里的 &lt;code>[[build]]&lt;/code>。&lt;code>--yes&lt;/code> 跳过确认（脚本里用），&lt;code>--ref&lt;/code> 把版本钉在某个 ref 上。&lt;/p>
&lt;p>自己写的时候走 link：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr plugin link &amp;lt;path&amp;gt; &lt;span class="c1"># 插件目录，或直接的 manifest 路径&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr plugin unlink &amp;lt;plugin_id&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>link&lt;/code> &lt;strong>不跑&lt;/strong> build 命令，构建是你自己的事；&lt;code>unlink&lt;/code> 只注销、不动文件；&lt;code>uninstall&lt;/code> 会连 Herdr 管理的 GitHub checkout 一起删。反过来，&lt;strong>已经 link 的插件不能被 install 覆盖&lt;/strong>，得先 unlink。&lt;/p>
&lt;p>四个容易踩的点：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>插件和启用状态是&amp;quot;当前用户全局&amp;quot;的&lt;/strong>，不属于某个会话。任何会话里装或启用，其他会话立刻可见。0.7.3 时代在命名会话里装的插件需要重新装一次。&lt;/li>
&lt;li>&lt;strong>v1 没有 &lt;code>plugin update&lt;/code>&lt;/strong>，刷新 = 重新 install（会替换托管的 checkout）。&lt;/li>
&lt;li>&lt;code>install&lt;/code> 和 &lt;code>link&lt;/code> 都会顺手把插件的 config / state 目录建好；只想拿路径就用 &lt;code>herdr plugin config-dir &amp;lt;id&amp;gt;&lt;/code>。&lt;/li>
&lt;li>没有 server 在跑也能装、能 link——它们只是往全局插件表里注册，不等 server。&lt;/li>
&lt;/ul>
&lt;p>装之前本机正好是空态，当基线很干净（装完之后长什么样见 9.6.7）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr plugin list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">No plugins installed.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="gp">$&lt;/span> herdr plugin list --json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">{&amp;#34;id&amp;#34;:&amp;#34;cli:plugin&amp;#34;,&amp;#34;result&amp;#34;:{&amp;#34;plugins&amp;#34;:[],&amp;#34;type&amp;#34;:&amp;#34;plugin_list&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="963-用绑键开窗格">9.6.3 用：绑键、开窗格
&lt;/h4>&lt;p>装完插件不会自己冒出来。它对外提供的是 action 和窗格入口，接上手最常用的是绑键：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">command&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;prefix+alt+f&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;plugin_action&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;herdr-file-viewer.open-file-viewer&amp;#34;&lt;/span> &lt;span class="c"># 插件id.action&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">description&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;open file viewer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>command&lt;/code> 写 action id：&lt;strong>本地 id（&lt;code>open-file-viewer&lt;/code>）和限定名（&lt;code>插件id.action&lt;/code>）都可以&lt;/strong>，本地 id 命中多个插件时报 &lt;code>ambiguous_plugin_action&lt;/code> 让你补限定名——所以一开始就写限定名最省事，&lt;code>herdr plugin action invoke&lt;/code> 同理。&lt;/p>
&lt;p>窗格是插件的&amp;quot;界面&amp;quot;。&lt;code>herdr plugin pane open&lt;/code> 让 manifest 里声明的 &lt;code>[[panes]]&lt;/code> 命令变成一个 Herdr 管理的终端窗格，摆放方式可以在命令行覆盖 manifest 的默认值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr plugin pane open --plugin ID --entrypoint ID &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --placement split --direction right &lt;span class="o">[&lt;/span>--cwd PATH&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--env &lt;span class="nv">KEY&lt;/span>&lt;span class="o">=&lt;/span>VALUE&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;code>placement&lt;/code>&lt;/th>
&lt;th>行为&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>overlay&lt;/code>（manifest 默认）&lt;/td>
&lt;td>临时缩放的覆盖层，盖在当前 pane 上；关闭时恢复原来的焦点和缩放&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>popup&lt;/code>&lt;/td>
&lt;td>会话级模态弹窗，不改布局，拿到全部输入（含 Esc），命令退出或收到 &lt;code>popup.close&lt;/code> 就关&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>split&lt;/code>&lt;/td>
&lt;td>常规分割窗格&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tab&lt;/code>&lt;/td>
&lt;td>新标签页&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>zoomed&lt;/code>&lt;/td>
&lt;td>放大窗格&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>popup 是最&amp;quot;不像 pane&amp;quot;的一种，边界记清楚：&lt;strong>它没有 pane ID、不发 pane 生命周期事件、不参与布局/持久化/agent API&lt;/strong>，进程里也拿不到 &lt;code>HERDR_PANE_ID&lt;/code>（想引用底下那个平铺 pane，读上下文 JSON）。&lt;code>--width&lt;/code> / &lt;code>--height&lt;/code> 只对它有意义：默认半屏，支持 &lt;code>80%&lt;/code> 这类百分比，低于弹窗最小值会被夹紧；Settings、Copy mode 这类模态开着时开 popup 会返回 &lt;code>ui_busy&lt;/code>。&lt;/p>
&lt;p>&lt;strong>踩坑：&lt;code>--help&lt;/code> 不是完整真相。&lt;/strong> 0.9.0 的 &lt;code>herdr plugin pane open --help&lt;/code> 里，&lt;code>--placement&lt;/code> 的候选值只列了 &lt;code>overlay|split|tab|zoomed&lt;/code>，&lt;code>--width&lt;/code> / &lt;code>--height&lt;/code> 根本没出现。但解析器是接受 &lt;code>popup&lt;/code> 的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr plugin pane open --plugin nope.nope --entrypoint nope --placement popup
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">{&amp;#34;error&amp;#34;:{&amp;#34;code&amp;#34;:&amp;#34;plugin_not_found&amp;#34;,&amp;#34;message&amp;#34;:&amp;#34;plugin not found&amp;#34;},&amp;#34;id&amp;#34;:&amp;#34;cli:plugin&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="gp">$&lt;/span> herdr plugin pane open --plugin nope.nope --entrypoint nope --placement bogus
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">invalid pane placement: bogus
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一条已经走到&amp;quot;找不到插件&amp;quot;这一步，说明 &lt;code>popup&lt;/code> 通过了参数校验；第二条才是不合法值。顺带一提，&lt;code>zoomed&lt;/code> 还有个 &lt;code>fullscreen&lt;/code> 别名。&lt;strong>摆放这件事以文档和源码为准，别以 help 为准。&lt;/strong>&lt;/p>
&lt;p>跑过的动作和日志都查得到：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr plugin action list &lt;span class="o">[&lt;/span>--plugin ID&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr plugin action invoke &amp;lt;action_id&amp;gt; &lt;span class="o">[&lt;/span>--plugin ID&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr plugin log list &lt;span class="o">[&lt;/span>--plugin ID&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--limit N&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="964-插件进程能拿到什么">9.6.4 插件进程能拿到什么
&lt;/h4>&lt;p>调用时 Herdr 注入的环境变量：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>变量&lt;/th>
&lt;th>用途&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_ID&lt;/code> / &lt;code>HERDR_PLUGIN_ROOT&lt;/code>&lt;/td>
&lt;td>插件身份、安装（或 link）目录&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_CONFIG_DIR&lt;/code>&lt;/td>
&lt;td>用户可编辑的配置，&lt;code>.env&lt;/code> 放这里&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_STATE_DIR&lt;/code>&lt;/td>
&lt;td>插件自己的运行时状态&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_CONTEXT_JSON&lt;/code>&lt;/td>
&lt;td>完整调用上下文：workspace / tab / 焦点 pane / worktree / agent / 选中文本 / 被点击的 URL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_BIN_PATH&lt;/code> / &lt;code>HERDR_SOCKET_PATH&lt;/code>&lt;/td>
&lt;td>回调入口，优先用前者&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_ACTION_ID&lt;/code>&lt;/td>
&lt;td>被哪个 action 调起来的&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_EVENT&lt;/code> / &lt;code>HERDR_PLUGIN_EVENT_JSON&lt;/code>&lt;/td>
&lt;td>事件钩子的事件名与完整事件体&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_ENTRYPOINT_ID&lt;/code>&lt;/td>
&lt;td>窗格命令对应哪个 entrypoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PLUGIN_CLICKED_URL&lt;/code> / &lt;code>HERDR_PLUGIN_LINK_HANDLER_ID&lt;/code>&lt;/td>
&lt;td>链接处理器专用&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>一条规矩：&lt;code>HERDR_PLUGIN_ROOT&lt;/code> 是源码 checkout，&lt;strong>不要往里写凭据和状态&lt;/strong>；用户配置进 &lt;code>CONFIG_DIR&lt;/code>，运行时状态进 &lt;code>STATE_DIR&lt;/code>。这两个目录 Herdr 负责建（&lt;code>CONFIG_DIR&lt;/code> 还会从旧位置迁移一次），但里面的内容它不校验、不同步、也不删。另外 &lt;code>--env KEY=VALUE&lt;/code> 可以给启动的进程加变量，不过和 Herdr 自己的变量冲突时以 Herdr 为准。&lt;/p>
&lt;h4 id="965-写最小骨架">9.6.5 写：最小骨架
&lt;/h4>&lt;p>目录就两样东西：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">my-plugin/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> herdr-plugin.toml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> peek.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>manifest：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;me.file-peek&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;File Peek&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;0.1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">min_herdr_version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;0.7.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">description&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;在弹窗里看一眼文件&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">platforms&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;linux&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;macos&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">actions&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;peek&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">title&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;Peek file&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">contexts&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;pane&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;workspace&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;bash&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;peek.sh&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">panes&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;viewer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">title&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;Peek&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">placement&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;popup&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">width&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;80%&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">height&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;80%&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;bash&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;peek.sh&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 除了 action 和 pane，manifest 还能声明这四类（写法示意）：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">build&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;bash&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;build.sh&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">startup&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;node&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;dist/restore.js&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">events&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">on&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;worktree.created&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;node&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;dist/on-worktree.js&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">link_handlers&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">id&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;peek-path&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">title&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;Peek this file&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">pattern&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;^file://&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">action&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;peek&amp;#34;&lt;/span> &lt;span class="c"># 必须是同一个插件声明的 action&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>规则清单：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>command&lt;/code> 是 argv 数组，不走 shell。&lt;/strong> 没有 glob、管道、&lt;code>&amp;amp;&amp;amp;&lt;/code>；需要 shell 语义就在命令里显式起一个 shell。&lt;/li>
&lt;li>&lt;strong>id 规则不对称：&lt;/strong> 插件 id 可以用点（&lt;code>me.file-peek&lt;/code>），action / pane / link handler 的 id 不能用点。所以限定名 &lt;code>插件id.action&lt;/code> 天然不会歧义。&lt;/li>
&lt;li>&lt;strong>&lt;code>min_herdr_version&lt;/code> 是必填，而且真的会被强制&lt;/strong>：插件要求的版本比当前二进制新，install 和 link 都会拒绝。&lt;/li>
&lt;li>&lt;strong>&lt;code>platforms&lt;/code> 可以分层：&lt;/strong> 顶层不写也能 link（给个警告），item 级的 &lt;code>platforms&lt;/code> 覆盖顶层。Windows 上，build / action / event 命令会解析 &lt;code>npm.cmd&lt;/code>、&lt;code>bun.cmd&lt;/code> 这类 PATHEXT shim，而 pane 命令走 Herdr 常规的 Windows 启动路径，必须是合法的 Windows argv 命令。&lt;/li>
&lt;li>&lt;strong>&lt;code>[[build]]&lt;/code> 只在 install 时跑&lt;/strong>（link 不跑）。如果构建过程中 manifest 被改动，安装会中止——避免&amp;quot;预览看到的&amp;quot;和&amp;quot;实际装上的&amp;quot;不是同一个东西。&lt;/li>
&lt;li>&lt;strong>&lt;code>[[startup]]&lt;/code> 是一次性初始化，不是守护进程。&lt;/strong> 每个启用的插件在会话恢复后跑一次，live handoff 之后也会再跑一次；client 附着、配置热加载、插件新装或新启用都不触发。适合&amp;quot;把插件自己的状态恢复回去&amp;quot;，不适合挂常驻服务。&lt;/li>
&lt;li>&lt;strong>&lt;code>[[events]]&lt;/code> 的钩子由 Herdr 端触发&lt;/strong>，事件体通过 &lt;code>HERDR_PLUGIN_EVENT_JSON&lt;/code> 传进来。事件名要写当前版本支持的（未知事件名会在 link 时给出警告）。&lt;/li>
&lt;li>&lt;strong>&lt;code>[[link_handlers]]&lt;/code> 匹配的是 Ctrl+点击的 URL&lt;/strong>（macOS 也是 Ctrl，因为终端鼠标上报分不出 Command），&lt;code>pattern&lt;/code> 是 Rust 正则，按 manifest 顺序检查，&lt;code>action&lt;/code> 必须是同一插件的 action。被点的那条 URL 在 &lt;code>HERDR_PLUGIN_CLICKED_URL&lt;/code> 和上下文 JSON 里。&lt;/li>
&lt;li>&lt;strong>回调写 &lt;code>HERDR_BIN_PATH&lt;/code>&lt;/strong>：&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">spawnSync&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;node:child_process&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">herdr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">HERDR_BIN_PATH&lt;/span> &lt;span class="o">??&lt;/span> &lt;span class="s2">&amp;#34;herdr&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">spawnSync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">herdr&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">stdio&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;inherit&amp;#34;&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>官方有一份可以直接抄的例子仓库：&lt;code>ogulcancelik/herdr-plugin-examples&lt;/code>（&lt;code>agent-telegram-notify&lt;/code>、&lt;code>github-link-preview&lt;/code>、&lt;code>dev-layout-bootstrap&lt;/code>）。注意它是&amp;quot;示例&amp;quot;，不是官方维护的插件。&lt;/p>
&lt;h4 id="966-现成的几类值得装的">9.6.6 现成的：几类值得装的
&lt;/h4>&lt;p>市场里已经有能直接用的东西。下面这几条我都核过 manifest 或仓库说明：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>插件&lt;/th>
&lt;th>干什么&lt;/th>
&lt;th>备注&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a class="link" href="https://github.com/smarzban/herdr-file-viewer" target="_blank" rel="noopener"
>smarzban/herdr-file-viewer&lt;/a>&lt;/td>
&lt;td>只读文件查看器：目录树 + 内容面板，支持 diff、markdown 渲染、语法高亮，开在 split 里&lt;/td>
&lt;td>&lt;strong>只在显式动作下打开&lt;/strong>，装完要自己绑键；markdown / diff 的美化靠外部 &lt;code>glow&lt;/code> / &lt;code>delta&lt;/code> / &lt;code>bat&lt;/code>，没装会退回纯文本&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a class="link" href="https://github.com/persiyanov/herdr-reviewr" target="_blank" rel="noopener"
>persiyanov/herdr-reviewr&lt;/a>&lt;/td>
&lt;td>评审侧栏：看 Agent 改出来的 diff，加行内评论回传给 Agent&lt;/td>
&lt;td>macOS/Linux，要求 ≥ 0.7.5；带 &lt;code>worktree.created&lt;/code> 钩子，可以配成自动打开&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a class="link" href="https://github.com/alexarthurs/herdr-sidebar" target="_blank" rel="noopener"
>alexarthurs/herdr-sidebar&lt;/a>&lt;/td>
&lt;td>VS Code 式侧栏：文件树 + git source control，语法高亮预览和 diff&lt;/td>
&lt;td>manifest 在仓库的 &lt;code>plugins/herdr-sidebar/&lt;/code> 子目录&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a class="link" href="https://github.com/iurysza/termscope" target="_blank" rel="noopener"
>iurysza/termscope&lt;/a>&lt;/td>
&lt;td>把终端里已经出现的文件路径 / 链接在 split 里打开&lt;/td>
&lt;td>从屏幕内容取路径，不用手输&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>装法就是前面那条命令，三条都跑通了（完整过程见下一节）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr plugin install smarzban/herdr-file-viewer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr plugin action list --plugin herdr-file-viewer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr plugin pane open --plugin herdr-file-viewer --entrypoint file-viewer --placement split
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--plugin&lt;/code> / &lt;code>--entrypoint&lt;/code> 的取值来自这个插件的 manifest（&lt;code>id = &amp;quot;herdr-file-viewer&amp;quot;&lt;/code>、&lt;code>[[panes]] id = &amp;quot;file-viewer&amp;quot;&lt;/code>），它的两个 Unix action 是 &lt;code>open-file-viewer&lt;/code> 和 &lt;code>open-file-viewer-tab&lt;/code>（另有 &lt;code>-windows&lt;/code> 后缀的两个，Windows 上要绑那两个）。&lt;/p>
&lt;p>想找更多，官方有一个&lt;a class="link" href="https://herdr.dev/plugins/" target="_blank" rel="noopener"
>插件市场&lt;/a>：自动索引 GitHub 上打了 &lt;code>herdr-plugin&lt;/code> topic、默认分支里有合法 manifest 的公开仓库，按热度、活跃度、最新排序——今天查这个 topic 下已经有 1161 个仓库。要留意它是&lt;strong>未审核的索引&lt;/strong>：上榜只说明那个仓库给自己打了标签，不代表 Herdr 验过。装之前先读&lt;a class="link" href="https://herdr.dev/zh-cn/docs/plugins/" target="_blank" rel="noopener"
>信任与安全&lt;/a>那一节，扫一眼 manifest 和它要跑的命令——&lt;code>plugin install&lt;/code> 的那份预览就是为这个准备的。&lt;/p>
&lt;h4 id="967-试装实录herdr-file-viewer">9.6.7 试装实录：herdr-file-viewer
&lt;/h4>&lt;p>表里第一个我真装了一遍。选它的理由不在星数，在形状匹配：那几条里唯一 scope 就写着&amp;quot;只读文件查看&amp;quot;的、唯一过了 1.0 的（v1.16.0），而且 &lt;code>min_herdr_version = &amp;quot;0.7.0&amp;quot;&lt;/code> 是几个候选里最低的，兼容性最宽。&lt;/p>
&lt;p>&lt;strong>第一步：install，以及那份预览到底给不给你看。&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr plugin install smarzban/herdr-file-viewer --yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">Plugin install preview:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> id: herdr-file-viewer
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> version: 1.16.0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> source: smarzban/herdr-file-viewer
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> commit: 78441da81e63d5fc416ab63fa99d006161595523
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> actions: 4 startup commands: 0 events: 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> panes: 1 link handlers: 0 build commands: 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> build: /bin/sh scripts/fetch-or-build.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> build (skipped on macos): powershell -NoProfile ... fetch-or-build.ps1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> action open-file-viewer: bash scripts/open-file-viewer.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> action open-file-viewer-tab: bash scripts/open-file-viewer-tab.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> action open-file-viewer-windows: powershell -NoProfile ... open-file-viewer.ps1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> action open-file-viewer-tab-windows: powershell -NoProfile ... open-file-viewer-tab.ps1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> pane file-viewer: ./target/release/herdr-file-viewer
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">Installed herdr-file-viewer from smarzban/herdr-file-viewer.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">Config: /Users/zata/.config/herdr/plugins/config/herdr-file-viewer
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>预览是真东西，不是装饰：&lt;strong>它把 install 将要执行的每一条 &lt;code>[[build]]&lt;/code>、每一个 action 的实际 argv 全列出来&lt;/strong>，连那两条上千字符的 Windows PowerShell 单行也原样摊开，还标出哪些被当前平台跳过（&lt;code>build (skipped on macos)&lt;/code>）。要审计第三方插件，这一屏就是审计面。&lt;/p>
&lt;p>一个约束值得单独说：源码里有一条 &lt;code>remote plugin install requires --yes when stdin is not interactive&lt;/code>（我没实跑，一开始就带了 &lt;code>--yes&lt;/code>）。结论是&lt;strong>脚本、CI、Agent 里装插件必然带 &lt;code>--yes&lt;/code>，也就是必然跳过人工确认&lt;/strong>。所以更稳的顺序是先不带 &lt;code>--yes&lt;/code> 跑一次、人工读完预览、再决定要不要带。&lt;/p>
&lt;p>&lt;strong>第二步：&lt;code>[[build]]&lt;/code> 干了什么，东西落在哪。&lt;/strong>&lt;/p>
&lt;p>&lt;code>fetch-or-build.sh&lt;/code> 走快路径——按 manifest 版本加平台下载预编译二进制、校验 SHA-256，失配才回退 &lt;code>cargo build&lt;/code>。本机没回退，直接产出 &lt;code>target/release/herdr-file-viewer&lt;/code>，&lt;code>file&lt;/code> 出来是 Mach-O arm64、3.7MB。&lt;/p>
&lt;p>这一步建议真的看一眼：&lt;code>[[panes]]&lt;/code> 里声明的是&lt;strong>相对路径&lt;/strong> &lt;code>./target/release/herdr-file-viewer&lt;/code>，构建静默失败时 install 不会报错，要到你按键那一刻才发现窗格起不来。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>位置&lt;/th>
&lt;th>内容&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>~/.config/herdr/plugins/github/herdr-file-viewer-c993314e2614/&lt;/code>&lt;/td>
&lt;td>托管 checkout（含构建产物），&lt;code>uninstall&lt;/code> 会连它一起删&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>~/.config/herdr/plugins/config/herdr-file-viewer/&lt;/code>&lt;/td>
&lt;td>用户可编辑配置，&lt;code>herdr plugin config-dir &amp;lt;id&amp;gt;&lt;/code> 打印的就是这个&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>~/.local/state/herdr/plugins/herdr-file-viewer/&lt;/code>&lt;/td>
&lt;td>插件自己的运行时状态&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>对应 9.6.4 那条规矩：构建产物落在 &lt;code>PLUGIN_ROOT&lt;/code> 里，被重新 install 覆盖是正常行为，插件不该往这儿写状态。&lt;/p>
&lt;p>&lt;strong>第三步：绑键——别照抄 README 的键，先查默认表。&lt;/strong>&lt;/p>
&lt;p>README 推荐 &lt;code>prefix+f&lt;/code>。我先对着 0.9.0 的默认 prefix 表核了一遍，被占用的是 shift、n、c、p、minus、v、q、l、h、z、w、j、g、alt、x、t、r、k、b，&lt;code>f&lt;/code> 确实空闲。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">command&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;prefix+f&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;plugin_action&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;herdr-file-viewer.open-file-viewer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">description&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;open file viewer in split&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">command&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;prefix+shift+f&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;plugin_action&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">command&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;herdr-file-viewer.open-file-viewer-tab&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">description&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;open file viewer in tab&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后热加载，它会把诊断回给你：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr server reload-config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">{&amp;#34;id&amp;#34;:&amp;#34;cli:server:reload-config&amp;#34;,&amp;#34;result&amp;#34;:{&amp;#34;diagnostics&amp;#34;:[],&amp;#34;status&amp;#34;:&amp;#34;applied&amp;#34;,&amp;#34;type&amp;#34;:&amp;#34;config_reload&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>diagnostics: []&lt;/code> 就是干净。我只验证了&amp;quot;正常绑定不报错&amp;quot;这一侧，没去构造绑错的样本，所以不把这句话当完整错误清单用。&lt;/p>
&lt;p>&lt;strong>第四步：确认它真的画出来了。&lt;/strong>&lt;/p>
&lt;p>&lt;code>action invoke&lt;/code> 返回成功只代表命令被派发，不代表窗格活着。两个真凭据：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr plugin log list --plugin herdr-file-viewer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">... &amp;#34;exit_code&amp;#34;:0, &amp;#34;status&amp;#34;:&amp;#34;succeeded&amp;#34; ... &amp;#34;type&amp;#34;:&amp;#34;plugin_pane_opened&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go"> &amp;#34;pane_id&amp;#34;:&amp;#34;wB:p7&amp;#34;, &amp;#34;label&amp;#34;:&amp;#34;Files&amp;#34;, &amp;#34;focused&amp;#34;:true
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>再加一次屏幕读取（&lt;code>--source detection&lt;/code> 读后台缓冲，不受你滚动的影响）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-console" data-lang="console">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> herdr pane &lt;span class="nb">read&lt;/span> wB:p7 --source detection --format text
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">┌zata_code_template─────┐┌.claude────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">│ ▸ .claude ▐││Directory: select a file to view │
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">│ ▸ .cursor ▐││ │
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">│● ▸ docs ││ │
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">│ ▸ src ││ │
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">│▄▄▄▄▄▄▄▄▄▄▄▄▄ ││ │
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="go">└fix/alembi…consistency─┘└─────────────────────────────────────────────────? help┘
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两栏、git 状态标记（&lt;code>●&lt;/code>）、底栏分支名都在，才算装成。&lt;/p>
&lt;p>&lt;strong>装完留下的五条经验&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>CLI 调 action 打在&amp;quot;焦点窗格&amp;quot;上，不是你的窗格。&lt;/strong> 我在 &lt;code>wC&lt;/code> 的 pane 里执行 &lt;code>plugin action invoke&lt;/code>，split 却开在了另一个工作区 &lt;code>wB&lt;/code>。返回值里的 &lt;code>context.focused_pane_id&lt;/code> / &lt;code>workspace_id&lt;/code> 会告诉你目标是谁。人在界面上按键没这个问题，脚本里容易踩。&lt;/li>
&lt;li>&lt;strong>plugin pane 的 &lt;code>cwd&lt;/code> 是插件根目录，不是工作区。&lt;/strong> &lt;code>pane open&lt;/code> 返回里 &lt;code>cwd&lt;/code> 和 &lt;code>foreground_cwd&lt;/code> 都是 &lt;code>&amp;lt;plugin_root&amp;gt;&lt;/code>。所以插件里用相对路径当参数会指到自己家；要拿工作区路径得读 &lt;code>HERDR_PLUGIN_CONTEXT_JSON&lt;/code>。&lt;/li>
&lt;li>&lt;strong>验证完要收尾，而且 &lt;code>close&lt;/code> 是位置参数。&lt;/strong> &lt;code>herdr plugin pane close &amp;lt;pane_id&amp;gt;&lt;/code>，写成 &lt;code>--pane-id&lt;/code> 会打 usage 纠正你。再用 &lt;code>pgrep -f &amp;lt;binary&amp;gt;&lt;/code> 确认子进程真退了——窗格关掉不等于进程消失，别默认它干净。&lt;/li>
&lt;li>&lt;strong>&amp;ldquo;支持 markdown 渲染&amp;quot;这类话要看外部依赖。&lt;/strong> 它的美化接的是外部 &lt;code>glow&lt;/code> / &lt;code>delta&lt;/code> / &lt;code>bat&lt;/code>，我本机三个都没有，于是走纯文本 fallback。功能没坏，但和插件表里那句期待的不完全是一回事。装之前 &lt;code>command -v glow&lt;/code> 扫一眼。&lt;/li>
&lt;li>&lt;strong>回滚是干净的。&lt;/strong> &lt;code>herdr plugin uninstall smarzban/herdr-file-viewer&lt;/code> 删托管 checkout，键位是自己加进 &lt;code>config.toml&lt;/code> 的两段、手删，没有残留状态要清。&lt;/li>
&lt;/ol>
&lt;p>顺带印证了 9.6.2 那句&amp;quot;v1 没有 &lt;code>plugin update&lt;/code>&amp;quot;：&lt;code>herdr plugin --help&lt;/code> 的十一个子命令里确实没有 &lt;code>update&lt;/code>，刷新就是重新 &lt;code>install&lt;/code>——而重新 install 会替换托管 checkout，连 &lt;code>target/release/&lt;/code> 里的构建产物一起没，所以插件自己的状态一定要放在 state 目录而不是根目录。&lt;/p>
&lt;p>这一节如果只记一句话：&lt;strong>插件不是&amp;quot;装了就有的功能&amp;rdquo;，而是&amp;quot;宿主给你挂载点、你自己把行为接上去&amp;quot;。&lt;/strong> 它也解释了为什么 Herdr 本体能一直保持这么瘦。&lt;/p>
&lt;hr>
&lt;h2 id="十多机本地和远程在同一个窗口">十、多机：本地和远程在同一个窗口
&lt;/h2>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 临时接一台机器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr --remote workbox
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或者存下来，之后在侧边栏里按机器切&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr machine add workbox --label &lt;span class="s2">&amp;#34;Build machine&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr machine list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr --remote-keybindings server &lt;span class="c1"># 远程用服务器的按键绑定，而不是本地的&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>&amp;ldquo;远程&amp;quot;有三种走法，先选对。&lt;/strong> 差别不只是习惯：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>走法&lt;/th>
&lt;th>谁在跑 server&lt;/th>
&lt;th>适合&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>本机 &lt;code>herdr&lt;/code>&lt;/td>
&lt;td>本机&lt;/td>
&lt;td>日常&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ssh you@server&lt;/code> 再跑 &lt;code>herdr&lt;/code>&lt;/td>
&lt;td>远端，纯 tmux 式&lt;/td>
&lt;td>手机 SSH 客户端；你本来就活在 SSH 里&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>herdr --remote workbox&lt;/code>&lt;/td>
&lt;td>远端，UI 由本地 client 画&lt;/td>
&lt;td>想要&amp;quot;远程会话手感像本地&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>第三种和第二种有一个实际差异值得知道：&lt;strong>先 ssh 再跑 herdr，Herdr 整个在远端运行，读不到你本地桌面的剪贴板&lt;/strong>；而 &lt;code>--remote&lt;/code> 时 client 在你本机，本地截图可以直接粘到远端 pane 里（&lt;code>remote_image_paste&lt;/code> 默认绑 &lt;code>ctrl+v&lt;/code>，只在 &lt;code>--remote&lt;/code> 下生效）。要给远端的 Agent 贴图，只有这条路走得通。&lt;/p>
&lt;p>&lt;strong>手机上盯 Agent。&lt;/strong> Herdr 没有手机 App，也没打算做 web dashboard——装个 SSH 客户端连上跑着任务的机器，敲 &lt;code>herdr&lt;/code>，同一套持久会话就在窄屏里打开了。TUI 会自适应：侧边栏收成移动版切换器，Agent 行带上 tab 上下文，切工作区、看 pane、查状态都不用离开 SSH。iPhone 上官方点名 &lt;a class="link" href="https://getmoshi.app/" target="_blank" rel="noopener"
>moshi&lt;/a> 用着顺。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-mobile-switch.jpeg"
width="943"
height="2048"
srcset="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-mobile-switch_hu12938115667461341023.jpeg 480w, https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/images/index/herdr-mobile-switch_hu1513378505759060055.jpeg 1024w"
loading="lazy"
alt="手机上经 SSH 附着同一个会话后的 switch 面板：spaces 段列出各工作区与 git 分支，agents 段能看到每个 Agent 的状态（working · codex、working · pi、idle · pi），底部是 settings / keybinds / reload config / detach 菜单（来源：herdr 官方文档）"
class="gallery-image"
data-flex-grow="46"
data-flex-basis="110px"
>&lt;/p>
&lt;p>设计上有两个细节做得对：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>本地和远程的 Agent 汇总在同一个列表里&lt;/strong>，每台机器的连接独立重连——断了一台不会拖垮其他。&lt;/li>
&lt;li>&lt;strong>展示类设置跟随 client，运行类设置跟随 server。&lt;/strong> 主题、侧边栏布局、复制行为来自&lt;strong>你本地 client 的配置&lt;/strong>（你 SSH 过去，看到的还是自己的主题）；pane 默认值、worktree、集成、自定义命令属于&lt;strong>跑着 pane 的那台 server&lt;/strong>。这个切分很清晰，避免了&amp;quot;我改了主题怎么远程机器没变&amp;quot;这类困惑。&lt;/li>
&lt;/ol>
&lt;p>&lt;code>herdr --remote&lt;/code> 默认会生成一份&lt;strong>私有 SSH config&lt;/strong>（先 include 你的 &lt;code>~/.ssh/config&lt;/code>，再补上 &lt;code>ServerAliveInterval&lt;/code> / &lt;code>ServerAliveCountMax&lt;/code> 作为兜底，&lt;strong>你自己设的 keepalive 优先&lt;/strong>），并用私有 control socket 复用第一次认证的连接。不想让它插手就 &lt;code>manage_ssh_config = false&lt;/code>。&lt;/p>
&lt;p>另一个不在 tmux 里的东西是 &lt;strong>worktree&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr worktree list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr worktree create --branch feature/x --base main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr worktree open --branch feature/x
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr worktree remove --workspace w7
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它把 git worktree 拉成工作区的一组子行。&lt;strong>关闭父工作区会关掉这一组，但不会删掉 checkout 目录和分支&lt;/strong>；&lt;code>worktree remove&lt;/code> 会先请求安全删除，git 拒绝（有改动或未跟踪文件）时再二次确认，然后才 force。&lt;strong>分支永远不会被删掉。&lt;/strong> 这个安全设计我认为是正确的默认。&lt;/p>
&lt;hr>
&lt;h2 id="十一配置速查">十一、配置速查
&lt;/h2>&lt;p>配置文件：&lt;code>~/.config/herdr/config.toml&lt;/code>（Windows 是 &lt;code>%APPDATA%\herdr\config.toml&lt;/code>）。相关命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">herdr --help &lt;span class="c1"># 会显示解析到的配置路径&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr --default-config &lt;span class="c1"># 打印完整默认配置（带注释，是最好的文档）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr --default-config &amp;gt; ~/.config/herdr/config.toml &lt;span class="c1"># 全量落盘再改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr server reload-config &lt;span class="c1"># 热加载&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">herdr config reset-keys &lt;span class="c1"># 按键绑定重置&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Herdr &lt;strong>没有配置文件也能跑&lt;/strong>；值非法会退回安全默认并在启动时告警（不静默）。&lt;/p>
&lt;p>几个我认为值得一改的项：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">terminal&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">shell_mode&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;auto&amp;#34;&lt;/span> &lt;span class="c"># macOS 上 auto = 登录 shell，/usr/libexec/path_helper 和 Homebrew 的 PATH 才会生效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">new_cwd&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;follow&amp;#34;&lt;/span> &lt;span class="c"># follow(继承来源 pane) | home | current | 固定路径如 &amp;#34;~/Projects&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">status_indicators&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;symbols&amp;#34;&lt;/span> &lt;span class="c"># 用形状而非仅颜色区分四种状态&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">agent_panel_sort&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;priority&amp;#34;&lt;/span> &lt;span class="c"># 变成&amp;#34;谁在等我&amp;#34;的注意力队列&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">prompt_new_tab_name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">false&lt;/span> &lt;span class="c"># 新建 tab 不弹窗问名字，省一次交互&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toast&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">delivery&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;terminal&amp;#34;&lt;/span> &lt;span class="c"># SSH 场景下让外层终端发通知&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">server&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">headless_cols&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">160&lt;/span> &lt;span class="c"># 没有 client 连接时的虚拟终端宽度（默认 120x40）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">headless_rows&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">50&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>headless_rows/cols&lt;/code> 这个&lt;strong>不是小事&lt;/strong>：没有 client 连着时，server 用一个 120×40 的虚拟终端来算布局。&lt;strong>Agent 的 TUI 渲染是按 pane 尺寸做的&lt;/strong>——尺寸变了它会重排。所以你在无 client 状态下跑的 Agent，看到的界面尺寸和你回头附上去时不完全一致。这不是 bug，但值得知道。&lt;/p>
&lt;p>&lt;strong>主题与外观。&lt;/strong> 内置 11 套主题，也可以在主题上逐个覆盖颜色 token：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">theme&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;catppuccin&amp;#34;&lt;/span> &lt;span class="c"># catppuccin / terminal / tokyo-night / dracula / nord /&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c"># gruvbox / one-dark / solarized / kanagawa / rose-pine / vesper&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">auto_switch&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="c"># 跟随外层终端的明暗自动切换&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">dark_name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;catppuccin&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">light_name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;catppuccin-latte&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">theme&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">custom&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c"># 在选中的主题上覆盖单个颜色 token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">sidebar_bg&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;#181825&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">selection_bg&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;#313244&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">accent&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;#f5c2e7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>theme.custom&lt;/code> 的值可以是 hex、颜色名或 &lt;code>rgb(r,g,b)&lt;/code>，也能给某个面设 &lt;code>panel_bg = &amp;quot;reset&amp;quot;&lt;/code> 交还给外层终端；开了 &lt;code>auto_switch&lt;/code> 之后还能用 &lt;code>[theme.custom.light]&lt;/code> / &lt;code>[theme.custom.dark]&lt;/code> 给明暗两态各自叠一层。&lt;/p>
&lt;p>&lt;strong>中文输入法的三个开关。&lt;/strong> 都在 &lt;code>[experimental]&lt;/code> 里、默认关，但每个都值得中文用户试一次：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">experimental&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 按 prefix 时临时把系统输入源切成 ASCII，退出后恢复——中文输入法开着也能触发 prefix 命令&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">switch_ascii_input_source_in_prefix&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Claude Code / pi / codex 这类 TUI 自己画光标，macOS 输入法会把候选框跟丢；&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 把 pane 光标暴露给外层终端，输入法就能继续跟随&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">reveal_hidden_cursor_for_cjk_ime&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 只对检测到的这些 Agent 生效；留空则对所有聚焦 pane 生效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">cjk_ime_agents&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;claude&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;codex&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>代价和边界也说清楚：第一个是 best-effort、只覆盖 macOS 和 Windows，而且 &lt;strong>Windows 上目前只处理韩文 IME&lt;/strong>（中文输入法不受益），macOS 上是临时切 ASCII 键盘布局、可用；第二个会让&lt;strong>那些&amp;quot;隐藏光标又不用别的东西代替&amp;quot;的应用&lt;/strong>多出一个可见光标（比如 vim 的普通模式），介意的话就把 &lt;code>cjk_ime_agents&lt;/code> 列成常用 Agent，让它只在这些 pane 里生效。&lt;/p>
&lt;p>&lt;strong>几个散点：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>[terminal] kitty_graphics&lt;/code>（默认开）：外层终端支持 Kitty 图形协议时，pane 里的图片会真正渲染出来。&lt;/li>
&lt;li>&lt;strong>索引绑定&lt;/strong>：&lt;code>switch_tab = &amp;quot;prefix+1..9&amp;quot;&lt;/code>（默认）、&lt;code>switch_workspace = &amp;quot;prefix+shift+1..9&amp;quot;&lt;/code>、&lt;code>focus_agent = &amp;quot;prefix+alt+1..9&amp;quot;&lt;/code>——一行配置换来&amp;quot;按序号直达&amp;quot;；旧的 &lt;code>[keys.indexed]&lt;/code> 仍在解析，但新配置用这三个。&lt;/li>
&lt;li>&lt;code>[advanced] scrollback_limit_bytes&lt;/code>：每个 pane 的回滚上限，默认 10MB（对齐 Ghostty）。&lt;/li>
&lt;/ul>
&lt;p>环境变量一览：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>变量&lt;/th>
&lt;th>用途&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>HERDR_CONFIG_PATH&lt;/code>&lt;/td>
&lt;td>覆盖配置文件路径&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_SESSION&lt;/code>&lt;/td>
&lt;td>为 CLI 命令选择命名会话&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_SOCKET_PATH&lt;/code>&lt;/td>
&lt;td>覆盖 socket 路径&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_ENV&lt;/code>&lt;/td>
&lt;td>在 Herdr 管理的 pane 进程内设为 &lt;code>1&lt;/code>（技能文件的护栏靠它判断）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_PANE_ID&lt;/code> / &lt;code>HERDR_TAB_ID&lt;/code> / &lt;code>HERDR_WORKSPACE_ID&lt;/code>&lt;/td>
&lt;td>当前 pane 的标识&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_LOG&lt;/code>&lt;/td>
&lt;td>日志过滤，如 &lt;code>HERDR_LOG=herdr=debug&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>HERDR_DISABLE_SOUND&lt;/code>&lt;/td>
&lt;td>关掉声音&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>排障时日志在这三个文件：&lt;code>~/.config/herdr/herdr.log&lt;/code>、&lt;code>herdr-client.log&lt;/code>、&lt;code>herdr-server.log&lt;/code>。提问时记得连&lt;strong>轮转后的兄弟文件&lt;/strong>一起带上。&lt;/p>
&lt;hr>
&lt;h2 id="十二避坑清单">十二、避坑清单
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>坑&lt;/th>
&lt;th>现象&lt;/th>
&lt;th>正确做法&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>不装集成就指望状态准确&lt;/td>
&lt;td>&lt;code>agent list&lt;/code> 为空，全 &lt;code>unknown&lt;/code>&lt;/td>
&lt;td>&lt;code>herdr integration install &amp;lt;name&amp;gt;&lt;/code>，然后&lt;strong>在 Herdr 内重启该 Agent&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>以为重启能恢复进程&lt;/td>
&lt;td>shell 里跑到一半的命令没了&lt;/td>
&lt;td>只有&lt;strong>装了集成并上报过 session 引用&lt;/strong>的 Agent 能恢复对话，进程不恢复&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>&lt;code>popup&lt;/code> 命令里读 &lt;code>HERDR_PANE_ID&lt;/code>&lt;/td>
&lt;td>拿到空值，脚本行为异常&lt;/td>
&lt;td>popup 是会话级，用 &lt;code>HERDR_ACTIVE_PANE_ID&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>分两步 prompt + wait&lt;/td>
&lt;td>Agent 干太快，&lt;code>wait&lt;/code> 漏掉已完成状态&lt;/td>
&lt;td>用 &lt;code>--wait --until&lt;/code>，或 socket 层的 &lt;code>agent.prompt&lt;/code> + 内嵌 &lt;code>wait&lt;/code>（原子）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>拿 &lt;code>report-agent&lt;/code> 报展示信息&lt;/td>
&lt;td>通知和 wait 被错误状态触发，编排读错信号&lt;/td>
&lt;td>展示信息走 &lt;code>report-metadata&lt;/code>，语义状态才走 &lt;code>report-agent&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>读日志用 &lt;code>recent&lt;/code>&lt;/td>
&lt;td>长行被软折行切断，结构破碎&lt;/td>
&lt;td>读日志用 &lt;code>recent-unwrapped&lt;/code>；看用户所见用 &lt;code>visible&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>绑定普通按键没留意&lt;/td>
&lt;td>输入的字符被 Herdr 拦截&lt;/td>
&lt;td>一律用 &lt;code>prefix+&lt;/code>，除非明确要一个无前缀模式&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>改完配置不热加载&lt;/td>
&lt;td>改了半天没生效&lt;/td>
&lt;td>&lt;code>herdr server reload-config&lt;/code>；按键乱掉用 &lt;code>herdr config reset-keys&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>忘了无 client 时的尺寸&lt;/td>
&lt;td>Agent TUI 在附着前后布局不一致&lt;/td>
&lt;td>用 &lt;code>[server] headless_cols/rows&lt;/code> 定成你常用的尺寸&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>&lt;code>rows_by_agent&lt;/code> 用检测别名&lt;/td>
&lt;td>配置不生效&lt;/td>
&lt;td>key 必须是规范 ID（&lt;code>claude&lt;/code>），不能用 &lt;code>claude-code&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>11&lt;/td>
&lt;td>以为关工作区会删 worktree&lt;/td>
&lt;td>担心丢代码&lt;/td>
&lt;td>关父工作区只关 group，&lt;strong>不删目录、不删分支&lt;/strong>；&lt;code>worktree remove&lt;/code> 有二次确认&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>12&lt;/td>
&lt;td>从 Herdr 里再启一个 Herdr&lt;/td>
&lt;td>起不来&lt;/td>
&lt;td>&lt;code>[experimental] allow_nested = false&lt;/code> 是默认，确实需要才开&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>13&lt;/td>
&lt;td>&lt;code>herdr update&lt;/code> 以为会立即生效&lt;/td>
&lt;td>更新了但行为没变&lt;/td>
&lt;td>看 &lt;code>herdr status&lt;/code> 里的 &lt;code>restart_needed&lt;/code> / &lt;code>server_binary_stale&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>14&lt;/td>
&lt;td>右键开不出 Herdr 菜单&lt;/td>
&lt;td>该 pane 开了右键转发&lt;/td>
&lt;td>右键&lt;strong>窗格边框&lt;/strong>唤回菜单；或右键菜单里切回 &amp;ldquo;Use Herdr right-click menu&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>15&lt;/td>
&lt;td>对 brew / mise / Nix 安装跑 &lt;code>herdr update&lt;/code>&lt;/td>
&lt;td>不生效或被拒&lt;/td>
&lt;td>用各自的包管理器升级；只有直接安装能用 &lt;code>herdr update&lt;/code> 和预览通道&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>以为 server 重启后屏幕内容还在&lt;/td>
&lt;td>恢复出的是空白新 shell&lt;/td>
&lt;td>屏幕回放要开 &lt;code>[experimental] pane_history = true&lt;/code>，且其中内容按终端历史对待（可能有敏感输出）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>17&lt;/td>
&lt;td>中文输入法开着时 prefix 按键失灵&lt;/td>
&lt;td>prefix 命令不触发&lt;/td>
&lt;td>&lt;code>switch_ascii_input_source_in_prefix = true&lt;/code>（macOS 可用；Windows 目前只处理韩文 IME）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>18&lt;/td>
&lt;td>装完插件等它自己出现&lt;/td>
&lt;td>什么都没发生&lt;/td>
&lt;td>插件只声明 action 和窗格入口，要自己绑 &lt;code>[[keys.command]] type = &amp;quot;plugin_action&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>19&lt;/td>
&lt;td>以为插件只属于某个会话&lt;/td>
&lt;td>别的窗口里也看到了&lt;/td>
&lt;td>插件与启用状态是当前用户全局的；v1 也没有 &lt;code>plugin update&lt;/code>，刷新靠重装&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>20&lt;/td>
&lt;td>拿 &lt;code>plugin pane open --help&lt;/code> 当选型依据&lt;/td>
&lt;td>以为开不了 popup&lt;/td>
&lt;td>0.9.0 的 help 少列了 &lt;code>popup&lt;/code> 和 &lt;code>--width&lt;/code> / &lt;code>--height&lt;/code>，以文档和源码为准&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="十三它适合谁">十三、它适合谁
&lt;/h2>&lt;p>我觉得下面这几类人会立刻感觉到收益：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>多 Agent 并行的人&lt;/strong>：侧边栏统一状态 + &lt;code>blocked&lt;/code> 优先队列，直接替掉了&amp;quot;挨个窗口看一眼&amp;quot;这个动作&lt;/li>
&lt;li>&lt;strong>经常 SSH 到别的机器跑长任务的人&lt;/strong>：&lt;code>--remote&lt;/code> + 已保存机器，本地和远程混在一个视图里；临时想看一眼进度，手机装个 SSH 客户端就能盯同一批 Agent&lt;/li>
&lt;li>&lt;strong>在搭多 Agent 编排的人&lt;/strong>：这是 Herdr 真正的差异点——&lt;code>agent.prompt --wait&lt;/code> 和 &lt;code>events.subscribe&lt;/code> 让你可以用&lt;strong>语义状态&lt;/strong>而不是输出字符串匹配来调度 Agent。绝大多数同类工具只做到&amp;quot;起了个 pane&amp;quot;&lt;/li>
&lt;li>&lt;strong>不想学新键位的人&lt;/strong>：鼠标能完成一切，前缀键是 &lt;code>ctrl+b&lt;/code>，tmux 用户可以无痛切换&lt;/li>
&lt;/ul>
&lt;p>反过来说，如果你只是单窗口单 Agent 偶尔用一下，Herdr 的价值会被大幅摊薄——它解决的问题主要来自&amp;quot;数量&amp;quot;。&lt;/p>
&lt;p>也必须承认，它几乎没有全新概念：window 换成 workspace、tmux 式的状态栏、轮询换成事件订阅，每一项都能在别处找到。它的价值在&lt;strong>组合&lt;/strong>——把这些拼成一个&amp;quot;以 Agent 为一等公民&amp;quot;的工作区，而且做得相当克制（Rust 单二进制、无 Electron、不做静默自动更新）。&lt;/p>
&lt;p>如果只能记一件事：&lt;strong>Herdr 的重点不是&amp;quot;同时开好几个 Agent&amp;quot;，而是让 Agent 的状态变成可被程序读取、可被别的 Agent 等待的信号。&lt;/strong> 想明白这一点，&lt;code>agent wait&lt;/code> 和 &lt;code>report-agent&lt;/code> 那一堆 API 的用法就都顺了。&lt;/p>
&lt;hr>
&lt;h2 id="参考">参考
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://herdr.dev" target="_blank" rel="noopener"
>herdr.dev&lt;/a> —— 官网与文档（有中文）&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/herdrdev/herdr" target="_blank" rel="noopener"
>github.com/herdrdev/herdr&lt;/a> —— 源码（Apache-2.0，Rust）&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/zh-cn/docs/quick-start/" target="_blank" rel="noopener"
>快速开始&lt;/a> / &lt;a class="link" href="https://herdr.dev/zh-cn/docs/configuration/" target="_blank" rel="noopener"
>配置&lt;/a> / &lt;a class="link" href="https://herdr.dev/zh-cn/docs/cli-reference/" target="_blank" rel="noopener"
>CLI 参考&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/zh-cn/docs/session-state/" target="_blank" rel="noopener"
>会话状态与恢复&lt;/a> —— 从活持久到热交接的五种恢复路径&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/zh-cn/docs/how-to-work/" target="_blank" rel="noopener"
>怎么用：本地、SSH、手机&lt;/a> —— 三种远程姿势怎么选&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/zh-cn/docs/socket-api/" target="_blank" rel="noopener"
>Socket API&lt;/a> —— 编程控制与事件订阅&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/zh-cn/docs/agent-skill/" target="_blank" rel="noopener"
>Agent 技能文件&lt;/a> —— 给 Agent 看的用法说明&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/plugins/" target="_blank" rel="noopener"
>插件市场&lt;/a> —— GitHub 上 &lt;code>herdr-plugin&lt;/code> 仓库的自动索引（未审核）&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/zh-cn/docs/plugins/" target="_blank" rel="noopener"
>插件作者文档&lt;/a> —— manifest、命令、环境变量与信任模型的权威参考&lt;/li>
&lt;li>&lt;a class="link" href="https://herdr.dev/agent-guide.md" target="_blank" rel="noopener"
>agent-guide.md&lt;/a> —— 让 Agent 帮你装/排查 Herdr 的提示词指南&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>图片来源&lt;/strong>：文中截图取自 herdr 官方文档与 GitHub README 的演示素材（&lt;a class="link" href="https://herdr.dev/zh-cn/docs/how-to-work/" target="_blank" rel="noopener"
>herdr.dev/zh-cn/docs/how-to-work&lt;/a> 与 &lt;a class="link" href="https://github.com/herdrdev/herdr" target="_blank" rel="noopener"
>README&lt;/a> 中的演示视频，手机截图来自前者），项目以 Apache-2.0 授权。截图中的仓库名、分支名、模型名为官方演示环境内容。&lt;/p>
&lt;/blockquote></description></item><item><title>CC Switch 详解：一个应用管住八个 AI 编程 CLI</title><link>https://www.zata.cc/p/cc-switch-guide/</link><pubDate>Tue, 15 Sep 2026 09:30:00 +0800</pubDate><guid>https://www.zata.cc/p/cc-switch-guide/</guid><description>&lt;img src="https://www.zata.cc/p/cc-switch-guide/images/index/index.svg" alt="Featured image of post CC Switch 详解：一个应用管住八个 AI 编程 CLI" />&lt;p>整理开发机时发现一件小事：&lt;code>brew list --cask --versions cc-switch&lt;/code> 报的版本是 3.14.1，但打开 CC Switch 的界面，关于页写的是 3.20.0。不是装错了——这个 cask 带 &lt;code>auto_updates&lt;/code> 标记，应用自己会联网更新，Homebrew 的账本追不上它。&lt;/p>
&lt;p>一个配置管理工具，几个月里自己跑出去六个小版本。这个更新频率本身就是信号：它要追的那群 AI 编程 CLI，配置面正在快速膨胀。&lt;/p>
&lt;p>这篇文章把 CC Switch 讲清楚——它解决什么问题、切换时内部到底改了什么、六大功能模块各自能做到什么程度、装和升级要注意什么，以及最容易被忽略的两块风险。先说结论：&lt;strong>它本质上不是一个「供应商切换器」，而是给八个 AI 编程工具做的一层本地配置管理层，切换只是这层管理层里最容易被看见的那个动作。&lt;/strong>&lt;/p>
&lt;h2 id="一它要解决的问题配置面已经失控">一、它要解决的问题：配置面已经失控
&lt;/h2>&lt;p>现代 AI 编程很少只用一个 CLI。常见组合是 Claude Code 写主力逻辑、Codex 处理特定任务、Gemini CLI 做交叉验证，再加上 OpenCode、OpenClaw 这类各有侧重的工具。&lt;/p>
&lt;p>麻烦在于，每个工具一套配置，格式还都不一样：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Claude Code&lt;/strong> 走 &lt;code>~/.claude/&lt;/code> 下的 settings 与环境变量（&lt;code>ANTHROPIC_BASE_URL&lt;/code>、&lt;code>ANTHROPIC_AUTH_TOKEN&lt;/code>）；&lt;/li>
&lt;li>&lt;strong>Codex&lt;/strong> 走 &lt;code>~/.codex/config.toml&lt;/code> 加 &lt;code>auth.json&lt;/code>；&lt;/li>
&lt;li>其他工具各有自己的 JSON、TOML 或 &lt;code>.env&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>切换一个 API 供应商，意味着要按各家的格式分别手改一遍：改 base URL、换 token、别把别的不相关字段写坏。用几个工具就要重复几遍，漏改一个，那个工具就会继续拿着旧地址和旧 key 请求——而且往往不是立刻报错，是在你以为一切正常的时候返回一堆看不懂的失败。&lt;/p>
&lt;p>MCP 服务器和 Skills 更麻烦。同一份 MCP 配置要在多个应用里各维护一份；提示词文件更是分裂成 &lt;code>CLAUDE.md&lt;/code>、&lt;code>AGENTS.md&lt;/code>、&lt;code>GEMINI.md&lt;/code> 三个名字，内容基本一样，改一处就要记得同步另外两处。&lt;/p>
&lt;p>CC Switch 就是冲着这一层来的。需要先划清边界：&lt;strong>它管的是「配置怎么存、怎么切、怎么同步」，不负责判断某个供应商好不好。&lt;/strong> 本文同样不评价任何具体供应商的质量，只讨论管理这件事本身。&lt;/p>
&lt;h2 id="二cc-switch-是什么">二、CC Switch 是什么
&lt;/h2>&lt;p>一句话：一个开源的跨平台桌面应用，把上述所有工具的供应商配置、MCP、Skills、用量和会话收拢到一个界面里管理。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>项目&lt;/th>
&lt;th>情况&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>仓库&lt;/td>
&lt;td>GitHub &lt;code>farion1231/cc-switch&lt;/code>，MIT 协议，官网 ccswitch.io&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>技术栈&lt;/td>
&lt;td>Tauri 2 + Rust 后端，React 18 + TypeScript 前端&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>体量&lt;/td>
&lt;td>创建于 2025 年 8 月，截至 2026-09-15 约 13.2 万 star、9,100 fork&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>平台&lt;/td>
&lt;td>Windows 10+ / macOS 12+ / Ubuntu 22.04+、Debian 11+、Fedora 34+&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>支持工具&lt;/td>
&lt;td>Claude Code、Claude Desktop、Codex、Gemini CLI、Grok Build、OpenCode、OpenClaw、Hermes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-main.png"
width="1400"
height="937"
srcset="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-main_hu12993664452230040854.png 480w, https://www.zata.cc/p/cc-switch-guide/images/cc-switch-main_hu14237404917552155016.png 1024w"
loading="lazy"
alt="CC Switch 主界面"
class="gallery-image"
data-flex-grow="149"
data-flex-basis="358px"
>&lt;/p>
&lt;p>&lt;em>▲ 主界面顶部按应用分标签页（Claude / Codex / Gemini…），每个供应商一张卡片；当前激活的那张会显示用量与余额。图：CC Switch 官方仓库&lt;/em>&lt;/p>
&lt;p>八个工具的配置，被抽象成同一套「供应商」模型：一张卡片 = 一组配置 = 一个可以一键启用的状态。这就是它全部设计的地基，后面所有功能都建立在这一点上。&lt;/p>
&lt;h2 id="三切换的时候内部到底改了什么">三、切换的时候，内部到底改了什么
&lt;/h2>&lt;p>理解 CC Switch 的关键，是搞清楚它的数据存在哪、切换时动了什么。&lt;/p>
&lt;p>&lt;strong>数据分两层存&lt;/strong>。可同步的数据——供应商、MCP、提示词、技能——进 SQLite（&lt;code>~/.cc-switch/cc-switch.db&lt;/code>），它被当作单一事实源（SSOT）；只跟这台设备有关的偏好设置进 &lt;code>~/.cc-switch/settings.json&lt;/code>。分开的好处很直接：把数据库同步到多台机器，不会把 A 机器的窗口位置和 B 机器的主题设置搅在一起。&lt;/p>
&lt;p>&lt;strong>切换是双向同步，不是单向覆盖。&lt;/strong> 启用某个供应商时，它把配置写进对应 CLI 的 live 配置文件；反过来，当你编辑当前正在生效的那个供应商时，它会先从 live 文件把数据回填回来。这条设计解决的是一个很实际的场景：你在 CLI 里手动调过某个参数，如果不回填，下次切换就会把你的手改抹掉。&lt;/p>
&lt;p>&lt;strong>写入用「临时文件 + 重命名」的原子操作&lt;/strong>，避免写到一半崩溃留下半截 JSON 把工具搞挂。同时有自动备份机制，&lt;code>~/.cc-switch/backups/&lt;/code> 轮换保留最近 10 份，Skills 相关另有一份保留 20 份。&lt;/p>
&lt;p>&lt;strong>设计原则是「最小侵入」&lt;/strong>：任何时刻至少保留一个激活中的供应商，所以你没法把配置删空；反过来，就算直接卸载 CC Switch，已经写进各工具的配置仍然有效，不会让工具用不了。&lt;/p>
&lt;p>有一个细节值得单独说：&lt;strong>生效方式不统一&lt;/strong>。大多数工具需要重启终端或 CLI 才能读到新配置，只有 Claude Code 支持热切换，改完即生效。所以「我切了怎么没反应」这类困惑，多数时候答案就是重启终端。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-add-provider.png"
width="1400"
height="907"
srcset="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-add-provider_hu443146391153092523.png 480w, https://www.zata.cc/p/cc-switch-guide/images/cc-switch-add-provider_hu11409294693094292484.png 1024w"
loading="lazy"
alt="添加供应商界面"
class="gallery-image"
data-flex-grow="154"
data-flex-basis="370px"
>&lt;/p>
&lt;p>&lt;em>▲ 添加供应商时，预设列表覆盖了主流模型厂商与社区中转服务，选中后通常只需填 API Key，请求地址已预设好。图：CC Switch 官方仓库&lt;/em>&lt;/p>
&lt;p>还有个容易踩的坑值得提前知道：切换供应商后，你在某个工具里装的插件配置可能「不见了」。原因是插件配置往往写在同一个配置文件里，切换时没被带过去。官方的解法是「通用配置片段」——在编辑供应商的面板里点「从当前供应商提取」，把 Key 和请求地址之外的通用数据抽出来存成片段；之后新建供应商时勾选「应用通用配置」（默认勾选），这些数据就会被一并写入。你首次导入的那份默认供应商也会完整保留所有配置项。&lt;/p>
&lt;h2 id="四功能地图六个模块各自做到什么程度">四、功能地图：六个模块各自做到什么程度
&lt;/h2>&lt;h3 id="41-供应商管理">4.1 供应商管理
&lt;/h3>&lt;p>核心是预设体系：内置 50+ 预设，覆盖 AWS Bedrock、NVIDIA NIM 以及大量社区中转服务，多数预设只需填 API Key。此外支持拖拽排序、导入导出、系统托盘一键切换。&lt;/p>
&lt;p>有一个值得注意的抽象叫「&lt;strong>通用供应商&lt;/strong>」：一份配置同时同步到 Claude Code、Codex 和 Gemini CLI。如果你的诉求是「所有工具都指向同一个入口」，这比逐工具配置省事得多。&lt;/p>
&lt;p>回切官方登录也是支持的：添加一个「官方登录」预设，切过去后跑一遍 Log out / Log in 流程，之后就能在官方与第三方之间来回切。Codex 还支持在多个官方账号（比如 Plus 和 Team）之间切换。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-keep-official-login.png"
width="1400"
height="911"
srcset="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-keep-official-login_hu724014555415596348.png 480w, https://www.zata.cc/p/cc-switch-guide/images/cc-switch-keep-official-login_hu10315290773723943246.png 1024w"
loading="lazy"
alt="切换第三方时保留官方登录"
class="gallery-image"
data-flex-grow="153"
data-flex-basis="368px"
>&lt;/p>
&lt;p>&lt;em>▲ 设置里的「Codex 应用增强」：开启后，使用第三方 API 期间仍可保留官方登录态，从而继续使用官方插件与手机远程操作等功能。图：CC Switch 官方仓库&lt;/em>&lt;/p>
&lt;h3 id="42-本地代理与故障转移">4.2 本地代理与故障转移
&lt;/h3>&lt;p>这是整个应用里分量最重、也最需要理解成本的一块。&lt;/p>
&lt;p>它会在本地起一个代理服务（默认 &lt;code>http://127.0.0.1:15721&lt;/code>），支持&lt;strong>格式转换&lt;/strong>——把一种 API 协议转成另一种，让某个工具走它本来不支持的供应商；支持&lt;strong>自动故障转移&lt;/strong>和&lt;strong>熔断器&lt;/strong>，某个上游挂了自动切到备用；还有&lt;strong>供应商健康监控&lt;/strong>和&lt;strong>整流器&lt;/strong>。&lt;/p>
&lt;p>&lt;strong>应用级接管&lt;/strong>是另一个维度：可以独立为 Claude、Codex、Gemini 或 Grok Build 配置代理，粒度细到单个供应商。也就是说，你可以只让 Codex 走本地代理，Claude Code 保持直连。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-local-route.png"
width="1400"
height="1462"
srcset="https://www.zata.cc/p/cc-switch-guide/images/cc-switch-local-route_hu3487382886892356485.png 480w, https://www.zata.cc/p/cc-switch-guide/images/cc-switch-local-route_hu13352276742254584529.png 1024w"
loading="lazy"
alt="本地路由设置"
class="gallery-image"
data-flex-grow="95"
data-flex-basis="229px"
>&lt;/p>
&lt;p>&lt;em>▲ 设置 → 路由：路由总开关、逐应用接管开关（图中只开了 Codex）、服务地址与实时统计。图：CC Switch 官方仓库&lt;/em>&lt;/p>
&lt;p>代价要说清楚：启用了本地代理，请求路径就多了一跳，排查问题时需要先判断是上游的问题还是本地代理的问题。好在开关是显式的，出问题可以直接关掉路由总开关回到直连。&lt;/p>
&lt;h3 id="43-mcpprompts-与-skills-统一管理">4.3 MCP、Prompts 与 Skills 统一管理
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>MCP&lt;/strong>：一个面板管理 Claude、Codex、Gemini、Grok Build、OpenCode、Hermes 六个应用的 MCP 服务器，支持双向同步和 Deep Link 导入。&lt;/li>
&lt;li>&lt;strong>Prompts&lt;/strong>：带 Markdown 编辑器，能跨应用同步到 &lt;code>CLAUDE.md&lt;/code> / &lt;code>AGENTS.md&lt;/code> / &lt;code>GEMINI.md&lt;/code>，并且有回填保护——不会把你手写的文件覆盖掉。&lt;/li>
&lt;li>&lt;strong>Skills&lt;/strong>：从 GitHub 仓库或 ZIP 一键安装，支持自定义仓库管理，同步方式可选软连接（省磁盘、实时同步）或文件复制（Windows 上更省事）。&lt;/li>
&lt;/ul>
&lt;h3 id="44-用量与成本追踪">4.4 用量与成本追踪
&lt;/h3>&lt;p>跨供应商追支出、请求数和 Token 用量，配趋势图表、逐条请求日志和自定义模型定价。对同时挂着多个供应商的人来说，这是把「这个月钱花哪了」变成可回答问题的功能。会话记录的扫描做了增量优化，大文件的解析速度在 3.20.1 里从秒级降到了毫秒级。&lt;/p>
&lt;h3 id="45-会话管理与工作区">4.5 会话管理与工作区
&lt;/h3>&lt;p>可以浏览、搜索、恢复各工具的历史会话。OpenClaw 用户另有工作区编辑器，能直接编辑 &lt;code>AGENTS.md&lt;/code>、&lt;code>SOUL.md&lt;/code> 这类 Agent 文件并预览 Markdown。&lt;/p>
&lt;h3 id="46-系统集成">4.6 系统集成
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>云同步&lt;/strong>：把配置目录挂到 Dropbox、OneDrive、iCloud、坚果云、NAS，或直接用 WebDAV 服务器同步，实现多机一致。&lt;/li>
&lt;li>&lt;strong>Deep Link&lt;/strong>：&lt;code>ccswitch://&lt;/code> 协议，用一条 URL 导入供应商、MCP、提示词或技能——适合团队分发统一配置。&lt;/li>
&lt;li>&lt;strong>日常项&lt;/strong>：深浅色主题、开机自启、自动更新、国际化（简中/繁中/英/日），以及一组解决首次安装登录确认、签名限制、插件同步等问题的「小工具」。&lt;/li>
&lt;/ul>
&lt;h2 id="五安装与升级">五、安装与升级
&lt;/h2>&lt;p>各平台路径都很常规，macOS 推荐 Homebrew：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew install --cask cc-switch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew upgrade --cask cc-switch &lt;span class="c1"># 升级&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Windows 用 Releases 页的 &lt;code>.msi&lt;/code> 或便携版 zip；Arch 用 &lt;code>paru -S cc-switch-bin&lt;/code>；其他 Linux 发行版用 &lt;code>.deb&lt;/code> / &lt;code>.rpm&lt;/code> / &lt;code>.AppImage&lt;/code>。macOS 包已经过 Apple 签名和公证，装完直接打开，不需要额外绕过 Gatekeeper。&lt;/p>
&lt;p>回到开头那个版本号对不上的现象，它有个实际影响：&lt;strong>别把 Homebrew 的版本号当成真实版本&lt;/strong>。因为 cask 带 &lt;code>auto_updates&lt;/code>，应用会自己更新，&lt;code>brew list&lt;/code> 显示的可能是几个月前的老数字。想知道当前版本，看应用界面更准。&lt;/p>
&lt;p>升级路径上有一个需要留意的节点。3.20.x 这一轮里，&lt;strong>3.20.1 带了数据库 schema 从 v17 到 v18 的迁移&lt;/strong>（升级前会自动备份，但如果要降级，必须用备份恢复）；同一版还调整了 Codex 的第三方切换机制，改成只写配置文件、不再动 &lt;code>auth.json&lt;/code>，Codex 的 OAuth 账号需要重新登录一次。后面两个版本没有 schema 变更。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>版本&lt;/th>
&lt;th>日期&lt;/th>
&lt;th>关键变化&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>3.20.1&lt;/td>
&lt;td>08-28&lt;/td>
&lt;td>适配 Codex CLI 0.149，切换改为「仅写配置」；同工作区多 ChatGPT 账号不再互相覆盖；DB schema v17→v18&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3.20.2&lt;/td>
&lt;td>09-07&lt;/td>
&lt;td>Grok 走 xAI 原生 Responses API；恢复并行工具调用；修复提示词前缀缓存被破坏等问题&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3.20.3&lt;/td>
&lt;td>09-11&lt;/td>
&lt;td>新增「禁用 Artifact 工具」开关；修复空 reasoning 占位符刷屏、Codex 长任务中途停止；无 schema 变更&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="六边界与风险">六、边界与风险
&lt;/h2>&lt;p>任何工具的能力边界都值得写清楚，这个尤其。&lt;/p>
&lt;p>&lt;strong>它适合的&lt;/strong>：同时用两个以上 AI 编程 CLI、需要频繁在多个供应商之间切换、在多地多台机器上用同一套配置、或者想给团队分发统一配置的人。收益随工具数量和切换频率线性上升。&lt;/p>
&lt;p>&lt;strong>它不适合的&lt;/strong>：只用一家供应商、只用一个工具、配置写死就不动的人。这种情况下它的全部价值只剩一个可视化的配置编辑器，装它的必要不大。&lt;/p>
&lt;p>&lt;strong>风险一，也是最需要清醒对待的：预设列表不等于推荐列表。&lt;/strong> 内置的 50+ 预设里，有相当一部分是第三方 API 中转服务——这一点从 README 里占了大篇幅的赞助商区块就能直观感受到。CC Switch 的立场是中立的：它提供的是「方便地接入」这个能力，不对任何预设的模型质量、稳定性、计费透明度或合规性做背书。&lt;strong>工具替你降低了切换成本，但没有替你承担选择成本。&lt;/strong> 用第三方中转意味着你的请求和密钥要经过第三方，这部分判断得自己做。&lt;/p>
&lt;p>&lt;strong>风险二，本地代理接管改变了请求路径。&lt;/strong> 排障时多一层变量，建议在真正依赖它之前先小范围试一次，确认故障转移的触发条件和日志能看懂。&lt;/p>
&lt;p>&lt;strong>风险三，配置文件的归属权冲突。&lt;/strong> CC Switch 会写各工具的 live 配置文件。如果你另有脚本、dotfiles 管理工具或手工流程也在改同一份文件，两边会互相覆盖。原子写入和自动备份能防损坏，防不了逻辑冲突。&lt;/p>
&lt;p>顺带一个冷知识，Linux + NVIDIA 用户可能会撞上：AppImage 默认强制走 XWayland，在较新的 Wayland + NVIDIA 环境下会出现界面点不动或缩放后黑屏。官方的逃生开关是 &lt;code>CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage&lt;/code>。&lt;/p>
&lt;h2 id="七我的判断">七、我的判断
&lt;/h2>&lt;p>CC Switch 真正的价值，不在于「一键切换」这个动作节省的那几十秒，而在于它把散落在八个工具、三种格式里的配置，收敛成了一个有备份、有回滚、有单一事实源的本地管理层。&lt;/p>
&lt;p>这个转变的意义在于：你开始可以&lt;strong>回答&lt;/strong>「我现在到底在用哪套配置」这个问题了。在此之前，这个问题要靠翻四五个目录、比对若干份配置文件才能回答；现在它是一张卡片上的一行字。&lt;/p>
&lt;p>值得提醒的是这个结论成立的前提：它成立，是因为你把多个工具和多个供应商当成常态来用。如果你的工作流只有一个工具加一个供应商，CC Switch 解决的是一个你并不存在的问题。工具的价值永远取决于它减掉的那部分复杂度，是不是真的存在于你的工作里。&lt;/p>
&lt;h2 id="总结">总结
&lt;/h2>&lt;ul>
&lt;li>CC Switch 是给八个 AI 编程 CLI 做配置管理的开源桌面应用（Tauri 2 + Rust，MIT，13 万 star），&lt;strong>配置管理层&lt;/strong>才是它的定位，供应商切换只是入口。&lt;/li>
&lt;li>内部机制是 SQLite 单一事实源 + 设备级 JSON 双层存储、切换双向同步、原子写入、自动备份；设计上保证最小侵入，卸载不留坑。&lt;/li>
&lt;li>六个功能模块里，本地代理与故障转移最重、也最需要理解成本；MCP / Prompts / Skills 的统一管理是日常使用中最省事的部分。&lt;/li>
&lt;li>安装优先走 Homebrew 或官方包；升级注意 3.20.1 的数据库迁移和 Codex OAuth 需要重新登录一次；别信 &lt;code>brew list&lt;/code> 的版本号，应用会自更新。&lt;/li>
&lt;li>预设列表里有大量第三方中转服务，工具降低了切换成本，但没有降低&lt;strong>判断该切到哪儿&lt;/strong>的成本——这部分始终要自己做。&lt;/li>
&lt;/ul>
&lt;p>它把「切换」这件事的成本压到了最低，剩下的成本，是你判断该切到哪儿的成本。&lt;/p></description></item><item><title>为 AI 而写的 CLI 设计指南：原则、避坑与难点</title><link>https://www.zata.cc/p/agent-friendly-cli/</link><pubDate>Thu, 10 Sep 2026 15:30:00 +0800</pubDate><guid>https://www.zata.cc/p/agent-friendly-cli/</guid><description>&lt;img src="https://www.zata.cc/p/agent-friendly-cli/images/index/index.svg" alt="Featured image of post 为 AI 而写的 CLI 设计指南：原则、避坑与难点" />&lt;p>最近在给 FreshAI 写一个供 AI 操作外部能力的命令行工具，落地之后回头总结，发现它和「给人用的 CLI」几乎是两套设计哲学。传统 CLI 是给一个坐在 tty 前、能看进度、会按 &lt;code>y/n&lt;/code> 的人设计的；而 Agent 看不到屏幕，它只能拿到两样东西：&lt;strong>stdout 的字节流&lt;/strong>和&lt;strong>进程退出码&lt;/strong>。一旦这两样东西被污染或语义不清，Agent 就会「飞盲」——重试已经成功的操作、放弃其实失败了的操作，或者把一段散文当数据去解析。&lt;/p>
&lt;p>这篇文章把我踩过的坑整理成一套可复用的设计准则，骨架来自 FreshAI CLI（&lt;code>packages/fresh-cli&lt;/code>，唯一依赖 &lt;code>httpx&lt;/code> 的独立 Python 包），并对照 &lt;a class="link" href="https://aclig.dev/" target="_blank" rel="noopener"
>Agent CLI Guidelines&lt;/a>、&lt;a class="link" href="https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46" target="_blank" rel="noopener"
>12 Factor CLI Apps&lt;/a>、&lt;a class="link" href="https://clig.dev/" target="_blank" rel="noopener"
>clig.dev&lt;/a> 这些公开规范。核心心法只有一句：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>把 CLI 当成一个「传输层是 argv / stdout / 退出码」的 RPC 接口，人只是其中一个调用方。&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="一为什么-agent-时代-cli-又重要了">一、为什么 Agent 时代 CLI 又重要了
&lt;/h2>&lt;ul>
&lt;li>&lt;strong>Agent 天生会用 CLI。&lt;/strong> LLM 的训练语料里有大量 shell 命令，它对 &lt;code>git submodule add&lt;/code>、&lt;code>pip install&lt;/code> 这类「动词-名词」结构有很强的先验。相比之下，让 Agent 现学一个私有 SDK 的调用姿势，要额外喂文档、额外占上下文。&lt;/li>
&lt;li>&lt;strong>CLI 是最省 token、最可组合的能力接口。&lt;/strong> 不需要常驻进程、不需要握手协议，一个 &lt;code>exec&lt;/code> 就能调用；输出可以裁剪到 Agent 真正需要的字段。&lt;/li>
&lt;li>&lt;strong>但传统 CLI 的三条默认行为，恰好都是 Agent 的天敌：&lt;/strong> ①默认交互、②进度和结果混在一起打印、③错误只给人看。&lt;/li>
&lt;/ul>
&lt;p>所以「为 AI 写 CLI」不是发明一套新东西，而是把 CLI 里那些&lt;strong>给机器看的契约&lt;/strong>显式化、稳定化。&lt;/p>
&lt;hr>
&lt;h2 id="二设计原则到底该怎么写">二、设计原则：到底该怎么写
&lt;/h2>&lt;h3 id="21-默认非交互non-interactive-by-default">2.1 默认非交互（non-interactive by default）
&lt;/h3>&lt;p>Agent 没有 tty，任何 &lt;code>input()&lt;/code> / &lt;code>confirm()&lt;/code> / 分页器都会把它卡死直到超时。FreshAI CLI 的做法是：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>只有显式命令进入交互流程。&lt;/strong> 全部命令里，只有 &lt;code>fresh login&lt;/code> 会打开浏览器，其余命令一律非交互。&lt;/li>
&lt;li>&lt;strong>危险操作不靠「逐次确认」保护，而靠「独立命令 + 最小权限」。&lt;/strong> 「直接公开发布」不是给草稿命令加一个 &lt;code>--yes&lt;/code>，而是一条独立命令 &lt;code>fresh blog publish&lt;/code>，并且在登录授权时就必须单独拿到 &lt;code>blogs:publish&lt;/code> scope。这样 Agent 的「公开」意图是可审计、可拒绝的，而不是被一个交互弹窗挡住的。&lt;/li>
&lt;/ul>
&lt;p>这是 &lt;a class="link" href="https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46" target="_blank" rel="noopener"
>12 Factor CLI Apps&lt;/a> 第 7 条（stdin 不是 tty 时也必须能跑完）在 Agent 场景下的强化版：&lt;strong>交互是例外，不是默认。&lt;/strong>&lt;/p>
&lt;h3 id="22-stdout-只放结果stderr-只放诊断">2.2 stdout 只放结果，stderr 只放诊断
&lt;/h3>&lt;p>这条老规矩在 Agent 场景里是生死线。Agent 往往直接 &lt;code>json.loads(stdout)&lt;/code>，你混进一行「正在上传…」，解析立刻崩。&lt;/p>
&lt;p>FreshAI CLI 的分工：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>通道&lt;/th>
&lt;th>内容&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>stdout&lt;/td>
&lt;td>最终结果：&lt;code>--json&lt;/code> 时是唯一的 envelope；文本模式是正文或结果行&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>stderr&lt;/td>
&lt;td>进度、提示、诊断、&lt;code>request-id&lt;/code>、健康检查详情、中断消息&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>实现上就两个函数，所有输出都必须走它们，不允许 &lt;code>print()&lt;/code> 裸奔：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_emit_progress&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;输出脱敏进度信息到 stderr。&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stderr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">print_json_envelope&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;把 envelope 以 UTF-8 JSON 写到 stdout（单行结束，无多余日志）。&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dump&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">flush&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意 &lt;code>flush()&lt;/code>：Agent 常常边读边解析，缓冲没冲出去就是「命令卡住」的经典假象。&lt;/p>
&lt;h3 id="23-结构化输出--版本化-schema">2.3 结构化输出 + 版本化 schema
&lt;/h3>&lt;p>成功的输出和失败的输出必须是&lt;strong>同一个形状&lt;/strong>，这样 Agent 只需要一个解析器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;schema_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;request_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;…&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>失败时 &lt;code>ok=false, result=null&lt;/code>，把细节放进 &lt;code>error&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;schema_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;code&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;scope_denied&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;当前凭证缺少 blogs:publish 授权&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;http_status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">403&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;execution_status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;not_started&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;request_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三个细节值得强调：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>schema_version&lt;/code> 是给未来留的活路。&lt;/strong> 字段只增不改不删（append-only contract），Agent 脚本就不会因为一次升级集体失灵。&lt;/li>
&lt;li>&lt;strong>&lt;code>error.code&lt;/code> 是 string，不是 HTTP 状态码的复读。&lt;/strong> &lt;code>invalid_input / login_required / authorization_expired / scope_denied / not_found / request_conflict / resource_state_changed / network_error / protocol_error&lt;/code> 这些码才是 Agent 分支判断的依据。&lt;/li>
&lt;li>&lt;strong>Token-bounded。&lt;/strong> 列表命令默认分页（&lt;code>--limit 1-100&lt;/code>、&lt;code>--offset&lt;/code>），不要把几千行喷进 Agent 的上下文窗口——上下文是它最贵的资源。&lt;/li>
&lt;/ol>
&lt;h3 id="24-语义化退出码并写进文档">2.4 语义化退出码，并写进文档
&lt;/h3>&lt;p>Agent 可以&lt;strong>只读退出码&lt;/strong>就决定下一步，不必解析正文。FreshAI CLI 的约定：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>退出码&lt;/th>
&lt;th>含义&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>成功&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>本地输入 / 配置错误（未联网）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>登录 / 授权错误&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>资源 / 冲突 / 其他 HTTP 错误&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>网络 / 超时&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>协议或凭证落盘错误&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>130&lt;/td>
&lt;td>用户中断（128 + SIGINT）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>两个坑：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>argparse 出错时默认就 exit 2&lt;/strong>，正好可以复用来表达「本地输入错误」，但你要&lt;strong>知道&lt;/strong>它是 2，别让它和你自定义的码打架。&lt;/li>
&lt;li>&lt;strong>130 是 128+SIGINT 的惯例&lt;/strong>，&lt;code>Ctrl+C&lt;/code> 中断应显式返回 130，而不是 1，否则脚本无法区分「被打断」和「执行失败」。&lt;/li>
&lt;/ul>
&lt;h3 id="25-错误要可迁移可恢复并且脱敏">2.5 错误要「可迁移、可恢复」，并且脱敏
&lt;/h3>&lt;p>好的错误信息同时服务两种读者：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>给人看：&lt;/strong> &lt;code>message&lt;/code> 说清楚发生了什么；&lt;/li>
&lt;li>&lt;strong>给程序看：&lt;/strong> &lt;code>code&lt;/code> + &lt;code>http_status&lt;/code> + &lt;code>execution_status&lt;/code> 让 Agent 能自动纠错。&lt;/li>
&lt;/ul>
&lt;p>同时，&lt;strong>错误输出是泄密高发区&lt;/strong>。服务端可能返回一整页 HTML、异常堆栈里可能带 token、正文可能出现在报错里。这类内容一律不能回显：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_extract_error_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">response&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Response&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;从错误响应中提取脱敏说明；不回显服务端 HTML 或原始异常。&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">response&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">ValueError&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">body_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">response&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">body_text&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">startswith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&amp;lt;&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;HTTP &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">response&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">status_code&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">：服务端返回了非 JSON 响应&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跨层映射也要固定下来，Agent 才能预测：&lt;code>401→登录类&lt;/code>、&lt;code>403→scope/授权类&lt;/code>、&lt;code>404→not_found&lt;/code>、&lt;code>409→冲突类&lt;/code>、&lt;code>5xx/429→网络类&lt;/code>。&lt;/p>
&lt;h3 id="26-幂等所有写操作的第一公民">2.6 幂等：所有写操作的第一公民
&lt;/h3>&lt;p>&lt;strong>为什么 Agent 特别需要幂等？&lt;/strong> 因为它会重试。超时了重试、进程被 kill 了重试、并发调度重试——人手动敲命令很少连敲两次，Agent 会。没有幂等键，就是重复发文章、重复下单、重复扣款。&lt;/p>
&lt;p>FreshAI CLI 的做法：客户端生成一个 UUID 请求编号，作为 &lt;code>Idempotency-Key&lt;/code> 发给服务端，并且&lt;strong>在请求发出前就把它打到 stderr、留在错误里&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;request-id: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">resolved_request_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stderr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># …&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">extra_headers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;Idempotency-Key&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">idempotency_key&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>服务端按 &lt;code>(author, request_id)&lt;/code> + 内容摘要去重，语义必须明确写清楚：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>情况&lt;/th>
&lt;th>服务端行为&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>同编号 + 同内容 + 目标状态未变&lt;/td>
&lt;td>返回原结果，&lt;code>replayed=true&lt;/code>，不新建&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>同编号 + 不同内容 / 不同模式&lt;/td>
&lt;td>&lt;code>409 request_conflict&lt;/code>，不覆盖&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>同编号但原稿已发布 / 已删除&lt;/td>
&lt;td>&lt;code>409 resource_state_changed&lt;/code>，不复活、不自动公开&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>这样「重试」就变成了安全操作。反过来，&lt;strong>如果 CLI 每次重试都生成新编号，幂等就形同虚设&lt;/strong>——这一点下面「难点」还会展开。&lt;/p>
&lt;h3 id="27-区分失败和结果未知">2.7 区分「失败」和「结果未知」
&lt;/h3>&lt;p>这是全篇&lt;strong>最重要、也最容易做错&lt;/strong>的一点，值得单列一节，我放在第三部分重点讲。&lt;/p>
&lt;h3 id="28-配置解析要有确定的优先级">2.8 配置解析要有确定的优先级
&lt;/h3>&lt;p>环境一多，站点/后端地址就会飘。FreshAI CLI 把解析顺序定死，并且让 &lt;code>doctor&lt;/code> 命令把&lt;strong>来源&lt;/strong>也报出来：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">--server &amp;gt; 进程环境变量 DOMAIN &amp;gt; .env.local &amp;gt; .env &amp;gt; 已保存的唯一站点凭证
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两条纪律：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>全部缺失时，在联网前就报 &lt;code>invalid_input&lt;/code>（退出码 2）&lt;/strong>，绝不连接到未知站点。&lt;/li>
&lt;li>&lt;strong>绝不静默回退。&lt;/strong> 显式给了无效值就报错；本地存了多个站点就要求显式指定，不「猜」一个。&lt;/li>
&lt;/ul>
&lt;h3 id="29-认证让-agent-能无人值守登录但密钥不进-argv--history">2.9 认证：让 Agent 能无人值守登录，但密钥不进 argv / history
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>永远不要提供 &lt;code>--token&lt;/code> 参数。&lt;/strong> 命令行会进入 shell history、进程列表（&lt;code>ps&lt;/code>）、CI 日志。FreshAI CLI 明确「无明文 &lt;code>--token&lt;/code> 参数」。&lt;/li>
&lt;li>&lt;strong>用设备码授权（&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc8628" target="_blank" rel="noopener"
>RFC 8628&lt;/a> 风格）&lt;/strong>：CLI 创建挑战 → 用户在浏览器确认 → CLI 按 &lt;code>interval&lt;/code> 轮询、领取&lt;strong>只出现一次&lt;/strong>的 token → 原子写入本地凭证文件。&lt;/li>
&lt;li>&lt;strong>最小权限 + 有效期 + 可撤销 + 不自动续期。&lt;/strong> scope 分开（&lt;code>drafts:create&lt;/code> / &lt;code>drafts:read&lt;/code> / &lt;code>publish&lt;/code>），30 天到期，网页可逐台撤销。&lt;/li>
&lt;li>&lt;strong>必须给一条无头路径。&lt;/strong> &lt;code>fresh login --no-browser&lt;/code> 只打印 URL 和核对码，方便在别的设备/无 GUI 环境完成授权。&lt;/li>
&lt;/ul>
&lt;h3 id="210-本地先校验未联网前拒绝非法输入">2.10 本地先校验，未联网前拒绝非法输入
&lt;/h3>&lt;p>能本地判定的错误，就别浪费一次网络往返，更别让它产生半成品远程状态。FreshAI CLI 在打包阶段就把这些挡掉（退出码 2）：&lt;/p>
&lt;ul>
&lt;li>标题为空；&lt;code>--file&lt;/code> 与 &lt;code>--package&lt;/code> 同时给；&lt;/li>
&lt;li>文件不是合法 UTF-8；内容全空白；&lt;/li>
&lt;li>超过大小上限（Markdown ≤ 1 MiB、HTML ≤ 2 MiB、ZIP ≤ 20 MiB）；&lt;/li>
&lt;li>&lt;code>--request-id&lt;/code> 不是合法 UUID；&lt;code>--file&lt;/code> 误传 &lt;code>.zip&lt;/code>（提示改用 &lt;code>--package&lt;/code>）。&lt;/li>
&lt;/ul>
&lt;h3 id="211-命令树与命名一致性--聪明">2.11 命令树与命名：一致性 &amp;gt; 聪明
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>动词-名词分层&lt;/strong>，和 &lt;code>git&lt;/code> 一致：&lt;code>fresh blog draft create&lt;/code> / &lt;code>fresh blog draft get &amp;lt;id&amp;gt;&lt;/code> / &lt;code>fresh blog draft list&lt;/code> / &lt;code>fresh blog publish&lt;/code>。&lt;/li>
&lt;li>&lt;strong>参数命名要全链统一。&lt;/strong> 这一条是用真实返工换来的：初版写成了 &lt;code>--site&lt;/code>，与规格里的 &lt;code>--server&lt;/code> 不一致，最后从参数、&lt;code>args&lt;/code>、帮助文案、报错文案到测试和文档全部更名了一遍。&lt;/li>
&lt;li>&lt;strong>帮助即文档。&lt;/strong> &lt;code>prog&lt;/code>、&lt;code>description&lt;/code>、每个参数的 &lt;code>help&lt;/code> 都写清楚，让 Agent（通过 &lt;code>--help&lt;/code>）能自学命令树。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="三重点与难点拆解">三、重点与难点拆解
&lt;/h2>&lt;h3 id="难点一结果未知网络失败不等于操作失败">难点一：「结果未知」——网络失败不等于操作失败
&lt;/h3>&lt;p>&lt;strong>现象：&lt;/strong> 一个创建文章的请求发出去，读响应时超时了。这次创建到底成功了没有？——&lt;strong>不知道。&lt;/strong>&lt;/p>
&lt;p>如果 CLI 简单地把超时当「失败」，Agent 会：&lt;/p>
&lt;ul>
&lt;li>认为文章没发出去 → 重发 → 如果第一次其实成功了，就产生了重复；&lt;/li>
&lt;li>或者反过来，Agent 以为成功了，实际服务端根本没收到。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>正确做法：把「请求执行状态」提升为一等公民。&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">EXECUTION_COMPLETED&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;completed&amp;#34;&lt;/span> &lt;span class="c1"># 已知结果&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">EXECUTION_UNKNOWN&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;unknown&amp;#34;&lt;/span> &lt;span class="c1"># 已发出，结果未知&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">EXECUTION_NOT_STARTED&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;not_started&amp;#34;&lt;/span> &lt;span class="c1"># 本地拒绝，没发出&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>关键在&lt;strong>区分「连接阶段失败」和「发送后失败」&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">except&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ConnectError&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ConnectTimeout&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">exc&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 连都没连上，请求肯定没送达&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">TransportError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;无法连接 …&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">timed_out&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">except&lt;/span> &lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">TimeoutException&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">exc&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 发出去了但没等到响应，可能已经成功&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">TransportError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;请求超时 …&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">timed_out&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后对「结果未知」给出&lt;strong>可执行的重试指令&lt;/strong>，而不是一句「失败了」：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="n">transport_failure&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sent&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">retry_hint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;请求已发送但结果未知，请用相同命令与相同编号重试：--request-id &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">request_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;（不要切换 draft/publish 模式）。&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">execution_status&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">EXECUTION_UNKNOWN&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>5xx 也一样处理：&lt;strong>服务端 500 可能是写了一半，属于结果未知&lt;/strong>，不能当作确定失败。&lt;/p>
&lt;blockquote>
&lt;p>这条原则的价值：错误分类错一档，Agent 的决策就错一个方向。区分 &lt;code>not_started&lt;/code>（可安全重发）和 &lt;code>unknown&lt;/code>（必须原编号重试）是「Agent 敢用这个 CLI」的前提。&lt;/p>
&lt;/blockquote>
&lt;h3 id="难点二幂等的边界是一个状态机">难点二：幂等的边界是一个状态机
&lt;/h3>&lt;p>幂等键不是「查一下有没有就返回」那么简单，资源会&lt;strong>变状态&lt;/strong>：草稿会被网页发布成公开文章、会被删除。此时同编号的旧请求该怎么答？答案是&lt;strong>冲突，而不是复活或覆盖&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">draft --(网页 publish)--&amp;gt; published --(不可逆)--&amp;gt; 公开文章
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">draft --(网页 delete)--&amp;gt; deleted --(不可恢复)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>原稿还在草稿态 → 同编号同内容重放，返回原 &lt;code>id&lt;/code>；&lt;/li>
&lt;li>原稿已发布/已删除 → &lt;code>409 resource_state_changed&lt;/code>，&lt;strong>不复活、不自动公开、不改动已公开文章&lt;/strong>。&lt;/li>
&lt;/ul>
&lt;p>同时，网页端的操作和 CLI 的创建请求可能并发，必须保证「同一状态转换只有一方成功」。这套状态机（而不是「有就返回」）才是真正可用的幂等。&lt;/p>
&lt;h3 id="难点三凭证存储的原子性与权限">难点三：凭证存储的原子性与权限
&lt;/h3>&lt;p>本地凭证文件里是明文 token，一旦损坏或权限过宽就是事故。要做到：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>目录 0700、文件 0600&lt;/strong>，读取时&lt;strong>校验权限&lt;/strong>，过宽直接拒绝（&lt;code>CredentialStoreError&lt;/code>）；&lt;/li>
&lt;li>&lt;strong>原子写入&lt;/strong>：同目录临时文件 → &lt;code>fchmod&lt;/code> → &lt;code>write&lt;/code> → &lt;code>flush&lt;/code> → &lt;code>fsync&lt;/code> → &lt;code>os.replace&lt;/code>，任何一步失败都清理临时文件、&lt;strong>不破坏既有文件&lt;/strong>；&lt;/li>
&lt;li>&lt;strong>拒绝损坏内容&lt;/strong>，而不是静默返回空凭证（否则用户会莫名「被登出」）；&lt;/li>
&lt;li>&lt;strong>支持 &lt;code>FRESH_HOME&lt;/code> 重定向&lt;/strong>，这是让测试隔离真实 &lt;code>~/.fresh&lt;/code> 的关键（下面测试部分会用到）。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">descriptor&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">temporary_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tempfile&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkstemp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">credentials_path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parent&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="err">…&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fchmod&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">descriptor&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">CREDENTIALS_FILE_MODE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fdopen&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">descriptor&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">encoding&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;utf-8&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">fh&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dump&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">all_credentials&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fh&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sort_keys&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fh&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">flush&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fh&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fileno&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">temporary_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">credentials_path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">except&lt;/span> &lt;span class="ne">BaseException&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">unlink&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">temporary_name&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 失败不留垃圾，也不动旧文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="难点四超时要分层">难点四：超时要分层
&lt;/h3>&lt;p>一个 &lt;code>timeout&lt;/code> 值打天下是不行的：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>连接超时&lt;/strong>和&lt;strong>读超时&lt;/strong>语义不同（前者可安全重试）；&lt;/li>
&lt;li>&lt;strong>创建/发布类&lt;/strong>请求要传文件、服务端要解包落库，&lt;strong>只读&lt;/strong>请求则应该快速失败。FreshAI CLI 给创建类 120s 总墙钟、只读 10s；&lt;/li>
&lt;li>Agent 侧还得有&lt;strong>自己的调用超时&lt;/strong>，且要大于 CLI 超时，否则 Agent 先把 CLI 杀了，就永远拿不到 &lt;code>unknown&lt;/code> 这个语义。&lt;/li>
&lt;/ul>
&lt;h3 id="难点五轮询类交互的细节">难点五：轮询类交互的细节
&lt;/h3>&lt;p>设备码登录是一条&lt;strong>长时间、可能抖动&lt;/strong>的轮询链路，几个坑：&lt;/p>
&lt;ul>
&lt;li>必须尊重服务端给的 &lt;code>interval&lt;/code>，过快会被回 &lt;code>slow_down&lt;/code>，要&lt;strong>顺延并退避&lt;/strong>；&lt;/li>
&lt;li>要有&lt;strong>总截止时间&lt;/strong>（&lt;code>expires_in&lt;/code>，默认 10 分钟），到点报 &lt;code>authorization_expired&lt;/code>；&lt;/li>
&lt;li>&lt;strong>网络抖动不等于授权失败&lt;/strong>，应打印进度并继续轮询，而不是直接退出；&lt;/li>
&lt;li>token &lt;strong>只发放一次&lt;/strong>，已领取再轮询要报「请重新登录」而不是重复发；&lt;/li>
&lt;li>领取成功后&lt;strong>落盘失败，要尽力撤销刚拿到的授权&lt;/strong>，否则会留下一份「用户以为没登录、服务端却存在」的僵尸凭证。&lt;/li>
&lt;/ul>
&lt;h3 id="难点六分发与版本一致性一个真金白银的坑">难点六：分发与版本一致性（一个真金白银的坑）
&lt;/h3>&lt;p>CLI 源码改了，但&lt;strong>分发的 wheel 版本号没变&lt;/strong>（一直是 &lt;code>0.1.0&lt;/code>），会发生什么？&lt;code>uv&lt;/code> 看到 URL 和版本都没变，直接用缓存里的旧构建——&lt;strong>你改了代码，用户装到的还是旧的，而且毫无报错&lt;/strong>。&lt;/p>
&lt;p>规避方式：&lt;/p>
&lt;ul>
&lt;li>改源码后&lt;strong>必须重新构建并提交 wheel&lt;/strong>，让分发产物与源码一致；&lt;/li>
&lt;li>从站点安装时显式强制刷新：&lt;code>uv tool install --reinstall --refresh &amp;quot;&amp;lt;wheel URL&amp;gt;&amp;quot;&lt;/code>；&lt;/li>
&lt;li>wheel URL 必须以真实文件名（&lt;code>.whl&lt;/code>）结尾，&lt;code>uv&lt;/code> 才认，所以先查元信息端点再拼 URL；&lt;/li>
&lt;li>更好的做法是让版本号真正随发布递增，而不是恒定。&lt;/li>
&lt;/ul>
&lt;p>再往上一层是&lt;strong>更新机制本身的设计&lt;/strong>，对 Agent 场景有两条特殊纪律：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>不要做静默自动更新。&lt;/strong> 人用 CLI，更新后行为变了会自己发现；Agent 的脚本是按当次 &lt;code>--help&lt;/code>/契约写的，后台悄悄换版本等于脚本脚下抽地板。正确姿势：提供 &lt;code>--version&lt;/code>，可以启动时提示「有新版本」，但升级必须由调用方显式触发。&lt;/li>
&lt;li>&lt;strong>升级必须遵守 append-only contract。&lt;/strong> 旧脚本依赖的命令、参数、退出码、JSON 字段一个都不能变、不能删、不能改语义——否则每次更新都是一次全量脚本回归。&lt;code>schema_version&lt;/code> 与版本提示配合，让 Agent 能感知并自行决定是否迁移。&lt;/li>
&lt;/ul>
&lt;h3 id="难点七测试策略契约测试--真实入口-e2e">难点七：测试策略——契约测试 + 真实入口 E2E
&lt;/h3>&lt;p>Agent 驱动的 CLI 出错代价高，测试要分两层：&lt;/p>
&lt;p>&lt;strong>① 契约测试（快，不发真实网络）。&lt;/strong> 用 &lt;code>httpx.MockTransport&lt;/code> 注入假响应，验证 envelope 形状、错误码映射、退出码、站点解析、打包逻辑。关键是把副作用隔离掉：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@pytest.fixture&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">autouse&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">isolated_home&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tmp_path&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;FRESH_HOME&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tmp_path&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;fresh-home&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 别碰真实 ~/.fresh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;DOMAIN&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">raising&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chdir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tmp_path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@pytest.fixture&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">autouse&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">no_real_sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">monkeypatch&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setattr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;time.sleep&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">lambda&lt;/span> &lt;span class="n">seconds&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 跳过轮询等待&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@pytest.fixture&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">autouse&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">no_real_browser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">monkeypatch&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setattr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cli_auth&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">webbrowser&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;open&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">lambda&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">**&lt;/span>&lt;span class="n">k&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>② 真实入口 E2E（慢，但不可省）。&lt;/strong> 用真实浏览器走授权页、真实 CLI 子进程（从 wheel 安装、&lt;code>cwd&lt;/code> 在仓库外、&lt;code>HOME&lt;/code> 隔离、不传 &lt;code>--server&lt;/code> 走主路径解析），验证「安装 → 登录 → 草稿 → 发布 → 撤销」的完整闭环。只测 mock 的 CLI 会在真实授权页、真实 CSP、真实跳转上翻车。&lt;/p>
&lt;hr>
&lt;h2 id="四避坑清单速查表">四、避坑清单（速查表）
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>坑&lt;/th>
&lt;th>症状&lt;/th>
&lt;th>正确做法&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>默认交互&lt;/td>
&lt;td>Agent 卡在确认/分页，直到超时&lt;/td>
&lt;td>默认非交互，交互只留给显式命令&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>进度写 stdout&lt;/td>
&lt;td>&lt;code>json.loads(stdout)&lt;/code> 失败&lt;/td>
&lt;td>进度/日志一律 stderr&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>JSON 模式混入人类文案/彩色&lt;/td>
&lt;td>解析器被 emoji、ANSI 码带偏&lt;/td>
&lt;td>JSON 模式只输出一个 envelope&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>直接抛 traceback&lt;/td>
&lt;td>Agent 读到一堆栈，无法纠错&lt;/td>
&lt;td>结构化 &lt;code>error.code&lt;/code> + 可执行建议&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>明文 &lt;code>--token&lt;/code>&lt;/td>
&lt;td>进 shell history / ps / CI 日志&lt;/td>
&lt;td>设备码授权，token 只经本地凭证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>把超时当失败&lt;/td>
&lt;td>重复副作用（重复发文）&lt;/td>
&lt;td>区分 &lt;code>not_started&lt;/code> / &lt;code>unknown&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>忽略幂等&lt;/td>
&lt;td>重试即重复写&lt;/td>
&lt;td>&lt;code>Idempotency-Key&lt;/code> + 状态机&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>静默切换站点/环境&lt;/td>
&lt;td>写错环境，且无人察觉&lt;/td>
&lt;td>优先级固定，缺失即报错&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>退出码随手写&lt;/td>
&lt;td>脚本无法分支&lt;/td>
&lt;td>语义化并写进文档，&lt;code>Ctrl+C&lt;/code> 用 130&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>错误里带 secret/正文&lt;/td>
&lt;td>泄密&lt;/td>
&lt;td>错误脱敏，不回显 HTML/异常原文&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>11&lt;/td>
&lt;td>只读命令也强制登录&lt;/td>
&lt;td>&lt;code>doctor&lt;/code> 之类无法排障&lt;/td>
&lt;td>只读诊断不要求身份校验&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>12&lt;/td>
&lt;td>凭证权限 0644&lt;/td>
&lt;td>明文 token 被同机他人读取&lt;/td>
&lt;td>目录 0700、文件 0600，原子写&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>13&lt;/td>
&lt;td>跟随重定向跨 origin&lt;/td>
&lt;td>串站/SSRF，写错站点&lt;/td>
&lt;td>关闭自动重定向，校验同 origin&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>14&lt;/td>
&lt;td>wheel 版本恒定&lt;/td>
&lt;td>改了源码，用户装到旧构建&lt;/td>
&lt;td>重建并提交 wheel，&lt;code>--reinstall --refresh&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>15&lt;/td>
&lt;td>CLI import 后端内部代码&lt;/td>
&lt;td>耦合重、装不上、版本打架&lt;/td>
&lt;td>独立包，只依赖 HTTP 客户端&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>人类表格和机器输出共用一条路径&lt;/td>
&lt;td>机器解析人类排版&lt;/td>
&lt;td>&lt;code>--json&lt;/code> 与文本模式分路&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>17&lt;/td>
&lt;td>分页缺失/无上限&lt;/td>
&lt;td>列表撑爆上下文&lt;/td>
&lt;td>&lt;code>--limit&lt;/code>/&lt;code>--offset&lt;/code>，默认有界&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>18&lt;/td>
&lt;td>只按扩展名判定内容格式&lt;/td>
&lt;td>&lt;code>.htm&lt;/code> / 无扩展名的 HTML 被静默当 Markdown 发成字面文本&lt;/td>
&lt;td>内容嗅探（&lt;code>&amp;lt;!doctype html&lt;/code>）兜底，或对可疑组合给出警告&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="五一个最小骨架">五、一个最小骨架
&lt;/h2>&lt;p>把上面的原则压成一个可以直接抄的骨架（Python + argparse + httpx）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># output.py —— 输出契约与退出码&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SCHEMA_VERSION&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">EXIT_OK&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">EXIT_LOCAL_INPUT&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">EXIT_AUTH&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">EXIT_HTTP&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">EXIT_NETWORK&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">EXIT_PROTOCOL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">EXIT_INTERRUPTED&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">130&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">CliError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="ne">Exception&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">http_status&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">execution_status&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;completed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">exit_code&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">EXIT_HTTP&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">request_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">super&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">message&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">message&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">http_status&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execution_status&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">http_status&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">execution_status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">exit_code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">request_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">exit_code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">request_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">success_envelope&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">request_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;schema_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">SCHEMA_VERSION&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;request_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">request_id&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">error_envelope&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">error&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">CliError&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;schema_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">SCHEMA_VERSION&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;code&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;http_status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">http_status&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;execution_status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execution_status&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;request_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">request_id&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># main.py —— 统一错误出口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">argv&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">build_parser&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parse_args&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">argv&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">_dispatch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="n">CliError&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_emit_cli_error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">error&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">as_json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">bool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">getattr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">False&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">exit_code&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">KeyboardInterrupt&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;已中断&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stderr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">EXIT_INTERRUPTED&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配套的 &lt;code>_emit_cli_error&lt;/code> / &lt;code>print_json_envelope&lt;/code> 记住两条：&lt;strong>JSON 模式走 stdout，文本模式走 stderr；进度永远 stderr。&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="六交付前自检清单">六、交付前自检清单
&lt;/h2>&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 默认非交互；危险操作靠「独立命令 + 最小 scope」，而不是交互确认&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> stdout 只有结果，stderr 只有诊断；JSON 模式不混入人类文案&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 统一 envelope + &lt;code>schema_version&lt;/code>，成功/失败同形状&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 退出码语义化、已文档化，&lt;code>Ctrl+C&lt;/code> 返回 130&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 所有写操作幂等：&lt;code>request-id&lt;/code> / &lt;code>Idempotency-Key&lt;/code>，并定义重放/冲突/状态变更语义&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 严格区分 &lt;code>completed&lt;/code> / &lt;code>unknown&lt;/code> / &lt;code>not_started&lt;/code>，对 &lt;code>unknown&lt;/code> 给出原编号重试指令&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 错误脱敏，不回显 token、正文、服务端 HTML、原始异常&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 配置优先级明确且可诊断；缺失时联网前报错；不静默回退&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 无明文 &lt;code>--token&lt;/code>；支持无头/设备码登录；凭证 0600 原子写&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 本地输入校验（编码、空值、大小、互斥、格式）先于联网&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 列表有界（分页/limit），不撑爆上下文&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 契约测试用 mock transport + 隔离 HOME；E2E 走真实入口&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 分发产物（wheel）版本与源码一致&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="参考">参考
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://aclig.dev/" target="_blank" rel="noopener"
>Agent CLI Guidelines（aclig.dev）&lt;/a> —— 面向 Agent 的十条不变量：只读默认、自描述、有界输出、注入防护、可无头认证、只增契约等&lt;/li>
&lt;li>&lt;a class="link" href="https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46" target="_blank" rel="noopener"
>12 Factor CLI Apps（Heroku / JDX）&lt;/a> —— 帮助、flags、stdout/stderr、错误处理、XDG&lt;/li>
&lt;li>&lt;a class="link" href="https://clig.dev/" target="_blank" rel="noopener"
>clig.dev&lt;/a> —— 命令行界面设计通用准则&lt;/li>
&lt;li>&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc8628" target="_blank" rel="noopener"
>RFC 8628：OAuth 2.0 Device Authorization Grant&lt;/a> —— 设备码授权&lt;/li>
&lt;li>FreshAI CLI 源码：&lt;code>packages/fresh-cli/src/fresh_cli/&lt;/code>（&lt;code>main.py&lt;/code> / &lt;code>output.py&lt;/code> / &lt;code>client.py&lt;/code> / &lt;code>credentials.py&lt;/code> / &lt;code>config.py&lt;/code> / &lt;code>blogs.py&lt;/code> / &lt;code>auth.py&lt;/code>）&lt;/li>
&lt;/ul></description></item><item><title>用 AI 把文章做成口播视频：三条路线、工具盘点与落地管线</title><link>https://www.zata.cc/p/ai-article-to-video/</link><pubDate>Wed, 09 Sep 2026 22:09:48 +0800</pubDate><guid>https://www.zata.cc/p/ai-article-to-video/</guid><description>&lt;img src="https://www.zata.cc/p/ai-article-to-video/images/index/index.svg" alt="Featured image of post 用 AI 把文章做成口播视频：三条路线、工具盘点与落地管线" />&lt;h2 id="总览一条口播视频拆成四个环节">总览：一条口播视频拆成四个环节
&lt;/h2>&lt;p>一篇现成的文章要变成口播视频，中间其实是四个环节，每个环节都有对应的 AI 方案：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>环节&lt;/th>
&lt;th>做什么&lt;/th>
&lt;th>AI 能帮什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>稿&lt;/td>
&lt;td>文章 → 口播稿&lt;/td>
&lt;td>LLM 改写：口语化、加钩子、删图表依赖&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>音&lt;/td>
&lt;td>口播稿 → 旁白&lt;/td>
&lt;td>TTS 配音，或数字人口播&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>图&lt;/td>
&lt;td>内容 → 可视画面&lt;/td>
&lt;td>AI 生成图表动画、信息图、字幕&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>剪&lt;/td>
&lt;td>素材 → 成片&lt;/td>
&lt;td>素材自动匹配、自动字幕、一键成片&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>按投入程度，主流是三条路线，投入和质感递增：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>路线&lt;/th>
&lt;th>做法&lt;/th>
&lt;th>适合&lt;/th>
&lt;th>质感&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A. 一站式图文成片&lt;/td>
&lt;td>文章粘进去，自动出片&lt;/td>
&lt;td>快速试水、批量产出&lt;/td>
&lt;td>一般，AI 味重&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B. 半自动流水线&lt;/td>
&lt;td>每步用 AI，关键点人工把关&lt;/td>
&lt;td>个人知识类账号（推荐）&lt;/td>
&lt;td>可做到精致&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C. 全代码管线&lt;/td>
&lt;td>Remotion/Manim + FFmpeg，成片即代码&lt;/td>
&lt;td>会写代码、要批量复用&lt;/td>
&lt;td>最高，可沉淀&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="路线-a一站式图文成片">路线 A：一站式「图文成片」
&lt;/h2>&lt;p>把文章直接粘贴进工具，自动完成配音（TTS）+ 配画面 + 字幕，几分钟出一条片。&lt;/p>
&lt;p>常见工具（2026-08 实测盘点）：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>工具&lt;/th>
&lt;th>特点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>剪映「图文成片」&lt;/td>
&lt;td>生态最成熟，抖音/视频号素材匹配&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>腾讯智影&lt;/td>
&lt;td>免费，数字人，对接视频号生态&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>爆款兔 PopTo&lt;/td>
&lt;td>数字人 + 脚本 + 分发一站式&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>小云雀 AI&lt;/td>
&lt;td>剪映生态，主打抖音场景&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>蝉镜 AI&lt;/td>
&lt;td>企业 IP 形象克隆&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>万兴播爆&lt;/td>
&lt;td>跨境多语言口播&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>局限也明显：素材匹配比较泛（说什么都给你配城市航拍），TTS 味道重，图表类内容表现力差。适合先跑通感受下限，不适合当主力。&lt;/p>
&lt;h2 id="路线-b半自动流水线推荐">路线 B：半自动流水线（推荐）
&lt;/h2>&lt;p>大多数看起来「很精致」的知识类视频，真实做法是这条：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">文章
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ AI 改写成口播稿（口语化、加钩子）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ TTS 配音 或 自己录
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ 图表动画垫在音轨上
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ 空镜头素材穿插
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ AI 自动字幕
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ 成片
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每一步都有 AI 参与，但口播稿和图表两个关键点保留人工控制。各环节的常用选择：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>口播稿改写&lt;/strong>：交给 LLM（文章是「看」的，口播稿是「听」的，必须改写，见下文长度换算）；&lt;/li>
&lt;li>&lt;strong>配音&lt;/strong>：剪映内置音色（免费、够用），或更高质量 TTS；不想出镜可直接用数字人（腾讯智影、蝉镜、HeyGen）；&lt;/li>
&lt;li>&lt;strong>画面&lt;/strong>：图表动画（见「AI 图表」一节）+ 空镜头（见「B-roll」一节）交替，避免纯脸出镜；&lt;/li>
&lt;li>&lt;strong>字幕&lt;/strong>：剪映「识别字幕」一键生成，准确率已经很高。&lt;/li>
&lt;/ul>
&lt;h2 id="路线-c全代码管线">路线 C：全代码管线
&lt;/h2>&lt;p>整个片子就是代码：口播稿 → TTS → Remotion/Manim 渲染图表动画 → FFmpeg 拼接 → 成片。可版本管理、可批量复用，改一个参数重新渲染全片。适合已经跑通路线 B、想规模化的人。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>工具/项目&lt;/th>
&lt;th>定位&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a class="link" href="https://www.remotion.dev/" target="_blank" rel="noopener"
>Remotion&lt;/a>&lt;/td>
&lt;td>用 React 写视频，组件化、参数化批量出片&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a class="link" href="https://www.manim.community/" target="_blank" rel="noopener"
>Manim&lt;/a>&lt;/td>
&lt;td>3Blue1Brown 同款数学/架构动画引擎&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HyperFrames&lt;/td>
&lt;td>图文转视频框架&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FFmpeg&lt;/td>
&lt;td>视频拼接、转码的底座工具&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a class="link" href="https://github.com/Kevin-Kaiyo/techscript-video-pipeline" target="_blank" rel="noopener"
>techscript-video-pipeline&lt;/a>&lt;/td>
&lt;td>开源的技术脚本转视频管线&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>video-use / OpenMontage&lt;/td>
&lt;td>口播自动剪辑 / 全链路自动拼接（开源）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="内容长度怎么算文章转口播不是念文章">内容长度怎么算：文章转口播不是「念文章」
&lt;/h2>&lt;p>&lt;strong>换算公式&lt;/strong>：口播语速约 240～280 字/分钟。一篇 4500 字的文章念完是 16～19 分钟——B 站/YouTube 知识区合适，抖音/视频号太长。所以多数情况要做减法：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>拆系列&lt;/strong>：长文按小节拆成多期，每期一个子主题，3～5 分钟；&lt;/li>
&lt;li>&lt;strong>精华版&lt;/strong>：只保留主判断做一支 8～10 分钟，完整论述留给图文版。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>文章和口播稿是两种文体&lt;/strong>，不能直接念：&lt;/p>
&lt;ul>
&lt;li>文章靠结构和视觉（标题、图表、加粗），口播靠听觉（短句、重复、递进）；&lt;/li>
&lt;li>ASCII 架构图、表格没法口播，要改成「嘴上说结论、画面放图」的配合；&lt;/li>
&lt;li>开头 15 秒需要一个钩子（问题、反常识判断），而不是背景铺垫。&lt;/li>
&lt;/ul>
&lt;h2 id="ai-图表动画的三种做法">AI 图表动画的三种做法
&lt;/h2>&lt;p>别人视频里那些动的图表和数据动画，主流实现有三种：&lt;/p>
&lt;p>&lt;strong>1. AI 写代码渲染动画（主流，可控性最强）&lt;/strong>&lt;/p>
&lt;p>让 LLM 写 Manim 或 Remotion 代码，渲染成几秒到几十秒的动画片段，插进时间线。Generative Manim 这类开源工具已经做到「一句话生成精准动画」——本质就是 AI 编程 + 动画引擎。对会代码的人，这是图表质感上限最高的做法。文章里现成的 ASCII 架构图（如 Model → Runtime → Memory/Knowledge/Skills 分层图）特别适合转成逐层浮现的动画。&lt;/p>
&lt;p>&lt;strong>2. 文生信息图工具（最快）&lt;/strong>&lt;/p>
&lt;p>Napkin AI、Gamma 这类工具，把一段话自动变成信息图，导出图片或简单动画垫进视频。快，但风格受限。&lt;/p>
&lt;p>&lt;strong>3. 剪辑软件内置模板（最省事）&lt;/strong>&lt;/p>
&lt;p>剪映的图表/数字滚动动画模板，改几个数字就能用，样式固定。&lt;/p>
&lt;h2 id="b-roll素材拼接的实现原理">B-roll：素材拼接的实现原理
&lt;/h2>&lt;p>看起来「随便找视频拼起来」的做法，原理非常简单：&lt;strong>口播是一条音轨，上面再叠一条「空镜头」画面轨&lt;/strong>，画面跟着内容切换。要解决的只有两件事：素材从哪来、怎么对上位置。&lt;/p>
&lt;p>&lt;strong>素材来源&lt;/strong>：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>来源&lt;/th>
&lt;th>说明&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Pexels / Pixabay&lt;/td>
&lt;td>免费可商用，按关键词下载&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Storyblocks&lt;/td>
&lt;td>付费素材库，量大质高&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI 生成&lt;/td>
&lt;td>可灵、即梦、Runway 生成几秒定制镜头，补素材库没有的画面&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>自动匹配（AI 对位置）&lt;/strong>：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>工具&lt;/th>
&lt;th>能力&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>剪映「图文成片」&lt;/td>
&lt;td>按台词自动配素材&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>阿里云智能图文匹配成片&lt;/td>
&lt;td>同类能力，开放 API&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Adobe Quick Cut（2026 新功能）&lt;/td>
&lt;td>按指令自动剪素材和 B-roll 初稿&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>手工做法也不复杂：定好口播音轨后，在时间线上方叠画面轨，每个论点配一个 3～5 秒空镜；图表动画和空镜交替出现，节奏就不会闷。&lt;/p>
&lt;h2 id="落地路径三步走">落地路径：三步走
&lt;/h2>&lt;ol>
&lt;li>&lt;strong>试水（1 小时）&lt;/strong>：把一篇文章粘进剪映「图文成片」跑一版，看自动生成的下限；&lt;/li>
&lt;li>&lt;strong>半自动（主力）&lt;/strong>：AI 改口播稿 → 自己录或 TTS → 图表用 AI 写 Manim/Remotion 渲染 → 剪映组装 + 空镜穿插；&lt;/li>
&lt;li>&lt;strong>管线化（跑通后）&lt;/strong>：把第二步沉淀成脚本管线（TTS + 动画渲染 + FFmpeg 拼接），以后每篇文章一键转视频。&lt;/li>
&lt;/ol>
&lt;h2 id="参考">参考
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://tech.ifeng.com/c/8w2fmjhsoDG" target="_blank" rel="noopener"
>国内口播短视频实测，从选题到成片一站式工具盘点 - 凤凰网科技&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://baijiahao.baidu.com/s?id=1870572151233153978" target="_blank" rel="noopener"
>从口播剪辑到动画生成，6 套 AI 视频工具怎么选&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://m.blog.csdn.net/gitblog_00420/article/details/154631280" target="_blank" rel="noopener"
>Generative Manim 终极指南：如何用AI将文字秒变动画视频&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://m.toutiao.com/article/7645391061447213619/" target="_blank" rel="noopener"
>一夜爆火！用 HTML 写视频的 AI 框架来了&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://help.aliyun.com/zh/ims/user-guide/image-text-matching" target="_blank" rel="noopener"
>智能图文匹配成片 - 阿里云&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.woshipm.com/share/6382900.html" target="_blank" rel="noopener"
>我做了一个工具，让剪映剪辑师彻底告别手动找 B-Roll 的痛苦 - 人人都是产品经理&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://emcreative.eastmoney.com/app_fortune/article/index.html?artCode=20260226080152983283410" target="_blank" rel="noopener"
>Adobe「快剪」Quick Cut：AI 根据指令自动剪辑素材和 B-roll&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>扩展式 RPA：从 Chrome 扩展原理到 Playwright 实战方案</title><link>https://www.zata.cc/p/%E6%89%A9%E5%B1%95%E5%BC%8Frpa%E4%BB%8Echrome%E6%89%A9%E5%B1%95%E6%8E%A5%E7%AE%A1%E5%88%B0playwright%E5%AE%9E%E6%88%98/</link><pubDate>Mon, 27 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.zata.cc/p/%E6%89%A9%E5%B1%95%E5%BC%8Frpa%E4%BB%8Echrome%E6%89%A9%E5%B1%95%E6%8E%A5%E7%AE%A1%E5%88%B0playwright%E5%AE%9E%E6%88%98/</guid><description>&lt;img src="https://www.zata.cc/p/%E6%89%A9%E5%B1%95%E5%BC%8Frpa%E4%BB%8Echrome%E6%89%A9%E5%B1%95%E6%8E%A5%E7%AE%A1%E5%88%B0playwright%E5%AE%9E%E6%88%98/images/index/index.svg" alt="Featured image of post 扩展式 RPA：从 Chrome 扩展原理到 Playwright 实战方案" />&lt;h2 id="一个观察">一个观察
&lt;/h2>&lt;p>之前在 [[../L../Library/Python_Lib/playwright/index.md]] 里写过 Playwright 自己起隔离浏览器的常规打法。但在看到一些 RPA 工具的形态后，问题就变成另一个：&lt;/p>
&lt;blockquote>
&lt;p>为什么有些 RPA 工具是装成 Chrome 扩展，一打开就能直接「操控」用户已经登录好的页面？&lt;/p>
&lt;/blockquote>
&lt;p>答案并不复杂：这两种工具在底层就是两套完全不同的思路。&lt;/p>
&lt;h2 id="核心原理谁拥有那个浏览器">核心原理：谁拥有那个浏览器
&lt;/h2>&lt;p>普通 Playwright(或者类似 Puppeteer、Selenium)启动的是&lt;strong>全新的、独立的浏览器实例&lt;/strong>，和你日常用的 Chrome 没有任何关系。Cookies 是空的，需要重新登录或注入 &lt;code>storage_state&lt;/code>。&lt;/p>
&lt;p>而 Chrome 扩展型 RPA 长这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">┌──────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ 你的 Chrome 浏览器（同进程） │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌────────────┐ ┌────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Tab: 微博 │ │ Extension: RPA 工具 │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (已登录) │ │ 注入 content script │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────────────┘ └────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ↑ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ 共享同一个 cookie 存储 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ 共享同一个 origin 权限 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>扩展里跑的内容脚本 (&lt;code>content script&lt;/code>) 语法上是独立脚本，但它&lt;strong>直接被注入到登录页面的 DOM 里&lt;/strong>，运行在该页面的 JS context 中。所以当它调用 &lt;code>fetch('/api/user/info')&lt;/code> 时，浏览器自动带上微博的 cookies，服务器认为这就是用户本人。&lt;/p>
&lt;p>整件事可以浓缩成一句话：&lt;/p>
&lt;blockquote>
&lt;p>扩展是装在你浏览器里的代码，你在浏览器里登录了什么，它就拥有什么。&lt;/p>
&lt;/blockquote>
&lt;p>这也是为什么做需要登录态的 RPA 任务，扩展方案往往比 Playwright 简单十倍 —— 因为它根本不需要解决「登录」这个问题。&lt;/p>
&lt;h3 id="几块关键拼图">几块关键拼图
&lt;/h3>&lt;p>扩展形式看起来神秘，其实就是下面这几块常规 MV3 API 拼起来的：&lt;/p>
&lt;p>&lt;code>manifest.json&lt;/code> 声明权限：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;permissions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;cookies&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;storage&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;tabs&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;scripting&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;activeTab&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;host_permissions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;*://*.weibo.com/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;*://*.example.com/*&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>content.js&lt;/code> 注入目标页面，所有 &lt;code>fetch&lt;/code> 自动带登录态：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">autoPost&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">content&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;#editor&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;#submit&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">chrome&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">runtime&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">onMessage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addListener&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">msg&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">sender&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">sendResponse&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">msg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">cmd&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;post&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">autoPost&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">msg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">text&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sendResponse&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">ok&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>后台脚本 &lt;code>background.js&lt;/code> 里直接读 cookie：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">chrome&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">cookies&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getAll&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">domain&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;.weibo.com&amp;#39;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">cookies&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">cookies&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 直接拿到用户的所有登录态
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把上面三块加一个 &lt;code>popup.html&lt;/code>，就是最简版「扩展式 RPA」。&lt;/p>
&lt;h3 id="两种方案对比">两种方案对比
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>维度&lt;/th>
&lt;th>Playwright / 独立 RPA&lt;/th>
&lt;th>扩展式 RPA&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>启动方式&lt;/td>
&lt;td>独立进程，自己的浏览器&lt;/td>
&lt;td>跑在用户已有的 Chrome&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>登录态&lt;/td>
&lt;td>无，需要自登录或注入 &lt;code>storage_state&lt;/code>&lt;/td>
&lt;td>&lt;strong>天然继承&lt;/strong>用户浏览器的登录态&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>反爬检测&lt;/td>
&lt;td>高（指纹、IP、行为模式）&lt;/td>
&lt;td>低（就是用户本人）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>用户授权&lt;/td>
&lt;td>无感（代码里随便用）&lt;/td>
&lt;td>需要用户主动装扩展&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>部署难度&lt;/td>
&lt;td>简单，纯代码&lt;/td>
&lt;td>需要打包扩展、上架商店或 crx 安装&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>资源占用&lt;/td>
&lt;td>完整浏览器进程&lt;/td>
&lt;td>几乎为零&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>适用场景&lt;/td>
&lt;td>批量任务、服务器端&lt;/td>
&lt;td>辅助个人用户、低频任务&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="反爬角度的思考">反爬角度的思考
&lt;/h3>&lt;p>扩展式 RPA 看起来「开挂」，但也不是万能：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>指纹检测&lt;/strong>：&lt;code>window.chrome.runtime&lt;/code> 等扩展特征可以被检测&lt;/li>
&lt;li>&lt;strong>行为检测&lt;/strong>：点击速度、操作序列如果太机械，仍能被识别&lt;/li>
&lt;li>&lt;strong>数据回流&lt;/strong>：云端 SaaS 形态的 RPA 在网络层会被分析&lt;/li>
&lt;li>&lt;strong>扩展权限&lt;/strong>：Chrome 在安装时展示权限，敏感权限可能让用户警觉&lt;/li>
&lt;li>&lt;strong>Manifest V3 限制&lt;/strong>：MV3 收紧了远程代码执行、跨域请求，扩展型 RPA 的能力被削弱&lt;/li>
&lt;/ol>
&lt;h3 id="商业产品参考">商业产品参考
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>UI.Vision (Kantu)&lt;/strong>：经典老牌，扩展形式，直接读用户登录态&lt;/li>
&lt;li>&lt;strong>Browserflow&lt;/strong>：Chrome 商店里很常见&lt;/li>
&lt;li>&lt;strong>Axiom.ai&lt;/strong>：扩展 + 云端&lt;/li>
&lt;li>&lt;strong>影刀 / 八爪鱼&lt;/strong>：部分功能走扩展模式，处理需要登录的网站&lt;/li>
&lt;li>&lt;strong>Automa&lt;/strong>（开源）：GitHub 上有源码，可以学到完整的实现&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="用-playwright-模拟扩展式-rpa">用 Playwright 模拟扩展式 RPA
&lt;/h2>&lt;p>虽然「真扩展」的形态在 Playwright 这边不好复刻（Playwright 是独立进程，无法直接读取用户 Chrome 的 cookies），但有一种折中方案：让 Playwright &lt;strong>复用用户已登录的浏览器&lt;/strong>。技术上叫 &lt;strong>Persistent Context&lt;/strong>。&lt;/p>
&lt;p>核心 API 是 &lt;code>launch_persistent_context&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 关键：指向真实 Chrome 的用户数据目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># macOS : ~/Library/Application Support/Google/Chrome&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Windows: %LOCALAPPDATA%\Google\Chrome\User Data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch_persistent_context&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_data_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;/Users/zata/Library/Application Support/Google/Chrome&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">channel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;chrome&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># 强制用系统 Chrome，不是 chromium&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headless&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># 必须可视化（见下面）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">no_viewport&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">args&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;--disable-blink-features=AutomationControlled&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;--no-first-run&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_page&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;https://weibo.com&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 此时你已经是登录状态了，直接用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.follow-btn&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">first&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">click&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="但这条路的坑很大">但这条路的坑很大
&lt;/h3>&lt;p>和同仓库那篇 [[../L../Library/Python_Lib/playwright/index.md]] 的警告一致 —— 复用真实 Chrome 的 Profile 风险极高：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>必须先完全关闭 Chrome&lt;/strong>：用户数据目录是文件锁定的，Chrome 在开的时候 Playwright 启动会直接报 &lt;code>Failed to create a ProcessSingleton for your profile&lt;/code>。&lt;/li>
&lt;li>&lt;strong>&lt;code>headless=False&lt;/code> 是硬需求&lt;/strong>：不是「看得到」这么简单的诉求，而是 headless 模式下被检测概率会显著上升，自动化特征也更明显。&lt;/li>
&lt;li>&lt;strong>User Data 目录要用整个 &lt;code>User Data&lt;/code>，而不是 &lt;code>Default&lt;/code>&lt;/strong>。&lt;/li>
&lt;li>&lt;strong>不要做 &lt;code>rm -rf&lt;/code> 这种清理动作&lt;/strong>：容易连带覆盖或损坏历史、书签。&lt;/li>
&lt;/ol>
&lt;p>写个辅助函数降低出错的可能：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">subprocess&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">time&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">kill_chrome&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;杀进程,确保数据目录不被 Chrome 自己锁住&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;pkill&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;-9&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Google Chrome&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">check&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="实际可运行的完整脚本">实际可运行的完整脚本
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># rpa.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">subprocess&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">time&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CHROME_PATH&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;/Applications/Google Chrome.app/Contents/MacOS/Google Chrome&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">USER_DATA&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">home&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s1">&amp;#39;Library/Application Support/Google/Chrome&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">ensure_chrome_closed&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;pkill&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;-9&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;-f&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Google Chrome&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">check&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">run&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ensure_chrome_closed&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Playwright 启动一个临时 Chrome 进程,加载你的用户数据&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch_persistent_context&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_data_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">USER_DATA&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">executable_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">CHROME_PATH&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headless&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">no_viewport&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">args&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;--disable-blink-features=AutomationControlled&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ignore_default_args&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;--enable-automation&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pages&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_page&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;https://weibo.com&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 验证:这里应该能直接看到登录态&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="s1">&amp;#39;登录&amp;#39;&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="s1">&amp;#39;用户态异常,可能 Chrome 未正常启动&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 开始自动化...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;https://weibo.com/u/1234567890&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;text=关注&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">first&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">click&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;✅ 关注成功&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">finally&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;__main__&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="进阶cdp-远程连接">进阶：CDP 远程连接
&lt;/h3>&lt;p>&lt;code>launch_persistent_context&lt;/code> 有一个尴尬的体验：用户在自己用 Chrome，你想跑 RPA，必须先杀 Chrome 再启动一次。更优雅的方案是 &lt;strong>Chrome DevTools Protocol（CDP）远程连接&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 第一步：用户在终端手动启动 Chrome（带远程调试）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">google-chrome --remote-debugging-port&lt;span class="o">=&lt;/span>&lt;span class="m">9222&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 第二步：Playwright 接管这个 Chrome,而不是新启&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">connect_over_cdp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;http://localhost:9222&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">contexts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 复用用户的 context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 接管用户当前的 tab&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;https://weibo.com&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>CDP 方案的好处：&lt;/p>
&lt;ul>
&lt;li>不需要杀 Chrome&lt;/li>
&lt;li>用户当前打开的 tab 可以直接被我们接管&lt;/li>
&lt;li>跑完可以「无缝」还给用户&lt;/li>
&lt;/ul>
&lt;h3 id="playwright--chrome-extension">Playwright + Chrome Extension
&lt;/h3>&lt;p>如果目标是&lt;strong>让用户装一个扩展，由扩展来驱动云端 Playwright&lt;/strong>，逻辑反过来：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ 用户 Chrome │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 你的 Extension (MV3) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ └── WebSocket 客户端 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 普通网页 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 被扩展的 content script 操控 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└────────────┬────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ws://localhost:8765
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ 你的 Python 服务 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── WebSocket Server │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── Playwright(可选,云端兜底) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这种架构适合做 &lt;strong>SaaS 化的 RPA&lt;/strong>：扩展做「采集 + 可视化」，云端 Playwright 做「批量执行」。&lt;/p>
&lt;hr>
&lt;h2 id="方案选型表">方案选型表
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>场景&lt;/th>
&lt;th>推荐方案&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>个人脚本，偶尔跑一下&lt;/td>
&lt;td>&lt;code>launch_persistent_context&lt;/code> + 杀 Chrome&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>个人脚本，经常跑&lt;/td>
&lt;td>CDP 远程连接 + 一个一键启动 Chrome 的 &lt;code>.command&lt;/code> 文件&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>工具化，要给同事用&lt;/td>
&lt;td>CDP 方案 + 配启动器&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>做产品，要分发&lt;/td>
&lt;td>写 Chrome 扩展（完全独立，无需 Playwright）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>前三种方案都没跳出 Playwright 的能力范围，基本能覆盖 90% 的需求。最后一种是真正的「扩展式 RPA」，但投入就大很多了 —— 它本身就是一个独立的产品形态，从 manifest、签名、商店审核到 MV3 限制都是另一套工程。&lt;/p>
&lt;p>如果是从零开始想把 RPA 能力给到个人用户，&lt;strong>先从 CDP 方案做起&lt;/strong>，把脚本跑通、把页面录制做成、把回放做成；等到扩展的能力真的有必要了，再考虑写 MV3 分发。&lt;/p>
&lt;h2 id="参考资料">参考资料
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context" target="_blank" rel="noopener"
>Playwright: &lt;code>launch_persistent_context&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://playwright.dev/python/docs/other-ports#connect-over-cdp" target="_blank" rel="noopener"
>Playwright: Connect over CDP&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3" target="_blank" rel="noopener"
>Chrome Extensions: Manifest V3 Migration&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.chrome.com/docs/extensions/reference/api/cookies" target="_blank" rel="noopener"
>Chrome Extensions: Cookies API&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/rrweb-io/rrweb" target="_blank" rel="noopener"
>rrweb / rrweb-player&lt;/a>（会话录制的开源参考实现）&lt;/li>
&lt;li>&lt;a class="link" href="https://www.automa.site/" target="_blank" rel="noopener"
>Automa - Chrome 扩展式 RPA 开源实现&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>浏览器会话录制与接口回放：方案调研</title><link>https://www.zata.cc/p/%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BC%9A%E8%AF%9D%E5%BD%95%E5%88%B6%E4%B8%8E%E6%8E%A5%E5%8F%A3%E5%9B%9E%E6%94%BE%E6%96%B9%E6%A1%88%E8%B0%83%E7%A0%94/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.zata.cc/p/%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BC%9A%E8%AF%9D%E5%BD%95%E5%88%B6%E4%B8%8E%E6%8E%A5%E5%8F%A3%E5%9B%9E%E6%94%BE%E6%96%B9%E6%A1%88%E8%B0%83%E7%A0%94/</guid><description>&lt;img src="https://www.zata.cc/p/%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BC%9A%E8%AF%9D%E5%BD%95%E5%88%B6%E4%B8%8E%E6%8E%A5%E5%8F%A3%E5%9B%9E%E6%94%BE%E6%96%B9%E6%A1%88%E8%B0%83%E7%A0%94/images/index/index.svg" alt="Featured image of post 浏览器会话录制与接口回放：方案调研" />&lt;h2 id="背景与目标">背景与目标
&lt;/h2>&lt;p>想做这样一个工具：&lt;/p>
&lt;ol>
&lt;li>用户打开一个目标网站&lt;/li>
&lt;li>在浏览器里手动操作（点击、输入、滚动、跳转）&lt;/li>
&lt;li>后台实时记录每一步 DOM 事件 + 每个 HTTP 接口的请求/响应&lt;/li>
&lt;li>操作结束后，能看到一份按时间线排列的&amp;quot;操作 + 接口&amp;quot;清单，必要时还能重放&lt;/li>
&lt;/ol>
&lt;p>这类能力在业内通常叫 &lt;strong>Session Replay / 用户会话录制&lt;/strong>。&lt;/p>
&lt;h2 id="核心原理">核心原理
&lt;/h2>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────┐ 事件 / 网络上报 ┌──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ 浏览器注入 │ ────────────────────&amp;gt; │ 后端 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ SDK / 扩展 │ │ 落库 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────────┘ └──────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ DOM 快照 + 事件流 ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────────────────&amp;gt; 回放页面
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (rrweb-player 等)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>关键三件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DOM 事件捕获&lt;/strong>：&lt;code>click&lt;/code> / &lt;code>input&lt;/code> / &lt;code>scroll&lt;/code> / &lt;code>navigation&lt;/code>，通常用 &lt;code>addEventListener&lt;/code> 全局委托 + 事件冒泡&lt;/li>
&lt;li>&lt;strong>网络拦截&lt;/strong>：浏览器侧 &lt;code>XMLHttpRequest&lt;/code> / &lt;code>fetch&lt;/code> 包装一层钩子，或者用 Service Worker / 浏览器扩展的 &lt;code>webRequest&lt;/code> API&lt;/li>
&lt;li>&lt;strong>DOM 变化记录&lt;/strong>：&lt;code>MutationObserver&lt;/code> 监听 DOM 增量变化，配合初始快照实现像素级回放&lt;/li>
&lt;/ul>
&lt;h2 id="现有方案">现有方案
&lt;/h2>&lt;h3 id="商业产品开箱即用">商业产品（开箱即用）
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>工具&lt;/th>
&lt;th>特点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>SessionStack&lt;/strong>&lt;/td>
&lt;td>像素级回放 + 错误捕获 + 性能分析，企业级&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FullStory&lt;/strong>&lt;/td>
&lt;td>数字体验分析，强在漏斗和搜索&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LogRocket&lt;/strong>&lt;/td>
&lt;td>类 SessionStack，回放 + Redux/Zustand 状态回放&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Hotjar&lt;/strong>&lt;/td>
&lt;td>更偏热力图和漏斗&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Microsoft Clarity&lt;/strong>&lt;/td>
&lt;td>免费，基础回放 + 热力图&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="开源方案">开源方案
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>&lt;a class="link" href="https://github.com/rrweb-io/rrweb" target="_blank" rel="noopener"
>rrweb&lt;/a>&lt;/strong> — 最主流的开源前端录制库，用 &lt;code>MutationObserver&lt;/code> + 事件监听完整还原 DOM 变化，配套有 &lt;code>rrweb-player&lt;/code> 回放器。LogRocket 早期也借鉴过类似思路&lt;/li>
&lt;li>&lt;strong>rrweb-snapshot / rrweb-player&lt;/strong> — 序列化和回放子包&lt;/li>
&lt;li>&lt;strong>Playwright Trace Viewer&lt;/strong> / &lt;strong>Patchright&lt;/strong> — 如果场景是&amp;quot;复现自动化流程&amp;quot;，Playwright 自带的 trace 就能记录每一步点击、请求、截图&lt;/li>
&lt;/ul>
&lt;h2 id="自己从零做的思路">自己从零做的思路
&lt;/h2>&lt;h3 id="最简-mvp">最简 MVP
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">浏览器扩展
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ content script 注入目标页面
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ 监听 click / input / scroll
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ 包装 XHR / fetch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ 周期性上报到 /collect
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">后端
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ POST /collect → 追加写入 events.jsonl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">本地 CLI
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ 解析 events.jsonl → 打印时间线 / 启动 rrweb-player
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>代码量很小，半天能跑通。&lt;/p>
&lt;h3 id="进阶版像素级回放">进阶版（像素级回放）
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 浏览器侧：rrweb 录制
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">record&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;rrweb&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">events&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">record&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">emit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">events&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 周期性批量上报到后端
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">setInterval&lt;/span>&lt;span class="p">(()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">postEvents&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">events&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">splice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)),&lt;/span> &lt;span class="mi">1000&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 回放侧
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="nx">rrwebPlayer&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;rrweb-player&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">rrwebPlayer&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">props&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">events&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">fetchedEvents&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="选型建议">选型建议
&lt;/h2>&lt;p>按&amp;quot;想做什么&amp;quot;分场景推荐：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>需求&lt;/th>
&lt;th>推荐方案&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>自己排查自动化 bug&lt;/td>
&lt;td>&lt;strong>Playwright Trace&lt;/strong>（&lt;code>context.tracing.start()&lt;/code>），零成本&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>给自家产品做用户行为分析&lt;/td>
&lt;td>&lt;strong>rrweb + 自建后端&lt;/strong>，可控、私有化部署友好&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>想要现成的商业级方案&lt;/td>
&lt;td>&lt;strong>LogRocket / SessionStack&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>只想看热力图&lt;/td>
&lt;td>&lt;strong>Hotjar / Microsoft Clarity&lt;/strong>（免费）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="与现有-patchright-项目的结合点">与现有 patchright 项目的结合点
&lt;/h2>&lt;p>本仓库（kimi-patchright）本身在做浏览器自动化相关的 slide 调试。如果只是想在调试 slide 自动化流程时复盘浏览器里的真实操作 + 接口调用，&lt;strong>Playwright Trace + patchright 的 &lt;code>page.on('request')&lt;/code> / &lt;code>page.on('response')&lt;/code> 就够用了&lt;/strong>，不需要单独再搭一套录制系统。&lt;/p>
&lt;p>如果目标是 toC 的通用录制工具，那需要再想清楚定位：是要 rrweb 那种像素回放，还是只做事件流（轻量、可被搜索聚合）。&lt;/p>
&lt;h2 id="参考资料">参考资料
&lt;/h2>&lt;ul>
&lt;li>rrweb: &lt;a class="link" href="https://github.com/rrweb-io/rrweb" target="_blank" rel="noopener"
>https://github.com/rrweb-io/rrweb&lt;/a>&lt;/li>
&lt;li>SessionStack 原理: &lt;a class="link" href="https://www.sessionstack.com/" target="_blank" rel="noopener"
>https://www.sessionstack.com/&lt;/a>&lt;/li>
&lt;li>Playwright Trace: &lt;a class="link" href="https://playwright.dev/docs/trace-viewer" target="_blank" rel="noopener"
>https://playwright.dev/docs/trace-viewer&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>AI 生成前端的 E2E 实践：用 Playwright 做视觉回归和功能兜底</title><link>https://www.zata.cc/p/ai-generated-frontend-e2e-testing/</link><pubDate>Thu, 25 Jun 2026 14:00:00 +0800</pubDate><guid>https://www.zata.cc/p/ai-generated-frontend-e2e-testing/</guid><description>&lt;img src="https://www.zata.cc/p/ai-generated-frontend-e2e-testing/images/index/index.svg" alt="Featured image of post AI 生成前端的 E2E 实践：用 Playwright 做视觉回归和功能兜底" />&lt;blockquote>
&lt;p>这是「AI 时代的前端设计与实现」系列的一篇补充。前面我们聊了角色、设计系统、rules 模板和 Figma MCP；这一篇聚焦工程化落地里最常被低估的环节——&lt;strong>端到端测试&lt;/strong>。&lt;/p>
&lt;/blockquote>
&lt;h2 id="为什么-ai-生成的前端特别需要-e2e">为什么 AI 生成的前端特别需要 E2E
&lt;/h2>&lt;p>AI 写前端的速度很快，但它有两个天然短板：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>没有全局视角&lt;/strong>：改 A 页面时很容易把 B 页面的样式或交互带崩。&lt;/li>
&lt;li>&lt;strong>对边界状态不敏感&lt;/strong>：loading、error、empty、无权限这些状态经常被忽略。&lt;/li>
&lt;/ol>
&lt;p>更麻烦的是，AI 生成的代码往往&lt;strong>看起来没问题，一跑就报错&lt;/strong>。TypeScript 和 ESLint 能拦住类型和语法错误，但拦不住这些问题：&lt;/p>
&lt;ul>
&lt;li>按钮点了没反应&lt;/li>
&lt;li>表单提交后没跳转&lt;/li>
&lt;li>某个弹窗在特定分辨率下错位&lt;/li>
&lt;li>改完配色后整个页面气质变了&lt;/li>
&lt;/ul>
&lt;p>E2E 测试就是针对这些问题的最后一道防线。它不只是测功能，还能帮你做&lt;strong>视觉验收&lt;/strong>和&lt;strong>回归防护&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="e2e-要测什么">E2E 要测什么
&lt;/h2>&lt;p>不要试图把所有东西都用 E2E 测。抓住三类高价值场景：&lt;/p>
&lt;h3 id="1-核心用户流程happy-path">1. 核心用户流程（Happy Path）
&lt;/h3>&lt;p>用户最常用的路径必须稳定。例如电商站的「搜索 → 加购 → 结算 → 支付」。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">test&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">expect&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;@playwright/test&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;用户完成购买流程&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/products&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;product-card&amp;#34;]:first-child&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;add-to-cart&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;checkout-button&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[name=&amp;#34;email&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;user@example.com&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;submit-order&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;text=订单已创建&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">toBeVisible&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-错误和边界状态">2. 错误和边界状态
&lt;/h3>&lt;p>AI 最容易漏的恰恰是这些：&lt;/p>
&lt;ul>
&lt;li>网络失败时的 fallback UI&lt;/li>
&lt;li>搜索无结果&lt;/li>
&lt;li>表单校验失败&lt;/li>
&lt;li>未登录访问需要权限的页面&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;搜索无结果展示空状态&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/search&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[name=&amp;#34;q&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;xyznotfound123&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">press&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[name=&amp;#34;q&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Enter&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;empty-state&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">toBeVisible&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;text=没有找到相关结果&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">toBeVisible&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-视觉回归">3. 视觉回归
&lt;/h3>&lt;p>这是 AI 前端最关键的能力。每次 AI 改完代码，自动截图和基准图对比，像素级变化都能发现。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;首页视觉回归&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveScreenshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;homepage.png&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">maxDiffPixels&lt;/span>: &lt;span class="kt">100&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="工具链选择">工具链选择
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>工具&lt;/th>
&lt;th>适用场景&lt;/th>
&lt;th>推荐度&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Playwright&lt;/strong>&lt;/td>
&lt;td>功能测试、截图回归、多浏览器、移动端模拟、trace 调试&lt;/td>
&lt;td>⭐⭐⭐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cypress&lt;/td>
&lt;td>社区成熟，但多标签页、iframe 支持弱&lt;/td>
&lt;td>⭐⭐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Selenium&lt;/td>
&lt;td>老项目维护，新项目不推荐&lt;/td>
&lt;td>⭐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chromatic / Percy&lt;/td>
&lt;td>专业 UI 回归 SaaS，适合组件级视觉对比&lt;/td>
&lt;td>⭐⭐⭐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>axe-core&lt;/td>
&lt;td>无障碍扫描&lt;/td>
&lt;td>⭐⭐⭐&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>建议：&lt;/strong> 新项目直接用 Playwright。它一个工具就能覆盖功能测试、视觉回归、响应式、多浏览器，失败时还能看 trace 回放。&lt;/p>
&lt;hr>
&lt;h2 id="playwright-实战配置">Playwright 实战配置
&lt;/h2>&lt;h3 id="安装">安装
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npm init playwright@latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="基础配置">基础配置
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// playwright.config.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">defineConfig&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">devices&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;@playwright/test&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="nx">defineConfig&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">testDir&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;./e2e&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fullyParallel&lt;/span>: &lt;span class="kt">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">forbidOnly&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="o">!!&lt;/span>&lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">CI&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">retries&lt;/span>: &lt;span class="kt">process.env.CI&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">2&lt;/span> : &lt;span class="kt">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">workers&lt;/span>: &lt;span class="kt">process.env.CI&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">1&lt;/span> : &lt;span class="kt">undefined&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">reporter&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[[&lt;/span>&lt;span class="s1">&amp;#39;html&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;list&amp;#39;&lt;/span>&lt;span class="p">]],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">baseURL&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;http://localhost:3000&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">trace&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;on-first-retry&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">screenshot&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;only-on-failure&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">video&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;retain-on-failure&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">projects&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;chromium&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span>&lt;span class="nx">devices&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Desktop Chrome&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;firefox&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span>&lt;span class="nx">devices&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Desktop Firefox&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;webkit&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span>&lt;span class="nx">devices&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Desktop Safari&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Mobile Chrome&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span>&lt;span class="nx">devices&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Pixel 5&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Mobile Safari&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span>&lt;span class="nx">devices&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;iPhone 12&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">webServer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">command&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;npm run dev&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">url&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;http://localhost:3000&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">reuseExistingServer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">CI&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>关键配置解读：&lt;/p>
&lt;ul>
&lt;li>&lt;code>trace: 'on-first-retry'&lt;/code>：失败时保留 trace，可以逐帧回放。&lt;/li>
&lt;li>&lt;code>screenshot: 'only-on-failure'&lt;/code>：失败自动截图。&lt;/li>
&lt;li>&lt;code>retries: 2&lt;/code>：CI 里重试 2 次，排除 flaky。&lt;/li>
&lt;li>&lt;code>webServer&lt;/code>：自动起本地服务。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="视觉回归怎么做">视觉回归怎么做
&lt;/h2>&lt;h3 id="基础截图对比">基础截图对比
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;登录页视觉回归&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/login&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">waitForSelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;login-form&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveScreenshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;login-page.png&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">maxDiffPixels&lt;/span>: &lt;span class="kt">100&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">threshold&lt;/span>: &lt;span class="kt">0.2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="组件级视觉回归">组件级视觉回归
&lt;/h3>&lt;p>如果你想测某个组件在不同状态下的样子：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;按钮各状态截图&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/button-demos&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;hover-trigger&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;button-primary&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">toHaveScreenshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;button-hover.png&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;focus-trigger&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;button-primary&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">toHaveScreenshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;button-focus.png&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;loading-trigger&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">locator&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;button-primary&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">toHaveScreenshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;button-loading.png&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="让视觉回归更稳定">让视觉回归更稳定
&lt;/h3>&lt;p>视觉回归最怕 flaky。几个工程化技巧：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>固定字体加载&lt;/strong>：截图前确保字体已加载。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">waitForFunction&lt;/span>&lt;span class="p">(()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fonts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ready&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>屏蔽动态内容&lt;/strong>：时间、随机数、动画元素用 CSS 隐藏或 mock。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addStyleTag&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;[data-testid=&amp;#34;current-time&amp;#34;] { visibility: hidden !important; }&amp;#39;&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>统一 viewport 和缩放&lt;/strong>：在 &lt;code>playwright.config.ts&lt;/code> 里统一设备配置。&lt;/li>
&lt;li>&lt;strong>动画禁用&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addStyleTag&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }&amp;#39;&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="响应式测试">响应式测试
&lt;/h2>&lt;p>AI 生成的页面常常在桌面端好看，移动端就崩。Playwright 可以很方便地多分辨率截图：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">describe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;响应式首页&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">viewport&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">width&lt;/span>: &lt;span class="kt">375&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>: &lt;span class="kt">667&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;移动端&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveScreenshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;homepage-mobile.png&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">describe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;响应式首页桌面端&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">viewport&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">width&lt;/span>: &lt;span class="kt">1440&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>: &lt;span class="kt">900&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;桌面端&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveScreenshot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;homepage-desktop.png&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="无障碍测试">无障碍测试
&lt;/h2>&lt;p>AI 生成的代码容易忽略 &lt;code>alt&lt;/code>、&lt;code>aria-label&lt;/code>、对比度等。用 axe-core 跑一遍：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">test&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">expect&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;@playwright/test&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nx">AxeBuilder&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;@axe-core/playwright&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;首页无严重无障碍问题&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">AxeBuilder&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">withTags&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;wcag2a&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;wcag2aa&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">analyze&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">results&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">violations&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toEqual&lt;/span>&lt;span class="p">([]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="让-ai-参与测试工作流">让 AI 参与测试工作流
&lt;/h2>&lt;p>E2E 不只是写测试，还可以让 AI 帮你生成和维护测试。&lt;/p>
&lt;h3 id="1-从用户故事生成测试">1. 从用户故事生成测试
&lt;/h3>&lt;p>把 PRD 或用户故事喂给 AI：&lt;/p>
&lt;blockquote>
&lt;p>用户搜索商品，选择第一个结果加入购物车，进入结算页填写邮箱，提交订单后看到成功提示。&lt;/p>
&lt;/blockquote>
&lt;p>AI 输出 Playwright 测试代码，你只需微调选择器。&lt;/p>
&lt;h3 id="2-从-bug-报告生成回归测试">2. 从 Bug 报告生成回归测试
&lt;/h3>&lt;p>遇到一个 bug 后，先让 AI 写一个能复现它的 E2E 测试。修完 bug 再跑这个测试，确保不会回归。&lt;/p>
&lt;h3 id="3-自动分析失败原因">3. 自动分析失败原因
&lt;/h3>&lt;p>Playwright 失败时会生成 trace。你可以把 trace 截图或错误日志丢给 AI，让它给出修复建议，比如：&lt;/p>
&lt;ul>
&lt;li>某个选择器不稳定，建议加 &lt;code>data-testid&lt;/code>&lt;/li>
&lt;li>页面还没加载完，建议加 &lt;code>waitForSelector&lt;/code>&lt;/li>
&lt;li>某个 API 响应慢，建议 mock&lt;/li>
&lt;/ul>
&lt;h3 id="4-ai-视觉验收闭环">4. AI 视觉验收闭环
&lt;/h3>&lt;p>结合 Chrome MCP / Playwright，可以建立这样的 workflow：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">AI 生成代码
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Playwright 跑 E2E + 截图
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">AI 对比截图 vs Figma 设计稿
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">AI 提出样式修复建议
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">AI 再次生成代码
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">重新跑 E2E
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个闭环能大幅减少人工 Design QA 的工作量。&lt;/p>
&lt;hr>
&lt;h2 id="推荐的目录结构">推荐的目录结构
&lt;/h2>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">e2e/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── fixtures/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── users.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── products.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── page-objects/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── LoginPage.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── ProductPage.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── CheckoutPage.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── specs/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── auth.spec.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── product.spec.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── checkout.spec.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── visual/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── homepage.visual.spec.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── login.visual.spec.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── utils/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── test-helpers.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── mock-api.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── snapshots/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── baseline/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Page Object 模式&lt;/strong>很重要。把选择器封装到 Page Object 里，AI 改了 DOM 结构时，只需要改一处：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// e2e/page-objects/LoginPage.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">LoginPage&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kr">private&lt;/span> &lt;span class="nx">page&lt;/span>: &lt;span class="kt">Page&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">async&lt;/span> &lt;span class="kr">goto&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/login&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">async&lt;/span> &lt;span class="nx">login&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">email&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">password&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;email-input&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">email&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;password-input&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">password&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[data-testid=&amp;#34;login-button&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="cicd-集成">CI/CD 集成
&lt;/h2>&lt;p>在 GitHub Actions 里跑：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">E2E Tests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">main]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">e2e&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/setup-node@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">node-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cache&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;npm&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">npm ci&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">npx playwright install --with-deps&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">npx playwright test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Upload Playwright report&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">failure()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/upload-artifact@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">playwright-report&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> playwright-report/
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> test-results/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="视觉回归基准图管理">视觉回归基准图管理
&lt;/h3>&lt;p>视觉回归最怕 baseline 不同步。建议：&lt;/p>
&lt;ul>
&lt;li>本地开发改 UI 时，跑 &lt;code>npx playwright test --update-snapshots&lt;/code> 更新自己负责的快照。&lt;/li>
&lt;li>PR 里如果改了 UI，必须在 PR 描述里说明更新了哪些截图。&lt;/li>
&lt;li>CI 里不允许自动更新快照，防止 silently 通过视觉回归。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="给-ai-的-rules-建议">给 AI 的 Rules 建议
&lt;/h2>&lt;p>把 E2E 要求写进 &lt;code>.cursorrules&lt;/code> 或 &lt;code>.cursor/rules/*.mdc&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## E2E 规范
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> 新增页面或核心交互后，必须在 &lt;span class="sb">`e2e/specs/`&lt;/span> 下补充对应测试
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> 优先使用 Playwright 和 &lt;span class="sb">`data-testid`&lt;/span> 选择器，避免依赖 CSS 类名
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> 每个核心流程测试必须覆盖：正常流程、空状态、错误状态
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> 视觉改动必须更新 Playwright baseline 截图
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> 提交前运行 &lt;span class="sb">`npm run test:e2e`&lt;/span>，确保通过
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> 不要写死等待时间，优先用 &lt;span class="sb">`waitForSelector`&lt;/span> / &lt;span class="sb">`toBeVisible`&lt;/span> 等断言
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="总结">总结
&lt;/h2>&lt;p>E2E 测试对 AI 生成的前端有三层价值：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>功能兜底&lt;/strong>：核心流程不崩溃、边界状态有处理。&lt;/li>
&lt;li>&lt;strong>视觉验收&lt;/strong>：每次改动自动截图对比，防止 UI 漂移。&lt;/li>
&lt;li>&lt;strong>回归保险&lt;/strong>：后续 AI 迭代时，不会把已有的功能改坏。&lt;/li>
&lt;/ol>
&lt;p>它和 design token、组件库、TypeScript、rules 组合起来，才能把「AI 生成前端」从 demo 变成可维护、可上线的产品。&lt;/p>
&lt;hr>
&lt;h2 id="参考">参考
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://playwright.dev/" target="_blank" rel="noopener"
>Playwright 官方文档&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.npmjs.com/package/@axe-core/playwright" target="_blank" rel="noopener"
>axe-core for Playwright&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.chromatic.com/" target="_blank" rel="noopener"
>Chromatic 视觉回归&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>从 noVNC 到 Playwright 截图流：容器内 VNC 踩坑记</title><link>https://www.zata.cc/p/%E4%BB%8E-novnc-%E5%88%B0-playwright-%E6%88%AA%E5%9B%BE%E6%B5%81%E5%AE%B9%E5%99%A8%E5%86%85-vnc-%E8%B8%A9%E5%9D%91%E8%AE%B0/</link><pubDate>Tue, 09 Jun 2026 14:00:00 +0800</pubDate><guid>https://www.zata.cc/p/%E4%BB%8E-novnc-%E5%88%B0-playwright-%E6%88%AA%E5%9B%BE%E6%B5%81%E5%AE%B9%E5%99%A8%E5%86%85-vnc-%E8%B8%A9%E5%9D%91%E8%AE%B0/</guid><description>&lt;img src="https://www.zata.cc/p/%E4%BB%8E-novnc-%E5%88%B0-playwright-%E6%88%AA%E5%9B%BE%E6%B5%81%E5%AE%B9%E5%99%A8%E5%86%85-vnc-%E8%B8%A9%E5%9D%91%E8%AE%B0/images/index/index.png" alt="Featured image of post 从 noVNC 到 Playwright 截图流：容器内 VNC 踩坑记" />&lt;h2 id="背景">背景
&lt;/h2>&lt;p>项目是一个 FastAPI 后端 + Playwright 驱动的浏览器自动化服务，目标是让用户在网页里远程操作一个跑在 Docker 容器内的 Chrome，完成 &lt;code>kimi.com&lt;/code> 的一次性扫码/账号登录。登录态写入持久化 profile，后续 PPT 生成任务复用。&lt;/p>
&lt;p>远程桌面的需求看起来很简单：&lt;/p>
&lt;ul>
&lt;li>容器内：&lt;code>Xvfb&lt;/code>（虚拟显示）+ &lt;code>openbox&lt;/code>（窗口管理）+ &lt;code>x11vnc&lt;/code>（VNC 服务端）&lt;/li>
&lt;li>后端：FastAPI WebSocket 做字节中继，把前端的 RFB 流量转发到 &lt;code>127.0.0.1:5910&lt;/code>&lt;/li>
&lt;li>前端：&lt;code>@novnc/novnc&lt;/code> 的 &lt;code>RFB&lt;/code> 类连接 WebSocket，渲染桌面&lt;/li>
&lt;/ul>
&lt;p>架构上也很规矩：5910 不对外暴露，WebSocket 走 443/HTTPS，用管理员 token + 一次性 ticket 鉴权，没有旁路端口。&lt;/p>
&lt;p>PRD 写得整整齐齐，验收清单全部打勾，看起来万无一失。然后我们就掉进了坑里。&lt;/p>
&lt;hr>
&lt;h2 id="坑一mit-shm-0-byte-画面">坑一：MIT-SHM 0-byte 画面
&lt;/h2>&lt;p>&lt;strong>症状&lt;/strong>：noVNC 连接成功，但画面全黑，或者显示 0-byte 的帧。&lt;/p>
&lt;p>&lt;strong>根因&lt;/strong>：x11vnc 默认使用 X11 的 MIT-SHM（共享内存扩展）加速画面抓取。在 Docker 容器里，共享内存行为跟宿主机不一致，x11vnc 读不到像素数据。&lt;/p>
&lt;p>&lt;strong>修复&lt;/strong>（&lt;code>2fa2602&lt;/code>）：给 x11vnc 加参数绕过 MIT-SHM：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">x11vnc -noxshm -noxkb -noxrecord -noxfixes -noxdamage ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或者通过环境变量透传：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">X11VNC_EXTRA_ARGS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;-noxshm -ncache 0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个坑还算好填，加参数就能绕过。&lt;/p>
&lt;hr>
&lt;h2 id="坑二chrome-vizdisplaycompositor-导致黑屏">坑二：Chrome VizDisplayCompositor 导致黑屏
&lt;/h2>&lt;p>&lt;strong>症状&lt;/strong>：x11vnc 终于有画面了，但 Chrome 窗口区域永远是黑的。其他窗口（xterm、openbox）显示正常。&lt;/p>
&lt;p>&lt;strong>根因&lt;/strong>：Chrome 默认启用 Viz Display Compositor，把实际渲染放到独立的合成器进程里，用 GPU 或特殊缓冲区，导致普通的 X11 像素读取（&lt;code>XGetImage&lt;/code>、&lt;code>XShmGetImage&lt;/code>）抓不到 Chrome 内容。&lt;/p>
&lt;p>&lt;strong>修复&lt;/strong>（&lt;code>58cb8ed&lt;/code>）：启动 Chrome 时禁用 VizDisplayCompositor：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">google-chrome --disable-features&lt;span class="o">=&lt;/span>VizDisplayCompositor ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个坑比较隐蔽，因为其他应用都能正常显示，唯独 Chrome 黑屏。排查了好一阵才定位到是 Chrome 的合成器机制。&lt;/p>
&lt;hr>
&lt;h2 id="坑三x11vnc-在-azure-vm-上的兼容性">坑三：x11vnc 在 Azure VM 上的兼容性
&lt;/h2>&lt;p>&lt;strong>症状&lt;/strong>：本地 Mac Docker Desktop 测试正常，推到 Azure VM（Ubuntu 24.04，内核 &lt;code>6.14.0-1017-azure&lt;/code>）后，x11vnc 要么崩溃，要么帧率极低，要么随机断开。&lt;/p>
&lt;p>&lt;strong>根因&lt;/strong>：x11vnc 对内核版本、X11 扩展、共享内存的依赖比较深，不同宿主机环境表现差异很大。&lt;/p>
&lt;p>这时候我们开始尝试替换 VNC 服务端。&lt;/p>
&lt;h3 id="尝试-1x0vncserver">尝试 1：x0vncserver
&lt;/h3>&lt;p>TigerVNC 的 &lt;code>x0vncserver&lt;/code> 更轻量，但 Ubuntu 24.04 的软件源里根本没有这个包（&lt;code>6765172&lt;/code>）。&lt;/p>
&lt;h3 id="尝试-2xtigervnc">尝试 2：Xtigervnc
&lt;/h3>&lt;p>切换到 &lt;code>Xtigervnc&lt;/code>（&lt;code>1d3222c&lt;/code>），但发现它的 X0 扩展支持不完整，抓取现有 X 会话的能力不如 x11vnc。&lt;/p>
&lt;h3 id="尝试-3x11vnc--ncache">尝试 3：x11vnc + ncache
&lt;/h3>&lt;p>最后又滚回 x11vnc，加上 &lt;code>-ncache 10&lt;/code> 做客户端缓存（&lt;code>8ba411d&lt;/code>），试图缓解帧率问题。&lt;/p>
&lt;p>这一系列反复说明一个问题：&lt;strong>在容器化 + 云服务器环境下跑传统 VNC 服务端，是一件非常脆弱的事&lt;/strong>。每个环境变量、每个内核版本、每个 X11 扩展都可能成为压垮骆驼的最后一根稻草。&lt;/p>
&lt;hr>
&lt;h2 id="最终方案彻底放弃-vnc改用-playwright-截图">最终方案：彻底放弃 VNC，改用 Playwright 截图
&lt;/h2>&lt;p>与其继续跟 x11vnc 搏斗，我们换了一个思路：&lt;/p>
&lt;p>&lt;strong>既然 Playwright 已经开着 Chrome，为什么不直接让 Playwright 截屏？&lt;/strong>&lt;/p>
&lt;p>新方案（&lt;code>40b9014&lt;/code>）：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>后端&lt;/strong>：Playwright 的 &lt;code>Page.screenshot()&lt;/code> 每 250ms 截一张图&lt;/li>
&lt;li>&lt;strong>传输&lt;/strong>：自定义 &lt;code>WebSocketFrameBridge&lt;/code>，把 PNG 字节包上 length-prefixed JSON envelope，通过现有 WebSocket 推给前端&lt;/li>
&lt;li>&lt;strong>前端&lt;/strong>：不用 noVNC RFB 了，直接用原生 &lt;code>WebSocket&lt;/code> 收帧，把 PNG blob 塞进 &lt;code>&amp;lt;img&amp;gt;&lt;/code> 标签&lt;/li>
&lt;li>&lt;strong>输入&lt;/strong>：前端把鼠标/键盘事件序列化成 JSON，通过同一个 WebSocket 发回后端，后端用 &lt;code>page.mouse.click()&lt;/code>、&lt;code>page.keyboard.press()&lt;/code> 回放&lt;/li>
&lt;/ol>
&lt;p>Wire 格式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="mi">4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">byte&lt;/span> &lt;span class="n">LE&lt;/span> &lt;span class="n">length&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">uint32&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">N&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="n">JSON&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;frame&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">N&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;size&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">M&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;format&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;jpeg&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">4&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">byte&lt;/span> &lt;span class="n">LE&lt;/span> &lt;span class="n">length&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">uint32&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">M&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="n">PNG&lt;/span> &lt;span class="n">payload&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="新旧方案对比">新旧方案对比
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>维度&lt;/th>
&lt;th>noVNC + x11vnc&lt;/th>
&lt;th>Playwright 截图流&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>依赖&lt;/td>
&lt;td>Xvfb + openbox + x11vnc + MIT-SHM workaround&lt;/td>
&lt;td>只有 Xvfb&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chrome 兼容性&lt;/td>
&lt;td>需要 &lt;code>--disable-features=VizDisplayCompositor&lt;/code>&lt;/td>
&lt;td>不需要特殊 flags&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>容器稳定性&lt;/td>
&lt;td>极易受宿主机内核/环境差异影响&lt;/td>
&lt;td>纯用户态，跟宿主机无关&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>带宽&lt;/td>
&lt;td>RFB 协议有增量压缩，带宽低&lt;/td>
&lt;td>每帧完整 PNG/JPEG，带宽高&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>延迟&lt;/td>
&lt;td>RFB 增量更新，延迟低&lt;/td>
&lt;td>250ms 轮询，延迟稍高&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>实现复杂度&lt;/td>
&lt;td>需要 RFB 中继、VNC 参数调优&lt;/td>
&lt;td>自定义帧协议，前后端约 200 行&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>可维护性&lt;/td>
&lt;td>x11vnc 是黑盒，出问题难排查&lt;/td>
&lt;td>Playwright 截图是白盒，可控&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>牺牲了一点带宽和延迟，换来的是&lt;strong>极高的稳定性和可维护性&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="教训">教训
&lt;/h2>&lt;ol>
&lt;li>
&lt;p>&lt;strong>VNC 协议是为局域网桌面共享设计的&lt;/strong>，强行塞进 Docker + 浏览器自动化的场景，属于削足适履。MIT-SHM、GPU 合成器、X11 扩展这些历史包袱在容器里会集中爆发。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>当你在同一个组件上打了 3 个以上补丁还搞不定时，应该考虑替换它&lt;/strong>。我们在 x11vnc 上修了 MIT-SHM、VizDisplayCompositor、ncache、环境参数透传……最后发现换掉它比修好它更容易。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>用你已经有的工具链解决问题&lt;/strong>。既然 Playwright 已经开着浏览器，截屏是它的一等公民 API，为什么不直接用？不要为了追求&amp;quot;标准方案&amp;quot;而引入一整套 VNC 技术栈。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>&amp;ldquo;网页远程桌面&amp;quot;不等于&amp;quot;必须实现 VNC/RDP 协议&amp;rdquo;&lt;/strong>。只要用户能看到画面、能发鼠标键盘，用什么协议不重要。自定义的 JSON+PNG 帧流完全够用。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="代码速查">代码速查
&lt;/h2>&lt;p>如果你也在用 Playwright + Docker + 远程桌面，可以直接参考这个模式。&lt;/p>
&lt;p>&lt;strong>后端截屏&lt;/strong>（Python）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">next_frame&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">bytes&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">screenshot&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;jpeg&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">quality&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">70&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">full_page&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>前端渲染&lt;/strong>（TypeScript/React）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">ws&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">WebSocket&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">url&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ws&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">binaryType&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;arraybuffer&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ws&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">onmessage&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">blob&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Blob&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;image/jpeg&amp;#34;&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">setFrameUrl&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">URL&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createObjectURL&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">blob&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>后端输入回放&lt;/strong>（Python）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">dispatch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;click&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mouse&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;keydown&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">keyboard&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">press&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;key&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Playwright Chromium 在 Docker 内 SIGTRAP 启动崩溃排查实录</title><link>https://www.zata.cc/p/playwright-chromium-%E5%9C%A8-docker-%E5%86%85-sigtrap-%E5%90%AF%E5%8A%A8%E5%B4%A9%E6%BA%83%E6%8E%92%E6%9F%A5%E5%AE%9E%E5%BD%95/</link><pubDate>Mon, 08 Jun 2026 15:30:00 +0800</pubDate><guid>https://www.zata.cc/p/playwright-chromium-%E5%9C%A8-docker-%E5%86%85-sigtrap-%E5%90%AF%E5%8A%A8%E5%B4%A9%E6%BA%83%E6%8E%92%E6%9F%A5%E5%AE%9E%E5%BD%95/</guid><description>&lt;img src="https://www.zata.cc/p/playwright-chromium-%E5%9C%A8-docker-%E5%86%85-sigtrap-%E5%90%AF%E5%8A%A8%E5%B4%A9%E6%BA%83%E6%8E%92%E6%9F%A5%E5%AE%9E%E5%BD%95/images/index/index.png" alt="Featured image of post Playwright Chromium 在 Docker 内 SIGTRAP 启动崩溃排查实录" />&lt;h2 id="背景">背景
&lt;/h2>&lt;p>一个 FastAPI 后端 + Playwright（驱动 Chromium 做爬虫/自动化）的项目，通过 Dokploy 部署到一台 Azure VM（内核 &lt;code>6.14.0-1017-azure&lt;/code>，Ubuntu 24.04）。前端点登录按钮，后端通过 &lt;code>launch_persistent_context&lt;/code> 起 Chromium 让用户走 VNC 完成一次性登录。&lt;/p>
&lt;p>部署后第一次点登录就报：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">TargetClosedError: BrowserType.launch_persistent_context:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Target page, context or browser has been closed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;launched&amp;gt; pid=191
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[pid=191][err] chrome_crashpad_handler: --database is required
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[pid=191][err] [191:191:...:ERROR:.../socket.cc:120] recvmsg: Connection reset by peer (104)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[pid=191] &amp;lt;process did exit: exitCode=null, signal=SIGTRAP&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>chrome_crashpad_handler: --database is required&lt;/code> 看起来像主角，其实只是&lt;strong>遗书&lt;/strong>：父 chrome 进程已经 SIGTRAP，crashpad handler 被附带拉起来时没拿到参数，才吐了 usage。先理清这一点才不会被带偏。&lt;/p>
&lt;p>下面把六轮被自己打脸的错误假设逐一列出，再讲真正根因。&lt;strong>值得保留的不是答案，是思路&lt;/strong>。&lt;/p>
&lt;h2 id="假设-1旧-profile-跨版本残留">假设 1：旧 profile 跨版本残留
&lt;/h2>&lt;p>&lt;code>docker-compose&lt;/code> 把 &lt;code>kimi-ppt-browser-profile&lt;/code> 命名卷挂到 &lt;code>/app/.courier/browser-profiles&lt;/code>。第一反应：上次构建的 Chromium 写下的 profile 文件和这次不兼容，启动 assert。&lt;/p>
&lt;p>&lt;strong>结果&lt;/strong>：用户说&amp;quot;全新部署，没操作过任何东西&amp;quot;。卷是 fresh 的，假设不成立。&lt;/p>
&lt;h2 id="假设-2docker-默认-seccomp-拦-clone3">假设 2：Docker 默认 seccomp 拦 &lt;code>clone3&lt;/code>
&lt;/h2>&lt;p>Chromium 1223 (Playwright ≥ 1.49 bundled) 大量用 &lt;code>clone3&lt;/code>。老 Docker daemon（≤ 20.10.10）的默认 seccomp profile 直接 EPERM。&lt;/p>
&lt;p>修了一行 compose：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kimi-ppt-backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">security_opt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">seccomp=unconfined&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>结果&lt;/strong>：重新部署 → 同样的 SIGTRAP。排除。&lt;/p>
&lt;h2 id="假设-3换系统-google-chrome-stable">假设 3：换系统 Google Chrome stable
&lt;/h2>&lt;p>既然 bundled chromium 1322 跑不起来，换 Google 官方 deb 装的 &lt;code>google-chrome-stable&lt;/code> 总行了吧？把 &lt;code>Dockerfile&lt;/code> 改成从 Google APT 装，&lt;code>KIMI_SLIDES_BROWSER_CHANNEL=chrome&lt;/code> 让 Playwright 通过 &lt;code>channel='chrome'&lt;/code> 调它。&lt;/p>
&lt;p>&lt;strong>结果&lt;/strong>：chrome 一样 SIGTRAP。说明不是 chromium binary 自身的 bug。&lt;/p>
&lt;h2 id="假设-4apparmor-拦-userns">假设 4：AppArmor 拦 userns
&lt;/h2>&lt;p>写了一个分层探针脚本进入容器实际查原因。关键输出：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">[1] docker inspect
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> AppArmorProfile: docker-default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> SecurityOpt: [seccomp=unconfined]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[2] kernel sysctls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> apparmor_restrict_unprivileged_userns = 1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[4] strace of chrome stable
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> chrome 主 (pid 353) sendmsg(fd4, 40 bytes) → 给 crashpad handler 发 IPC 配置
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> crashpad handler (pid 355) execve(...)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> crashpad handler +++ exited with 1 +++
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> chrome 主 --- SIGTRAP si_code=SI_KERNEL ← IMMEDIATE_CRASH(int3)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>apparmor_restrict_unprivileged_userns=1&lt;/code> 配合 &lt;code>docker-default&lt;/code> profile 会让 chrome 创建 user namespace 失败。给 compose 加：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kimi-ppt-backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">security_opt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">seccomp=unconfined&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apparmor=unconfined&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>结果&lt;/strong>：AppArmor 是 unconfined 了，chrome 仍然 SIGTRAP。&lt;code>strace&lt;/code> 显示 crashpad handler 启动后没拿到 &lt;code>--database=&lt;/code> 参数就退（更详尽的 strace 揭示真正原因——见下），所以 userns 不是真正卡点。&lt;strong>这是被自己打脸的一轮&lt;/strong>：加 &lt;code>apparmor=unconfined&lt;/code> 之后我又跑了一次 strace，发现 crashpad handler 根本没走到 userns 那一步，它在解析 argv 阶段就 fail 了。&lt;/p>
&lt;blockquote>
&lt;p>留这一段不是为了丢脸——是因为这次&amp;quot;加了一行还是没好&amp;quot;教会我：&lt;strong>每加一行配置就重新跑一次 strace 看新输出&lt;/strong>，不能凭印象假设前一次 trace 仍然成立。&lt;/p>
&lt;/blockquote>
&lt;h2 id="真正根因home-目录不存在">真正根因：HOME 目录不存在
&lt;/h2>&lt;p>回头看 [4] 那段 strace：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">355 (handler) execve args = [...--initial-client-fd=5 --shared-client-connection]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↑ 注意：没有 --database=
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">355 write(stderr, &amp;#34;chrome_crashpad_handler: --datab...&amp;#34;)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">355 +++ exited with 1 +++
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>chrome 主进程传给 crashpad handler 的 argv 里没有 &lt;code>--database=&lt;/code>&lt;/strong>。为什么？因为 chrome 决定 &lt;code>--database=&lt;/code> 路径的算法是 &lt;code>~/.config/google-chrome/Crash Reports&lt;/code>，根据 &lt;code>$HOME&lt;/code> 推。&lt;/p>
&lt;p>继续探针：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">[2] HOME 探测
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> HOME=/home/appuser
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ls -ld /home/appuser → No such file or directory
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[3] 同样的 chrome 命令用 root 跑
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt; ← 正常！
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Dockerfile&lt;/code> 里有一行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="k">RUN&lt;/span> groupadd -r appgroup &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> useradd -r -g appgroup appuser&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>useradd -r&lt;/code> 创建系统用户，&lt;strong>默认不创 home 目录&lt;/strong>。&lt;code>appuser&lt;/code> 进了 &lt;code>/etc/passwd&lt;/code> 但 &lt;code>/home/appuser&lt;/code> 不存在。chrome 拿不到可写 &lt;code>$HOME&lt;/code> → 不传 &lt;code>--database=&lt;/code> 给 crashpad handler → handler exit 1 → chrome 主进程检测到 handler init 失败 → &lt;code>IMMEDIATE_CRASH()&lt;/code> 调 &lt;code>__builtin_debugtrap()&lt;/code> → SIGTRAP。&lt;/p>
&lt;p>root 能跑是因为 &lt;code>HOME=/root&lt;/code>，目录存在。&lt;/p>
&lt;p>&lt;strong>修复（一行 Dockerfile）&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="k">RUN&lt;/span> groupadd -r appgroup &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> useradd -r -g appgroup -m -d /home/appuser appuser &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> mkdir -p /app/logs /app/downloads /app/.courier/browser-profiles &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> chown -R appuser:appgroup /app /home/appuser&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>useradd -m -d /home/appuser&lt;/code> 才会真创建 home 目录。&lt;/p>
&lt;p>&lt;strong>Dokploy Redeploy（重新 build 镜像，3-5 分钟，不是 9 秒的 hot restart）后，chrome 启动正常。&lt;/strong>&lt;/p>
&lt;h2 id="顺手修了一个-selector-bug">顺手修了一个 selector bug
&lt;/h2>&lt;p>chrome 起来后，前端点登录立刻弹&amp;quot;已检测到登录成功&amp;quot;——其实没登录。查代码发现 &lt;code>sites/kimi-slides.yaml:11&lt;/code> 配的是 &lt;code>logged_in_selector: &amp;quot;.chat-input-editor&amp;quot;&lt;/code>，而 kimi.com 的未登录落地页&lt;strong>就&lt;/strong>有 &lt;code>.chat-input-editor&lt;/code>，selector 立即命中，&lt;code>on_login_detected&lt;/code> 被错误 fire。&lt;/p>
&lt;p>从 VNC 里看登录后页面才出现的元素：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;user-avatar-container&amp;#34;&lt;/span> &lt;span class="na">style&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;width: 28px; height: 28px;&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">img&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;user-avatar&amp;#34;&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;...music_head.png&amp;#34;&lt;/span> &lt;span class="na">alt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;登月者9246&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">span&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;user-name&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>登月者9246&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;membership-upgrade&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>升级&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>membership-upgrade&lt;/code>（&amp;ldquo;升级&amp;quot;按钮）和 &lt;code>.user-avatar-container&lt;/code> 都是只在登录后才出现的——是更可靠的 post-auth marker。把 selector 换成 &lt;code>.user-avatar-container&lt;/code> 即可。&lt;/p>
&lt;h2 id="5-条复盘">5 条复盘
&lt;/h2>&lt;h3 id="1-chrome_crashpad_handler---database-is-required-是结果不是原因">1. &lt;code>chrome_crashpad_handler: --database is required&lt;/code> 是结果不是原因
&lt;/h3>&lt;p>每次看到这一行都要立刻去找父 chrome 进程的退出信号。&lt;code>SIGTRAP&lt;/code> / &lt;code>trap int3&lt;/code> 几乎一定是 Chromium 内部 &lt;code>IMMEDIATE_CRASH()&lt;/code>。crashpad 那段 usage 是它被附带拉起来时打出来的&amp;quot;遗言&amp;rdquo;，跟死因无关。&lt;/p>
&lt;h3 id="2-剥变量比改-compose-重要">2. 剥变量比改 compose 重要
&lt;/h3>&lt;p>报错涉及：卷 / 权限 / seccomp / X server / GPU / 内核 / CPU 指令 / 缺库 / 容器 runtime / app config / 用户身份。每一条都可能引发同一种症状。写脚本一次性把可控变量全部剥掉（手动跑 chromium、用 &lt;code>/tmp&lt;/code> 隔离卷、headless 隔离 X、&lt;code>--no-zygote --single-process&lt;/code> 隔离 sandbox），比改一行 compose 重新部署等结果效率高得多。&lt;/p>
&lt;p>特别有价值的一步是 &lt;strong>strace 死前最后那个 syscall&lt;/strong>——能直接定位到 &lt;code>clone3&lt;/code> / &lt;code>seccomp&lt;/code> / &lt;code>userfaultfd&lt;/code> / &lt;code>prctl&lt;/code> 哪一类失败。这一步把&amp;quot;猜容器层问题&amp;quot;变成&amp;quot;看 kernel 在哪一步拒绝&amp;quot;。&lt;/p>
&lt;h3 id="3---no-sandbox-不等于-sandbox-完全关掉">3. &lt;code>--no-sandbox&lt;/code> 不等于 sandbox 完全关掉
&lt;/h3>&lt;p>Chromium 至少有三层 sandbox：setuid sandbox、user-namespace sandbox、seccomp-bpf filter。&lt;code>--no-sandbox&lt;/code> 主要关掉前两层，仍可能在 seccomp 初始化阶段崩。要全关需要：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">--no-sandbox --disable-setuid-sandbox --disable-namespace-sandbox --disable-seccomp-filter-sandbox
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但这只用于诊断——生产即便有问题也不要靠这个绕。&lt;/p>
&lt;h3 id="4-bundled-chromium-不是万能">4. bundled chromium 不是万能
&lt;/h3>&lt;p>Playwright 自带的 Chromium 是 Playwright 团队 patched 过的版本，覆盖矩阵窄于 Google 官方 Chrome stable。Azure / 较新 Hyper-V 内核 / 某些 ARM 宿主&lt;strong>三者叠加&lt;/strong>就可能让 bundled chromium 启动崩、而系统 Chrome stable 正常——&lt;strong>反过来&lt;/strong>也成立。生产容器优先用 &lt;code>channel='chrome'&lt;/code> 跑系统 Chrome 更保守。&lt;/p>
&lt;p>但！这次我们后来发现&amp;quot;换 Chrome stable&amp;quot;本身&lt;strong>也不是修复&lt;/strong>——根因（HOME 缺失）对两个 binary 一视同仁。换 binary 治不了真正的病，只是让症状换了个皮。&lt;/p>
&lt;h3 id="5-dockerfile-一行--m-缺失藏得最深">5. Dockerfile 一行 &lt;code>-m&lt;/code> 缺失，藏得最深
&lt;/h3>&lt;p>&lt;code>useradd -r&lt;/code> 不创 home——这是发行版默认行为，dev 阶段本地跑的时候从来不会触发（Mac 跑 docker 不走 rootfs，mount 行为不一样），CI 测试也跑过（其实没跑过——&lt;code>just test&lt;/code> 不覆盖 docker build）。这种 bug 只能靠&amp;quot;上 prod 看现象&amp;quot;暴露。&lt;/p>
&lt;p>防御：&lt;/p>
&lt;ul>
&lt;li>Dockerfile 优先用 &lt;code>adduser&lt;/code>（Debian 系）而不是 &lt;code>useradd&lt;/code>，&lt;code>adduser&lt;/code> 默认创 home 且交互友好&lt;/li>
&lt;li>或者显式 &lt;code>-m -d /home/xxx&lt;/code> 让意图不依赖隐式默认&lt;/li>
&lt;li>在 CI 里至少跑一次 &lt;code>docker build &amp;amp;&amp;amp; docker run&lt;/code> 验证 chrome 类应用能起，单纯跑 pytest 不够&lt;/li>
&lt;/ul>
&lt;h2 id="完整最终改动">完整最终改动
&lt;/h2>&lt;ul>
&lt;li>&lt;code>src/backend/Dockerfile&lt;/code>：&lt;code>useradd&lt;/code> 加 &lt;code>-m -d /home/appuser&lt;/code>；&lt;code>chown&lt;/code> 补 &lt;code>/home/appuser&lt;/code>&lt;/li>
&lt;li>&lt;code>docker-compose.dokploy.yml&lt;/code>：&lt;code>security_opt: [seccomp=unconfined, apparmor=unconfined]&lt;/code>（虽然对 chromium 启动不是必须的，但保险）&lt;/li>
&lt;li>&lt;code>sites/kimi-slides.yaml&lt;/code>：&lt;code>login_detection_selector&lt;/code> 从 &lt;code>.chat-input-editor&lt;/code> 换成 &lt;code>.user-avatar-container&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="附录诊断脚本模式">附录：诊断脚本模式
&lt;/h2>&lt;p>发现 &lt;code>strace&lt;/code> 末尾 &amp;ldquo;死前最后几个 syscall&amp;rdquo; 价值最高，标准化成模板：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CONTAINER&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> sh -c &lt;span class="s1">&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> CHROME=/opt/google/chrome/chrome
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> rm -rf /tmp/cp /tmp/strace.log
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> timeout 5 strace -f -y \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> -e trace=execve,clone,clone3,prctl,seccomp,unshare,ptrace,personality,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">membarrier,userfaultfd,pidfd_open \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> -o /tmp/strace.log \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;$CHROME&amp;#34; --headless=new --no-sandbox --disable-dev-shm-usage --disable-gpu \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> --user-data-dir=/tmp/cp about:blank &amp;gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> HANDLER_PID=$(grep &amp;#34;execve.*chrome_crashpad_handler&amp;#34; /tmp/strace.log | head -1 | awk &amp;#34;{print \$1}&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> grep &amp;#34;^$HANDLER_PID &amp;#34; /tmp/strace.log | tail -40
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>-y&lt;/code> 会显示 fd 对应的资源；&lt;code>-f&lt;/code> 跟踪子进程；trace 列表只列我们关心的 syscall 防止输出爆炸。&lt;/p>
&lt;p>任何 chromium 启动崩的现场，这套脚本是首选入口。&lt;/p></description></item><item><title>gh使用教程</title><link>https://www.zata.cc/p/gh%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</link><pubDate>Sun, 12 Apr 2026 21:50:00 +0800</pubDate><guid>https://www.zata.cc/p/gh%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</guid><description>&lt;img src="https://www.zata.cc/p/gh%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index.png" alt="Featured image of post gh使用教程" />&lt;h2 id="1-常用命令速查">1. 常用命令速查
&lt;/h2>&lt;p>&lt;strong>case&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建私有仓库并推送本地代码&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo create my-project --private --source&lt;span class="o">=&lt;/span>. --push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看帮助与版本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh --version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh &lt;span class="nb">help&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh &amp;lt;command&amp;gt; --help
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 登录与认证&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh auth login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh auth status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh auth &lt;span class="nb">logout&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo clone owner/repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo create
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo view --web
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo fork
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo sync
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Pull Request&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr create --fill
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr view
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr checkout &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr diff
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr review --approve
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr merge
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Issue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue create
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue view &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue comment &lt;span class="m">123&lt;/span> --body &lt;span class="s2">&amp;#34;补充说明&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue close &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># GitHub Actions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh workflow list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh run list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh run view &amp;lt;run-id&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh run watch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh run download &amp;lt;run-id&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Release&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh release create v1.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh release list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh release view v1.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh release upload v1.0.0 dist/app.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 浏览器与搜索&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh browse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh browse &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh search prs --author @me
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh search issues --owner your-org
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># API 与脚本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh api user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh api repos/OWNER/REPO/pulls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr list --json number,title,url
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 配置与效率&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh &lt;span class="nb">alias&lt;/span> &lt;span class="nb">set&lt;/span> pv &lt;span class="s2">&amp;#34;pr view&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh config &lt;span class="nb">set&lt;/span> editor vim
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh extension search
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh extension install owner/gh-extension-name
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="2-gh-是什么">2. &lt;code>gh&lt;/code> 是什么
&lt;/h2>&lt;p>&lt;code>gh&lt;/code> 是 GitHub 官方命令行工具，核心价值不是替代 &lt;code>git&lt;/code>，而是把原本要在 GitHub 网页上完成的事情搬到终端里。&lt;/p>
&lt;p>适合用 &lt;code>git&lt;/code> 做的事情：&lt;/p>
&lt;ul>
&lt;li>本地提交、分支切换、变基、合并、查看工作区变更&lt;/li>
&lt;li>与任意 Git 远程交互，而不局限于 GitHub&lt;/li>
&lt;/ul>
&lt;p>适合用 &lt;code>gh&lt;/code> 做的事情：&lt;/p>
&lt;ul>
&lt;li>登录 GitHub 账号&lt;/li>
&lt;li>创建和管理 Pull Request、Issue、Release&lt;/li>
&lt;li>查看和触发 GitHub Actions&lt;/li>
&lt;li>直接调用 GitHub API&lt;/li>
&lt;li>安装 GitHub CLI 扩展&lt;/li>
&lt;/ul>
&lt;p>最实用的理解方式是：&lt;code>git&lt;/code> 管仓库历史，&lt;code>gh&lt;/code> 管 GitHub 平台能力。&lt;/p>
&lt;h2 id="3-安装与初始化">3. 安装与初始化
&lt;/h2>&lt;h3 id="安装">安装
&lt;/h3>&lt;p>常见安装方式如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># macOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew install gh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Windows&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">winget install --id GitHub.cli
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Ubuntu / Debian&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">type&lt;/span> -p curl &amp;gt;/dev/null &lt;span class="o">||&lt;/span> sudo apt install curl -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sudo dd &lt;span class="nv">of&lt;/span>&lt;span class="o">=&lt;/span>/usr/share/keyrings/githubcli-archive-keyring.gpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;deb [arch=&lt;/span>&lt;span class="k">$(&lt;/span>dpkg --print-architecture&lt;span class="k">)&lt;/span>&lt;span class="s2"> signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sudo tee /etc/apt/sources.list.d/github-cli.list &amp;gt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt install gh -y
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安装后先确认版本：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="登录">登录
&lt;/h3>&lt;p>最常用的是交互式登录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh auth login
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>执行后通常会依次选择：&lt;/p>
&lt;ul>
&lt;li>登录 &lt;code>GitHub.com&lt;/code> 还是企业版 GitHub&lt;/li>
&lt;li>使用 &lt;code>HTTPS&lt;/code> 还是 &lt;code>SSH&lt;/code>&lt;/li>
&lt;li>通过浏览器登录还是输入令牌&lt;/li>
&lt;/ul>
&lt;p>登录完成后可以检查状态：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh auth status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你已经有环境变量令牌，也可以让 &lt;code>gh&lt;/code> 直接使用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GITHUB_TOKEN&lt;/span>&lt;span class="o">=&lt;/span>your_token
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh auth status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>说明：&lt;/p>
&lt;ul>
&lt;li>日常个人开发，交互式 &lt;code>gh auth login&lt;/code> 最省事&lt;/li>
&lt;li>在 CI、脚本、自动化环境里，更常见的是 &lt;code>GITHUB_TOKEN&lt;/code>&lt;/li>
&lt;li>如果你使用 GitHub Enterprise，可以用 &lt;code>gh auth login --hostname &amp;lt;hostname&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="4-仓库操作">4. 仓库操作
&lt;/h2>&lt;h3 id="克隆仓库">克隆仓库
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh repo clone owner/repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它本质上还是帮你完成 GitHub 仓库级别的克隆，但写法更统一，也方便后续继续使用 &lt;code>gh&lt;/code>。&lt;/p>
&lt;h3 id="创建仓库">创建仓库
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh repo create
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个命令会进入交互流程，通常会问你：&lt;/p>
&lt;ul>
&lt;li>仓库名称&lt;/li>
&lt;li>是否公开&lt;/li>
&lt;li>是否添加 README&lt;/li>
&lt;li>是否推送当前目录&lt;/li>
&lt;/ul>
&lt;p>如果你想快速把当前项目推到 GitHub，经常很好用。&lt;/p>
&lt;h3 id="查看仓库信息">查看仓库信息
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh repo view
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo view --web
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo view owner/repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常见用途：&lt;/p>
&lt;ul>
&lt;li>在终端查看仓库概要&lt;/li>
&lt;li>直接在浏览器打开仓库页面&lt;/li>
&lt;li>脱离当前目录查看别的仓库&lt;/li>
&lt;/ul>
&lt;h3 id="fork-与同步">Fork 与同步
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh repo fork
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh repo sync
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>典型场景：&lt;/p>
&lt;ol>
&lt;li>&lt;code>fork&lt;/code> 别人的仓库到自己账号下&lt;/li>
&lt;li>在自己的 fork 上开发&lt;/li>
&lt;li>上游仓库更新后，用 &lt;code>gh repo sync&lt;/code> 同步&lt;/li>
&lt;/ol>
&lt;p>如果你平时经常参与开源项目，这两个命令非常顺手。&lt;/p>
&lt;h2 id="5-pull-request-工作流">5. Pull Request 工作流
&lt;/h2>&lt;p>&lt;code>gh&lt;/code> 最有价值的部分之一，就是把 PR 的完整链路搬到命令行里。&lt;/p>
&lt;h3 id="创建-pr">创建 PR
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr create
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr create --fill
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--fill&lt;/code> 会自动用当前分支的提交信息帮你生成标题和描述，适合已经写好了比较规范 commit message 的情况。&lt;/p>
&lt;p>常见流程：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout -b feature/login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;feat: add login page&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push -u origin feature/login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr create --fill
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="查看-pr">查看 PR
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr view
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr view &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr view --web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>适合查看：&lt;/p>
&lt;ul>
&lt;li>当前仓库有哪些 PR&lt;/li>
&lt;li>某个 PR 的详情&lt;/li>
&lt;li>直接跳转浏览器继续看评论和检查项&lt;/li>
&lt;/ul>
&lt;h3 id="切换到某个-pr">切换到某个 PR
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr checkout &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个命令很适合代码评审场景。你不用先找分支名，直接通过 PR 编号就能把代码拉到本地。&lt;/p>
&lt;h3 id="查看差异检查评审合并">查看差异、检查、评审、合并
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr diff &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr checks &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr review &lt;span class="m">123&lt;/span> --approve
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr review &lt;span class="m">123&lt;/span> --comment -b &lt;span class="s2">&amp;#34;建议补一个测试&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr merge &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这一组命令基本覆盖了 PR 审查最常见的动作：&lt;/p>
&lt;ul>
&lt;li>看 diff&lt;/li>
&lt;li>看 CI 是否通过&lt;/li>
&lt;li>approve / comment / request changes&lt;/li>
&lt;li>合并 PR&lt;/li>
&lt;/ul>
&lt;p>如果团队流程比较规范，&lt;code>gh&lt;/code> 能明显减少频繁切浏览器的次数。&lt;/p>
&lt;h2 id="6-issue-管理">6. Issue 管理
&lt;/h2>&lt;h3 id="创建与查看">创建与查看
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh issue create
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue view &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue view &lt;span class="m">123&lt;/span> --web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>适合用来做：&lt;/p>
&lt;ul>
&lt;li>快速提 bug&lt;/li>
&lt;li>记录待办事项&lt;/li>
&lt;li>查看当前仓库未关闭的问题&lt;/li>
&lt;/ul>
&lt;h3 id="评论编辑关闭">评论、编辑、关闭
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh issue comment &lt;span class="m">123&lt;/span> --body &lt;span class="s2">&amp;#34;我来处理这个问题&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue edit &lt;span class="m">123&lt;/span> --add-label bug
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue close &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue reopen &lt;span class="m">123&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你把 GitHub Issues 当轻量项目管理工具，这一组命令会非常高频。&lt;/p>
&lt;h2 id="7-github-actions-与-cicd">7. GitHub Actions 与 CI/CD
&lt;/h2>&lt;p>&lt;code>gh&lt;/code> 对 Actions 的支持很实用，尤其适合你在终端里排查 CI。&lt;/p>
&lt;h3 id="查看工作流和运行记录">查看工作流和运行记录
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh workflow list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh run list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh run view &amp;lt;run-id&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="观察运行状态">观察运行状态
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh run watch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个命令适合在你刚 push 代码后盯 CI 结果，不用一直刷新网页。&lt;/p>
&lt;h3 id="下载产物与重新运行">下载产物与重新运行
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh run download &amp;lt;run-id&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh run rerun &amp;lt;run-id&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常见场景：&lt;/p>
&lt;ul>
&lt;li>下载构建产物&lt;/li>
&lt;li>CI 因偶发网络问题失败时重跑&lt;/li>
&lt;li>快速定位是哪一步报错&lt;/li>
&lt;/ul>
&lt;h2 id="8-release-管理">8. Release 管理
&lt;/h2>&lt;p>如果你的项目通过 GitHub Release 发版本，&lt;code>gh&lt;/code> 可以把整个流程命令化。&lt;/p>
&lt;h3 id="创建与查看-release">创建与查看 Release
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh release create v1.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh release list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh release view v1.0.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="上传构建产物">上传构建产物
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh release upload v1.0.0 dist/app.tar.gz
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这在发布二进制、压缩包、安装包时很方便，尤其适合配合脚本或 CI 自动化。&lt;/p>
&lt;h2 id="9-gh-api把-github-api-直接带进终端">9. &lt;code>gh api&lt;/code>：把 GitHub API 直接带进终端
&lt;/h2>&lt;p>很多人低估了 &lt;code>gh api&lt;/code>，但它其实是 &lt;code>gh&lt;/code> 最强的能力之一。&lt;/p>
&lt;h3 id="基本调用">基本调用
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh api user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh api repos/OWNER/REPO
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh api repos/OWNER/REPO/issues
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果某个能力暂时没有现成的 &lt;code>gh &amp;lt;group&amp;gt; &amp;lt;subcommand&amp;gt;&lt;/code>，往往可以直接退回到 &lt;code>gh api&lt;/code>。&lt;/p>
&lt;h3 id="搭配-json-输出">搭配 JSON 输出
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr list --json number,title,url
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh issue list --json number,title,labels
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh workflow list --json id,name,path,state
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这类写法很适合：&lt;/p>
&lt;ul>
&lt;li>给 shell 脚本喂数据&lt;/li>
&lt;li>做自动化统计&lt;/li>
&lt;li>和 &lt;code>jq&lt;/code> 配合筛选字段&lt;/li>
&lt;/ul>
&lt;p>例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr list --json number,title,author --jq &lt;span class="s1">&amp;#39;.[] | {number, title, author: .author.login}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你经常写脚本，建议尽快把 &lt;code>--json&lt;/code>、&lt;code>--jq&lt;/code> 这套用起来。&lt;/p>
&lt;h2 id="10-提效能力别名配置扩展">10. 提效能力：别名、配置、扩展
&lt;/h2>&lt;h3 id="alias">alias
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh &lt;span class="nb">alias&lt;/span> &lt;span class="nb">set&lt;/span> pv &lt;span class="s2">&amp;#34;pr view&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh &lt;span class="nb">alias&lt;/span> &lt;span class="nb">set&lt;/span> pc &lt;span class="s2">&amp;#34;pr create --fill&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>适合把高频长命令压缩成自己熟悉的缩写。&lt;/p>
&lt;h3 id="config">config
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh config &lt;span class="nb">set&lt;/span> editor vim
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh config get editor
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最常见的是设置默认编辑器，避免某些命令唤起你不想用的编辑器。&lt;/p>
&lt;h3 id="extension">extension
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh extension search
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh extension install owner/gh-extension-name
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh extension list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh extension upgrade --all
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>扩展机制可以理解为给 &lt;code>gh&lt;/code> 加外挂命令。对于一些官方没内置、但社区已经做好了的能力，扩展通常是最快方案。&lt;/p>
&lt;h2 id="11-推荐的实际使用方式">11. 推荐的实际使用方式
&lt;/h2>&lt;p>如果你是个人开发者，可以先掌握这条最实用链路：&lt;/p>
&lt;ol>
&lt;li>&lt;code>gh auth login&lt;/code>&lt;/li>
&lt;li>&lt;code>gh repo clone owner/repo&lt;/code>&lt;/li>
&lt;li>&lt;code>git checkout -b feature/xxx&lt;/code>&lt;/li>
&lt;li>本地开发并提交&lt;/li>
&lt;li>&lt;code>git push -u origin feature/xxx&lt;/code>&lt;/li>
&lt;li>&lt;code>gh pr create --fill&lt;/code>&lt;/li>
&lt;li>&lt;code>gh pr checks&lt;/code>&lt;/li>
&lt;li>&lt;code>gh pr merge&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>如果你负责 CI/CD 或平台维护，再补上这些：&lt;/p>
&lt;ul>
&lt;li>&lt;code>gh workflow list&lt;/code>&lt;/li>
&lt;li>&lt;code>gh run list&lt;/code>&lt;/li>
&lt;li>&lt;code>gh run watch&lt;/code>&lt;/li>
&lt;li>&lt;code>gh release create&lt;/code>&lt;/li>
&lt;li>&lt;code>gh api&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>如果你经常参与开源协作，再补上这些：&lt;/p>
&lt;ul>
&lt;li>&lt;code>gh repo fork&lt;/code>&lt;/li>
&lt;li>&lt;code>gh repo sync&lt;/code>&lt;/li>
&lt;li>&lt;code>gh issue list&lt;/code>&lt;/li>
&lt;li>&lt;code>gh pr checkout&lt;/code>&lt;/li>
&lt;li>&lt;code>gh pr review&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="12-常见问题">12. 常见问题
&lt;/h2>&lt;h3 id="gh-和-git-到底怎么分工">&lt;code>gh&lt;/code> 和 &lt;code>git&lt;/code> 到底怎么分工？
&lt;/h3>&lt;p>一句话：&lt;/p>
&lt;ul>
&lt;li>&lt;code>git&lt;/code> 负责版本控制&lt;/li>
&lt;li>&lt;code>gh&lt;/code> 负责 GitHub 平台交互&lt;/li>
&lt;/ul>
&lt;p>最合理的方式不是二选一，而是组合使用。&lt;/p>
&lt;h3 id="为什么有些仓库命令要加--r">为什么有些仓库命令要加 &lt;code>-R&lt;/code>？
&lt;/h3>&lt;p>因为 &lt;code>gh&lt;/code> 默认优先使用当前目录对应的仓库上下文。如果你不在仓库目录里，或者你想操作别的仓库，就显式指定：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr list -R OWNER/REPO
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="命令太多记不住怎么办">命令太多记不住怎么办？
&lt;/h3>&lt;p>先记住三件事：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh &amp;lt;command&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh &amp;lt;command&amp;gt; --help
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>gh&lt;/code> 的帮助系统本身就做得很好，遇到不熟悉的子命令时直接查帮助通常最快。&lt;/p>
&lt;h2 id="13-参考资料">13. 参考资料
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://cli.github.com/manual/index" target="_blank" rel="noopener"
>GitHub CLI manual&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://docs.github.com/en/enterprise-cloud@latest/github-cli/github-cli/quickstart" target="_blank" rel="noopener"
>GitHub CLI quickstart&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://docs.github.com/en/github-cli/github-cli/about-github-cli" target="_blank" rel="noopener"
>About GitHub CLI&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Docker Swarm 实战（二）：Traefik 反向代理部署</title><link>https://www.zata.cc/p/docker-swarm-traefik-deployment/</link><pubDate>Sun, 12 Apr 2026 11:00:00 +0800</pubDate><guid>https://www.zata.cc/p/docker-swarm-traefik-deployment/</guid><description>&lt;img src="https://www.zata.cc/p/docker-swarm-traefik-deployment/images/index/index.png" alt="Featured image of post Docker Swarm 实战（二）：Traefik 反向代理部署" />&lt;blockquote>
&lt;p>记录一次真实的生产环境部署过程，包含踩过的坑和解决方案。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>环境说明&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>服务器：阿里云 ECS（Ubuntu）&lt;/li>
&lt;li>应用栈：FastAPI 后端 + Next.js 前端 + Celery + Redis&lt;/li>
&lt;li>反向代理：Traefik v2.11&lt;/li>
&lt;li>容器编排：Docker Swarm&lt;/li>
&lt;li>镜像仓库：阿里云 ACR&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>第一步：安装 Docker&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -fsSL https://get.docker.com &lt;span class="p">|&lt;/span> sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>国内服务器如果无法访问，通过 DaoCloud 镜像拉取：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 拉取镜像示例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker pull docker.m.daocloud.io/traefik:v2.11
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>验证安装：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>&lt;strong>第二步：初始化 Docker Swarm&lt;/strong>&lt;/p>
&lt;p>单机部署时，本机既是 manager 也是 worker：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker swarm init --advertise-addr &amp;lt;服务器公网IP&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>执行后会输出一个 &lt;code>docker swarm join&lt;/code> 命令，如果以后需要加入 worker 节点可以用它。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>第三步：创建 Traefik 公共网络&lt;/strong>&lt;/p>
&lt;p>Traefik 通过 overlay 网络发现各服务，这个网络必须在部署任何 stack 之前创建好：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker network create --driver overlay --attachable traefik-public
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>重要&lt;/strong>：应用的 &lt;code>docker-compose.stack.yml&lt;/code> 中 &lt;code>traefik-public&lt;/code> 网络标记为 &lt;code>external: true&lt;/code>，如果这个网络不存在，&lt;code>docker stack deploy&lt;/code> 会直接报错失败。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;p>&lt;strong>第四步：准备 Traefik 配置文件&lt;/strong>&lt;/p>
&lt;p>将以下 &lt;code>stack.yml&lt;/code> 上传到服务器 &lt;code>/opt/myapp/traefik/stack.yml&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traefik&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.m.daocloud.io/traefik:v2.11&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">providers.docker=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">providers.docker.swarmMode=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">providers.docker.endpoint=unix:///var/run/docker.sock&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">providers.docker.exposedByDefault=false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">providers.docker.network=traefik-public&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">entrypoints.web.address=:80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">entrypoints.websecure.address=:443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">entrypoints.web.http.redirections.entrypoint.to=websecure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">entrypoints.web.http.redirections.entrypoint.scheme=https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">certificatesresolvers.letsencrypt.acme.email=${TRAEFIK_ACME_EMAIL}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">certificatesresolvers.letsencrypt.acme.storage=/var/lib/traefik/acme/acme.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">ping=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">published&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">published&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/opt/myapp/traefik/acme.json:/var/lib/traefik/acme/acme.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">traefik-public&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthcheck&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;CMD&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;traefik&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;healthcheck&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--ping&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">30s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retries&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">replicated&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">placement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">constraints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">node.role==manager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traefik-public&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik-public&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>&lt;strong>第五步：准备 ACME 证书存储文件&lt;/strong>&lt;/p>
&lt;p>Traefik 会自动申请 Let&amp;rsquo;s Encrypt 证书并写入这个文件，&lt;strong>文件必须提前创建且权限为 600&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p /opt/myapp/traefik
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch /opt/myapp/traefik/acme.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> /opt/myapp/traefik/acme.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>文件内容是空的完全没问题，Traefik 首次启动后会自动填充。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;p>&lt;strong>第六步：部署 Traefik&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/myapp/traefik
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">TRAEFIK_ACME_EMAIL&lt;/span>&lt;span class="o">=&lt;/span>你的邮箱@example.com &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>docker stack deploy -c stack.yml traefik
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>验证是否正常运行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker stack services traefik
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 应看到 REPLICAS 为 1/1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service logs traefik_traefik --tail &lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 应看到 &amp;#34;Configuration loaded from flags.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>&lt;strong>踩坑记录&lt;/strong>&lt;/p>
&lt;p>&lt;strong>坑 1：Traefik v3 的 swarm provider 与 Docker 29 不兼容&lt;/strong>&lt;/p>
&lt;p>&lt;strong>现象&lt;/strong>：日志持续报错 &lt;code>client version 1.24 is too old. Minimum supported API version is 1.40&lt;/code>，服务始终 &lt;code>0/1&lt;/code>。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：Traefik v3.3.5 的 swarm provider 内部 Docker 客户端使用了旧版 API（1.24），而 Docker 29.x daemon 最低只支持 API 1.40。设置 &lt;code>DOCKER_API_VERSION&lt;/code> 环境变量无效，因为 Traefik 的 swarm provider 没有使用该变量。&lt;/p>
&lt;p>&lt;strong>解决方案&lt;/strong>：降级到 Traefik v2.11，v2 的 Docker provider 对 Swarm 支持成熟稳定。&lt;/p>
&lt;p>同时注意 v2 和 v3 的命令参数不同：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>功能&lt;/th>
&lt;th>v3&lt;/th>
&lt;th>v2&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>启用 Swarm&lt;/td>
&lt;td>&lt;code>--providers.swarm=true&lt;/code>&lt;/td>
&lt;td>&lt;code>--providers.docker.swarmMode=true&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>端点&lt;/td>
&lt;td>&lt;code>--providers.swarm.endpoint&lt;/code>&lt;/td>
&lt;td>&lt;code>--providers.docker.endpoint&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>默认不暴露&lt;/td>
&lt;td>&lt;code>--providers.swarm.exposedByDefault&lt;/code>&lt;/td>
&lt;td>&lt;code>--providers.docker.exposedByDefault&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>坑 2：静态配置文件与 CLI 参数冲突&lt;/strong>&lt;/p>
&lt;p>&lt;strong>现象&lt;/strong>：同时使用 &lt;code>--configFile=/etc/traefik/traefik.yml&lt;/code> 和 CLI 参数时，ACME 存储路径用了配置文件里的旧路径，导致 &lt;code>open /letsencrypt/acme.json: no such file or directory&lt;/code>。&lt;/p>
&lt;p>&lt;strong>解决方案&lt;/strong>：二选一，不要混用。推荐只用 CLI 参数，去掉 &lt;code>--configFile&lt;/code> 和 &lt;code>configs&lt;/code> 挂载。&lt;/p>
&lt;p>&lt;strong>坑 3：SCP 上传目录时目标路径不存在&lt;/strong>&lt;/p>
&lt;p>&lt;strong>现象&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">scp&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">realpath&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">opt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">myapp&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">traefik&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">No&lt;/span> &lt;span class="n">such&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">scp&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">upload&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">path&lt;/span> &lt;span class="n">canonicalization&lt;/span> &lt;span class="n">failed&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>解决方案&lt;/strong>：上传前先在服务器上创建目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ssh root@your-server &lt;span class="s2">&amp;#34;mkdir -p /opt/myapp/traefik&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>坑 4：host-mode 端口被占用导致 Pending&lt;/strong>&lt;/p>
&lt;p>&lt;strong>现象&lt;/strong>：&lt;code>docker service ps&lt;/code> 看到 &lt;code>host-mode port already in use on 1 node&lt;/code>，新任务处于 Pending 状态。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：滚动更新时旧容器仍在运行占用端口，新容器无法启动。等旧容器退出后会自动恢复正常，不需要手动干预。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>部署顺序总结&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">1. 安装 Docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2. docker swarm init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3. docker network create --driver overlay --attachable traefik-public
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">4. 准备 acme.json（touch + chmod 600）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">5. docker stack deploy -c stack.yml traefik ← Traefik 先于应用部署
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">6. docker stack deploy -c docker-compose.stack.yml &amp;lt;app-stack-name&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Traefik 会监听 Docker Swarm 的服务变化，自动根据 &lt;code>deploy.labels&lt;/code> 中的配置生成路由规则和申请 HTTPS 证书，无需重启。&lt;/p></description></item><item><title>Docker Swarm 实战（一）：核心概念与集群管理</title><link>https://www.zata.cc/p/docker-swarm-guide/</link><pubDate>Sun, 12 Apr 2026 10:00:00 +0800</pubDate><guid>https://www.zata.cc/p/docker-swarm-guide/</guid><description>&lt;img src="https://www.zata.cc/p/docker-swarm-guide/images/index/index.png" alt="Featured image of post Docker Swarm 实战（一）：核心概念与集群管理" />&lt;h2 id="系列文章">系列文章
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>篇&lt;/th>
&lt;th>主题&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>一（本篇）&lt;/td>
&lt;td>核心概念与集群管理&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a class="link" href="../docker-swarm-traefik-deployment/" >二&lt;/a>&lt;/td>
&lt;td>Traefik 反向代理部署&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="为什么选择-docker-swarm">为什么选择 Docker Swarm
&lt;/h2>&lt;p>在容器编排领域，Kubernetes 是行业标准，但对于中小规模项目，Docker Swarm 往往是更务实的选择：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>零额外依赖&lt;/strong>：Docker 自带，无需单独安装&lt;/li>
&lt;li>&lt;strong>学习曲线平缓&lt;/strong>：&lt;code>docker-compose.yml&lt;/code> 稍加改造就能用于 Swarm&lt;/li>
&lt;li>&lt;strong>运维成本低&lt;/strong>：单机也能跑，按需扩展到多节点&lt;/li>
&lt;li>&lt;strong>足够稳定&lt;/strong>：生产可用，社区成熟&lt;/li>
&lt;/ul>
&lt;p>如果你的业务规模不需要 K8s 的复杂调度能力，Swarm 是一个省心的选择。&lt;/p>
&lt;hr>
&lt;h2 id="核心概念">核心概念
&lt;/h2>&lt;h3 id="node节点">Node（节点）
&lt;/h3>&lt;p>Swarm 集群由多个 Docker 主机（Node）组成，分两类角色：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>角色&lt;/th>
&lt;th>职责&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Manager&lt;/strong>&lt;/td>
&lt;td>维护集群状态、调度任务、处理 API 请求&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Worker&lt;/strong>&lt;/td>
&lt;td>执行容器任务，不参与调度决策&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;ul>
&lt;li>Manager 节点同时也承担 Worker 职责（可配置为纯 Manager）&lt;/li>
&lt;li>生产环境建议部署奇数个 Manager（3 或 5），保证 Raft 共识算法的容错能力&lt;/li>
&lt;li>单机部署时，本机同时充当 Manager 和 Worker&lt;/li>
&lt;/ul>
&lt;h3 id="service服务">Service（服务）
&lt;/h3>&lt;p>Service 是 Swarm 的基本部署单元，描述&amp;quot;运行什么、运行几份、如何更新&amp;quot;：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Service
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── replicated mode：固定副本数，调度器负责在节点间分配
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── global mode：每个节点各运行一个副本（适合日志收集、监控 agent 等）
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="task任务">Task（任务）
&lt;/h3>&lt;p>Task 是 Service 的最小执行单元，对应一个运行中的容器实例。调度器将 Task 分配到合适的节点上执行。&lt;/p>
&lt;h3 id="stack栈">Stack（栈）
&lt;/h3>&lt;p>Stack 是一组相关 Service 的集合，通过 &lt;code>docker-compose.yml&lt;/code>（需加 &lt;code>deploy&lt;/code> 字段）定义，用 &lt;code>docker stack deploy&lt;/code> 一键部署。&lt;/p>
&lt;h3 id="overlay-network覆盖网络">Overlay Network（覆盖网络）
&lt;/h3>&lt;p>Swarm 使用 overlay 网络让不同节点上的容器互相通信，底层通过 VXLAN 封装实现跨主机网络。&lt;/p>
&lt;hr>
&lt;h2 id="安装与初始化">安装与初始化
&lt;/h2>&lt;h3 id="安装-docker">安装 Docker
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -fsSL https://get.docker.com &lt;span class="p">|&lt;/span> sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>国内网络可通过 DaoCloud 镜像安装：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sSL https://get.daocloud.io/docker &lt;span class="p">|&lt;/span> sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="初始化-swarm">初始化 Swarm
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 初始化，指定对外通告的 IP（通常为服务器公网 IP 或内网 IP）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm init --advertise-addr &amp;lt;IP&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>初始化成功后会输出加入集群的命令，记录备用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">To add a worker to this swarm, run the following command:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> docker swarm join --token SWMTKN-1-xxx &amp;lt;IP&amp;gt;:2377
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">To add a manager to this swarm, run &amp;#39;docker swarm join-token manager&amp;#39; and follow the instructions.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="查看集群状态">查看集群状态
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker node ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出示例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">abc123 * node-1 Ready Active Leader
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">def456 node-2 Ready Active
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ghi789 node-3 Ready Active Reachable
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="节点管理">节点管理
&lt;/h2>&lt;h3 id="添加节点">添加节点
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在 Manager 节点上获取加入 token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm join-token worker &lt;span class="c1"># 获取 worker 加入命令&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm join-token manager &lt;span class="c1"># 获取 manager 加入命令&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在目标机器上执行输出的 join 命令&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm join --token &amp;lt;TOKEN&amp;gt; &amp;lt;MANAGER_IP&amp;gt;:2377
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="更改节点角色">更改节点角色
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Worker 升级为 Manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node promote &amp;lt;NODE_ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Manager 降级为 Worker&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node demote &amp;lt;NODE_ID&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="节点维护与下线">节点维护与下线
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将节点置为维护模式（停止接受新任务，现有任务迁移到其他节点）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node update --availability drain &amp;lt;NODE_ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 恢复为可调度状态&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node update --availability active &amp;lt;NODE_ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 从集群中删除节点（需先 drain）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node rm &amp;lt;NODE_ID&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="服务管理">服务管理
&lt;/h2>&lt;h3 id="创建服务">创建服务
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 基本格式&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service create &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name my-web &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --replicas &lt;span class="m">3&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --publish &lt;span class="nv">published&lt;/span>&lt;span class="o">=&lt;/span>8080,target&lt;span class="o">=&lt;/span>&lt;span class="m">80&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> nginx:alpine
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常用参数：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>参数&lt;/th>
&lt;th>说明&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>--replicas N&lt;/code>&lt;/td>
&lt;td>副本数量&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--publish&lt;/code>&lt;/td>
&lt;td>端口映射&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--network&lt;/code>&lt;/td>
&lt;td>加入指定网络&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--env KEY=VAL&lt;/code>&lt;/td>
&lt;td>环境变量&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--constraint&lt;/code>&lt;/td>
&lt;td>调度约束（如 &lt;code>node.role==manager&lt;/code>）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--limit-cpu / --limit-memory&lt;/code>&lt;/td>
&lt;td>资源限制&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--restart-condition&lt;/code>&lt;/td>
&lt;td>重启策略&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--update-parallelism&lt;/code>&lt;/td>
&lt;td>滚动更新并发数&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="查看服务">查看服务
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service ls &lt;span class="c1"># 列出所有服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service ps &amp;lt;SERVICE&amp;gt; &lt;span class="c1"># 查看服务的任务列表（各副本状态）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service inspect &amp;lt;SERVICE&amp;gt; &lt;span class="c1"># 查看服务详细配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service inspect --pretty &amp;lt;SERVICE&amp;gt; &lt;span class="c1"># 易读格式&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="扩缩容">扩缩容
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service scale my-web&lt;span class="o">=&lt;/span>&lt;span class="m">5&lt;/span> &lt;span class="c1"># 将副本数调整为 5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service scale &lt;span class="nv">svc1&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">3&lt;/span> &lt;span class="nv">svc2&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">2&lt;/span> &lt;span class="c1"># 同时调整多个服务&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="滚动更新">滚动更新
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 更新镜像版本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service update --image nginx:1.25 my-web
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 控制更新节奏&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service update &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --update-parallelism &lt;span class="m">2&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># 每批更新 2 个副本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --update-delay 10s &lt;span class="se">\ &lt;/span> &lt;span class="c1"># 每批之间等待 10 秒&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --image nginx:1.25 my-web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>更新失败时回滚：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service rollback my-web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="删除服务">删除服务
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service rm my-web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="stack-部署">Stack 部署
&lt;/h2>&lt;p>Stack 是推荐的生产部署方式，用 Compose 文件统一描述整个应用栈。&lt;/p>
&lt;h3 id="compose-文件关键差异">Compose 文件关键差异
&lt;/h3>&lt;p>与普通 &lt;code>docker-compose.yml&lt;/code> 相比，Stack 文件需要增加 &lt;code>deploy&lt;/code> 字段，并且&lt;strong>不支持&lt;/strong> &lt;code>build&lt;/code>（镜像必须提前构建好）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myregistry/api:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app-net&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">update_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parallelism&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delay&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failure_action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rollback&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart_policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">on&lt;/span>-&lt;span class="l">failure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">delay&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">max_attempts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.5&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">512M&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.api.rule=Host(`api.example.com`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app-net&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">driver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">overlay&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="部署与更新">部署与更新
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 部署（stack 不存在则创建，已存在则更新）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stack deploy -c docker-compose.stack.yml myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 带环境变量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ENV_VAR&lt;/span>&lt;span class="o">=&lt;/span>value docker stack deploy -c stack.yml myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看 stack 中的服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stack services myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看 stack 中所有任务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stack ps myapp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除整个 stack&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stack rm myapp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="网络管理">网络管理
&lt;/h2>&lt;h3 id="创建-overlay-网络">创建 overlay 网络
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 基本创建&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker network create --driver overlay my-network
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 可附加（允许独立容器加入，非 Swarm service 也能连接）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker network create --driver overlay --attachable my-network
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="网络模式对比">网络模式对比
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>模式&lt;/th>
&lt;th>适用场景&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>overlay&lt;/code>&lt;/td>
&lt;td>Swarm service 之间的跨节点通信（默认）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>host&lt;/code>&lt;/td>
&lt;td>直接使用宿主机网络栈，性能最高，但端口冲突风险&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ingress&lt;/code>&lt;/td>
&lt;td>Swarm 内置的负载均衡网络（自动创建，无需手动管理）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="端口发布模式">端口发布模式
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">published&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># host 模式：直接绑定到宿主机，绕过 Swarm 的 ingress 负载均衡&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;80:80&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># ingress 模式（默认）：通过 Swarm routing mesh 转发，任意节点均可接收流量&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="配置与密钥管理">配置与密钥管理
&lt;/h2>&lt;h3 id="secret敏感数据">Secret（敏感数据）
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建 secret&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;my_db_password&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> docker secret create db_password -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker secret create ssl_cert ./cert.pem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在 service 中使用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service create &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name db &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --secret db_password &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> postgres:15
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 Compose 文件中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres:15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secrets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">db_password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">secrets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db_password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用已存在的 secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Secret 会以文件形式挂载到容器的 &lt;code>/run/secrets/&amp;lt;secret_name&amp;gt;&lt;/code>。&lt;/p>
&lt;h3 id="config非敏感配置">Config（非敏感配置）
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建 config&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker config create nginx_conf ./nginx.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在 service 中使用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service create &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name web &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --config &lt;span class="nv">source&lt;/span>&lt;span class="o">=&lt;/span>nginx_conf,target&lt;span class="o">=&lt;/span>/etc/nginx/nginx.conf &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> nginx
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="常用运维操作">常用运维操作
&lt;/h2>&lt;h3 id="查看服务日志">查看服务日志
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service logs &amp;lt;SERVICE&amp;gt; &lt;span class="c1"># 输出所有副本的日志&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service logs -f &amp;lt;SERVICE&amp;gt; &lt;span class="c1"># 实时跟踪&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service logs --tail &lt;span class="m">50&lt;/span> &amp;lt;SERVICE&amp;gt; &lt;span class="c1"># 最近 50 行&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="强制重新部署不改配置">强制重新部署（不改配置）
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service update --force &amp;lt;SERVICE&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="查看任务失败原因">查看任务失败原因
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service ps --no-trunc &amp;lt;SERVICE&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--no-trunc&lt;/code> 会显示完整的错误信息，方便排查启动失败的原因。&lt;/p>
&lt;h3 id="清理已停止的任务">清理已停止的任务
&lt;/h3>&lt;p>Swarm 默认保留已完成的任务记录，可通过以下方式清理：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker system prune &lt;span class="c1"># 清理未使用的容器、网络、镜像&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker system prune --volumes &lt;span class="c1"># 同时清理 volume&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="常见问题">常见问题
&lt;/h2>&lt;p>&lt;strong>Q：&lt;code>docker stack deploy&lt;/code> 报错 &lt;code>network not found&lt;/code>？&lt;/strong>&lt;/p>
&lt;p>A：&lt;code>external: true&lt;/code> 的网络必须在 deploy 之前手动创建，否则 Swarm 不会自动创建它：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker network create --driver overlay --attachable traefik-public
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Q：服务副本一直是 &lt;code>0/1&lt;/code>，&lt;code>docker service ps&lt;/code> 显示 &lt;code>pending&lt;/code>？&lt;/strong>&lt;/p>
&lt;p>A：常见原因：&lt;/p>
&lt;ul>
&lt;li>节点资源不足（CPU/内存超出限制）&lt;/li>
&lt;li>调度约束无法满足（如 &lt;code>node.role==manager&lt;/code> 但 manager 不可用）&lt;/li>
&lt;li>host-mode 端口冲突（等旧容器退出自动恢复）&lt;/li>
&lt;li>镜像拉取失败（检查仓库权限和网络）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Q：滚动更新后服务异常，如何回滚？&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker service rollback &amp;lt;SERVICE&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Q：单机 Swarm 和 docker compose up 有什么区别？&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>特性&lt;/th>
&lt;th>docker compose&lt;/th>
&lt;th>docker stack (Swarm)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>高可用&lt;/td>
&lt;td>无&lt;/td>
&lt;td>副本自动重启、跨节点调度&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>滚动更新&lt;/td>
&lt;td>无&lt;/td>
&lt;td>内置，支持回滚&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Secret 管理&lt;/td>
&lt;td>无&lt;/td>
&lt;td>原生支持&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>扩缩容&lt;/td>
&lt;td>手动&lt;/td>
&lt;td>&lt;code>docker service scale&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>适用场景&lt;/td>
&lt;td>本地开发&lt;/td>
&lt;td>生产部署&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="下一步">下一步
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="../docker-swarm-traefik-deployment/" >Docker Swarm 实战（二）：Traefik 反向代理部署&lt;/a> — 在 Swarm 上部署 Traefik v2，实现 HTTPS 自动证书和路由转发&lt;/li>
&lt;/ul></description></item><item><title>Self-hosted Runner</title><link>https://www.zata.cc/p/self-hosted-runner/</link><pubDate>Tue, 24 Feb 2026 09:22:30 +0800</pubDate><guid>https://www.zata.cc/p/self-hosted-runner/</guid><description>&lt;img src="https://www.zata.cc/p/self-hosted-runner/images/index/index.png" alt="Featured image of post Self-hosted Runner" />&lt;h1 id="实现从-windows-开发机通过-cicd-部署到另一台-windows-机器的方法">实现从 Windows 开发机通过 CI/CD 部署到另一台 Windows 机器的方法
&lt;/h1>&lt;p>要实现从一台 Windows 开发机通过 CI/CD 直接部署到另一台 Windows 机器，最主流且最稳定的做法是：在目标 Windows 机器上安装一个“自托管运行器 (Self-hosted Runner 或 Agent)”。&lt;/p>
&lt;p>这样一来，CI/CD 平台就可以直接向你的目标机器发送指令，让它自动拉取最新代码（或构建好的程序包）、替换旧文件并重启软件。&lt;/p>
&lt;p>以下是实现这一目标的标准流程，以目前最常用的 &lt;strong>GitHub Actions&lt;/strong> 为例（GitLab CI/CD 或 Azure DevOps 的原理几乎完全相同）：&lt;/p>
&lt;p>开发机上，你编写代码，然后执行 &lt;code>git push&lt;/code> 命令将代码推送到云端仓库（如 GitHub）。&lt;/p>
&lt;p>云端代码仓库（如 GitHub）检测到代码更新后，会触发 CI/CD 流程。此时，目标机器上运行的后台程序（Runner）接收到来自云端的指令，会自动在本地执行编译、覆盖文件和重启软件的脚本。&lt;/p>
&lt;p>在你的目标机器上配置 Runner 是最关键的一步。你需要让目标机器和云端仓库建立连接。具体步骤如下：&lt;/p>
&lt;p>在 GitHub 上，进入你的代码仓库，点击 &lt;strong>Settings&lt;/strong>，然后选择 &lt;strong>Actions&lt;/strong>，再进入 &lt;strong>Runners&lt;/strong> 页面。点击 &lt;strong>New self-hosted runner&lt;/strong>，选择 &lt;strong>Windows&lt;/strong> 操作系统及对应的架构（通常是 x64）。&lt;/p>
&lt;p>随后，在你的目标 Windows 机器上打开 PowerShell，按照 GitHub 页面上提供的步骤逐行复制并运行命令。这些命令会下载 Runner 程序、进行身份验证并将其注册到你的仓库中。&lt;/p>
&lt;p>在配置的最后一步，建议将 Runner 安装为 &lt;strong>Windows 服务 (Windows Service)&lt;/strong>，这样目标机器每次开机都会自动启动 Runner，随时准备接收部署任务。&lt;/p>
&lt;p>在目标机器上更新软件时，通常会遇到“文件被占用”的问题。因此，你需要写一个简单的 PowerShell 脚本来处理部署逻辑。例如，你的软件运行在 &lt;code>C:\MyApp&lt;/code>：&lt;/p>
&lt;ol>
&lt;li>停止正在运行的软件进程（或 Windows 服务）。&lt;/li>
&lt;li>将新编译好的文件复制到 &lt;code>C:\MyApp&lt;/code> 并覆盖。&lt;/li>
&lt;li>重新启动软件。&lt;/li>
&lt;/ol>
&lt;p>在你的开发机上，在项目根目录下创建一个文件夹 &lt;code>.github/workflows/&lt;/code>，并在其中新建一个文件（例如 &lt;code>deploy.yml&lt;/code>），填入以下内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Windows Auto Deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 当推送到 main 分支时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build-and-deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 这里的 self-hosted 是关键，它告诉 GitHub 在你的目标机器上运行这些步骤&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">self-hosted &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="l">. 获取最新代码&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="l">. 编译项目 (根据你的语言修改)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> echo &amp;#34;开始编译...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 例如 C# 项目: dotnet build -c Release
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 例如 Node 项目: npm install &amp;amp;&amp;amp; npm run build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="l">. 停止当前运行的程序&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 假设你的程序叫 MyApp.exe
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Stop-Process -Name &amp;#34;MyApp&amp;#34; -ErrorAction SilentlyContinue
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 如果是 Windows 服务，使用: Stop-Service -Name &amp;#34;MyService&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&lt;/span>&lt;span class="l">. 部署/覆盖文件&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> echo &amp;#34;复制文件到运行目录...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 将编译好的产物复制到目标文件夹 (根据实际路径修改)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Copy-Item -Path &amp;#34;.\bin\Release\net8.0\*&amp;#34; -Destination &amp;#34;C:\Deployments\MyApp&amp;#34; -Recurse -Force&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="l">. 重启程序&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> echo &amp;#34;启动程序...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 启动进程或重启服务
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Start-Process -FilePath &amp;#34;C:\Deployments\MyApp\MyApp.exe&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 如果是 Windows 服务，使用: Start-Service -Name &amp;#34;MyService&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在配置 Runner 时，需要注意权限问题。Runner 默认以配置它的用户身份运行。如果你需要操作 C 盘敏感目录或管理 Windows 服务，确保 Runner 对应的用户（或服务账号）具有&lt;strong>管理员权限&lt;/strong>。&lt;/p>
&lt;p>此外，如果你的目标机器性能较弱，或者不想安装编译工具链，可以将 CI/CD 拆分为两个 Job：第一个 Job 使用 GitHub 官方的云端 Windows 服务器进行编译并上传产物 (Artifacts)；第二个 Job 运行在 &lt;code>self-hosted&lt;/code> 的目标机器上，只负责下载产物并覆盖文件。&lt;/p>
&lt;h2 id="self-hosted-runner-搭建教程">Self-hosted Runner 搭建教程
&lt;/h2>&lt;p>当 GitHub Actions 的免费额度用完，或者需要在特定硬件/内网环境上执行构建时，可以将自己的机器注册为 &lt;strong>Self-hosted Runner&lt;/strong>。它不消耗 GitHub Actions 分钟数，job 会直接运行在你的机器上。&lt;/p>
&lt;h3 id="注册-runner">注册 Runner
&lt;/h3>&lt;p>进入仓库 &lt;strong>Settings → Actions → Runners → New self-hosted runner&lt;/strong>（组织级别则在组织 Settings 中），选择对应的操作系统和架构，页面会生成一段包含一次性 registration token 的命令，照着执行即可。&lt;/p>
&lt;h3 id="linux-安装示例">Linux 安装示例
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir actions-runner &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> actions-runner
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 版本号以 GitHub 页面给出的为准&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -o actions-runner-linux-x64-2.325.0.tar.gz -L &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> https://github.com/actions/runner/releases/download/v2.325.0/actions-runner-linux-x64-2.325.0.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tar xzf actions-runner-linux-x64-*.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./config.sh --url https://github.com/&amp;lt;用户名&amp;gt;/&amp;lt;仓库&amp;gt; --token &amp;lt;页面生成的token&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 前台测试运行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./run.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 生产环境建议注册为系统服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo ./svc.sh install
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo ./svc.sh start
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="windows-安装示例">Windows 安装示例
&lt;/h3>&lt;p>在 PowerShell 中执行（建议在 &lt;code>C:\actions-runner&lt;/code> 这类位于驱动器根目录下的短路径中安装；注册为服务时需要在管理员权限下执行）：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/self-hosted-runner/images/index/new-runner-setup.png"
width="1072"
height="1085"
srcset="https://www.zata.cc/p/self-hosted-runner/images/index/new-runner-setup_hu3330039181884697727.png 480w, https://www.zata.cc/p/self-hosted-runner/images/index/new-runner-setup_hu17718820863316748869.png 1024w"
loading="lazy"
alt="GitHub 生成的 Windows Self-hosted Runner 配置页面"
class="gallery-image"
data-flex-grow="98"
data-flex-basis="237px"
>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 在驱动器根目录下创建安装目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mkdir&lt;/span> &lt;span class="nb">actions-runner&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">cd actions-runner&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 下载最新 Runner 包；版本号以 GitHub 页面给出的为准&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Invoke-WebRequest&lt;/span> &lt;span class="n">-Uri&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="p">//&lt;/span>&lt;span class="n">github&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">actions&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">runner&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">releases&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">download&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">v2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">337&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mf">0&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="nb">actions-runner&lt;/span>&lt;span class="n">-win-x64&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="mf">2.337&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">zip&lt;/span> &lt;span class="n">-OutFile&lt;/span> &lt;span class="nb">actions-runner&lt;/span>&lt;span class="n">-win-x64&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="mf">2.337&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">zip&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 可选：校验 SHA256 哈希，避免下载包损坏&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nb">Get-FileHash&lt;/span> &lt;span class="n">-Path&lt;/span> &lt;span class="nb">actions-runner&lt;/span>&lt;span class="n">-win-x64&lt;/span>&lt;span class="p">-&lt;/span>&lt;span class="mf">2.337&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">zip&lt;/span> &lt;span class="n">-Algorithm&lt;/span> &lt;span class="n">SHA256&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="py">Hash&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">ToUpper&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-ne&lt;/span> &lt;span class="s1">&amp;#39;1150692afa94e71f872017e254ea55b6eece1eece3fe7e3a6d4c93d0a1b85cfc&amp;#39;&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">ToUpper&lt;/span>&lt;span class="p">()){&lt;/span> &lt;span class="k">throw&lt;/span> &lt;span class="s1">&amp;#39;Computed checksum did not match&amp;#39;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 解压到当前目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Add-Type&lt;/span> &lt;span class="n">-AssemblyName&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">IO&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">Compression&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">FileSystem&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="no">System.IO.Compression.ZipFile&lt;/span>&lt;span class="p">]::&lt;/span>&lt;span class="n">ExtractToDirectory&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PWD&lt;/span>&lt;span class="s2">/actions-runner-win-x64-2.337.0.zip&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PWD&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">./&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">cmd&lt;/span> &lt;span class="p">-&lt;/span>&lt;span class="n">-url&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="p">//&lt;/span>&lt;span class="n">github&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>&lt;span class="p">/&amp;lt;&lt;/span>&lt;span class="n">用户名&lt;/span>&lt;span class="p">&amp;gt;/&amp;lt;&lt;/span>&lt;span class="n">仓库&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">-&lt;/span>&lt;span class="n">-token&lt;/span> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">页面生成的token&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 前台测试运行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">./&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">cmd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 注册为 Windows 服务并启动&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">./&lt;/span>&lt;span class="n">svc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">cmd&lt;/span> &lt;span class="n">install&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">./&lt;/span>&lt;span class="n">svc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">cmd&lt;/span> &lt;span class="nb">start
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>按页面给出的命令完成下载、校验、解压和 &lt;code>config.cmd&lt;/code> 配置后，Runner 就会注册到仓库的 &lt;strong>Runners&lt;/strong> 列表中。只要状态显示为 &lt;strong>Idle&lt;/strong>，说明它已经在线并等待接收任务：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/self-hosted-runner/images/index/runners-idle.png"
width="754"
height="239"
srcset="https://www.zata.cc/p/self-hosted-runner/images/index/runners-idle_hu8511784814163634307.png 480w, https://www.zata.cc/p/self-hosted-runner/images/index/runners-idle_hu15969862009530225096.png 1024w"
loading="lazy"
alt="Runners 列表中显示注册完成的 Windows Runner"
class="gallery-image"
data-flex-grow="315"
data-flex-basis="757px"
>&lt;/p>
&lt;p>配置过程中会给 Runner 起名并选择 label（默认 &lt;code>self-hosted&lt;/code>、&lt;code>linux&lt;/code> 或 &lt;code>windows&lt;/code> 等）。注册为服务后，即使关闭终端或注销用户，Runner 也会在后台常驻。&lt;/p>
&lt;h3 id="在-workflow-中使用">在 Workflow 中使用
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">self-hosted &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 或 [self-hosted, linux] / [self-hosted, windows] 指定更细的 label&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">echo &amp;#34;running on my own machine&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="注意事项">注意事项
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>安全&lt;/strong>：公开仓库不要随意接入 self-hosted runner，任何 fork 的 PR 都可能执行代码。建议仅用于私有仓库，或在仓库设置中严格限制 workflow 权限。&lt;/li>
&lt;li>&lt;strong>环境隔离&lt;/strong>：job 直接运行在宿主机上，会留下缓存和文件。建议用 Docker 容器或一次性虚拟机运行 Runner，或在每次 job 前后做清理。&lt;/li>
&lt;li>&lt;strong>服务账户权限&lt;/strong>：Windows 下 Runner 默认以 &lt;code>NETWORK SERVICE&lt;/code> 运行，如果 job 需要 Docker、文件系统或网络共享权限，需在安装服务时指定有权限的账户，或在 &lt;code>services.msc&lt;/code> 中修改服务登录账户。&lt;/li>
&lt;li>&lt;strong>环境依赖&lt;/strong>：确保 Runner 所在机器已安装构建所需的工具链（Node、Python、Docker 等），且服务账户的 PATH 中可访问。&lt;/li>
&lt;li>&lt;strong>在线要求&lt;/strong>：Runner 需要保持在线才能接收任务，建议通过 &lt;code>systemd&lt;/code>（Linux）或 Windows 服务常驻运行。&lt;/li>
&lt;/ul></description></item><item><title>Docker 私有镜像仓库registry</title><link>https://www.zata.cc/p/docker-%E7%A7%81%E6%9C%89%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93registry/</link><pubDate>Mon, 23 Feb 2026 00:20:36 +0800</pubDate><guid>https://www.zata.cc/p/docker-%E7%A7%81%E6%9C%89%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93registry/</guid><description>&lt;img src="https://www.zata.cc/p/docker-%E7%A7%81%E6%9C%89%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93registry/images/index/index.png" alt="Featured image of post Docker 私有镜像仓库registry" />&lt;h1 id="安装和配置">安装和配置
&lt;/h1>&lt;h2 id="dokploy-搭建自带自动清理功能的私有-docker-镜像仓库-----20260225">Dokploy ：搭建自带“自动清理”功能的私有 Docker 镜像仓库 &amp;mdash; 20260225
&lt;/h2>&lt;p>在个人服务器上搭建私有 Docker Registry（镜像仓库）是很多开发者的刚需。使用 Dokploy 部署虽然简单，但很多人很快会遇到一个棘手的问题：&lt;strong>磁盘空间爆炸&lt;/strong>。&lt;/p>
&lt;p>原生的 Docker Registry 像个“只进不出”的貔貅，默认既不支持删除镜像，删除了也不释放空间。&lt;/p>
&lt;p>本文将带你通过 Dokploy 部署一个&lt;strong>带 Web 管理界面&lt;/strong>、&lt;strong>支持删除&lt;/strong>且&lt;strong>每晚自动释放磁盘空间&lt;/strong>的完美私有仓库。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>1. 为什么 Registry 的清理这么麻烦？&lt;/strong>&lt;/p>
&lt;p>在开始配置之前，我们需要理解为什么 Docker 官方把 Registry 设计得这么“难用”。很多人发现，即便开启了删除功能，删除了镜像，磁盘占用依然没变。&lt;/p>
&lt;p>这是因为 Registry 的清理机制分为两步：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>软删除 (Soft Delete)&lt;/strong>：当你通过 API 或 UI 点击删除时，Registry 只是删除了&lt;strong>清单（Manifest）&lt;/strong>。这就像你在 Windows 上把文件拖进了回收站，文件其实还在磁盘上。&lt;/li>
&lt;li>&lt;strong>垃圾回收 (Garbage Collection)&lt;/strong>：这是真正的物理删除。因为 Docker 的镜像是由很多“层（Layers）”组成的，且不同镜像会&lt;strong>共享&lt;/strong>底层。Registry 必须通过扫描全盘，计算引用关系，确认某一层真的没有被任何镜像使用后，才敢真正删除它。&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>此外，官方默认禁用删除操作（&lt;code>REGISTRY_STORAGE_DELETE_ENABLED: false&lt;/code>）主要基于以下考量：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>层共享保护&lt;/strong>：防止误删被其他镜像依赖的基础层。&lt;/li>
&lt;li>&lt;strong>并发安全&lt;/strong>：防止在清理时，刚好有新镜像正在推送（Push）导致数据损坏。&lt;/li>
&lt;li>&lt;strong>不可变基础设施&lt;/strong>：发布的镜像应当被视为不可变的历史存档。&lt;/li>
&lt;/ul>
&lt;p>但在个人或小团队场景下，为了省钱（服务器磁盘贵啊！），我们需要它能够清理。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>2. 编写 &lt;code>docker-compose.yml&lt;/code>&lt;/strong>&lt;/p>
&lt;p>我们将通过 Dokploy 部署三个服务：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Registry&lt;/strong>：核心仓库服务。&lt;/li>
&lt;li>&lt;strong>Auto-Auth&lt;/strong>：一个辅助容器，启动时生成账号密码文件，然后自动退出。&lt;/li>
&lt;li>&lt;strong>Registry UI&lt;/strong>：一个轻量级的 Web 界面，方便我们查看和删除镜像。&lt;/li>
&lt;/ol>
&lt;p>在 Dokploy 的 &lt;code>Compose&lt;/code> 选项卡中，填入以下配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 定义数据卷，持久化存储镜像和认证信息&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry-auth-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 使用 Dokploy 预置的外部网络&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dokploy-network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 1. 认证辅助服务：生成账号 admin / 密码 123456&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 启动一次生成文件后就会退出，这是正常的&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auto-setup-auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">httpd:alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/bin/sh -c &amp;#34;htpasswd -Bbn admin 123456 &amp;gt; /auth/htpasswd&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">registry-auth-data:/auth&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 2. 核心 Registry 服务&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry:2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-private-registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">dokploy-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 映射端口方便本地调试，生产环境主要靠 Traefik 转发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;5000:5000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 认证配置&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_AUTH&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">htpasswd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_AUTH_HTPASSWD_REALM&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Registry Realm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_AUTH_HTPASSWD_PATH&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/auth/htpasswd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 反向代理配置&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_X_FORWARDED_FOR&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_X_FORWARDED_PROTO&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 【关键配置】开启删除权限&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_STORAGE_DELETE_ENABLED&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 【跨域配置】允许 UI 界面通过浏览器调用 API&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_ACCESS_CONTROL_ALLOW_ORIGIN&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;[https://registry-ui.zata.cafe]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_ACCESS_CONTROL_ALLOW_METHODS&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;[HEAD,GET,OPTIONS,DELETE]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_ACCESS_CONTROL_ALLOW_HEADERS&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;[Authorization,Accept,Cache-Control]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_ACCESS_CONTROL_EXPOSE_HEADERS&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;[Docker-Content-Digest]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">registry-data:/var/lib/registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">registry-auth-data:/auth&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auto-setup-auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_completed_successfully&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Traefik 标签：配置域名和 HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry.rule=Host(`registry.zata.cafe`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.registry.loadbalancer.server.port=5000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 取消请求体大小限制，防止 Push 大镜像失败&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 3. Web UI 管理界面&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry-ui&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">joxit/docker-registry-ui:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry-ui&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">dokploy-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 删除 REGISTRY_URL，改用下面这两个变量：&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">NGINX_PROXY_PASS_URL=http://my-private-registry:5000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">SINGLE_REGISTRY=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">DELETE_IMAGES=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">REGISTRY_TITLE=Zata Registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry-ui.rule=Host(`registry-ui.zata.cafe`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry-ui.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry-ui.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.registry-ui.loadbalancer.server.port=80&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>注意&lt;/strong>：请将代码中的 &lt;code>zata.cafe&lt;/code> 替换为你自己的域名，并确保 DNS 解析已指向你的服务器。&lt;/p>
&lt;/blockquote>
&lt;p>点击 &lt;strong>Deploy&lt;/strong> 部署应用。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>3. 实现自动化清理 (GC)&lt;/strong>&lt;/p>
&lt;p>部署完成后，你现在可以通过 &lt;code>registry-ui.zata.cafe&lt;/code> 访问界面，并手动删除不需要的镜像 Tag。但这只是“软删除”。为了释放磁盘空间，我们需要在&lt;strong>宿主机&lt;/strong>上配置定时任务。&lt;/p>
&lt;p>&lt;strong>步骤 1：创建清理脚本&lt;/strong>&lt;/p>
&lt;p>SSH 登录到你的服务器，创建一个脚本文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">nano /root/clean_registry.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写入以下内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Starting Registry Garbage Collection: &lt;/span>&lt;span class="k">$(&lt;/span>date&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 调用容器内的 registry 工具执行垃圾回收&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -m 参数表示同时删除不再被引用的 manifest（清单）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> my-private-registry /bin/registry garbage-collect /etc/docker/registry/config.yml -m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Garbage Collection Finished: &lt;/span>&lt;span class="k">$(&lt;/span>date&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>保存并退出（Ctrl+O, Enter, Ctrl+X），然后赋予执行权限：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">chmod +x /root/clean_registry.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>步骤 2：配置定时任务 (Crontab)&lt;/strong>&lt;/p>
&lt;p>我们希望在每天凌晨（比如 4 点）没人使用的时候执行清理。&lt;/p>
&lt;p>输入命令编辑定时任务：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">crontab -e
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在文件末尾添加一行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 每天凌晨 4:00 执行清理，并将日志输出到文件以便排查&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mi">0&lt;/span> &lt;span class="mi">4&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">clean_registry&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sh&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nb">log&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">registry_gc&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">log&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="o">&amp;gt;&amp;amp;&lt;/span>&lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>&lt;strong>4. 最终工作流&lt;/strong>&lt;/p>
&lt;p>完成以上配置后，你的私有仓库工作流变成了这样：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>日常开发&lt;/strong>：正常 &lt;code>docker push&lt;/code> 推送镜像。&lt;/li>
&lt;li>&lt;strong>手动维护&lt;/strong>：觉得某个镜像版本太老了？打开 &lt;code>registry-ui&lt;/code> 网页，点一下垃圾桶图标删除它（此时空间未释放）。&lt;/li>
&lt;li>&lt;strong>自动瘦身&lt;/strong>：不用管了，去睡觉吧。凌晨 4 点，服务器会自动运行 GC，扫描并彻底删除那些你标记为删除的文件块，第二天早上起来，磁盘空间就回来了。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;p>&lt;strong>总结&lt;/strong>&lt;/p>
&lt;p>通过 Dokploy 配合 Traefik，我们不仅拥有了一个 HTTPS 安全的私有仓库，还加上了可视化管理界面。再配合简单的 Shell 脚本和 Crontab，解决了 Docker Registry 最令人头疼的磁盘占用问题。&lt;/p>
&lt;p>Happy Coding! 🐳&lt;/p>
&lt;h2 id="docker-和-traefik-搭建私有镜像仓库及踩坑记录----20260223">Docker 和 Traefik 搭建私有镜像仓库（及踩坑记录）&amp;mdash; 202602/23
&lt;/h2>&lt;p>在 DevOps 流程中，拥有一个私有的 Docker Registry（镜像仓库）是必不可少的。虽然 Docker Hub 很好用，但出于隐私、速度和成本的考虑，自建仓库往往是更好的选择。本文将手把手教你如何使用 Docker Compose 和 Traefik 反向代理搭建一个带 HTTPS 和基础认证的轻量级私有仓库，并重点分析部署过程中最容易遇到的 “Gateway Timeout” 和 “Connection Refused” 等网络问题。&lt;/p>
&lt;p>&lt;strong>为什么选择这个方案？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>轻量级&lt;/strong>：基于官方 &lt;code>registry:2&lt;/code> 镜像，资源占用极小。&lt;/li>
&lt;li>&lt;strong>安全&lt;/strong>：通过 Traefik 自动管理 SSL 证书（Let&amp;rsquo;s Encrypt），并配置 &lt;code>htpasswd&lt;/code> 基础认证。&lt;/li>
&lt;li>&lt;strong>易维护&lt;/strong>：所有配置通过一个 &lt;code>docker-compose.yml&lt;/code> 文件管理。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>准备工作&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>一台安装了 Docker 和 Docker Compose 的 Linux 服务器。&lt;/li>
&lt;li>一个域名并解析到服务器 IP（例如：&lt;code>registry.example.com&lt;/code>）。&lt;/li>
&lt;li>&lt;strong>Traefik 已经在运行中&lt;/strong>（如果你使用的是 Dokploy、Coolify 等面板，Traefik 通常是内置好的）。&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>核心配置：docker-compose.yml&lt;/strong>&lt;/p>
&lt;p>为了方便演示，我们使用一个技巧：利用临时容器自动生成密码文件，避免手动安装 &lt;code>htpasswd&lt;/code> 工具的麻烦。请创建一个目录 &lt;code>my-registry&lt;/code>，并在其中新建 &lt;code>docker-compose.yml&lt;/code>，内容如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 定义数据卷，持久化存储镜像和认证信息&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry-auth-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 【重点】定义网络&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 这里必须使用 external: true，表示使用外部已经存在的 Traefik 网络&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 如果你用的是 Dokploy，网络名通常是 dokploy-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 如果你是自己部署的 Traefik，通常叫 traefik_public 或 proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dokploy-network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 1. 辅助服务：自动生成账号密码&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 启动后会生成密码文件并存入卷中，然后自动退出&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auto-setup-auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">httpd:alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 账号: admin, 密码: 123456 (生产环境请修改！)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/bin/sh -c &amp;#34;htpasswd -Bbn admin 123456 &amp;gt; /auth/htpasswd&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">registry-auth-data:/auth&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 2. 核心仓库服务&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry:2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-private-registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 【关键点 1】加入 Traefik 所在的网络&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">dokploy-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 端口映射（可选，仅用于本地调试，生产环境可注释掉）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;5000:5000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 启用认证&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_AUTH&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">htpasswd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_AUTH_HTPASSWD_REALM&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Registry Realm&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_AUTH_HTPASSWD_PATH&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/auth/htpasswd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 告诉 Registry 它运行在 HTTPS 反向代理后面&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_X_FORWARDED_FOR&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HTTP_HEADERS_X_FORWARDED_PROTO&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">registry-data:/var/lib/registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">registry-auth-data:/auth&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auto-setup-auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_completed_successfully&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 【关键点 2】Traefik 标签配置&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 修改为你的实际域名&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry.rule=Host(`registry.example.com`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定入口点为 HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 启用 TLS (自动申请证书)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.registry.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 【至关重要】告诉 Traefik 容器内部端口是 5000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.registry.loadbalancer.server.port=5000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 取消请求体大小限制（防止 docker push 大镜像失败）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配置完成后，使用 &lt;code>docker-compose up -d&lt;/code> 命令启动服务。&lt;/p>
&lt;p>&lt;strong>常见问题排查（Troubleshooting）&lt;/strong>&lt;/p>
&lt;p>在部署过程中，你可能会遇到以下几种错误，请根据现象进行排查：&lt;/p>
&lt;p>&lt;strong>1. 504 Gateway Timeout (网关超时)&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker-%E7%A7%81%E6%9C%89%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93registry/images/index/image.png"
width="3404"
height="2050"
srcset="https://www.zata.cc/p/docker-%E7%A7%81%E6%9C%89%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93registry/images/index/image_hu6010604155192436420.png 480w, https://www.zata.cc/p/docker-%E7%A7%81%E6%9C%89%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93registry/images/index/image_hu14076917848918260733.png 1024w"
loading="lazy"
alt="网关超时"
class="gallery-image"
data-flex-grow="166"
data-flex-basis="398px"
>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>现象&lt;/strong>：浏览器访问域名/v2/，加载很久后显示 &lt;code>504 Gateway Timeout&lt;/code>。&lt;/li>
&lt;li>&lt;strong>原因&lt;/strong>：这说明 Traefik 接到了请求，但是&lt;strong>无法连接到后端的 Registry 容器&lt;/strong>。这通常是因为 Traefik 和 Registry &lt;strong>不在同一个 Docker 网络&lt;/strong> 中。Traefik 就像大楼的前台，它想转接电话给 Registry，但发现线路不通。&lt;/li>
&lt;li>&lt;strong>解决方案&lt;/strong>：检查 &lt;code>docker-compose.yml&lt;/code> 中的 &lt;code>networks&lt;/code> 部分。必须确保 Registry 加入了 Traefik 所在的那个网络（通过 &lt;code>docker network ls&lt;/code> 查看网络名称）。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. Context Deadline Exceeded (连接超时)&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>现象&lt;/strong>：在使用 Dokploy 或命令行登录时，提示 &lt;code>Client.Timeout exceeded while awaiting headers&lt;/code>。&lt;/li>
&lt;li>&lt;strong>原因&lt;/strong>：这通常是&lt;strong>网络层面的物理隔绝&lt;/strong>。你的请求根本没有到达服务器，被防火墙拦在了外面。&lt;/li>
&lt;li>&lt;strong>解决方案&lt;/strong>：
&lt;ol>
&lt;li>检查云服务商（AWS/阿里云/腾讯云）的安全组，&lt;strong>必须开放 TCP 443 和 80 端口&lt;/strong>。&lt;/li>
&lt;li>检查服务器内部防火墙（如 &lt;code>ufw&lt;/code> 或 &lt;code>iptables&lt;/code>）。&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. 登录失败或 404 Not Found&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>现象&lt;/strong>：访问域名能通，但是 docker login 总是失败，或者页面显示 404。&lt;/li>
&lt;li>&lt;strong>原因&lt;/strong>：可能是 Traefik 把流量转发到了错误的端口。Registry 默认监听 &lt;strong>5000&lt;/strong>，而 Traefik 默认转发到 80。&lt;/li>
&lt;li>&lt;strong>解决方案&lt;/strong>：确保 &lt;code>labels&lt;/code> 中包含指定端口的配置：&lt;code>- &amp;quot;traefik.http.services.registry.loadbalancer.server.port=5000&amp;quot;&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>4. 推送镜像失败 (Blob Upload Unknown)&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>现象&lt;/strong>：登录成功，但在 &lt;code>docker push&lt;/code> 大文件时失败并重试。&lt;/li>
&lt;li>&lt;strong>原因&lt;/strong>：Traefik 或 Nginx 默认会限制上传文件的大小。&lt;/li>
&lt;li>&lt;strong>解决方案&lt;/strong>：添加中间件配置取消限制：&lt;code>- &amp;quot;traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=0&amp;quot;&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>验证与使用&lt;/strong>&lt;/p>
&lt;p>部署成功后，建议按以下步骤进行测试：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>浏览器访问&lt;/strong>：打开 &lt;code>https://registry.example.com/v2/&lt;/code>，应该弹出登录框，输入 &lt;code>admin&lt;/code> / &lt;code>123456&lt;/code>，页面显示 &lt;code>{}&lt;/code> 即为成功。&lt;/li>
&lt;li>&lt;strong>Docker 登录&lt;/strong>：在终端执行 &lt;code>docker login registry.example.com&lt;/code>，输入配置的用户名和密码。&lt;/li>
&lt;li>&lt;strong>推送镜像&lt;/strong>：
&lt;ul>
&lt;li>给本地镜像打标签：&lt;code>docker tag my-image:latest registry.example.com/my-image:latest&lt;/code>&lt;/li>
&lt;li>推送至仓库：&lt;code>docker push registry.example.com/my-image:latest&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>总结&lt;/strong>&lt;/p>
&lt;p>搭建私有仓库并不难，难点通常在于&lt;strong>网络配置&lt;/strong>。记住一句话：&lt;strong>“Traefik 是网关，防火墙是门卫，网络是内部通道。”&lt;/strong> 只有这三者都配置正确，你的镜像仓库才能顺畅运行。希望这篇指南能帮你避开常见的陷阱。&lt;/p>
&lt;h1 id="概念">概念
&lt;/h1>&lt;h2 id="镜像仓库命名空间概念详解">镜像仓库命名空间概念详解
&lt;/h2>&lt;p>在容器化技术日益普及的今天，镜像仓库成为了存储和管理 Docker 镜像的核心基础设施。而在镜像仓库的体系中，&lt;strong>命名空间（Namespace）&lt;/strong> 是一个至关重要却又容易被忽视的概念。本文将带您深入了解命名空间的定义、作用以及最佳实践。&lt;/p>
&lt;p>&lt;strong>什么是命名空间&lt;/strong>&lt;/p>
&lt;p>命名空间可以理解为镜像仓库中的一种逻辑隔离机制。它类似于文件系统中的文件夹，用于对镜像进行分类和组织。在一个公共或私有的镜像仓库中，命名空间允许不同的用户、团队或项目拥有独立的区域来存储他们的镜像，从而避免命名冲突。&lt;/p>
&lt;p>例如，一个完整的镜像地址通常遵循以下格式：&lt;/p>
&lt;ul>
&lt;li>registry/namespace/image-name:tag&lt;/li>
&lt;/ul>
&lt;p>其中，namespace 部分就是命名空间。对于个人用户，命名空间通常等同于用户名；对于组织用户，则是组织名称。&lt;/p>
&lt;p>&lt;strong>为什么需要命名空间&lt;/strong>&lt;/p>
&lt;p>引入命名空间主要为了解决以下几个核心问题：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>资源隔离与权限管理&lt;/strong>
不同的命名空间可以绑定不同的访问控制策略。管理员可以为特定命名空间分配独立的读写权限，确保只有授权人员才能推送或拉取特定镜像，从而实现多租户环境下的安全隔离。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>避免命名冲突&lt;/strong>
在没有命名空间的情况下，所有镜像都必须拥有全局唯一的名称。这显然是不现实的。通过命名空间，不同的团队可以使用相同的镜像名称（例如 web-app），只要它们位于不同的命名空间下（例如 team-a/web-app 和 team-b/web-app），就不会发生冲突。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>逻辑组织与分类&lt;/strong>
随着微服务架构的演进，一个项目可能包含数十甚至上百个镜像。通过合理的命名空间规划，可以将镜像按业务线、环境（开发、测试、生产）或团队进行归类，极大地提升了运维效率。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>命名空间的使用场景&lt;/strong>&lt;/p>
&lt;p>在实际操作中，命名空间的应用非常广泛：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>个人开发者&lt;/strong>：通常直接使用个人账户作为命名空间，用于存储个人实验性或开源项目镜像。&lt;/li>
&lt;li>&lt;strong>企业团队&lt;/strong>：会创建专门的组织命名空间，如 company-backend、company-frontend，以便区分不同职能团队的产物。&lt;/li>
&lt;li>&lt;strong>环境隔离&lt;/strong>：有些团队会使用命名空间来区分环境，例如 proj-dev、proj-prod，虽然更常见的做法是使用 Tag 或不同的仓库实例，但命名空间也是一种可行的隔离手段。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>最佳实践建议&lt;/strong>&lt;/p>
&lt;p>为了更高效地管理镜像仓库，建议遵循以下规范：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>保持命名简洁&lt;/strong>：使用小写字母、数字和连字符，避免特殊字符，确保兼容性和易读性。&lt;/li>
&lt;li>&lt;strong>统一命名规范&lt;/strong>：团队内部应制定明确的命名规则，例如 业务线 - 服务名，防止随意创建导致混乱。&lt;/li>
&lt;li>&lt;strong>定期清理&lt;/strong>：定期检查命名空间下的镜像，删除不再使用的旧版本，节省存储空间并减少安全隐患。&lt;/li>
&lt;li>&lt;strong>权限最小化&lt;/strong>：仅为必要的人员分配命名空间的写入权限，降低误操作风险。&lt;/li>
&lt;/ul>
&lt;p>综上所述，命名空间是镜像仓库管理中不可或缺的一部分。合理使用命名空间不仅能提升工作效率，还能增强系统的安全性和可维护性。希望本文能帮助您更好地规划和使用镜像仓库中的命名空间。&lt;/p></description></item><item><title>Docker Compose(dev\test\prod)</title><link>https://www.zata.cc/p/docker-composedev%5Ctest%5Cprod/</link><pubDate>Wed, 17 Dec 2025 23:05:55 +0800</pubDate><guid>https://www.zata.cc/p/docker-composedev%5Ctest%5Cprod/</guid><description>&lt;img src="https://www.zata.cc/p/docker-composedev%5Ctest%5Cprod/images/index/index.png" alt="Featured image of post Docker Compose(dev\test\prod)" />&lt;h1 id="docker-compose-全生命周期环境治理深度研究报告">Docker Compose 全生命周期环境治理深度研究报告
&lt;/h1>&lt;h2 id="从开发工作流到生产级交付的架构实践">从开发工作流到生产级交付的架构实践
&lt;/h2>&lt;h3 id="1-引言容器编排在软件生命周期中的演进">1. 引言：容器编排在软件生命周期中的演进
&lt;/h3>&lt;p>在云原生技术栈日益成熟的今天，Docker Compose 已超越了其作为“本地开发工具”的初始定位。随着 Docker Compose V2 规范的标准化，它成为定义多容器应用架构的通用语言。本报告旨在剖析如何利用 Compose 的分层合并机制、变量控制体系及不可变基础设施原则，构建一套覆盖 Dev、Test、Prod 的全生命周期治理方案。&lt;/p>
&lt;hr>
&lt;h3 id="2-docker-compose-配置管理的架构哲学">2. Docker Compose 配置管理的架构哲学
&lt;/h3>&lt;h4 id="21-核心矛盾一致性-vs-差异性">2.1 核心矛盾：一致性 vs 差异性
&lt;/h4>&lt;p>架构设计的核心挑战在于维持&lt;strong>基础拓扑的一致性&lt;/strong>（服务依赖、网络别名）与&lt;strong>环境配置的差异性&lt;/strong>（资源限制、暴露端口、卷挂载）。&lt;/p>
&lt;h4 id="22-覆盖模式override-pattern的数学逻辑">2.2 覆盖模式（Override Pattern）的数学逻辑
&lt;/h4>&lt;p>Docker Compose 使用文件名顺序（&lt;code>-f&lt;/code> 参数）来决定配置的最终形态。其合并算法（Merge Logic）如下：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>标量（Scalars）&lt;/strong>：直接替换。后加载的文件覆盖先加载的文件（如 &lt;code>image&lt;/code> tag, &lt;code>restart&lt;/code> 策略）。&lt;/li>
&lt;li>&lt;strong>映射（Maps）&lt;/strong>：键值合并。新键追加，旧键覆盖（如 &lt;code>environment&lt;/code>, &lt;code>labels&lt;/code>）。&lt;/li>
&lt;li>&lt;strong>序列（Sequences）&lt;/strong>：&lt;strong>追加（Union）&lt;/strong>。这是最容易出错的地方。例如，如果在 Base 中定义了端口 80，Override 中定义了 8080，结果是&lt;strong>两个端口都暴露&lt;/strong>。
&lt;ul>
&lt;li>&lt;em>例外&lt;/em>：挂载卷（Volumes）如果容器内路径相同，则为覆盖。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>最佳实践：基座极简原则（Minimalist Base Principle）&lt;/strong>
&lt;code>compose.yaml&lt;/code> 应只包含所有环境共有的配置。凡是可能在生产环境需要移除的配置（如调试端口、源代码 Bind Mount），&lt;strong>绝对不要&lt;/strong>写在 Base 文件中。&lt;/p>
&lt;hr>
&lt;h3 id="3-环境变量env的生态系统与优先级矩阵">3. 环境变量（ENV）的生态系统与优先级矩阵
&lt;/h3>&lt;p>环境变量是导致“配置漂移（Configuration Drift）”的头号原因。必须严格区分 &lt;strong>插值（Interpolation）&lt;/strong> 和 &lt;strong>注入（Injection）&lt;/strong> 两个阶段。&lt;/p>
&lt;h4 id="31-优先级真值表priority-truth-table">3.1 优先级真值表（Priority Truth Table）
&lt;/h4>&lt;p>当变量冲突时，Docker Compose V2 遵循以下优先级（由高到低）：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">优先级&lt;/th>
&lt;th style="text-align: left">来源&lt;/th>
&lt;th style="text-align: left">作用阶段&lt;/th>
&lt;th style="text-align: left">典型用途&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>1 (最高)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;strong>CLI 参数&lt;/strong> (&lt;code>run -e&lt;/code>)&lt;/td>
&lt;td style="text-align: left">容器运行时&lt;/td>
&lt;td style="text-align: left">临时调试、One-off 任务&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>2&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;strong>Shell 环境变量&lt;/strong> (&lt;code>export VAR=...&lt;/code>)&lt;/td>
&lt;td style="text-align: left">插值 &amp;amp; 运行时&lt;/td>
&lt;td style="text-align: left">CI/CD 流水线注入 Secrets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>3&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;strong>.env 文件&lt;/strong>&lt;/td>
&lt;td style="text-align: left">插值 &amp;amp; 运行时&lt;/td>
&lt;td style="text-align: left">开发者本地默认配置&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>4&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;strong>YAML &lt;code>environment&lt;/code> 列表&lt;/strong>&lt;/td>
&lt;td style="text-align: left">运行时&lt;/td>
&lt;td style="text-align: left">服务特定的显式配置&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>5&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;strong>YAML &lt;code>env_file&lt;/code> 指令&lt;/strong>&lt;/td>
&lt;td style="text-align: left">运行时&lt;/td>
&lt;td style="text-align: left">批量加载非敏感配置&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>6 (最低)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;strong>Dockerfile &lt;code>ENV&lt;/code> 指令&lt;/strong>&lt;/td>
&lt;td style="text-align: left">镜像构建时&lt;/td>
&lt;td style="text-align: left">镜像层面的默认值&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h4 id="32-关键语法技巧">3.2 关键语法技巧
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>强制校验&lt;/strong>：&lt;code>${DB_PASSWORD?err}&lt;/code> —— 如果未设置变量，阻止 Compose 启动并报错。适用于生产环境防止裸奔。&lt;/li>
&lt;li>&lt;strong>默认值&lt;/strong>：&lt;code>${TAG:-latest}&lt;/code> —— 允许 CI 传入特定 Tag，本地开发默认为 latest。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="4-环境一开发环境dev-极致反馈循环">4. 环境一：开发环境（Dev）—— 极致反馈循环
&lt;/h3>&lt;p>目标：代码修改后毫秒级生效，支持断点调试。&lt;/p>
&lt;h4 id="41-架构策略">4.1 架构策略
&lt;/h4>&lt;p>利用 &lt;code>compose.override.yaml&lt;/code>（默认自动加载）实现“源代码热注入”。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># compose.yaml (基座)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 基础镜像名&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-app:base&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">internal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># compose.override.yaml (开发专用)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">development&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 多阶段构建：开发阶段&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./src:/app/src &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 代码热重载核心&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/app/node_modules&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 匿名卷：防止宿主机覆盖容器依赖&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">DEBUG=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8080:8080&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 应用端口&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;9229:9229&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 调试端口&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;npm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;run&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;dev:watch&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 覆盖启动命令&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="42-性能优化">4.2 性能优化
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>VirtioFS&lt;/strong>: macOS/Windows Docker Desktop 用户务必开启 VirtioFS 以解决 Bind Mount IO 性能瓶颈。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="5-环境二测试环境test-自动化质量门禁">5. 环境二：测试环境（Test）—— 自动化质量门禁
&lt;/h3>&lt;p>目标：瞬态（Ephemeral）、确定性、自动化退出码。&lt;/p>
&lt;h4 id="51-架构策略">5.1 架构策略
&lt;/h4>&lt;p>CI 环境不需要 Bind Mount，而是需要构建出的镜像进行测试。利用 &lt;code>--exit-code-from&lt;/code> 实现自动化测试编排。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># compose.test.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sut&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># System Under Test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-app:${CI_COMMIT_SHA}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 确保测试的是当前构建产物&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;npm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_healthy&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 关键：等待依赖就绪&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">DB_HOST=db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres:15-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tmpfs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/var/lib/postgresql/data&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用内存盘，提升速度且自动清理&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthcheck&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;CMD-SHELL&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;pg_isready -U postgres&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retries&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="52-ci-流水线命令流">5.2 CI 流水线命令流
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 构建&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose -f compose.yaml -f compose.test.yaml build
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 运行并捕获退出码 (关键步骤)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># --exit-code-from sut: 当 sut 容器退出时，关闭所有容器并返回 sut 的退出码&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose -f compose.yaml -f compose.test.yaml up --exit-code-from sut
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 清理 (防止匿名卷残留)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose -f compose.yaml -f compose.test.yaml down -v
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="6-环境三生产环境prod-稳定性与安全">6. 环境三：生产环境（Prod）—— 稳定性与安全
&lt;/h3>&lt;p>目标：不可变基础设施（Immutable Artifacts）、资源隔离、零敏感信息泄露。&lt;/p>
&lt;h4 id="61-架构策略">6.1 架构策略
&lt;/h4>&lt;p>严禁包含 &lt;code>build&lt;/code> 指令，必须使用预构建并签名的镜像。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># compose.prod.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.com/my-app:v1.2.0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 锁定具体版本&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">unless-stopped&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;80:8080&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 仅暴露服务端口&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">NODE_ENV=production&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 生产级资源限制 (V2 支持非 Swarm 模式)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;1.0&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 硬限制&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">512M &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># OOM 熔断阈值&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">reservations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;0.2&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 保证资源&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128M&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 密钥管理：严禁使用 environment 传递密码&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secrets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">db_password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">secrets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db_password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./secrets/prod_db_pwd.txt&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="62-部署命令">6.2 部署命令
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># --remove-orphans: 清理旧版本遗留的容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -d: 后台运行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose -f compose.yaml -f compose.prod.yaml up -d --remove-orphans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="7-高级主题数据卷管理的陷阱">7. 高级主题：数据卷管理的陷阱
&lt;/h3>&lt;h4 id="71-无法删除悖论">7.1 “无法删除”悖论
&lt;/h4>&lt;p>Docker Compose 的合并逻辑是&lt;strong>追加&lt;/strong>。你无法在 &lt;code>compose.prod.yaml&lt;/code> 中编写指令来“删除” &lt;code>compose.yaml&lt;/code> 中定义的卷挂载。
&lt;strong>解决方案&lt;/strong>：如果某个卷只在开发环境需要（如 &lt;code>./src:/app/src&lt;/code>），&lt;strong>千万不要&lt;/strong>写在 &lt;code>compose.yaml&lt;/code> 里，必须只写在 &lt;code>compose.override.yaml&lt;/code> 里。&lt;/p>
&lt;h4 id="72-生产环境数据持久化">7.2 生产环境数据持久化
&lt;/h4>&lt;p>生产环境应使用 &lt;strong>命名卷（Named Volumes）&lt;/strong> 而非绑定挂载，以便于利用 Docker 卷插件进行备份或迁移，并规避宿主机文件权限问题。&lt;/p>
&lt;hr>
&lt;h3 id="8-总结全生命周期命令速查表">8. 总结：全生命周期命令速查表
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">环节&lt;/th>
&lt;th style="text-align: left">文件组合&lt;/th>
&lt;th style="text-align: left">关键参数&lt;/th>
&lt;th style="text-align: left">核心特征&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>开发 (Dev)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">(默认) &lt;code>base&lt;/code> + &lt;code>override&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>up -d&lt;/code>&lt;/td>
&lt;td style="text-align: left">源码挂载、调试端口开放、多阶段构建 Target: Dev&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>测试 (CI)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;code>-f base -f test&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>up --exit-code-from sut&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>tmpfs&lt;/code> 数据、健康检查依赖、自动化退出&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>生产 (Prod)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">&lt;code>-f base -f prod&lt;/code>&lt;/td>
&lt;td style="text-align: left">&lt;code>up -d --remove-orphans&lt;/code>&lt;/td>
&lt;td style="text-align: left">不可变镜像 Tag、资源限制、Secrets、重启策略&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>通过严格遵守&lt;strong>基座极简原则&lt;/strong>和&lt;strong>分层配置策略&lt;/strong>，Docker Compose 能够优雅地支撑从单机开发到生产交付的全链路需求，消除环境不一致带来的工程风险。&lt;/p></description></item><item><title>copier-using</title><link>https://www.zata.cc/p/copier-using/</link><pubDate>Fri, 12 Sep 2025 15:26:16 +0800</pubDate><guid>https://www.zata.cc/p/copier-using/</guid><description>&lt;img src="https://www.zata.cc/p/copier-using/images/index/index.png" alt="Featured image of post copier-using" />&lt;h3 id="copier-是什么">Copier 是什么？
&lt;/h3>&lt;p>&lt;strong>Copier&lt;/strong> 是一个用于创建和管理项目文件的现代化命令行工具。你可以把它理解为一个强大的“复制粘贴”工具，但它远不止于此。它能根据一个预设的“模板”（template），智能地生成一个新项目，并且在模板更新后，还能将这些更新应用到你已生成的项目中。&lt;/p>
&lt;p>它在软件开发领域非常受欢迎，尤其适合用于搭建标准化的项目初始结构，也就是我们常说的“脚手架”（scaffolding）。&lt;/p>
&lt;p>&lt;strong>核心特性：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>项目生成 (Project Generation)&lt;/strong>: 从一个模板快速生成一个新项目。在生成过程中，它会向你提问（例如，项目名称、作者、选择特定功能等），然后根据你的回答填充模板中的变量。&lt;/li>
&lt;li>&lt;strong>项目更新 (Project Updates)&lt;/strong>: 这是 Copier 相较于其前辈（如 Cookiecutter）最大的优势。当模板本身有了改进或修复（比如，升级了依赖库、修复了安全漏洞），你可以使用 Copier 将这些变更安全地同步到已经生成的项目中，而不会覆盖你自己的代码。&lt;/li>
&lt;li>&lt;strong>动态与交互式&lt;/strong>: 通过一系列问题引导用户完成项目配置，并将答案记录在 &lt;code>.copier-answers.yml&lt;/code> 文件中，方便未来更新。&lt;/li>
&lt;li>&lt;strong>版本控制友好&lt;/strong>: 它与 Git 紧密集成，能很好地处理版本变更和代码合并。&lt;/li>
&lt;li>&lt;strong>跨平台与语言无关&lt;/strong>: Copier 本身由 Python 编写，但它可以为任何编程语言（Go, Rust, JavaScript, Python 等）创建项目模板。&lt;/li>
&lt;/ol>
&lt;p>简单来说，&lt;strong>Copier = 项目脚手架 + 持续更新能力&lt;/strong>。它解决了传统脚手架工具“一次性生成，后续维护困难”的痛点。&lt;/p>
&lt;hr>
&lt;h3 id="copier-使用教程">Copier 使用教程
&lt;/h3>&lt;p>下面，我们将从安装、创建项目、更新项目等环节，一步步教你如何使用 Copier。&lt;/p>
&lt;h4 id="1-安装-copier">1. 安装 Copier
&lt;/h4>&lt;p>首先，你需要一个 Python 环境。然后使用 &lt;code>pip&lt;/code> 或 &lt;code>pipx&lt;/code>（推荐）来安装 Copier。&lt;code>pipx&lt;/code> 可以将 Copier 安装在独立的环境中，避免污染全局 Python 环境。&lt;/p>
&lt;p>&lt;strong>使用 pipx (推荐):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 安装 pipx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python -m pip install --user pipx
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python -m pipx ensurepath
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 使用 pipx 安装 copier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pipx install copier
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>使用 pip:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install copier
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安装完成后，你可以通过以下命令验证是否成功：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">copier --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="2-从模板创建新项目-copier-copy">2. 从模板创建新项目 (&lt;code>copier copy&lt;/code>)
&lt;/h4>&lt;p>Copier 的核心是模板。模板通常是一个 Git 仓库。我们以一个官方推荐的 Python 项目模板为例。&lt;/p>
&lt;p>&lt;strong>命令格式:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">copier copy &amp;lt;模板地址&amp;gt; &amp;lt;你的项目文件夹名称&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>实战演练:&lt;/strong>&lt;/p>
&lt;p>假设我们要创建一个名为 &lt;code>my-awesome-project&lt;/code> 的新项目。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">copier copy gh:copier-org/copier-python-template my-awesome-project
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>gh:copier-org/copier-python-template&lt;/code> 是模板的简写地址，它指向 GitHub 上的 &lt;code>copier-org/copier-python-template&lt;/code> 仓库。你也可以使用完整的 &lt;code>https://github.com/copier-org/copier-python-template.git&lt;/code> 地址。&lt;/li>
&lt;/ul>
&lt;p>执行命令后，Copier 会开始与你交互，提出一系列问题来配置项目：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">🎤 What is your project name?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (Default: My Awesome Project)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──&amp;gt; My Super FastAPI App # 输入你的项目名
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">🎤 What is your project description?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (Default: Awesome project, created with copier-python-template)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──&amp;gt; A demo project for learning Copier. # 输入描述
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">... (后续还会有作者、邮箱、选择许可证等问题)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你只需要根据提示回答问题即可。回答完毕后，Copier 会做两件事：&lt;/p>
&lt;ol>
&lt;li>在当前目录下创建一个 &lt;code>my-awesome-project&lt;/code> 文件夹。&lt;/li>
&lt;li>文件夹内包含了根据你的回答生成的所有项目文件。&lt;/li>
&lt;li>同时，文件夹里还会有一个特殊的 &lt;code>.copier-answers.yml&lt;/code> 文件，它记录了你刚才的所有回答。&lt;strong>这个文件非常重要，是未来项目更新的关键！&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>&lt;code>.copier-answers.yml&lt;/code> 文件示例:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Changes here will be overwritten by Copier&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">_commit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v0.2.2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">_src_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gh:copier-org/copier-python-template&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">author_email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">your_email@example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">author_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Your Name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">project_description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">A demo project for learning Copier.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">project_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">My Super FastAPI App&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="3-更新现有项目-copier-update">3. 更新现有项目 (&lt;code>copier update&lt;/code>)
&lt;/h4>&lt;p>这是 Copier 的“杀手级”功能。假设一段时间后，&lt;code>copier-python-template&lt;/code> 模板的作者发布了一个新版本，修复了一些 Bug 并增加了一些新功能。你想把这些更新应用到你的 &lt;code>my-awesome-project&lt;/code> 中。&lt;/p>
&lt;p>操作非常简单：&lt;/p>
&lt;p>&lt;strong>首先，进入你的项目目录：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> my-awesome-project
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>然后，执行更新命令：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">copier update
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Copier 会自动执行以下步骤：&lt;/p>
&lt;ol>
&lt;li>读取 &lt;code>.copier-answers.yml&lt;/code> 文件，找到原始模板地址和上次生成时的版本。&lt;/li>
&lt;li>检查模板仓库是否有新版本。&lt;/li>
&lt;li>使用你之前回答过的答案，重新生成一份最新的项目文件。&lt;/li>
&lt;li>将新生成的文件与你当前的项目文件进行对比，并尝试智能合并。&lt;/li>
&lt;/ol>
&lt;p>在合并过程中，如果遇到冲突（例如，模板的某个文件和你自己修改过的文件内容不一致），Copier 会生成标准的 &lt;code>.rej&lt;/code> 冲突文件，或者如果你在 Git 仓库中，它会产生 Git 冲突标记，让你手动解决。&lt;/p>
&lt;p>&lt;strong>更新时的注意事项:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>强烈建议在执行 &lt;code>copier update&lt;/code> 前，确保你的项目已经提交到 Git。这样即使更新出现问题，你也可以轻松回滚。&lt;/li>
&lt;li>Copier 会尽力保留你自己的代码，但最佳实践是，尽量不要修改由模板直接生成且预计会频繁更新的配置文件（除非你清楚自己在做什么）。&lt;/li>
&lt;/ul>
&lt;h4 id="4-创建自己的模板">4. 创建自己的模板
&lt;/h4>&lt;p>如果你想创建自己的项目模板，也非常简单。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>创建一个标准的项目结构&lt;/strong>。例如，一个包含 &lt;code>README.md&lt;/code>, &lt;code>.gitignore&lt;/code>, &lt;code>src/&lt;/code> 等文件的项目。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>将需要动态替换的内容改为 Jinja2 语法&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>文件名可以包含变量，例如 &lt;code>{{ project_name }}/main.py&lt;/code>。&lt;/li>
&lt;li>文件内容也可以包含变量，例如 &lt;code>README.md&lt;/code> 中可以这样写：&lt;/li>
&lt;/ul>
&lt;!-- end list -->
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gh"># {{ project_name }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ project_description }}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>创建一个 &lt;code>copier.yml&lt;/code> (或 &lt;code>copier.yaml&lt;/code>) 文件&lt;/strong>。这个文件用来定义 Copier 需要向用户提出的问题。&lt;/p>
&lt;p>&lt;strong>&lt;code>copier.yml&lt;/code> 示例:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 定义问题&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">project_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">str&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">help&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">What is your project name?&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">project_description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">str&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">help&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">What is your project description?&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;A cool project.&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 定义模板渲染后要执行的命令&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">_tasks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">git init&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">git add .&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">git commit -m &amp;#34;Initial commit from copier template&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里定义了两个问题：&lt;code>project_name&lt;/code> 和 &lt;code>project_description&lt;/code>。用户在生成项目时回答的答案会分别赋值给这两个变量。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>将模板推送到 Git 仓库&lt;/strong>（如 GitHub），然后你就可以像之前一样使用 &lt;code>copier copy&lt;/code> 来从你的模板创建新项目了。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="总结">总结
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>对于使用者&lt;/strong>: Copier 是一个能让你轻松使用标准化项目模板并保持模板同步更新的强大工具。只需 &lt;code>copier copy&lt;/code> 创建和 &lt;code>copier update&lt;/code> 更新。&lt;/li>
&lt;li>&lt;strong>对于模板维护者/团队&lt;/strong>: Copier 是统一团队技术栈、规范项目结构、分发最佳实践的利器。通过维护一个中央模板，所有团队成员都可以快速启动项目并享受持续的模板升级。&lt;/li>
&lt;/ul>
&lt;p>你最开始提到的那个脚本，正是在 Copier 这个大生态系统下的一个自动化辅助工具。它解决了 &lt;code>.env&lt;/code> 文件通常不适合使用 Jinja2 模板直接渲染的问题，通过在 Copier 更新后运行脚本，间接地将 &lt;code>.copier-answers.yml&lt;/code> 中的配置同步到 &lt;code>.env&lt;/code> 文件中，实现配置的无缝更新。这正是 Copier 强大扩展性的体现。&lt;/p></description></item><item><title>build x86 image in ARM MAC platform and devolopmet to remote server</title><link>https://www.zata.cc/p/build-x86-image-in-arm-mac-platform-and-devolopmet-to-remote-server/</link><pubDate>Thu, 11 Sep 2025 16:50:53 +0800</pubDate><guid>https://www.zata.cc/p/build-x86-image-in-arm-mac-platform-and-devolopmet-to-remote-server/</guid><description>&lt;img src="https://www.zata.cc/p/build-x86-image-in-arm-mac-platform-and-devolopmet-to-remote-server/images/index/index.png" alt="Featured image of post build x86 image in ARM MAC platform and devolopmet to remote server" />&lt;h1 id="在-arm-mac-上构建-x86-镜像并部署至远程服务器的终极指南">在 ARM Mac 上构建 x86 镜像并部署至远程服务器的终极指南
&lt;/h1>&lt;p>这份指南涵盖了从在 Apple Silicon (M1/M2/M3) Mac 上交叉编译构建 x86 (amd64) Docker 镜像，到将其以不同方式保存和部署的完整流程。&lt;/p>
&lt;hr>
&lt;h2 id="1-在-arm-mac-上构建-x86-镜像">1. 在 ARM Mac 上构建 x86 镜像
&lt;/h2>&lt;p>由于本地 Mac 是 &lt;code>arm64&lt;/code> 架构，而目标服务器通常是 &lt;code>x86_64&lt;/code> (即 &lt;code>amd64&lt;/code>)，我们需要进行交叉编译构建。Docker Desktop for Mac 内置的 QEMU 模拟器和 &lt;code>buildx&lt;/code> 工具使这一切变得简单。&lt;/p>
&lt;h3 id="准备工作">准备工作
&lt;/h3>&lt;ul>
&lt;li>一台 Apple Silicon 芯片的 Mac。&lt;/li>
&lt;li>安装最新版的 Docker Desktop for Mac。&lt;/li>
&lt;/ul>
&lt;h3 id="构建步骤">构建步骤
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>创建 Dockerfile&lt;/strong>
在一个空的项目目录中，创建一个 &lt;code>Dockerfile&lt;/code> 文件。明确指定基础镜像的平台是一个好习惯。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Dockerfile&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 使用 --platform 标志确保拉取的是 amd64 版本的镜像&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> --platform=linux/amd64 ubuntu:22.04&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">LABEL&lt;/span> &lt;span class="nv">maintainer&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Your Name&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">LABEL&lt;/span> &lt;span class="nv">target_architecture&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;x86_64/amd64&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># RUN 指令会在 QEMU 模拟的 x86 环境中执行&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apt-get update &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apt-get install -y curl&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 验证架构&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;本镜像架构为:&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> uname -m&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;bash&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>执行构建命令&lt;/strong>
使用 &lt;code>docker build&lt;/code> 命令并附带 &lt;code>--platform&lt;/code> 标志来指定目标架构。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在包含 Dockerfile 的目录下执行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将 my-x86-app:1.0 替换为你想要的镜像名和标签&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker build --platform linux/amd64 -t my-x86-app:1.0 .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>如果构建的时候网络失败，可以先使用 docker pull &amp;ndash;platform linux/amd64 &amp;lt;镜像名&amp;gt;:&amp;lt;标签&amp;gt; 提前拉取镜像&lt;/li>
&lt;li>&lt;code>--platform linux/amd64&lt;/code>: &lt;strong>核心参数&lt;/strong>，告诉 Docker 你要构建的是 x86 镜像。&lt;/li>
&lt;li>构建过程会比原生编译慢，因为指令需要通过 QEMU 模拟执行。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>验证本地镜像架构&lt;/strong>
构建完成后，镜像会保存在你的本地。使用 &lt;code>docker inspect&lt;/code> 来确认其架构。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker inspect my-x86-app:1.0 &lt;span class="p">|&lt;/span> grep Architecture
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>预期输出应为:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Architecture&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="s2">&amp;#34;amd64&amp;#34;&lt;/span>&lt;span class="err">,&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="2-保存与分发构建好的镜像">2. 保存与分发构建好的镜像
&lt;/h2>&lt;p>现在你本地有了一个 x86 架构的镜像 &lt;code>my-x86-app:1.0&lt;/code>，接下来可以根据需求将其分发出去。&lt;/p>
&lt;h3 id="方案-a-推送到镜像仓库团队协作cicd-推荐">方案 A: 推送到镜像仓库（团队协作/CI/CD 推荐）
&lt;/h3>&lt;p>这是最规范、最通用的方法，适用于所有需要网络访问的场景。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>标记镜像 (Tag)&lt;/strong>
为镜像打上符合仓库规范的标签。以 Docker Hub 为例，格式为 &lt;code>&amp;lt;用户名&amp;gt;/&amp;lt;仓库名&amp;gt;:&amp;lt;标签&amp;gt;&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker tag my-x86-app:1.0 your-dockerhub-username/my-x86-app:1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>登录仓库&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker login
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>推送镜像 (Push)&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker push your-dockerhub-username/my-x86-app:1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>推送后，任何有权限的人或服务器都可以通过 &lt;code>docker pull&lt;/code> 命令拉取此镜像。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="方案-b-直接传输到另一台服务器最高效的-p2p-方式">方案 B: 直接传输到另一台服务器（最高效的 P2P 方式）
&lt;/h3>&lt;p>如果你想将在本地构建好的镜像，快速地发送到一台已经配置好 SSH 访问的服务器上，这是最佳选择。&lt;strong>此方法不依赖镜像仓库，也不在本地产生中间文件&lt;/strong>。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>使用 &lt;code>docker save&lt;/code> 和 &lt;code>ssh&lt;/code> 管道流式传输&lt;/strong>
这条命令将本地镜像打包后，通过 SSH 管道直接在远程服务器上加载。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将 your-user@your-server 替换为你的 SSH 登录信息&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker save my-x86-app:1.0 &lt;span class="p">|&lt;/span> ssh your-user@your-server &lt;span class="s1">&amp;#39;docker load&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>工作原理&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>docker save my-x86-app:1.0&lt;/code>: 将镜像打包成 tar 流输出到标准输出。&lt;/li>
&lt;li>&lt;code>|&lt;/code>: 管道，将左侧命令的输出作为右侧命令的输入。&lt;/li>
&lt;li>&lt;code>ssh ... 'docker load'&lt;/code>: 连接到远程服务器，并在服务器上执行 &lt;code>docker load&lt;/code> 命令，该命令从标准输入读取 tar 流并加载镜像。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>验证&lt;/strong>
SSH 登录到你的服务器 &lt;code>ssh your-user@your-server&lt;/code>，然后运行 &lt;code>docker images&lt;/code>，即可看到 &lt;code>my-x86-app:1.0&lt;/code>。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="方案-c-导出为文件后手动传输离线场景">方案 C: 导出为文件后手动传输（离线场景）
&lt;/h3>&lt;p>适用于完全离线的环境，或者需要将镜像文件存档的场景。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>保存为 &lt;code>.tar&lt;/code> 文件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker save -o my-x86-app-1.0.tar my-x86-app:1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>拷贝文件&lt;/strong>
使用 &lt;code>scp&lt;/code> 或其他工具将 &lt;code>my-x86-app-1.0.tar&lt;/code> 文件传输到服务器。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">scp my-x86-app-1.0.tar your-user@your-server:/path/to/destination/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>在服务器上加载&lt;/strong>
SSH 登录服务器后，从文件加载镜像。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ssh your-user@your-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在服务器上执行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker load -i /path/to/destination/my-x86-app-1.0.tar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="附录-关于-docker-context-的说明">附录: 关于 &lt;code>docker context&lt;/code> 的说明
&lt;/h2>&lt;p>&lt;code>docker context&lt;/code> 是一个强大的工具，但它的用途与上述方案 B 和 C 不同。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>docker context&lt;/code> 的作用&lt;/strong>: 它是用来&lt;strong>切换 Docker CLI 的控制目标&lt;/strong>。当你 &lt;code>docker context use your-server&lt;/code> 时，你的本地终端就变成了远程服务器 Docker 的遥控器。&lt;/li>
&lt;li>&lt;strong>使用场景&lt;/strong>: 如果你希望&lt;strong>构建过程本身就发生在远程服务器上&lt;/strong>（利用服务器的 CPU 和原生 x86 架构），那么就应该先切换 context，再运行 &lt;code>docker build&lt;/code>。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 切换控制目标到服务器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker context use your-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 本地的源码会自动上传到服务器，并在服务器上完成构建&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker build -t my-app-built-on-server:1.0 .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 操作完成后切回本地&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker context use default
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>这与“在本地构建，然后把产物（镜像）发送过去”是两种不同的工作流。&lt;/p>
&lt;hr>
&lt;p>希望这份整合后的指南能清晰地解答你的所有疑问！&lt;/p></description></item><item><title>Playwright 使用实践与本地浏览器 Profile 避坑</title><link>https://www.zata.cc/p/playwright-%E4%BD%BF%E7%94%A8%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%9C%AC%E5%9C%B0%E6%B5%8F%E8%A7%88%E5%99%A8-profile-%E9%81%BF%E5%9D%91/</link><pubDate>Sun, 31 Aug 2025 08:13:49 +0800</pubDate><guid>https://www.zata.cc/p/playwright-%E4%BD%BF%E7%94%A8%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%9C%AC%E5%9C%B0%E6%B5%8F%E8%A7%88%E5%99%A8-profile-%E9%81%BF%E5%9D%91/</guid><description>&lt;img src="https://www.zata.cc/p/playwright-%E4%BD%BF%E7%94%A8%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%9C%AC%E5%9C%B0%E6%B5%8F%E8%A7%88%E5%99%A8-profile-%E9%81%BF%E5%9D%91/images/index/index.png" alt="Featured image of post Playwright 使用实践与本地浏览器 Profile 避坑" />&lt;h2 id="一个代价很大的坑">一个代价很大的坑
&lt;/h2>&lt;p>我之前写 Playwright 程序时，希望它直接使用电脑上安装的浏览器和已有登录状态，于是让它复用了本地 Chrome 的用户目录。结果自动化运行后，日常浏览器里的历史记录全部消失了。&lt;/p>
&lt;p>这个经历里最容易混淆的是两件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>使用本机安装的 Chrome 程序&lt;/strong>：通常没问题。&lt;/li>
&lt;li>&lt;strong>使用日常 Chrome 的用户数据目录（Profile）&lt;/strong>：风险很高，不应该这样做。&lt;/li>
&lt;/ul>
&lt;p>Playwright 默认创建隔离的 &lt;code>BrowserContext&lt;/code>，其行为类似一个全新的无痕环境，不会读取日常 Chrome 的历史记录、Cookie 或扩展。只有在使用 &lt;code>launch_persistent_context()&lt;/code> 并把 &lt;code>user_data_dir&lt;/code> 指向真实 Chrome 用户目录时，自动化程序才会直接读写日常浏览数据。&lt;/p>
&lt;h2 id="为什么真实-profile-可能出问题">为什么真实 Profile 可能出问题
&lt;/h2>&lt;p>Chrome 的用户目录中不只是 Cookie，还包含：&lt;/p>
&lt;ul>
&lt;li>&lt;code>History&lt;/code>：浏览历史数据库&lt;/li>
&lt;li>&lt;code>Cookies&lt;/code>：Cookie 数据库&lt;/li>
&lt;li>&lt;code>Login Data&lt;/code>：保存的登录信息&lt;/li>
&lt;li>&lt;code>Bookmarks&lt;/code>：书签&lt;/li>
&lt;li>&lt;code>Preferences&lt;/code>：浏览器配置&lt;/li>
&lt;li>扩展、缓存和站点本地存储&lt;/li>
&lt;/ul>
&lt;p>这些文件中有不少是 SQLite 数据库。普通 Chrome 和 Playwright 同时访问同一个 Profile，可能发生文件锁冲突、数据库损坏或配置覆盖。如果脚本还带有清理、复制、初始化目录的逻辑，就可能直接覆盖原数据。&lt;/p>
&lt;p>另外，Chrome 现在也限制自动化工具访问默认用户目录。Playwright 官方明确建议：不要把日常 Chrome 的主 &lt;code>User Data&lt;/code> 目录传给自动化程序，而应创建独立目录作为自动化 Profile。&lt;/p>
&lt;h2 id="错误示例直接复用日常-chrome-profile">错误示例：直接复用日常 Chrome Profile
&lt;/h2>&lt;p>macOS 上，Chrome 的用户数据通常位于：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">~/Library/Application Support/Google/Chrome
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不要这样写：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">chrome_user_data_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">home&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;Library/Application Support/Google/Chrome&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch_persistent_context&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_data_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">chrome_user_data_dir&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">channel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;chrome&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headless&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_page&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;https://example.com&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里的危险点不是 &lt;code>channel=&amp;quot;chrome&amp;quot;&lt;/code>，而是 &lt;code>user_data_dir&lt;/code> 指向了日常使用的 Chrome 数据目录。&lt;/p>
&lt;p>还要注意，&lt;code>chrome://version&lt;/code> 中显示的“个人资料路径”可能是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">.../Google/Chrome/Default
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>而 Chromium 启动参数所需的 &lt;code>user_data_dir&lt;/code> 是它的父目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">.../Google/Chrome
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不要因为两者层级不同，就尝试把 &lt;code>Default&lt;/code> 或它的父目录逐个传给脚本测试；它们都属于真实浏览数据。&lt;/p>
&lt;h2 id="正确方案一使用系统-chrome但创建专用-profile">正确方案一：使用系统 Chrome，但创建专用 Profile
&lt;/h2>&lt;p>如果需要有头浏览器、持久化 Cookie 或长期登录状态，可以为自动化单独创建目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">automation_profile_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cwd&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;.playwright-profile&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">automation_profile_dir&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkdir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parents&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">exist_ok&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch_persistent_context&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_data_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">automation_profile_dir&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">channel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;chrome&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headless&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pages&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pages&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_page&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;https://example.com&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这段代码中：&lt;/p>
&lt;ul>
&lt;li>&lt;code>channel=&amp;quot;chrome&amp;quot;&lt;/code> 使用本机安装的 Google Chrome。&lt;/li>
&lt;li>&lt;code>.playwright-profile&lt;/code> 只供自动化程序使用。&lt;/li>
&lt;li>关闭后 Cookie、本地存储等仍会保留，下一次运行可以继续使用。&lt;/li>
&lt;li>日常 Chrome 和自动化 Chrome 的数据完全分离。&lt;/li>
&lt;/ul>
&lt;p>应把专用 Profile 加入 &lt;code>.gitignore&lt;/code>，避免将 Cookie 或凭据提交到仓库：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">.playwright-profile/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="正确方案二只保存登录状态">正确方案二：只保存登录状态
&lt;/h2>&lt;p>如果只是运行测试，通常不需要保存整个浏览器 Profile。更轻量的做法是登录一次后导出 &lt;code>storage_state&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">auth_state_path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;playwright/.auth/user.json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">auth_state_path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parent&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkdir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parents&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">exist_ok&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">headless&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_context&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_page&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;https://example.com/login&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 在这里完成登录。&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">storage_state&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">auth_state_path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>后续运行时加载它：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_context&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">storage_state&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;playwright/.auth/user.json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_page&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;https://example.com&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>storage_state&lt;/code> 主要保存 Cookie、localStorage 和 IndexedDB，比复制整个 Profile 更适合自动化测试。但这个文件仍可能包含有效登录凭据，也必须加入 &lt;code>.gitignore&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">playwright/.auth/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="普通隔离模式">普通隔离模式
&lt;/h2>&lt;p>不需要复用登录状态时，直接使用 Playwright 默认的隔离模式最安全：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">playwright.sync_api&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sync_playwright&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">sync_playwright&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">playwright&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chromium&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">launch&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">channel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;chrome&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headless&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_context&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">new_page&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">page&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;https://example.com&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">browser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每个新建的 &lt;code>BrowserContext&lt;/code> 都是独立的干净环境。关闭 Context 后其中的临时 Cookie 和本地存储会被丢弃，但不会触碰日常 Chrome Profile。&lt;/p>
&lt;h2 id="安全检查清单">安全检查清单
&lt;/h2>&lt;p>在运行任何复用浏览器状态的 Playwright 程序前，至少检查以下事项：&lt;/p>
&lt;ol>
&lt;li>&lt;code>user_data_dir&lt;/code> 是否指向专用目录，而不是 Chrome、Edge 或 Chromium 的日常用户目录。&lt;/li>
&lt;li>普通浏览器和 Playwright 是否会同时打开同一个 Profile。&lt;/li>
&lt;li>cleanup 逻辑是否可能对未校验的路径执行 &lt;code>rmtree()&lt;/code>、&lt;code>rm -rf&lt;/code> 或覆盖复制。&lt;/li>
&lt;li>是否可以用 &lt;code>storage_state&lt;/code> 替代整个持久化 Profile。&lt;/li>
&lt;li>自动化 Profile 和认证状态文件是否已加入 &lt;code>.gitignore&lt;/code>。&lt;/li>
&lt;li>重要浏览器数据是否有同步或系统备份。&lt;/li>
&lt;/ol>
&lt;p>如果程序必须清理自动化 Profile，应使用固定的专用路径，并在删除前验证目录名称。不要对来自环境变量或外部输入、尚未解析和校验的路径直接执行递归删除。&lt;/p>
&lt;h2 id="历史记录消失后的排查">历史记录消失后的排查
&lt;/h2>&lt;p>发现历史记录不见后，先不要继续频繁启动 Chrome：&lt;/p>
&lt;ol>
&lt;li>完全退出普通 Chrome 和 Playwright。&lt;/li>
&lt;li>备份整个 Chrome 用户目录。&lt;/li>
&lt;li>打开 &lt;code>chrome://version&lt;/code>，确认普通 Chrome 当前使用的是 &lt;code>Default&lt;/code>、&lt;code>Profile 1&lt;/code> 还是其他 Profile。&lt;/li>
&lt;li>检查各 Profile 下是否仍有 &lt;code>History&lt;/code> 文件。&lt;/li>
&lt;li>检查 Chrome Sync、Time Machine 或其他系统备份。&lt;/li>
&lt;/ol>
&lt;p>macOS 上常见的历史数据库位置包括：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">~/Library/Application Support/Google/Chrome/Default/History
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">~/Library/Application Support/Google/Chrome/Profile 1/History
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>History&lt;/code> 是 SQLite 数据库。如果只是切换到了空 Profile，原数据可能仍在其他目录；如果数据库已被覆盖或重新创建，则需要从同步或备份中恢复。&lt;/p>
&lt;h2 id="总结">总结
&lt;/h2>&lt;p>使用 Playwright 时要区分“浏览器程序”和“浏览器数据”：&lt;/p>
&lt;ul>
&lt;li>想使用本地 Chrome：设置 &lt;code>channel=&amp;quot;chrome&amp;quot;&lt;/code>。&lt;/li>
&lt;li>想保存自动化登录状态：使用专用 &lt;code>user_data_dir&lt;/code>。&lt;/li>
&lt;li>只想复用测试登录态：优先使用 &lt;code>storage_state&lt;/code>。&lt;/li>
&lt;li>永远不要让自动化程序直接操作日常 Chrome Profile。&lt;/li>
&lt;/ul>
&lt;h2 id="参考资料">参考资料
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch-persistent-context" target="_blank" rel="noopener"
>Playwright：BrowserType.launch_persistent_context&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://playwright.dev/python/docs/auth" target="_blank" rel="noopener"
>Playwright：Authentication&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://playwright.dev/python/docs/browser-contexts" target="_blank" rel="noopener"
>Playwright：Browser contexts&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://cuiqingcai.com/36045.html" target="_blank" rel="noopener"
>Playwright Python 使用教程&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>github release</title><link>https://www.zata.cc/p/github-release/</link><pubDate>Thu, 01 May 2025 01:28:06 +0800</pubDate><guid>https://www.zata.cc/p/github-release/</guid><description>&lt;img src="https://www.zata.cc/p/github-release/images/index/index.png" alt="Featured image of post github release" />&lt;p>关于版本的管理可以参考：
&lt;a class="link" href="https://www.bilibili.com/video/BV1Ee411W79K" target="_blank" rel="noopener"
>https://www.bilibili.com/video/BV1Ee411W79K&lt;/a>&lt;/p>
&lt;hr>
&lt;p>GitHub Release 是 GitHub 上用来标记项目可发布版本的一种方式。它可以包含该版本的源代码快照、编译好的二进制文件以及相关的发行说明。创建 Release 有助于用户清晰地了解项目的版本历史和获取特定版本的可执行文件或库。&lt;/p>
&lt;p>&lt;strong>为什么使用 GitHub Release？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>版本管理：&lt;/strong> 清晰地标记项目的不同版本，方便追溯和管理。&lt;/li>
&lt;li>&lt;strong>软件分发：&lt;/strong> 提供稳定版本的下载链接，方便用户获取和使用。&lt;/li>
&lt;li>&lt;strong>发行说明：&lt;/strong> 在 Release 中包含详细的说明，介绍该版本的特性、改进、Bug 修复等。&lt;/li>
&lt;li>&lt;strong>自动化：&lt;/strong> 可以结合 CI/CD 工具实现自动化 Release 发布。&lt;/li>
&lt;/ul>
&lt;p>在创建 GitHub Release 之前，你需要满足以下条件：&lt;/p>
&lt;ol>
&lt;li>拥有一个 GitHub 账号。&lt;/li>
&lt;li>拥有一个 GitHub 仓库。&lt;/li>
&lt;li>对该仓库有写入权限。&lt;/li>
&lt;li>通常情况下，你需要有一个 Git Tag 与 Release 关联。Tag 用于标记代码库中的某个特定的提交（commit），代表着该版本代码的状态。&lt;/li>
&lt;/ol>
&lt;p>有两种主要的方式来创建 GitHub Release：通过 GitHub 网页界面和通过 Git 命令行工具（结合 GitHub CLI）。&lt;/p>
&lt;h3 id="方法一通过-github-网页界面创建-release">方法一：通过 GitHub 网页界面创建 Release
&lt;/h3>&lt;p>这是最直观和常用的方法。&lt;/p>
&lt;p>&lt;strong>步骤 1：进入你的仓库页面&lt;/strong>&lt;/p>
&lt;p>在浏览器中打开你的 GitHub 仓库主页。&lt;/p>
&lt;p>&lt;strong>步骤 2：创建新的 Release&lt;/strong>&lt;/p>
&lt;p>&amp;ldquo;&lt;strong>Create a new release&lt;/strong>&amp;quot;（创建新 Release），点击这个按钮。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-1.png"
width="1275"
height="642"
srcset="https://www.zata.cc/p/github-release/images/index/index-1_hu14233999473419316545.png 480w, https://www.zata.cc/p/github-release/images/index/index-1_hu15510812016515103560.png 1024w"
loading="lazy"
alt="Create a new release"
class="gallery-image"
data-flex-grow="198"
data-flex-basis="476px"
>&lt;/p>
&lt;p>&lt;strong>步骤 3：选择或创建 Tag&lt;/strong>&lt;/p>
&lt;p>在新 Release 页面，你需要关联一个 Git Tag。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>选择现有 Tag：&lt;/strong> 点击 &amp;ldquo;Choose a tag&amp;rdquo;（选择一个标签）下拉菜单，选择一个已经存在的 Tag。&lt;/li>
&lt;li>&lt;strong>创建新 Tag：&lt;/strong> 在输入框中直接输入你想要创建的新 Tag 名称（例如 &lt;code>v1.0.0&lt;/code>）。输入后，会显示 &amp;ldquo;&lt;strong>Create new tag on publish&lt;/strong>&amp;quot;（发布时创建新标签）。&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-2.png"
width="1270"
height="790"
srcset="https://www.zata.cc/p/github-release/images/index/index-2_hu12721207652665031077.png 480w, https://www.zata.cc/p/github-release/images/index/index-2_hu14229868557218555738.png 1024w"
loading="lazy"
alt="创建新tag"
class="gallery-image"
data-flex-grow="160"
data-flex-basis="385px"
>&lt;/p>
&lt;p>&lt;strong>重要提示：&lt;/strong> Tag 的命名通常遵循 &lt;a class="link" href="https://semver.org/" target="_blank" rel="noopener"
>Semantic Versioning&lt;/a> 规范，格式通常是 &lt;code>MAJOR.MINOR.PATCH&lt;/code>（大版本，小版本，bug修复版本），例如 &lt;code>v1.0.0&lt;/code>、&lt;code>v1.2.3&lt;/code> 等。建议在 Tag 前加上 &lt;code>v&lt;/code>。&lt;/p>
&lt;p>&lt;strong>步骤 4：选择目标分支 (如果创建新 Tag)&lt;/strong>&lt;/p>
&lt;p>如果你在步骤 4 中创建了一个新的 Tag，你需要选择这个 Tag 将关联到哪个分支上的最新提交。通常情况下，你会选择你的主分支（如 &lt;code>main&lt;/code> 或 &lt;code>master&lt;/code>）。点击 &amp;ldquo;Target&amp;rdquo;（目标）下拉菜单进行选择。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-4.png"
width="1268"
height="847"
srcset="https://www.zata.cc/p/github-release/images/index/index-4_hu6627979451046457891.png 480w, https://www.zata.cc/p/github-release/images/index/index-4_hu12306340666082991369.png 1024w"
loading="lazy"
alt="选择分支"
class="gallery-image"
data-flex-grow="149"
data-flex-basis="359px"
>&lt;/p>
&lt;p>&lt;strong>步骤 5：填写 Release 标题和描述&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Release title (发行标题):&lt;/strong> 输入你的 Release 的标题，通常与 Tag 名称相同或稍作描述（例如 &amp;ldquo;Version 1.0.0 Release&amp;rdquo;）。&lt;/li>
&lt;li>&lt;strong>Describe this release (描述此发行版):&lt;/strong> 在这里填写详细的发行说明（Release Notes）。你可以使用 Markdown 格式来美化你的说明。发行说明通常包括：
&lt;ul>
&lt;li>新功能 (New Features)&lt;/li>
&lt;li>改进 (Improvements)&lt;/li>
&lt;li>Bug 修复 (Bug Fixes)&lt;/li>
&lt;li>已知问题 (Known Issues)&lt;/li>
&lt;li>重大变化 (Breaking Changes) (如果适用)&lt;/li>
&lt;li>致谢 (Acknowledgements) (如果适用)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-5.png"
width="1274"
height="848"
srcset="https://www.zata.cc/p/github-release/images/index/index-5_hu13660114728688346705.png 480w, https://www.zata.cc/p/github-release/images/index/index-5_hu15621805022908629753.png 1024w"
loading="lazy"
alt="标题和描述"
class="gallery-image"
data-flex-grow="150"
data-flex-basis="360px"
>&lt;/p>
&lt;p>&lt;strong>步骤 6：自动生成 Release Notes (可选)&lt;/strong>&lt;/p>
&lt;p>GitHub 可以根据最近的提交历史自动生成 Release Notes。在描述框上方，点击 &amp;ldquo;&lt;strong>Generate release notes&lt;/strong>&amp;quot;（生成发行说明）按钮。你可以基于生成的笔记进行修改和完善。&lt;/p>
&lt;p>&lt;strong>步骤 7：上传二进制文件 (可选)&lt;/strong> 这只能上传25M，Github CLi可以2G&lt;/p>
&lt;p>如果你的 Release 包含编译好的可执行文件、安装包或其他二进制文件，可以在 &amp;ldquo;&lt;strong>Attach binaries by dropping them here or selecting them&lt;/strong>&amp;quot;（通过将二进制文件拖放到此处或选择它们来附加）区域上传。直接将文件拖放到该区域，或者点击选择文件。&lt;/p>
&lt;p>&lt;strong>步骤 8：选择 Release 类型 (可选)&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>This is a pre-release (这是一个预发行版):&lt;/strong> 如果这个 Release 是一个测试版本、Beta 版本或 RC (Release Candidate) 版本，勾选此选项。预发行版会在 Release 列表中有特殊的标记。&lt;/li>
&lt;li>&lt;strong>Set as the latest release (设置为最新发行版):&lt;/strong> 默认情况下，新发布的 Release 会被标记为最新发行版。如果你发布的是一个旧版本或一个特殊的 Release，可以取消勾选此选项。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>步骤 9：创建讨论 (可选)&lt;/strong>&lt;/p>
&lt;p>如果你的仓库开启了 GitHub Discussions 功能，你可以选择为这个 Release 创建一个讨论。勾选 &amp;ldquo;&lt;strong>Create a discussion for this release&lt;/strong>&amp;quot;（为此发行版创建讨论），并选择一个讨论分类。&lt;/p>
&lt;p>&lt;strong>步骤 10：发布 Release&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Publish release (发布发行版):&lt;/strong> 如果你准备好将 Release 公开，点击此按钮。&lt;/li>
&lt;li>&lt;strong>Save draft (保存草稿):&lt;/strong> 如果你还没有准备好发布，想稍后再完善，点击此按钮将 Release 保存为草稿。草稿状态的 Release 只有仓库的协作者可见。&lt;/li>
&lt;/ul>
&lt;p>发布后，你的 Release 将会在仓库的 Releases 页面列出，并且与你关联的 Tag 也会被创建（如果你创建了新 Tag）。&lt;/p>
&lt;h3 id="方法二通过-git-命令行和-github-cli-创建-release">方法二：通过 Git 命令行和 GitHub CLI 创建 Release
&lt;/h3>&lt;p>对于习惯使用命令行或需要自动化发布流程的用户，可以使用 Git 命令创建 Tag，然后结合 GitHub CLI 来创建 Release。&lt;/p>
&lt;p>&lt;strong>前提条件：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>安装并配置好 Git。&lt;/li>
&lt;li>安装并认证 GitHub CLI (&lt;a class="link" href="https://cli.github.com/" target="_blank" rel="noopener"
>https://cli.github.com/&lt;/a>)。&lt;/li>
&lt;/ul>
&lt;p>CLI可以看：&lt;a class="link" href="https://zhuanlan.zhihu.com/p/601200139" target="_blank" rel="noopener"
>GitHub CLI 命令行工具（gh)&lt;/a>&lt;/p>
&lt;p>&lt;strong>步骤 1：在本地仓库创建并推送 Tag&lt;/strong>&lt;/p>
&lt;p>首先，在你的本地 Git 仓库中创建并推送一个 Tag。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 切换到你想要打 Tag 的 commit (可选，如果不是最新的 commit)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git checkout &amp;lt;commit_hash&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个轻量级 Tag (不推荐用于 Release)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git tag &amp;lt;tag_name&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个附注 Tag (推荐用于 Release，可以包含说明信息)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git tag -a &amp;lt;tag_name&amp;gt; -m &lt;span class="s2">&amp;#34;发行说明&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 推送 Tag 到远程仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin &amp;lt;tag_name&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git tag -a v1.0.0 -m &lt;span class="s2">&amp;#34;Initial release&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin v1.0.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-6.png"
width="831"
height="414"
srcset="https://www.zata.cc/p/github-release/images/index/index-6_hu9834238509754044153.png 480w, https://www.zata.cc/p/github-release/images/index/index-6_hu8984073741517947065.png 1024w"
loading="lazy"
alt="设置tag"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="481px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-7.png"
width="505"
height="137"
srcset="https://www.zata.cc/p/github-release/images/index/index-7_hu15857070982743059655.png 480w, https://www.zata.cc/p/github-release/images/index/index-7_hu11037699988295714117.png 1024w"
loading="lazy"
alt="推送tag到远程"
class="gallery-image"
data-flex-grow="368"
data-flex-basis="884px"
>&lt;/p>
&lt;p>如果想要删除tag，使用下面的命令&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除本地tag&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git tag -d v1.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除远程tag&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin --delete v1.0.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>步骤 2：使用 GitHub CLI 创建 Release&lt;/strong>&lt;/p>
&lt;p>在 Tag 推送到 GitHub 后，你可以使用 GitHub CLI 命令来创建 Release。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-8.png"
width="1113"
height="439"
srcset="https://www.zata.cc/p/github-release/images/index/index-8_hu12451502603745015809.png 480w, https://www.zata.cc/p/github-release/images/index/index-8_hu1769589692897246825.png 1024w"
loading="lazy"
alt="登录和推送"
class="gallery-image"
data-flex-grow="253"
data-flex-basis="608px"
>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh release create &amp;lt;tag_name&amp;gt; &lt;span class="o">[&lt;/span>flags&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一些常用的 flag：&lt;/p>
&lt;ul>
&lt;li>&lt;code>-t, --title &amp;lt;string&amp;gt;&lt;/code>: 设置 Release 的标题。&lt;/li>
&lt;li>&lt;code>-n, --notes &amp;lt;string&amp;gt;&lt;/code>: 设置 Release 的描述/发行说明。&lt;/li>
&lt;li>&lt;code>-F, --notes-file &amp;lt;file&amp;gt;&lt;/code>: 从文件中读取 Release 的描述/发行说明。&lt;/li>
&lt;li>&lt;code>-d, --draft&lt;/code>: 将 Release 创建为草稿。&lt;/li>
&lt;li>&lt;code>-p, --prerelease&lt;/code>: 将 Release 标记为预发行版。&lt;/li>
&lt;li>&lt;code>--latest=false&lt;/code>: 不将此 Release 设置为最新发行版。&lt;/li>
&lt;li>&lt;code>&amp;lt;filename&amp;gt; | &amp;lt;pattern&amp;gt;...&lt;/code>: 指定要上传到 Release 的资产文件。&lt;/li>
&lt;/ul>
&lt;p>例如，创建一个包含描述并上传一个文件的 Release：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh release create v1.0.0 --title &lt;span class="s2">&amp;#34;Version 1.0.0&amp;#34;&lt;/span> --notes &lt;span class="s2">&amp;#34;This is the first release.&amp;#34;&lt;/span> my_app.zip
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你的发行说明比较长，可以将其写入一个文件（例如 &lt;code>release_notes.md&lt;/code>），然后使用 &lt;code>-F&lt;/code> 参数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh release create v1.0.0 --title &lt;span class="s2">&amp;#34;Version 1.0.0&amp;#34;&lt;/span> -F release_notes.md my_app.zip
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/github-release/images/index/index-9.png"
width="1113"
height="116"
srcset="https://www.zata.cc/p/github-release/images/index/index-9_hu8529872772572740933.png 480w, https://www.zata.cc/p/github-release/images/index/index-9_hu5092180901456231452.png 1024w"
loading="lazy"
alt="推送"
class="gallery-image"
data-flex-grow="959"
data-flex-basis="2302px"
>&lt;/p>
&lt;p>如果你只创建 Tag，然后想基于该 Tag 在 GitHub 上起草 Release，也可以直接去网页界面操作，选择刚刚推送的 Tag。但是网页只能上传25MB的附件&lt;/p>
&lt;p>&lt;strong>通过命令行编辑和删除 Release：&lt;/strong>&lt;/p>
&lt;p>GitHub CLI 也提供了编辑和删除 Release 的命令：&lt;/p>
&lt;ul>
&lt;li>编辑 Release：&lt;code>gh release edit &amp;lt;tag_name&amp;gt; [flags]&lt;/code>&lt;/li>
&lt;li>删除 Release：&lt;code>gh release delete &amp;lt;tag_name&amp;gt;&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="总结">总结
&lt;/h3>&lt;p>创建 GitHub Release 是项目版本管理和软件分发的重要环节。你可以选择使用直观的网页界面进行操作，也可以通过命令行工具实现更灵活和自动化的流程。无论哪种方式，核心都是为代码库的特定状态（通过 Git Tag 标记）创建一个带有详细说明和可选附件的版本发布。&lt;/p>
&lt;p>希望这个详细教程对你有帮助！&lt;/p></description></item><item><title>git-submodule-子模块</title><link>https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/</link><pubDate>Tue, 29 Apr 2025 11:10:22 +0800</pubDate><guid>https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/</guid><description>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/index.png" alt="Featured image of post git-submodule-子模块" />&lt;p>这是一份关于 Git Submodule（子模块）的详细教程，包括概念、使用场景、常用命令以及详细的可执行代码示例。&lt;/p>
&lt;p>Git Submodule 允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。这在你有一个主项目依赖于其他独立开发的库或项目时非常有用。子模块会固定在你指定的一个特定的提交（commit）上，而不是分支的最新状态。&lt;/p>
&lt;h2 id="知识点总结">知识点总结
&lt;/h2>&lt;h3 id="git-submodule-记录的是子模块仓库中一个特定的提交">Git Submodule 记录的是子模块仓库中一个特定的提交
&lt;/h3>&lt;p>Git Submodule 的一个核心概念是：主仓库记录的是子模块仓库中一个特定的提交（commit），而不是跟踪子模块的某个分支的最新状态。&lt;/p>
&lt;p>这意味着当你使用 git submodule add &amp;lt;远程仓库URL&amp;gt; &lt;path> 命令时：&lt;/p>
&lt;p>Git 会克隆远程子模块仓库到指定的路径。
默认情况下，Git 会检出子模块仓库的 默认分支（通常是 main 或 master）。
最重要的一点： 主仓库会记录下子模块仓库在 那个默认分支上当前的最新提交的 SHA-1 值。&lt;/p>
&lt;p>为什么是这样？&lt;/p>
&lt;p>这种设计是为了保证主项目的稳定性。主项目依赖于子模块的一个确定版本。如果主项目仅仅跟踪子模块的某个分支（比如 main），那么子模块的任何新提交都可能在你不经意间破坏主项目的功能。通过锁定到特定的提交，你可以精确控制主项目使用的子模块代码版本，确保每次克隆或更新主项目时，子模块都处于预期的状态。&lt;/p>
&lt;h3 id="如何指定添加子模块时使用的分支或标签">如何指定添加子模块时使用的分支或标签？
&lt;/h3>&lt;p>如果你不想使用子模块仓库的默认分支，可以在 git submodule add 命令中使用 -b 选项指定一个特定的分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Bash" data-lang="Bash">&lt;span class="line">&lt;span class="cl">git submodule add -b &amp;lt;分支名&amp;gt; &amp;lt;远程仓库URL&amp;gt; &amp;lt;子模块在主仓库中的路径&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>示例：指定 develop 分支&lt;/p>
&lt;p>假设你想添加子模块，并使用其 develop 分支的当前提交：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Bash" data-lang="Bash">&lt;span class="line">&lt;span class="cl">git submodule add -b develop https://github.com/your-username/your-submodule-repo.git submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，Git 会克隆子模块仓库，检出 develop 分支，然后主仓库记录 develop 分支当前的最新提交。&lt;/p>
&lt;p>你也可以指定一个标签（tag）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Bash" data-lang="Bash">&lt;span class="line">&lt;span class="cl">git submodule add -b &amp;lt;标签名&amp;gt; &amp;lt;远程仓库URL&amp;gt; &amp;lt;子模块在主仓库中的路径&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="更新子模块时如何处理分支">更新子模块时如何处理分支？
&lt;/h3>&lt;p>当你运行 git submodule update &amp;ndash;remote 命令来更新子模块时：&lt;/p>
&lt;p>Git 会进入子模块目录。&lt;/p>
&lt;p>它会拉取子模块远程仓库的最新提交。&lt;/p>
&lt;p>默认情况下，它会根据 .gitmodules 文件中记录的信息（如果添加时指定了 -b 分支，则使用该分支；否则使用远程仓库的默认分支）找到该分支的最新提交。&lt;/p>
&lt;p>然后，它会将子模块的工作树更新到该分支的最新提交，并更新主仓库对子模块的引用到这个新的提交。&lt;/p>
&lt;h3 id="为什么使用-git-submodule">&lt;strong>为什么使用 Git Submodule？&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>管理外部依赖：&lt;/strong> 当你的项目依赖于第三方库或其他内部独立项目时，可以使用子模块来包含它们，而不是直接复制代码。&lt;/li>
&lt;li>&lt;strong>分离关注点：&lt;/strong> 将大型项目拆分成更小的、可管理的独立仓库。&lt;/li>
&lt;li>&lt;strong>版本控制依赖：&lt;/strong> 精确控制主项目使用的外部依赖的版本（通过指定特定的提交）。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Git Submodule 的一些注意事项：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>复杂性增加：&lt;/strong> 使用子模块会增加工作流程的复杂性，特别是对于不熟悉子模块的团队成员。&lt;/li>
&lt;li>&lt;strong>更新子模块：&lt;/strong> 子模块不会随主项目自动更新到最新版本，需要手动更新。&lt;/li>
&lt;li>&lt;strong>克隆的特殊性：&lt;/strong> 克隆包含子模块的项目需要额外的步骤。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="具体教程">具体教程
&lt;/h2>&lt;h3 id="准备工作创建示例仓库">&lt;strong>准备工作：创建示例仓库&lt;/strong>
&lt;/h3>&lt;p>为了演示，我们需要创建两个 Git 仓库：一个作为主仓库（parent），另一个作为子模块仓库（submodule）。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>创建子模块仓库 (submodule_repo):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个目录作为子模块仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir submodule_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> submodule_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 初始化 Git 仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个示例文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;This is the submodule content.&amp;#34;&lt;/span> &amp;gt; submodule_file.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 添加并提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Initial commit for submodule&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (可选) 如果你使用 GitHub 或 Gitee，可以创建一个远程仓库并关联&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git remote add origin &amp;lt;你的远程仓库地址&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git push -u origin master&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>创建主仓库 (parent_repo):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 回到上一级目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ..
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个目录作为主仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir parent_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> parent_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 初始化 Git 仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个示例文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;This is the parent content.&amp;#34;&lt;/span> &amp;gt; parent_file.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 添加并提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Initial commit for parent&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (可选) 如果你使用 GitHub 或 Gitee，可以创建一个远程仓库并关联&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git remote add origin &amp;lt;你的远程仓库地址&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git push -u origin master&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>现在我们有了两个本地 Git 仓库：&lt;code>parent_repo&lt;/code> 和 &lt;code>submodule_repo&lt;/code>。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-1.png"
width="1024"
height="768"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-1_hu13857130514537436686.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-1_hu11638244521051072211.png 1024w"
loading="lazy"
alt="项目结构"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-2.png"
width="1024"
height="768"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-2_hu6489308407506496138.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-2_hu17428376519034220305.png 1024w"
loading="lazy"
alt="两个子模块在vscode中显示"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/p>
&lt;hr>
&lt;h3 id="1-添加子模块">&lt;strong>1. 添加子模块&lt;/strong>
&lt;/h3>&lt;p>在主仓库 &lt;code>parent_repo&lt;/code> 中，我们将 &lt;code>submodule_repo&lt;/code> 添加为一个子模块。&lt;/p>
&lt;p>打开终端，进入 &lt;code>parent_repo&lt;/code> 目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> parent_repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用 &lt;code>git submodule add&lt;/code> 命令添加子模块。语法是：&lt;code>git submodule add &amp;lt;子模块仓库地址&amp;gt; &amp;lt;子模块在主仓库中的路径&amp;gt;&lt;/code>&lt;/p>
&lt;p>如果你的子模块仓库是本地的，可以使用相对或绝对路径。如果是远程仓库（如 GitHub），使用其 URL。&lt;/p>
&lt;p>&lt;strong>示例：添加本地子模块&lt;/strong>&lt;/p>
&lt;p>因为 Git 的安全设置默认禁止通过 file:// 协议（也就是直接的本地文件路径）进行克隆，这包括添加本地子模块。&lt;/p>
&lt;p>这是 Git 为了防止某些安全风险而设置的。当你使用相对路径 ../submodule_repo 时，Git 内部会将其解析为 file:// 协议来尝试克隆。&lt;/p>
&lt;p>因此可以通过配置全局允许file协议解决&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 任何目录下执行，写入配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global protocol.file.allow always
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">## 如果想取消&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --unset protocol.file.allow
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>假设 &lt;code>submodule_repo&lt;/code> 目录与 &lt;code>parent_repo&lt;/code> 目录在同一级。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在主仓库目录，添加子模块&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git submodule add ../submodule_repo submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>../submodule_repo&lt;/code>: 子模块仓库的路径。&lt;/li>
&lt;li>&lt;code>submodule_directory&lt;/code>: 子模块在 &lt;code>parent_repo&lt;/code> 中的存放路径。&lt;/li>
&lt;/ul>
&lt;p>执行此命令后，你会看到类似以下的输出：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Cloning into &amp;#39;submodule_directory&amp;#39;...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">done.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-3.png"
width="595"
height="289"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-3_hu15841329082808505276.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-3_hu12847708670293884494.png 1024w"
loading="lazy"
alt="配置全局允许 file 协议 并添加子模块："
class="gallery-image"
data-flex-grow="205"
data-flex-basis="494px"
>&lt;/p>
&lt;p>&lt;strong>查看变化：&lt;/strong>&lt;/p>
&lt;p>运行 &lt;code>git status&lt;/code>，你会发现有两项变化：&lt;/p>
&lt;ul>
&lt;li>&lt;code>.gitmodules&lt;/code> 文件被创建。&lt;/li>
&lt;li>&lt;code>submodule_directory&lt;/code> 目录被添加。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出会显示：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">On branch master
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Changes to be committed:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (use &amp;#34;git reset HEAD &amp;lt;file&amp;gt;...&amp;#34; to unstage)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> new file: .gitmodules
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> new file: submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-4.png"
width="496"
height="122"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-4_hu1604706381832271288.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-4_hu2890236446369376098.png 1024w"
loading="lazy"
alt="status"
class="gallery-image"
data-flex-grow="406"
data-flex-basis="975px"
>&lt;/p>
&lt;p>&lt;code>.gitmodules&lt;/code> 文件记录了子模块的信息，包括路径和 URL。查看其内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat .gitmodules
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出类似：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[submodule &amp;#34;submodule_directory&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> path = submodule_directory
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> url = ../submodule_repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>submodule_directory&lt;/code> 目录现在是 &lt;code>submodule_repo&lt;/code> 的一个工作副本，但它处于一个特殊的“游离 HEAD”（detached HEAD）状态，指向添加子模块时 &lt;code>submodule_repo&lt;/code> 的最新提交。&lt;/p>
&lt;p>&lt;strong>提交子模块的添加：&lt;/strong>&lt;/p>
&lt;p>将 &lt;code>.gitmodules&lt;/code> 文件和子模块目录的添加提交到主仓库：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add .gitmodules submodule_directory
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Add submodule_repo as a submodule&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在，主仓库已经记录了子模块的存在以及它指向的特定提交。&lt;/p>
&lt;hr>
&lt;h3 id="2-克隆包含子模块的仓库">&lt;strong>2. 克隆包含子模块的仓库&lt;/strong>
&lt;/h3>&lt;p>当其他人克隆包含子模块的仓库时，默认情况下子模块目录是空的。需要额外的步骤来初始化和更新子模块。&lt;/p>
&lt;p>假设你要克隆上面创建的 &lt;code>parent_repo&lt;/code>。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>正常克隆主仓库：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 回到上一级目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ..
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 克隆主仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git clone parent_repo cloned_parent_repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>进入克隆后的 &lt;code>cloned_parent_repo&lt;/code> 目录，你会发现 &lt;code>submodule_directory&lt;/code> 目录存在，但里面是空的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> cloned_parent_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ls submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（没有输出或显示目录为空）&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-5.png"
width="1024"
height="768"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-5_hu2194991093576667738.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-5_hu2370671528723505282.png 1024w"
loading="lazy"
alt="克隆主仓库后"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/p>
&lt;ol start="2">
&lt;li>
&lt;p>&lt;strong>初始化并更新子模块：&lt;/strong>&lt;/p>
&lt;p>需要运行以下命令来初始化子模块配置并拉取子模块的代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git submodule init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git submodule update
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-6.png"
width="1024"
height="768"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-6_hu6123268521155332540.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-6_hu2777898186863299044.png 1024w"
loading="lazy"
alt="更新子模块"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/p>
&lt;ul>
&lt;li>&lt;code>git submodule init&lt;/code>: 这个命令会读取 &lt;code>.gitmodules&lt;/code> 文件，并将子模块的信息添加到主仓库的 &lt;code>.git/config&lt;/code> 文件中。&lt;/li>
&lt;li>&lt;code>git submodule update&lt;/code>: 这个命令会克隆子模块仓库到指定的路径，并检出 &lt;code>.gitmodules&lt;/code> 文件中记录的特定提交。&lt;/li>
&lt;/ul>
&lt;p>运行 &lt;code>git submodule update&lt;/code> 后，&lt;code>submodule_directory&lt;/code> 目录就会包含子模块的代码了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ls submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">submodule_file.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>更便捷的克隆方式：&lt;/strong>&lt;/p>
&lt;p>可以在克隆主仓库时直接使用 &lt;code>--recurse-submodules&lt;/code> 选项，一步完成克隆、初始化和更新子模块：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 回到上一级目录并删除之前克隆的仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ..
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rm -rf cloned_parent_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 使用 --recurse-submodules 选项克隆&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git clone --recurse-submodules parent_repo cloned_parent_repo_recursive
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 进入目录并查看子模块内容&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> cloned_parent_repo_recursive
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ls submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">submodule_file.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-7.png"
width="1024"
height="768"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-7_hu13062220883314404303.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-7_hu1665260754452644913.png 1024w"
loading="lazy"
alt="更便捷的克隆"
class="gallery-image"
data-flex-grow="133"
data-flex-basis="320px"
>&lt;/p>
&lt;p>这种方式更推荐，因为它简化了克隆包含子模块的项目流程。&lt;/p>
&lt;hr>
&lt;h3 id="3-更新子模块">&lt;strong>3. 更新子模块&lt;/strong>
&lt;/h3>&lt;p>子模块固定在主仓库记录的特定提交上。如果子模块仓库有了新的提交，你需要手动更新主仓库中对子模块的引用。&lt;/p>
&lt;p>假设 &lt;code>submodule_repo&lt;/code> 有了新的提交。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>在子模块仓库中进行新的提交：&lt;/strong>&lt;/p>
&lt;p>进入 &lt;code>submodule_repo&lt;/code> 目录（或者克隆后的 &lt;code>cloned_parent_repo/submodule_directory&lt;/code> 目录），进行修改并提交：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 进入子模块目录 (如果你还在 parent_repo 或其克隆目录中)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> submodule_directory &lt;span class="c1"># 或者 cd ../submodule_repo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建或修改文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Adding new content to submodule.&amp;#34;&lt;/span> &amp;gt;&amp;gt; submodule_file.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 添加并提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Add new content to submodule&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (如果子模块有远程仓库) 推送到子模块的远程仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git push origin master&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>在主仓库中更新子模块引用：&lt;/strong>&lt;/p>
&lt;p>回到主仓库（&lt;code>parent_repo&lt;/code> 或其克隆目录）。此时，主仓库仍然指向子模块的旧提交。进入子模块目录之后运行 &lt;code>git status&lt;/code> 会显示子模块目录已被修改：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> .. &lt;span class="c1"># 回到主仓库目录 (parent_repo 或 cloned_parent_repo)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> parent_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> submodule_directory
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出类似：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">On branch master
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Changes not staged for commit:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (use &amp;#34;git add &amp;lt;file&amp;gt;...&amp;#34; to update what will be committed)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (use &amp;#34;git checkout -- &amp;lt;file&amp;gt;...&amp;#34; to discard changes in working directory)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> modified: submodule_directory (new commits)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">no changes added to commit (use &amp;#34;git add&amp;#34; and/or &amp;#34;git commit -a&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-8.png"
width="594"
height="273"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-8_hu3725513492893021617.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-8_hu8899786905777909923.png 1024w"
loading="lazy"
alt="只在子模块中显示更新"
class="gallery-image"
data-flex-grow="217"
data-flex-basis="522px"
>&lt;/p>
&lt;p>要将主仓库更新到子模块的最新提交，可以使用 &lt;code>git submodule update --remote&lt;/code> 命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git submodule update --remote
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-9.png"
width="589"
height="251"
srcset="https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-9_hu11465150142233467127.png 480w, https://www.zata.cc/p/git-submodule-%E5%AD%90%E6%A8%A1%E5%9D%97/images/index/image-9_hu14689825837717233839.png 1024w"
loading="lazy"
alt="拉取子模块"
class="gallery-image"
data-flex-grow="234"
data-flex-basis="563px"
>&lt;/p>
&lt;p>这个命令会进入每个子模块目录，拉取最新的代码，并更新主仓库中子模块的引用到最新的提交。&lt;/p>
&lt;p>&lt;strong>注意：&lt;/strong> &lt;code>git submodule update --remote&lt;/code> 默认会更新到子模块仓库当前分支的最新提交。如果你希望更新到特定分支，可以使用 &lt;code>--remote --merge &amp;lt;分支名&amp;gt;&lt;/code> 或 &lt;code>--remote --rebase &amp;lt;分支名&amp;gt;&lt;/code>。&lt;/p>
&lt;p>更新后，再次运行 &lt;code>git status&lt;/code>，你会看到子模块目录再次被标记为已修改，但这次是因为它的引用指向了新的提交：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出类似：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">On branch master
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Changes to be committed:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (use &amp;#34;git reset HEAD &amp;lt;file&amp;gt;...&amp;#34; to unstage)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> modified: submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>提交主仓库的更新：&lt;/strong>&lt;/p>
&lt;p>将主仓库中对子模块引用的更新进行提交：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add submodule_directory &lt;span class="c1"># 当然直接git add . 也可以&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Update submodule_directory to latest commit&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (如果主仓库有远程仓库) 推送到主仓库的远程仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git push origin master&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，主仓库就记录了子模块的最新状态。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="4-在子模块中工作">&lt;strong>4. 在子模块中工作&lt;/strong>
&lt;/h3>&lt;p>在子模块目录中，你可以像在任何独立的 Git 仓库中一样进行操作：切换分支、创建提交、推拉代码等。&lt;/p>
&lt;p>进入子模块目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在你就在 &lt;code>submodule_repo&lt;/code> 的工作目录中了。你可以进行正常的 Git 操作：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看当前分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 切换分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git checkout &amp;lt;分支名&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建新的提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (修改文件，git add，git commit)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 推送到子模块的远程仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git push origin &amp;lt;分支名&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>完成在子模块中的工作后，回到主仓库目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ..
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果子模块中有了新的提交，主仓库会检测到变化（如上面更新子模块的步骤所示），你需要再次在主仓库中提交对子模块引用的更新。&lt;/p>
&lt;hr>
&lt;h3 id="5-切换分支包含子模块">&lt;strong>5. 切换分支（包含子模块）&lt;/strong>
&lt;/h3>&lt;p>在包含子模块的主仓库中切换分支可能会有些复杂，因为不同分支可能引用子模块的不同提交，甚至在某些分支上没有子模块。&lt;/p>
&lt;p>当切换到一个新分支时，Git 会尝试根据新分支的 &lt;code>.gitmodules&lt;/code> 文件和主仓库记录的子模块提交信息，自动更新子模块。&lt;/p>
&lt;p>&lt;strong>示例：切换到新分支&lt;/strong>&lt;/p>
&lt;p>假设在 &lt;code>parent_repo&lt;/code> 中创建一个新分支 &lt;code>feature_branch&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout -b feature_branch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果在 &lt;code>feature_branch&lt;/code> 上对子模块进行了修改（例如，更新到新的提交），然后切换回 &lt;code>master&lt;/code> 分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout master
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Git 会尽力将子模块恢复到 &lt;code>master&lt;/code> 分支所指向的提交。如果出现问题（例如，子模块目录有未提交的修改），Git 可能会报错或将子模块目录标记为未追踪。&lt;/p>
&lt;p>为了避免潜在的问题，建议在切换主仓库分支之前，确保子模块目录是干净的（没有未提交的修改）。如果子模块中有修改需要保留，先在子模块内部进行提交。&lt;/p>
&lt;hr>
&lt;h3 id="6-删除子模块">&lt;strong>6. 删除子模块&lt;/strong>
&lt;/h3>&lt;p>删除子模块比添加子模块要麻烦一些，需要执行几个步骤来彻底移除子模块的记录和文件。&lt;/p>
&lt;p>假设我们要从 &lt;code>parent_repo&lt;/code> 中删除 &lt;code>submodule_directory&lt;/code> 子模块。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>取消初始化子模块：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git submodule deinit submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个命令会移除 &lt;code>.git/config&lt;/code> 文件中关于该子模块的配置。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>移除 &lt;code>.gitmodules&lt;/code> 文件中的对应条目：&lt;/strong>&lt;/p>
&lt;p>使用文本编辑器打开 &lt;code>.gitmodules&lt;/code> 文件，删除对应子模块的配置段落（例如，&lt;code>[submodule &amp;quot;submodule_directory&amp;quot;]&lt;/code> 开始的部分）。&lt;/p>
&lt;p>或者使用命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git config -f .gitmodules --remove-section submodule.submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>从暂存区移除子模块：&lt;/strong>&lt;/p>
&lt;p>子模块在 Git 中被记录为一个特殊的工作树条目。需要将其从暂存区移除：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rm --cached submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>手动删除子模块的工作目录和 &lt;code>.git/modules&lt;/code> 中的相关文件：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">rm -rf submodule_directory
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rm -rf .git/modules/submodule_directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>rm -rf submodule_directory&lt;/code>: 删除子模块在主仓库中的工作目录。&lt;/li>
&lt;li>&lt;code>rm -rf .git/modules/submodule_directory&lt;/code>: 删除 Git 在主仓库内部为该子模块存储的元数据和对象。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>提交删除操作：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add .gitmodules
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Remove submodule_directory submodule&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在，子模块就彻底从主仓库中删除了。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="总结常用命令">&lt;strong>总结常用命令&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;code>git submodule add &amp;lt;url&amp;gt; &amp;lt;path&amp;gt;&lt;/code>: 添加子模块。&lt;/li>
&lt;li>&lt;code>git submodule init&lt;/code>: 初始化本地的子模块配置。&lt;/li>
&lt;li>&lt;code>git submodule update&lt;/code>: 克隆子模块仓库并检出主仓库指定的提交。&lt;/li>
&lt;li>&lt;code>git clone --recurse-submodules &amp;lt;url&amp;gt;&lt;/code>: 克隆主仓库并递归地初始化和更新子模块。&lt;/li>
&lt;li>&lt;code>git submodule update --remote&lt;/code>: 将子模块更新到其远程仓库的最新提交，并更新主仓库的引用。&lt;/li>
&lt;li>&lt;code>git submodule deinit &amp;lt;path&amp;gt;&lt;/code>: 取消初始化子模块。&lt;/li>
&lt;li>&lt;code>git rm --cached &amp;lt;path&amp;gt;&lt;/code>: 从暂存区移除子模块。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="更高级的用法和注意事项">&lt;strong>更高级的用法和注意事项&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>指定分支或标签：&lt;/strong> 添加子模块时，可以使用 &lt;code>-b &amp;lt;分支名&amp;gt;&lt;/code> 或指定一个特定的提交 SHA1 来控制子模块的初始状态。&lt;/li>
&lt;li>&lt;strong>子模块的子模块：&lt;/strong> 如果子模块本身也包含子模块，在克隆和更新时需要使用 &lt;code>--recursive&lt;/code> 选项，例如 &lt;code>git clone --recursive&lt;/code> 和 &lt;code>git submodule update --recursive&lt;/code>。&lt;/li>
&lt;li>&lt;strong>游离 HEAD 状态：&lt;/strong> 在子模块目录中进行修改并提交后，子模块会处于游离 HEAD 状态。如果你想在子模块中固定跟踪某个分支，需要在子模块目录中 &lt;code>git checkout &amp;lt;分支名&amp;gt;&lt;/code>，然后在主仓库中提交对子模块引用的更新。&lt;/li>
&lt;li>&lt;strong>使用 .gitattributes 忽略子模块的某些文件：&lt;/strong> 可以在主仓库的 &lt;code>.gitattributes&lt;/code> 文件中配置，以便 Git 在某些操作（如 &lt;code>git status&lt;/code>）中忽略子模块目录中的未追踪或已修改文件。&lt;/li>
&lt;li>&lt;strong>替代方案：&lt;/strong> 对于简单的依赖管理，考虑使用包管理器（如 npm, yarn, pip, Composer, Maven, Gradle 等），它们通常比 Git Submodule 更易于管理依赖版本和更新。Git Submodule 更适合于包含独立的、需要独立开发和版本控制的子项目或库。&lt;/li>
&lt;/ul>
&lt;p>希望这份详细的教程能帮助你理解和使用 Git Submodule！如果在实践中遇到问题，可以根据具体的错误信息进行搜索或提问。&lt;/p></description></item><item><title>Docker使用实战-compose教程</title><link>https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E5%AE%9E%E6%88%98-compose%E6%95%99%E7%A8%8B/</link><pubDate>Tue, 18 Mar 2025 15:48:18 +0800</pubDate><guid>https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E5%AE%9E%E6%88%98-compose%E6%95%99%E7%A8%8B/</guid><description>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E5%AE%9E%E6%88%98-compose%E6%95%99%E7%A8%8B/images/index/index.png" alt="Featured image of post Docker使用实战-compose教程" />&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#--------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 通用/推荐的关闭方式 (任何系统)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#--------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 向 Redis 服务器发送关闭信号 (会安全保存数据)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 这是最推荐的关闭方式&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">redis-cli shutdown
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#--------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 直接运行 / Docker 容器内 (你当前的环境)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#--------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动 (前台运行，日志会占满终端)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">redis-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动 (使用配置文件，推荐)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">redis-server /path/to/redis.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动 (后台运行，就像你之前用的)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">redis-server &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 关闭 (在 Docker 或直接运行时，用 cli 关闭)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">redis-cli shutdown
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 强行关闭 (如果上面的命令无效)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 找到进程 PID&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ps aux &lt;span class="p">|&lt;/span> grep redis-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 杀死进程 (用 PID 替换 12345)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">kill&lt;/span> &lt;span class="m">12345&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#--------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Linux (使用 systemd, 如 Ubuntu, CentOS)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#--------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl start redis-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (服务名也可能是 redis)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># sudo systemctl start redis&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 停止服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl stop redis-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 重启服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl restart redis-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看服务状态&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl status redis-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 设置开机自启&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl &lt;span class="nb">enable&lt;/span> redis-server
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下面是一个详细的 Docker Compose 教程，涵盖了从基础概念到实际操作的全面内容。教程将逐步讲解 Docker Compose 的用途、安装、配置文件编写以及常见使用场景，帮助你快速上手。&lt;/p>
&lt;hr>
&lt;h2 id="docker-compose-起步教程">Docker Compose 起步教程
&lt;/h2>&lt;h3 id="什么是-docker-compose">什么是 Docker Compose？
&lt;/h3>&lt;p>Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。它通过一个 YAML 文件（通常是 &lt;code>docker-compose.yml&lt;/code>）来配置多个服务、网络和卷，然后使用单一命令启动和管理这些容器。相比手动运行 &lt;code>docker run&lt;/code>，它更适合需要协调多个容器的复杂应用。&lt;/p>
&lt;h4 id="适用场景">适用场景
&lt;/h4>&lt;ul>
&lt;li>开发环境：快速搭建包含前端、后端、数据库等的本地开发环境。&lt;/li>
&lt;li>测试：运行集成测试或模拟生产环境。&lt;/li>
&lt;li>小型部署：单机多容器应用的部署。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="安装-docker-compose">安装 Docker Compose
&lt;/h3>&lt;p>Docker Compose 有两种形式：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>旧版（docker-compose）&lt;/strong>：一个独立的 Python 工具。&lt;/li>
&lt;li>&lt;strong>新版（docker compose）&lt;/strong>：集成在 Docker CLI 中的插件，从 Docker 20.10 开始默认包含。&lt;/li>
&lt;/ol>
&lt;h4 id="检查是否已安装">检查是否已安装
&lt;/h4>&lt;p>运行以下命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>如果输出类似 &lt;code>Docker Compose version v2.x.x&lt;/code>，说明已安装新版。&lt;/li>
&lt;li>如果提示未找到命令，尝试 &lt;code>docker-compose --version&lt;/code> 检查旧版。&lt;/li>
&lt;/ul>
&lt;h4 id="安装如果需要">安装（如果需要）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>Linux / macOS / Windows（新版）&lt;/strong>：
如果你的 Docker 是最新版（20.10+），无需额外安装，直接用 &lt;code>docker compose&lt;/code>。&lt;/li>
&lt;li>&lt;strong>旧版安装（可选）&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo curl -L &lt;span class="s2">&amp;#34;https://github.com/docker/compose/releases/latest/download/docker-compose-&lt;/span>&lt;span class="k">$(&lt;/span>uname -s&lt;span class="k">)&lt;/span>&lt;span class="s2">-&lt;/span>&lt;span class="k">$(&lt;/span>uname -m&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -o /usr/local/bin/docker-compose
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod +x /usr/local/bin/docker-compose
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h4 id="前提条件">前提条件
&lt;/h4>&lt;ul>
&lt;li>已安装 Docker Engine（运行 &lt;code>docker --version&lt;/code> 确认）。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="基本概念">基本概念
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>服务（Services）&lt;/strong>：一个服务对应一个容器，可以基于镜像启动，定义端口、卷等。&lt;/li>
&lt;li>&lt;strong>网络（Networks）&lt;/strong>：Compose 默认创建网络，所有服务可以互相通信。&lt;/li>
&lt;li>&lt;strong>卷（Volumes）&lt;/strong>：用于持久化数据，防止容器删除后数据丢失。&lt;/li>
&lt;li>&lt;strong>&lt;code>docker-compose.yml&lt;/code>&lt;/strong>：核心配置文件，描述整个应用。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="创建第一个-docker-compose-文件">创建第一个 Docker Compose 文件
&lt;/h3>&lt;p>假设我们要搭建一个简单的 Web 应用，包括：&lt;/p>
&lt;ul>
&lt;li>一个 Nginx Web 服务器。&lt;/li>
&lt;li>一个 Redis 数据库。&lt;/li>
&lt;/ul>
&lt;h4 id="1-创建项目目录">1. 创建项目目录
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir my-compose-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> my-compose-app
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="2-编写-docker-composeyml">2. 编写 &lt;code>docker-compose.yml&lt;/code>
&lt;/h4>&lt;p>创建一个名为 &lt;code>docker-compose.yml&lt;/code> 的文件，内容如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用最新稳定的版本&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx:latest &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用官方 Nginx 镜像&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8080:80&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 主机端口 8080 映射到容器端口 80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./html:/usr/share/nginx/html &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 挂载本地 html 文件夹&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">redis:latest &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用官方 Redis 镜像&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;6379:6379&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 映射 Redis 默认端口&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">redis-data:/data&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用命名卷存储数据&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 定义一个命名卷&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="文件解析">文件解析
&lt;/h4>&lt;ul>
&lt;li>&lt;code>version: '3.8'&lt;/code>：指定 Compose 文件格式版本。&lt;/li>
&lt;li>&lt;code>services&lt;/code>：定义两个服务：
&lt;ul>
&lt;li>&lt;code>web&lt;/code>：运行 Nginx，映射端口 8080:80，挂载本地 &lt;code>./html&lt;/code> 文件夹。&lt;/li>
&lt;li>&lt;code>redis&lt;/code>：运行 Redis，映射端口 6379，数据存储在命名卷 &lt;code>redis-data&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;code>volumes&lt;/code>：定义一个命名卷 &lt;code>redis-data&lt;/code>，用于持久化 Redis 数据。&lt;/li>
&lt;/ul>
&lt;h4 id="3-创建-html-文件可选">3. 创建 HTML 文件（可选）
&lt;/h4>&lt;p>在项目目录下创建 &lt;code>html&lt;/code> 文件夹，并添加一个简单的 &lt;code>index.html&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir html
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;h1&amp;gt;Hello, Docker Compose!&amp;lt;/h1&amp;gt;&amp;#34;&lt;/span> &amp;gt; html/index.html
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="4-启动应用">4. 启动应用
&lt;/h4>&lt;p>运行以下命令启动所有服务：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>-d&lt;/code> 表示后台运行。&lt;/li>
&lt;/ul>
&lt;h4 id="5-验证">5. 验证
&lt;/h4>&lt;ul>
&lt;li>检查运行中的容器：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>输出应显示 &lt;code>web&lt;/code> 和 &lt;code>redis&lt;/code> 服务状态为 &lt;code>Up&lt;/code>。&lt;/li>
&lt;li>访问 Nginx：打开浏览器，输入 &lt;code>http://localhost:8080&lt;/code>，应看到 &amp;ldquo;Hello, Docker Compose!&amp;quot;。&lt;/li>
&lt;li>检查 Redis：用 Redis 客户端连接 &lt;code>localhost:6379&lt;/code>。&lt;/li>
&lt;/ul>
&lt;h4 id="6-停止和清理">6. 停止和清理
&lt;/h4>&lt;p>停止服务并删除容器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose down
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果要删除卷数据：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose down -v
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="常用配置字段">常用配置字段
&lt;/h3>&lt;p>以下是 &lt;code>docker-compose.yml&lt;/code> 中常用的字段：&lt;/p>
&lt;h4 id="服务配置">服务配置
&lt;/h4>&lt;ul>
&lt;li>&lt;code>image&lt;/code>：指定镜像（如 &lt;code>nginx:latest&lt;/code>）。&lt;/li>
&lt;li>&lt;code>build&lt;/code>：构建镜像（替代 &lt;code>image&lt;/code>），指定 Dockerfile 路径：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">. &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 构建上下文路径&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dockerfile&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Dockerfile &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Dockerfile 文件名&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;code>ports&lt;/code>：端口映射，格式 &lt;code>&amp;quot;主机端口:容器端口&amp;quot;&lt;/code>。&lt;/li>
&lt;li>&lt;code>volumes&lt;/code>：卷挂载，支持本地路径或命名卷：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./data:/app/data &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 本地路径&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">my-volume:/data &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 命名卷&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;code>environment&lt;/code>：环境变量：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">DEBUG=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">PORT=8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;code>depends_on&lt;/code>：定义服务启动依赖：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">redis&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h4 id="全局配置">全局配置
&lt;/h4>&lt;ul>
&lt;li>&lt;code>networks&lt;/code>：自定义网络：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">my-network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">driver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;code>volumes&lt;/code>：定义命名卷：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">my-volume&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="进阶示例wordpress--mysql">进阶示例：WordPress + MySQL
&lt;/h3>&lt;p>以下是一个更复杂的例子，搭建 WordPress 和 MySQL：&lt;/p>
&lt;h4 id="docker-composeyml">&lt;code>docker-compose.yml&lt;/code>
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysql:8.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">db-data:/var/lib/mysql&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_ROOT_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">somewordpress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_DATABASE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wordpress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wordpress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wordpress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">unless-stopped&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">wordpress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wordpress:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8080:80&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">wp-data:/var/www/html&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WORDPRESS_DB_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WORDPRESS_DB_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wordpress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WORDPRESS_DB_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wordpress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WORDPRESS_DB_NAME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wordpress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">unless-stopped&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">wp-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="启动">启动
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>访问 &lt;code>http://localhost:8080&lt;/code>，即可看到 WordPress 安装页面。&lt;/p>
&lt;hr>
&lt;h3 id="常用命令">常用命令
&lt;/h3>&lt;ul>
&lt;li>启动服务：&lt;code>docker compose up -d&lt;/code>&lt;/li>
&lt;li>停止服务：&lt;code>docker compose stop&lt;/code>&lt;/li>
&lt;li>删除服务（保留卷）：&lt;code>docker compose down&lt;/code>&lt;/li>
&lt;li>查看日志：&lt;code>docker compose logs [服务名]&lt;/code>&lt;/li>
&lt;li>重启服务：&lt;code>docker compose restart&lt;/code>&lt;/li>
&lt;li>构建镜像：&lt;code>docker compose build&lt;/code>&lt;/li>
&lt;li>查看服务状态：&lt;code>docker compose ps&lt;/code>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="保存和迁移">保存和迁移
&lt;/h3>&lt;h4 id="保存镜像">保存镜像
&lt;/h4>&lt;ol>
&lt;li>提交容器为镜像：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker commit my-compose-app_web_1 mywebimage:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>导出镜像：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker save -o myimages.tar mywebimage:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h4 id="备份卷">备份卷
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run --rm -v my-compose-app_db-data:/data -v &lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>:/backup busybox tar cvf /backup/db-backup.tar /data
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="恢复">恢复
&lt;/h4>&lt;ol>
&lt;li>加载镜像：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker load -i myimages.tar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>恢复卷：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run --rm -v my-compose-app_db-data:/data -v &lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>:/backup busybox tar xvf /backup/db-backup.tar -C /data
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>启动：&lt;code>docker compose up -d&lt;/code>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="注意事项">注意事项
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>版本选择&lt;/strong>：根据 Docker Engine 版本选择合适的 &lt;code>version&lt;/code>（推荐 &lt;code>3.8&lt;/code> 或更高）。&lt;/li>
&lt;li>&lt;strong>命名冲突&lt;/strong>：多个 Compose 项目运行时，卷和网络名称可能冲突，可用 &lt;code>docker compose -p 项目名&lt;/code> 指定项目名。&lt;/li>
&lt;li>&lt;strong>最佳实践&lt;/strong>：尽量通过 &lt;code>build&lt;/code> 和 Dockerfile 定义镜像，而不是手动修改容器。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="dokploy--traefik-生产部署">Dokploy + Traefik 生产部署
&lt;/h2>&lt;h3 id="背景">背景
&lt;/h3>&lt;p>在 Dokploy 上使用 &lt;code>docker-compose.dokploy.yml&lt;/code> 部署应用后，页面访问返回 404。这个 404 通常不是前端 Nginx 的 SPA fallback 返回的，因为前端容器内的 &lt;code>/&lt;/code> 一般会 fallback 到 &lt;code>index.html&lt;/code>。&lt;/p>
&lt;p>更常见的原因是 Traefik 没有匹配到任何路由：&lt;/p>
&lt;ul>
&lt;li>&lt;code>DOMAIN&lt;/code> 没有被 Docker Compose 插值，生成了空的 &lt;code>Host(``)&lt;/code>。&lt;/li>
&lt;li>访问的域名不是 compose labels 里声明的 Host rule。&lt;/li>
&lt;li>容器没有接入 Dokploy 的 Traefik 网络。&lt;/li>
&lt;li>后端不健康，依赖后端健康状态的前端服务没有启动。&lt;/li>
&lt;/ul>
&lt;h3 id="dokploy-环境变量">Dokploy 环境变量
&lt;/h3>&lt;p>在 Dokploy 的 Compose Environment 中必须配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-env" data-lang="env">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">DOMAIN&lt;/span>&lt;span class="o">=&lt;/span>fsense.example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意：&lt;/p>
&lt;ul>
&lt;li>&lt;code>DOMAIN&lt;/code> 只写域名，不要写 &lt;code>http://&lt;/code>、&lt;code>https://&lt;/code> 或路径。&lt;/li>
&lt;li>本地 &lt;code>.env.dokploy&lt;/code> 不一定会被 Dokploy 自动加载。除非 Dokploy 的 compose 命令显式使用 &lt;code>--env-file .env.dokploy&lt;/code>，否则要把变量复制到 Dokploy UI 的 Environment 中。&lt;/li>
&lt;li>密钥、数据库地址、对象存储配置等也放在 Dokploy Environment，不要写入公开仓库。&lt;/li>
&lt;/ul>
&lt;h3 id="dns-记录">DNS 记录
&lt;/h3>&lt;p>如果 compose 使用三套前端域名，需要把这些域名都解析到 Dokploy 服务器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">fsense.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">admin.fsense.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">addin.fsense.example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>对应关系：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>域名&lt;/th>
&lt;th>服务&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>https://${DOMAIN}&lt;/code>&lt;/td>
&lt;td>&lt;code>outlook-index&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>https://admin.${DOMAIN}&lt;/code>&lt;/td>
&lt;td>&lt;code>admin-frontend&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>https://addin.${DOMAIN}&lt;/code>&lt;/td>
&lt;td>&lt;code>outlook-addin&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="traefik-labels-模板">Traefik labels 模板
&lt;/h3>&lt;p>主站前端示例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.docker.network=dokploy-network&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.middlewares.outlook-index-https-redirect.redirectscheme.scheme=https&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-index-web.rule=Host(`${DOMAIN:?Set DOMAIN in Dokploy environment}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-index-web.entrypoints=web&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-index-web.middlewares=outlook-index-https-redirect&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-index.rule=Host(`${DOMAIN:?Set DOMAIN in Dokploy environment}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-index.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-index.tls=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-index.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.outlook-index.loadbalancer.server.port=80&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>管理后台示例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.docker.network=dokploy-network&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.middlewares.admin-frontend-https-redirect.redirectscheme.scheme=https&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.admin-frontend-web.rule=Host(`admin.${DOMAIN:?Set DOMAIN in Dokploy environment}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.admin-frontend-web.entrypoints=web&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.admin-frontend-web.middlewares=admin-frontend-https-redirect&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.admin-frontend.rule=Host(`admin.${DOMAIN:?Set DOMAIN in Dokploy environment}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.admin-frontend.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.admin-frontend.tls=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.admin-frontend.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.admin-frontend.loadbalancer.server.port=80&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Outlook add-in 示例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">OUTLOOK_ADDIN_HOST_URL=https://addin.${DOMAIN:?Set DOMAIN in Dokploy environment}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.docker.network=dokploy-network&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.middlewares.outlook-addin-https-redirect.redirectscheme.scheme=https&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-addin-web.rule=Host(`addin.${DOMAIN:?Set DOMAIN in Dokploy environment}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-addin-web.entrypoints=web&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-addin-web.middlewares=outlook-addin-https-redirect&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-addin.rule=Host(`addin.${DOMAIN:?Set DOMAIN in Dokploy environment}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-addin.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-addin.tls=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.outlook-addin.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.outlook-addin.loadbalancer.server.port=80&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>关键点：&lt;/p>
&lt;ul>
&lt;li>&lt;code>${DOMAIN:?Set DOMAIN in Dokploy environment}&lt;/code> 可以让 compose 在变量缺失时直接失败，避免部署出空 Host rule。&lt;/li>
&lt;li>&lt;code>traefik.docker.network=dokploy-network&lt;/code> 明确告诉 Traefik 走 Dokploy 网络。&lt;/li>
&lt;li>&lt;code>web&lt;/code> 路由负责 HTTP 入口，并通过 middleware 跳到 HTTPS。&lt;/li>
&lt;li>&lt;code>websecure&lt;/code> 路由负责 HTTPS 入口和证书。&lt;/li>
&lt;/ul>
&lt;h3 id="网络配置">网络配置
&lt;/h3>&lt;p>服务需要接入 Dokploy 的外部网络：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dokploy-network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每个需要被 Traefik 或反向代理访问的服务都要加入该网络：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">dokploy-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果服务不需要公网访问，例如内部 backend，可以只 &lt;code>expose&lt;/code> 端口，不直接挂 Traefik label。&lt;/p>
&lt;h3 id="只重建后端导致前端-api-代理失效">只重建后端导致前端 API 代理失效
&lt;/h3>&lt;p>Dokploy / Docker Compose 部署时可能只重建发生变更的服务。例如后端代码变化后，只重建 &lt;code>backend&lt;/code>，而 &lt;code>frontend&lt;/code> 容器没有重启。&lt;/p>
&lt;p>这种情况下可能出现一个隐蔽问题：&lt;/p>
&lt;ul>
&lt;li>&lt;code>frontend&lt;/code> 里的 Nginx 启动时解析过 &lt;code>backend:8000&lt;/code>。&lt;/li>
&lt;li>后端容器重建后，Docker 内部 IP 发生变化。&lt;/li>
&lt;li>前端容器没有重启，Nginx 仍然可能持有旧的 upstream 解析结果。&lt;/li>
&lt;li>页面本身还能打开，但 &lt;code>/api/*&lt;/code> 代理到后端失败，看起来像&amp;quot;前端路由&amp;quot;或&amp;quot;接口路由&amp;quot;坏了。&lt;/li>
&lt;/ul>
&lt;p>推荐做法是让 Nginx 使用 Docker 内置 DNS，并定期重解析后端服务名：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resolver&lt;/span> &lt;span class="mi">127&lt;/span>&lt;span class="s">.0.0.11&lt;/span> &lt;span class="s">ipv6=off&lt;/span> &lt;span class="s">valid=5s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">location&lt;/span> &lt;span class="s">/api/&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">set&lt;/span> &lt;span class="nv">$backend_upstream&lt;/span> &lt;span class="n">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8000&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">rewrite&lt;/span> &lt;span class="s">^/api/?(.*)&lt;/span>$ &lt;span class="s">/&lt;/span>&lt;span class="nv">$1&lt;/span> &lt;span class="s">break&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">proxy_pass&lt;/span> &lt;span class="s">http://&lt;/span>&lt;span class="nv">$backend_upstream&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">proxy_http_version&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="s">.1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">proxy_set_header&lt;/span> &lt;span class="s">Host&lt;/span> &lt;span class="nv">$host&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">proxy_set_header&lt;/span> &lt;span class="s">X-Forwarded-For&lt;/span> &lt;span class="nv">$proxy_add_x_forwarded_for&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">proxy_set_header&lt;/span> &lt;span class="s">X-Forwarded-Proto&lt;/span> &lt;span class="nv">$scheme&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意：&lt;code>proxy_pass&lt;/code> 一旦使用变量，就不要再写成 &lt;code>proxy_pass http://$backend_upstream/;&lt;/code>。带变量的 &lt;code>proxy_pass&lt;/code> 不再沿用普通 &lt;code>proxy_pass http://backend:8000/;&lt;/code> 的 URI 替换语义，尾部 &lt;code>/&lt;/code> 很容易把请求错误地转发到根路径。&lt;/p>
&lt;p>这个问题和 Traefik Host rule 404 不是同一个层面：&lt;/p>
&lt;ul>
&lt;li>Traefik Host rule 错误：请求进不了前端容器，通常直接 404。&lt;/li>
&lt;li>Nginx backend DNS 过期：请求能进前端容器，但 &lt;code>/api/*&lt;/code> 代理后端失败。&lt;/li>
&lt;/ul>
&lt;h3 id="macos-docker-build-使用-clash-代理">macOS Docker build 使用 Clash 代理
&lt;/h3>&lt;p>在 macOS + Docker Desktop + Clash 环境里，&lt;code>docker compose build&lt;/code> 和宿主机终端不是同一个网络视角。宿主机里的 &lt;code>127.0.0.1&lt;/code> 是 Mac 自己，但 Docker build 容器里的 &lt;code>127.0.0.1&lt;/code> 是构建容器本身。&lt;/p>
&lt;p>一个典型现象是前端镜像构建时报平台原生依赖缺失，例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Error: Cannot find module &amp;#39;../lightningcss.linux-arm64-musl.node&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果日志里 &lt;code>RUN npm ci&lt;/code> 显示为 &lt;code>CACHED&lt;/code>，说明这次构建没有重新安装依赖。缓存层里可能缺了 Alpine / Linux / arm64 需要的 optional native package，例如 &lt;code>lightningcss-linux-arm64-musl&lt;/code>。这类问题看起来像 Node 包损坏，本质上经常是“旧 Docker build 缓存层 + 网络代理不稳定”共同导致的。&lt;/p>
&lt;p>先强制绕过缓存验证真实问题：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose build --no-cache kimi-ppt-frontend
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果错误变成 &lt;code>npm ERR! code ECONNRESET&lt;/code>，就说明重新拉包时 Docker build 到 npm registry 的链路不稳定。&lt;/p>
&lt;p>检查当前 shell 代理：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">printenv &lt;span class="p">|&lt;/span> grep -i proxy
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>检查 &lt;code>~/.zshrc&lt;/code> 是否有重复覆盖：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">grep -nE &lt;span class="s1">&amp;#39;proxy|Proxy|Clash|789[0-9]&amp;#39;&lt;/span> ~/.zshrc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>容易出现的错误配置是前面写了一个可用端口，后面又覆盖成另一个端口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">http_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://127.0.0.1:7897&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">https_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://127.0.0.1:7897&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ALL_PROXY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;socks5://127.0.0.1:7897&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 后面又覆盖了 http_proxy / https_proxy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">https_proxy&lt;/span>&lt;span class="o">=&lt;/span>http://127.0.0.1:7899 &lt;span class="nv">http_proxy&lt;/span>&lt;span class="o">=&lt;/span>http://127.0.0.1:7899
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>应该保留一套一致配置。普通宿主机命令可以继续使用 &lt;code>127.0.0.1&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">http_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://127.0.0.1:7897&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">https_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://127.0.0.1:7897&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">HTTP_PROXY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$http_proxy&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">HTTPS_PROXY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$https_proxy&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ALL_PROXY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;socks5://127.0.0.1:7897&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">all_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ALL_PROXY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">NO_PROXY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;127.0.0.1,localhost,::1,host.docker.internal,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">no_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$NO_PROXY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但 Docker build 里要把代理地址改成宿主机网关：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose build --no-cache &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --build-arg &lt;span class="nv">HTTP_PROXY&lt;/span>&lt;span class="o">=&lt;/span>http://host.docker.internal:7897 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --build-arg &lt;span class="nv">HTTPS_PROXY&lt;/span>&lt;span class="o">=&lt;/span>http://host.docker.internal:7897 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> kimi-ppt-frontend
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果前端镜像已经重新构建成功，再构建整套服务：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose build
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>验证入口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -I http://localhost/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://localhost:8000/health
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果 &lt;code>http://localhost/&lt;/code> 返回 &lt;code>200 OK&lt;/code>，&lt;code>/health&lt;/code> 返回类似 &lt;code>{&amp;quot;status&amp;quot;:&amp;quot;ok&amp;quot;}&lt;/code>，说明前端和后端容器都已经正常启动。&lt;/p>
&lt;p>注意：不要把完整 &lt;code>~/.zshrc&lt;/code> 粘贴到公开位置。很多人会把 API key、token 和代理配置都放在同一个 shell 配置文件里。&lt;/p>
&lt;h3 id="404-排查命令">404 排查命令
&lt;/h3>&lt;p>在服务器或 Dokploy 构建环境中检查最终插值结果：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose -f docker-compose.dokploy.yml config &lt;span class="p">|&lt;/span> grep &lt;span class="s1">&amp;#39;Host&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>正常情况下应该看到实际域名，例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Host(`fsense.example.com`)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host(`admin.fsense.example.com`)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host(`addin.fsense.example.com`)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>检查容器是否运行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker ps &lt;span class="p">|&lt;/span> grep app-
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>检查后端健康和启动日志：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker logs app-backend --tail&lt;span class="o">=&lt;/span>&lt;span class="m">100&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果后端不健康，依赖 &lt;code>condition: service_healthy&lt;/code> 的前端服务可能不会启动，Traefik 也就没有可用服务。&lt;/p>
&lt;h3 id="修复检查清单">修复检查清单
&lt;/h3>&lt;ul>
&lt;li>Dokploy Environment 中已设置 &lt;code>DOMAIN&lt;/code>，且值不包含协议和路径。&lt;/li>
&lt;li>DNS 已解析 &lt;code>${DOMAIN}&lt;/code>、&lt;code>admin.${DOMAIN}&lt;/code>、&lt;code>addin.${DOMAIN}&lt;/code>。&lt;/li>
&lt;li>compose labels 中有 &lt;code>traefik.docker.network=dokploy-network&lt;/code>。&lt;/li>
&lt;li>HTTPS router 使用 &lt;code>entrypoints=websecure&lt;/code>、&lt;code>tls=true&lt;/code>、&lt;code>tls.certresolver=letsencrypt&lt;/code>。&lt;/li>
&lt;li>HTTP router 使用 &lt;code>entrypoints=web&lt;/code> 并绑定 HTTPS redirect middleware。&lt;/li>
&lt;li>&lt;code>docker compose config&lt;/code> 输出的 Host rule 是实际域名，不是空字符串。&lt;/li>
&lt;li>&lt;code>backend&lt;/code> 健康检查通过，前端容器正常启动。&lt;/li>
&lt;li>如果只重建过 &lt;code>backend&lt;/code> 后 &lt;code>/api/*&lt;/code> 失效，检查前端 Nginx 是否使用 Docker DNS 运行时重解析。&lt;/li>
&lt;/ul>
&lt;hr></description></item><item><title>docker使用教程</title><link>https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</link><pubDate>Tue, 18 Mar 2025 09:57:06 +0800</pubDate><guid>https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</guid><description>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-10.png" alt="Featured image of post docker使用教程" />&lt;ul>
&lt;li>
&lt;p>&lt;a class="link" href="#docker%e5%ae%b9%e5%99%a8%e5%b8%b8%e7%94%a8%e5%91%bd%e4%bb%a4" >docker容器常用命令&lt;/a>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e9%95%9c%e5%83%8f-image-%e7%9b%b8%e5%85%b3%e5%91%bd%e4%bb%a4" >镜像 (Image) 相关命令&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%ae%b9%e5%99%a8-container-%e7%9b%b8%e5%85%b3%e5%91%bd%e4%bb%a4" >容器 (Container) 相关命令&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#docker-run-%e5%b8%b8%e7%94%a8%e9%80%89%e9%a1%b9" >&lt;code>docker run&lt;/code> 常用选项&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#docker-compose-%e7%9b%b8%e5%85%b3%e5%91%bd%e4%bb%a4" >Docker Compose 相关命令&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e7%b3%bb%e7%bb%9f%e4%b8%8e%e8%b5%84%e6%ba%90%e7%ae%a1%e7%90%86%e5%91%bd%e4%bb%a4" >系统与资源管理命令&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e7%ae%80%e5%8d%95%e5%bc%80%e5%a7%8b%e4%b8%80%e4%b8%aadocker%e9%95%9c%e5%83%8f%e7%9a%84%e4%bd%bf%e7%94%a8" >简单开始一个docker镜像的使用&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;a class="link" href="#docker%e4%bd%bf%e7%94%a8%e6%8a%80%e5%b7%a7" >Docker使用技巧&lt;/a>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#docker%e7%9a%84%e5%8d%b7%e6%8c%82%e8%bd%bd%e5%8a%9f%e8%83%bd" >docker的卷挂载功能&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%b0%86%e9%95%9c%e5%83%8f%e6%8e%a8%e9%80%81%e5%88%b0%e8%bf%9c%e7%a8%8b%e4%bb%93%e5%ba%93" >将镜像推送到远程仓库&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#docker-context-%e6%95%99%e7%a8%8b" >Docker Context 教程&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%9c%a8-windows-%e4%b8%8a%e5%ae%9e%e7%8e%b0-docker-in-docker-dind-%e7%bb%88%e6%9e%81%e6%95%99%e7%a8%8b" >在 Windows 上实现 Docker in Docker (DinD) 终极教程&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e4%bd%bf%e7%94%a8%e5%b7%b2%e6%9c%89%e5%ae%b9%e5%99%a8%e5%88%9b%e5%bb%ba%e9%95%9c%e5%83%8f" >使用已有容器创建镜像&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%bc%80%e5%90%af%e9%87%8d%e5%90%afssh%e6%9c%8d%e5%8a%a1" >开启/重启ssh服务&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#docker-%e6%96%87%e4%bb%b6%e4%bc%a0%e8%be%93" >docker 文件传输&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e4%bf%ae%e6%94%b9%e6%9c%8d%e5%8a%a1%e5%99%a8%e9%85%8d%e7%bd%ae%e5%85%81%e8%ae%b8%e9%80%9a%e8%bf%87%e6%ad%a4%e6%9c%8d%e5%8a%a1%e5%99%a8%e8%bf%9b%e8%a1%8cssh%e8%bd%ac%e5%8f%91" >修改服务器配置允许通过此服务器进行ssh转发&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#docker%e5%ae%b9%e5%99%a8%e6%b7%bb%e5%8a%a0%e5%af%b9%e5%a4%96%e6%98%a0%e5%b0%84%e7%ab%af%e5%8f%a3" >docker容器添加对外映射端口&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#docker%e8%ae%be%e7%bd%ae%e5%8f%af%e7%94%a8cpu%e6%a0%b8%e5%bf%83%e6%95%b0%e9%87%8f" >docker设置可用CPU核心数量&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%ae%b9%e5%99%a8%e7%ab%af%e5%8f%a3%e8%bd%ac%e5%8f%91%e9%97%ae%e9%a2%98%e6%8e%92%e6%9f%a5%e5%92%8c%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88" >容器端口转发问题排查和解决方案&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%b1%80%e5%9f%9f%e7%bd%91%e8%ae%bf%e9%97%aedocker%e5%ae%b9%e5%99%a8%e5%ae%8c%e6%95%b4%e6%8c%87%e5%8d%97" >局域网访问Docker容器完整指南&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#docker%e8%ae%be%e7%bd%ae%e5%8f%af%e7%94%a8cpu%e6%a0%b8%e5%bf%83%e6%95%b0%e9%87%8f" >docker设置可用CPU核心数量&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;a class="link" href="#docker%e5%ae%9e%e6%88%98" >docker实战&lt;/a>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#docker%e4%bd%bf%e7%94%a8%e5%ae%9e%e6%88%98-mysql%e5%92%8cubuntu%e5%ae%b9%e5%99%a8" >Docker使用实战-MySQL和Ubuntu容器&lt;/a>
&lt;ul>
&lt;li>&lt;a class="link" href="#1-%e5%ae%89%e8%a3%85docker-mysql%e5%ae%b9%e5%99%a8" >1. 安装docker mysql容器&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#2-%e5%ae%89%e8%a3%85ubuntu%e5%ae%b9%e5%99%a8%e5%b9%b6%e9%85%8d%e7%bd%ae" >2. 安装Ubuntu容器并配置&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a class="link" href="#docker%e4%bd%bf%e7%94%a8%e5%ae%9e%e6%88%98-%e5%ae%b9%e5%99%a8%e4%bf%9d%e5%ad%98%e5%92%8c%e8%bf%81%e7%a7%bb" >Docker使用实战-容器保存和迁移&lt;/a>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e6%96%b9%e6%b3%95%e4%b8%80%e5%b0%86%e5%ae%b9%e5%99%a8%e4%bf%9d%e5%ad%98%e4%b8%ba%e9%95%9c%e5%83%8f" >方法一：将容器保存为镜像&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e6%96%b9%e6%b3%95%e4%ba%8c%e7%9b%b4%e6%8e%a5%e5%af%bc%e5%87%ba%e5%ae%b9%e5%99%a8" >方法二：直接导出容器&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a class="link" href="#docker-%e6%9c%8d%e5%8a%a1%e9%83%a8%e7%bd%b2%e6%95%99%e7%a8%8b" >Docker 服务部署教程&lt;/a>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e7%8e%af%e5%a2%83%e5%87%86%e5%a4%87" >环境准备&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%ae%9e%e6%88%98%e6%bc%94%e7%bb%83%e9%83%a8%e7%bd%b2%e4%b8%80%e4%b8%aa-python-web-%e5%ba%94%e7%94%a8" >实战演练：部署一个 Python Web 应用&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e8%bf%9b%e9%98%b6%e4%bd%bf%e7%94%a8-docker-compose-%e7%bc%96%e6%8e%92%e6%9c%8d%e5%8a%a1" >进阶：使用 Docker Compose 编排服务&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a class="link" href="#docker%e6%9c%8d%e5%8a%a1%e9%83%a8%e7%bd%b2%e6%95%99%e7%a8%8b2-%e4%bd%bf%e7%94%a8uv%e6%9b%bf%e4%bb%a3pip" >Docker服务部署教程2-使用uv替代pip&lt;/a>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e4%bd%bf%e7%94%a8-uv-%e7%9a%84%e6%a0%b8%e5%bf%83%e6%94%b9%e5%8a%a8" >使用 UV 的核心改动&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e6%9b%b4%e6%96%b0%e5%90%8e%e7%9a%84-dockerfile-%e4%bd%bf%e7%94%a8-uv" >更新后的 &lt;code>Dockerfile&lt;/code> (使用 uv)&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a class="link" href="#docker%e5%ae%b9%e5%99%a8%e7%9a%84%e9%83%a8%e7%bd%b2%e6%95%99%e7%a8%8b3-%e5%b0%86%e5%ae%b9%e5%99%a8%e9%83%a8%e7%bd%b2%e5%88%b0%e6%9c%8d%e5%8a%a1%e5%99%a8%e4%b8%8a" >docker容器的部署教程3-将容器部署到服务器上&lt;/a>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e6%a0%b8%e5%bf%83%e6%b5%81%e7%a8%8b" >核心流程&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e7%ac%ac%e5%9b%9b%e6%ad%a5%e9%80%89%e6%8b%a9%e9%83%a8%e7%bd%b2%e6%96%b9%e6%a1%88-deploy" >第四步：选择部署方案 (Deploy)&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%a6%82%e4%bd%95%e9%80%89%e6%8b%a9" >如何选择？&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a class="link" href="#%e8%bf%90%e8%a1%8c%e5%ae%b9%e5%99%a8%e4%ba%86%e4%bd%86%e6%98%af%e5%b9%b6%e6%b2%a1%e6%9c%89%e7%94%9f%e6%95%88-%e9%80%9a%e8%bf%87%e6%9f%a5%e7%9c%8b%e6%97%a5%e5%bf%97%e8%a7%a3%e5%86%b3%e9%97%ae%e9%a2%98" >运行容器了但是并没有生效-通过查看日志解决问题&lt;/a>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e7%ac%ac%e4%b8%80%e6%ad%a5%e6%a3%80%e6%9f%a5%e5%ae%b9%e5%99%a8%e6%98%af%e5%90%a6%e6%ad%a3%e5%9c%a8%e8%bf%90%e8%a1%8c" >第一步：检查容器是否正在运行&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e7%ac%ac%e4%ba%8c%e6%ad%a5%e6%9f%a5%e7%9c%8b%e5%ae%b9%e5%99%a8%e6%97%a5%e5%bf%97" >第二步：查看容器日志&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e7%ac%ac%e4%b8%89%e6%ad%a5%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98%e5%92%8c%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88%e6%b1%87%e6%80%bb" >第三步：常见问题和解决方案汇总&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#from-werkzeugurls-import-url_quote-%e9%97%ae%e9%a2%98%e6%a0%b9%e6%ba%90" >from werkzeug.urls import url_quote 问题根源&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88" >解决方案&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a class="link" href="#wsl%e4%b8%ad%e9%95%9c%e5%83%8f%e7%bd%91%e7%bb%9c%e4%b8%8edocker%e7%bd%91%e7%bb%9c%e5%86%b2%e7%aa%81%e4%bd%bf%e7%94%a8netsh-winsock-reset%e4%bf%ae%e6%94%b9%e7%bd%91%e7%bb%9c%e8%ae%be%e7%bd%ae%e4%b9%8b%e5%90%8edocker%e8%bf%9e%e4%b8%8d%e4%b8%8a----%e8%a7%a3%e5%86%b3" >wsl中镜像网络与docker网络冲突，使用netsh winsock reset修改网络设置之后，docker连不上 &amp;ndash; 解决&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%85%b7%e6%9c%89SSL%e8%af%81%e4%b9%a6%e7%9a%84%e5%90%8e%e7%ab%af%e5%9c%a8docker%e7%8e%af%e5%a2%83%e4%b8%ad%e5%87%ba%e7%8e%b0%e9%97%ae%e9%a2%98" >具有SSL证书的后端在docker环境中出现问题&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h1 id="常用命令">常用命令
&lt;/h1>&lt;p>&lt;strong>docker 单机常用命令&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 列出所有正在运行的容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker ps &lt;span class="o">[&lt;/span>-a&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 停止并删除容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stop &amp;lt;容器ID或容器名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm &amp;lt;容器ID或容器名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除指定卷&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker volume rm &amp;lt;卷ID或卷名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看所有容器的统计信息, --no-stream 不实时更新&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stats &lt;span class="o">[&lt;/span>容器ID或容器名&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>--no-stream&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 清理所有停止的容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm &lt;span class="k">$(&lt;/span>docker ps -aq&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 清理所有悬空的镜像&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rmi &lt;span class="k">$(&lt;/span>docker images -qf &lt;span class="s2">&amp;#34;dangling=true&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 清理所有悬空的网络&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker network prune &lt;span class="k">$(&lt;/span>docker network ls -qf &lt;span class="s2">&amp;#34;dangling=true&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 清理所有悬空的卷&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker volume prune &lt;span class="k">$(&lt;/span>docker volume ls -qf &lt;span class="s2">&amp;#34;dangling=true&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将容器的当前状态保存为新的镜像&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker commit &amp;lt;my-container&amp;gt; &amp;lt;my-new-image&amp;gt; &lt;span class="o">[&lt;/span>:tag&lt;span class="o">]&lt;/span> &lt;span class="c1"># 如果tag不填就默认是latest&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>docker swarm 常用命令&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看容器日志&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker logs -f &amp;lt;容器ID或容器名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 初始化 Swarm 集群（管理节点）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm init --advertise-addr &amp;lt;IP地址&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 获取管理节点加入命令 &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm join-token manager
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 获取加入集群的命令（在工作节点执行）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm join-token worker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 加入 Swarm 集群&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm join --token &amp;lt;token&amp;gt; --advertise-addr &amp;lt;IP地址&amp;gt; --data-path-addr &amp;lt;IP地址&amp;gt; --listen-addr 0.0.0.0:2377 &amp;lt;管理节点IP&amp;gt;:2377
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 离开 Swarm 集群&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm leave
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 强制离开集群&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm leave --force
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 更新 Swarm 配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm update --param value
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看集群节点列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node ls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看节点详细信息&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node inspect &amp;lt;节点ID或名称&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 更新节点属性&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node update --availability active/pause/drain &amp;lt;节点ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 提升节点为管理节点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node promote &amp;lt;节点ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 降级管理节点为工作节点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node demote &amp;lt;节点ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除节点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker node rm &amp;lt;节点ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service create --name &amp;lt;服务名&amp;gt; --replicas &amp;lt;副本数&amp;gt; -p &amp;lt;端口&amp;gt; &amp;lt;镜像名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看服务列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service ls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看服务详细信息&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service inspect &amp;lt;服务名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看服务运行的任务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service ps &amp;lt;服务名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 扩展服务副本数&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service scale &amp;lt;服务名&amp;gt;&lt;span class="o">=&lt;/span>&amp;lt;副本数&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 更新服务配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service update --image &amp;lt;新镜像&amp;gt; &amp;lt;服务名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 更新服务端口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service update --publish-add &amp;lt;端口&amp;gt; &amp;lt;服务名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service rm &amp;lt;服务名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看服务日志&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker service logs &amp;lt;服务名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建覆盖网络&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker network create --driver overlay &amp;lt;网络名&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看网络列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker network ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>镜像 (Image) 相关命令&lt;/p>
&lt;p>镜像是创建容器的基础，包含了应用程序及其依赖的环境。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">命令&lt;/th>
&lt;th style="text-align: left">描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker images&lt;/code>&lt;/td>
&lt;td style="text-align: left">列出本地所有镜像。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker pull [镜像名]:[标签]&lt;/code>&lt;/td>
&lt;td style="text-align: left">从 Docker Hub 或其他镜像仓库拉取镜像 (例如: &lt;code>docker pull ubuntu:22.04&lt;/code>)。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker push [用户名]/[镜像名]:[标签]&lt;/code>&lt;/td>
&lt;td style="text-align: left">将本地镜像推送到 Docker Hub 或其他镜像仓库。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker build -t [镜像名]:[标签] .&lt;/code>&lt;/td>
&lt;td style="text-align: left">根据当前目录下的 Dockerfile 构建镜像 (例如: &lt;code>docker build -t my-app:1.0 .&lt;/code>)。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker rmi [镜像ID或镜像名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">删除一个或多个镜像 (例如: &lt;code>docker rmi ubuntu:22.04&lt;/code>)。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker tag [源镜像] [新镜像名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">为本地镜像添加一个新的标签 (例如: &lt;code>docker tag my-app:1.0 my-app:latest&lt;/code>)。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker history [镜像名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">查看镜像的构建历史。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker save -o [文件名.tar] [镜像名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">将镜像保存为一个 tar 归档文件。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker load -i [文件名.tar]&lt;/code>&lt;/td>
&lt;td style="text-align: left">从一个 tar 归档文件加载镜像。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker rmi $(docker images -qf &amp;quot;dangling=true&amp;quot;)&lt;/code>&lt;/td>
&lt;td style="text-align: left">删除所有悬空的（dangling）镜像。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>容器 (Container) 相关命令&lt;/p>
&lt;p>容器是镜像的运行实例，是真正运行应用程序的地方。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">命令&lt;/th>
&lt;th style="text-align: left">描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker run [选项] [镜像名] [命令]&lt;/code>&lt;/td>
&lt;td style="text-align: left">创建并启动一个新的容器。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker ps&lt;/code>&lt;/td>
&lt;td style="text-align: left">列出所有正在运行的容器。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker ps -a&lt;/code>&lt;/td>
&lt;td style="text-align: left">列出所有容器（包括已停止的）。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker start [容器ID或容器名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">启动一个或多个已停止的容器。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker stop [容器ID或容器名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">停止一个或多个正在运行的容器。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker restart [容器ID或容器名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">重启一个容器。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker rm [容器ID或容器名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">删除一个或多个容器。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker rm -f $(docker ps -aq)&lt;/code>&lt;/td>
&lt;td style="text-align: left">强制删除所有容器（无论运行中还是已停止）。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker logs [容器ID或容器名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">查看容器的日志输出 (&lt;code>-f&lt;/code> 选项可以持续跟踪日志)。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker exec -it [容器ID] [命令]&lt;/code>&lt;/td>
&lt;td style="text-align: left">在正在运行的容器中执行一个交互式命令 (例如: &lt;code>docker exec -it my-nginx /bin/bash&lt;/code>)。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker cp [本地路径] [容器ID]:[容器内路径]&lt;/code>&lt;/td>
&lt;td style="text-align: left">在宿主机和容器之间复制文件/文件夹。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker stats&lt;/code>&lt;/td>
&lt;td style="text-align: left">实时显示容器的资源使用情况。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker top [容器ID]&lt;/code>&lt;/td>
&lt;td style="text-align: left">查看容器内运行的进程。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker inspect [容器ID或镜像ID]&lt;/code>&lt;/td>
&lt;td style="text-align: left">查看容器或镜像的详细信息（元数据）。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="docker-run-常用选项">&lt;code>docker run&lt;/code> 常用选项
&lt;/h3>&lt;ul>
&lt;li>&lt;code>-d&lt;/code>: 后台运行容器（detached mode）。&lt;/li>
&lt;li>&lt;code>-p [宿主机端口]:[容器端口]&lt;/code>: 端口映射。&lt;/li>
&lt;li>&lt;code>-v [宿主机路径]:[容器内路径]&lt;/code>: 数据卷挂载。&lt;/li>
&lt;li>&lt;code>--name [容器名]&lt;/code>: 为容器指定一个名称。&lt;/li>
&lt;li>&lt;code>-it&lt;/code>: 启动交互式会话 (&lt;code>-i&lt;/code> 交互, &lt;code>-t&lt;/code> 分配一个伪终端)。&lt;/li>
&lt;li>&lt;code>--rm&lt;/code>: 容器停止后自动删除。&lt;/li>
&lt;li>&lt;code>-e [环境变量名]=[值]&lt;/code>: 设置环境变量。&lt;/li>
&lt;li>&lt;code>--network [网络名]&lt;/code>: 将容器连接到指定网络。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>示例:&lt;/strong>
&lt;code>docker run -d -p 8080:80 --name my-web-server -v /webapp:/usr/share/nginx/html nginx&lt;/code>&lt;/p>
&lt;p>Docker Compose 相关命令&lt;/p>
&lt;p>用于定义和运行多容器 Docker 应用程序的工具。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">命令&lt;/th>
&lt;th style="text-align: left">描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose up&lt;/code>&lt;/td>
&lt;td style="text-align: left">根据 &lt;code>docker-compose.yml&lt;/code> 创建并启动所有服务。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose up -d&lt;/code>&lt;/td>
&lt;td style="text-align: left">在后台创建并启动所有服务。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose down&lt;/code>&lt;/td>
&lt;td style="text-align: left">停止并移除由 &lt;code>up&lt;/code> 创建的容器、网络、卷。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose ps&lt;/code>&lt;/td>
&lt;td style="text-align: left">列出 &lt;code>docker-compose.yml&lt;/code> 文件中定义的所有容器的状态。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose logs&lt;/code>&lt;/td>
&lt;td style="text-align: left">查看所有服务的日志。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose logs -f [服务名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">实时跟踪特定服务的日志。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose build&lt;/code>&lt;/td>
&lt;td style="text-align: left">构建或重新构建服务。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose pull&lt;/code>&lt;/td>
&lt;td style="text-align: left">拉取服务依赖的镜像。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose exec [服务名] [命令]&lt;/code>&lt;/td>
&lt;td style="text-align: left">在指定的服务容器中执行命令。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose stop&lt;/code>&lt;/td>
&lt;td style="text-align: left">停止服务，但不删除容器。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker-compose start&lt;/code>&lt;/td>
&lt;td style="text-align: left">启动已停止的服务。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>系统与资源管理命令&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">命令&lt;/th>
&lt;th style="text-align: left">描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker system prune&lt;/code>&lt;/td>
&lt;td style="text-align: left">清理系统中未使用的 Docker 资源（容器、镜像、网络、卷）。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker system prune -a --volumes&lt;/code>&lt;/td>
&lt;td style="text-align: left">更彻底的清理，会删除所有未使用的镜像和数据卷。 &lt;strong>请谨慎使用！&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker system df&lt;/code>&lt;/td>
&lt;td style="text-align: left">查看 Docker 的磁盘使用情况。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker volume ls&lt;/code>&lt;/td>
&lt;td style="text-align: left">列出所有的数据卷。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker volume rm [卷名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">删除一个或多个数据卷。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker network ls&lt;/code>&lt;/td>
&lt;td style="text-align: left">列出所有的网络。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker network rm [网络名]&lt;/code>&lt;/td>
&lt;td style="text-align: left">删除一个或多个网络。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker login&lt;/code>&lt;/td>
&lt;td style="text-align: left">登录到 Docker Hub 或其他镜像仓库。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker logout&lt;/code>&lt;/td>
&lt;td style="text-align: left">登出 Docker Hub 或其他镜像仓库。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker info&lt;/code>&lt;/td>
&lt;td style="text-align: left">显示 Docker 系统范围的信息。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;code>docker version&lt;/code>&lt;/td>
&lt;td style="text-align: left">显示 Docker 的版本信息。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h1 id="docker-安装">docker 安装
&lt;/h1>&lt;h2 id="阿里云安装docker">阿里云安装docker
&lt;/h2>&lt;p>&lt;strong>阿里云服务器&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 先下载 GPGkey&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod a+r /etc/apt/keyrings/docker.asc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Add the Aliyun Repository&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;deb [arch=&lt;/span>&lt;span class="k">$(&lt;/span>dpkg --print-architecture&lt;span class="k">)&lt;/span>&lt;span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://mirrors.aliyun.com/docker-ce/linux/ubuntu &lt;/span>&lt;span class="k">$(&lt;/span>lsb_release -cs&lt;span class="k">)&lt;/span>&lt;span class="s2"> stable&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Update your Package List&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt-get update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install Docker &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#配置镜像加速&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo mkdir -p /etc/docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo tee /etc/docker/daemon.json &lt;span class="s">&amp;lt;&amp;lt;-&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;registry-mirrors&amp;#34;: [&amp;#34;https://docker.m.daocloud.io&amp;#34;,&amp;#34;https://&amp;lt;你的加速镜像地址，进入阿里云镜像加速服务获取&amp;gt;.mirror.aliyuncs.com&amp;#34;,&amp;#34;https://mirror.ccs.tencentyun.com&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#设置docker服务自启&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl daemon-reload
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl &lt;span class="nb">enable&lt;/span> --now docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl &lt;span class="nb">enable&lt;/span> containerd
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># verify the installation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#------------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 到这里docker到安装完成，下面是docker swarm的安装&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#------------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 初始化docker swarm 集群 --advertise-addr &amp;lt;ip地址&amp;gt; #如果没有指定，默认是当前机器的局域网ip地址&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker swarm init --advertise-addr &amp;lt;公网ip地址&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="腾讯云安装docker">腾讯云安装docker
&lt;/h2>&lt;p>&lt;strong>腾讯云服务器&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo install -m &lt;span class="m">0755&lt;/span> -d /etc/apt/keyrings
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSL http://mirrors.tencentyun.com/docker-ce/linux/ubuntu/gpg &lt;span class="p">|&lt;/span> sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod a+r /etc/apt/keyrings/docker.gpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;deb [arch=&lt;/span>&lt;span class="k">$(&lt;/span>dpkg --print-architecture&lt;span class="k">)&lt;/span>&lt;span class="s2"> signed-by=/etc/apt/keyrings/docker.gpg] http://mirrors.tencentyun.com/docker-ce/linux/ubuntu \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &lt;/span>&lt;span class="k">$(&lt;/span>. /etc/os-release &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$VERSION_CODENAME&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2"> stable&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt-get update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo mkdir -p /etc/docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo tee /etc/docker/daemon.json &lt;span class="s">&amp;lt;&amp;lt;-&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;registry-mirrors&amp;#34;: [&amp;#34;https://mirror.ccs.tencentyun.com&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl daemon-reload
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl restart docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl &lt;span class="nb">enable&lt;/span> --now docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl &lt;span class="nb">enable&lt;/span> containerd
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="windows-安装docker-desktop">windows 安装docker desktop
&lt;/h2>&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-9.png"
width="1920"
height="1032"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-9_hu5379139890529886184.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-9_hu17260824400583683544.png 1024w"
loading="lazy"
alt="wsl 设置"
class="gallery-image"
data-flex-grow="186"
data-flex-basis="446px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-10.png"
width="1469"
height="890"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-10_hu8897781883495024280.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-10_hu18270858830634013591.png 1024w"
loading="lazy"
alt="vpn设置"
class="gallery-image"
data-flex-grow="165"
data-flex-basis="396px"
>&lt;/p>
&lt;p>如果需要查询vpm有没有在容器里面生效&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-11.png"
width="738"
height="474"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-11_hu10040779969051181402.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-11_hu1036659357537563735.png 1024w"
loading="lazy"
alt="curl -L ipinfo.io"
class="gallery-image"
data-flex-grow="155"
data-flex-basis="373px"
>&lt;/p>
&lt;h1 id="注意事项">注意事项
&lt;/h1>&lt;h1 id="使用指南">使用指南
&lt;/h1>&lt;h3 id="docker自动创建的数据卷自动挂载到-mnt">docker自动创建的数据卷自动挂载到 /mnt
&lt;/h3>&lt;p>在很多场景下，我们习惯让 Docker 自己管理数据卷（即 Named Volumes，比如 &lt;code>docker volume create my_vol&lt;/code>）。这些卷默认存放在 &lt;code>/var/lib/docker/volumes/&lt;/code>。&lt;/p>
&lt;p>如果你想让 Docker 把这些自己创建的卷也放到 &lt;code>/mnt&lt;/code> 里，这里有三种方案，请根据实际需求选择：&lt;/p>
&lt;p>&lt;strong>方案 A：修改全局默认存储路径（强烈推荐，一劳永逸）&lt;/strong>&lt;/p>
&lt;p>这是最规范的做法。它不仅会把数据卷存到 &lt;code>/mnt&lt;/code>，还会把拉取的镜像、容器日志等所有 Docker 数据都转移过去，彻底解放系统盘。&lt;/p>
&lt;p>操作步骤：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>停止 Docker 服务：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo systemctl stop docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#如果docker.socket服务没有停止，需要先停止它&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl stop docker.socket
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>修改或创建配置文件：&lt;/strong>
编辑 &lt;code>/etc/docker/daemon.json&lt;/code> 文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo nano /etc/docker/daemon.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写入以下配置（假设你要把数据存在 &lt;code>/mnt/docker_data&lt;/code>）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;data-root&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/mnt/docker_data&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>（重要）迁移历史数据：&lt;/strong>
为了防止你以前创建的容器和镜像丢失，务必将原数据同步到新目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo rsync -aP /var/lib/docker/ /mnt/docker_data/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>重启 Docker：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo systemctl start docker
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>效果：&lt;/strong> 以后你执行任何关于 Docker 的操作，数据都会自动落在大硬盘 &lt;code>/mnt/docker_data&lt;/code> 里了！&lt;/p>
&lt;p>&lt;strong>方案 B：使用软链接（简单粗暴）&lt;/strong>&lt;/p>
&lt;p>如果你只想把数据卷（Volumes）挪到 &lt;code>/mnt&lt;/code>，而镜像等其他数据还要留在系统盘，可以使用 Linux 的软链接魔法。&lt;/p>
&lt;p>操作步骤：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>停止服务：&lt;code>sudo systemctl stop docker&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>在 &lt;code>/mnt&lt;/code> 创建目标文件夹：&lt;code>sudo mkdir -p /mnt/docker_volumes&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>迁移现有的卷数据：&lt;code>sudo mv /var/lib/docker/volumes/* /mnt/docker_volumes/&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>删除原本的空文件夹：&lt;code>sudo rm -rf /var/lib/docker/volumes&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>创建软链接：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo ln -s /mnt/docker_volumes /var/lib/docker/volumes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>重启服务：&lt;code>sudo systemctl start docker&lt;/code>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>效果：&lt;/strong> 欺骗了 Docker。Docker 以为自己还是写在 &lt;code>/var/lib&lt;/code> 下，但实际上数据已经悄悄存进了 &lt;code>/mnt&lt;/code>。&lt;/p>
&lt;p>&lt;strong>方案 C：在 Docker Compose 中按需指定物理路径（优雅灵活）&lt;/strong>&lt;/p>
&lt;p>如果你不想修改系统的任何配置，只想在部署某个特定项目时，让它自己的数据卷落在 &lt;code>/mnt&lt;/code>，可以在定义 volume 时使用 local 驱动。&lt;/p>
&lt;p>这在编写 &lt;code>docker-compose.yml&lt;/code> 时非常实用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysql:8.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">mysql_data:/var/lib/mysql&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mysql_data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">driver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">driver_opts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">none&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">o&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bind&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">device&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mnt/my_project/mysql_data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>效果：&lt;/strong> 既保留了 Named Volumes 的便捷性，又实现了数据的物理分离，非常适合项目迁移和备份。&lt;/p>
&lt;p>&lt;strong>总结建议&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>如果你的系统盘马上就要满了：别犹豫，直接用 &lt;strong>方案 A&lt;/strong>，修改 &lt;code>data-root&lt;/code> 把整个 Docker 搬家到大硬盘。&lt;/li>
&lt;li>如果你只是想把项目数据单独分类存放大盘：推荐 &lt;strong>方案 C&lt;/strong>，在 &lt;code>docker-compose.yml&lt;/code> 中使用 &lt;code>driver_opts&lt;/code>，干净且优雅。&lt;/li>
&lt;/ul>
&lt;p>希望这篇文章能帮你解决 Docker 存储空间的烦恼！如果遇到权限问题（Permission Denied），记得检查 &lt;code>/mnt&lt;/code> 对应目录的用户权限哦（chown）。&lt;/p>
&lt;p>(Tags: Docker, Linux, 运维，存储优化，DockerVolume)&lt;/p>
&lt;h3 id="简单开始一个docker镜像的使用">简单开始一个docker镜像的使用
&lt;/h3>&lt;h4 id="1-拉取镜像">1. 拉取镜像
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker pull ubuntu
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="2查看镜像是否拉取成功">2.查看镜像是否拉取成功
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker images
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="3-运行容器">3. 运行容器
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -itd --name &amp;lt;容器名称&amp;gt; -p &amp;lt;主机端口&amp;gt;:&amp;lt;容器端口&amp;gt; --cpus&lt;span class="o">=&lt;/span>&lt;span class="m">30&lt;/span> ubuntu
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -p设置端口 --cpus/-c 设置核心 &lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="4-通过-exec-命令进入-ubuntu-容器">4. 通过 exec 命令进入 ubuntu 容器
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> -it &amp;lt;容器名&amp;gt; /bin/bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="5-安装ssh">5. 安装ssh
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">apt-get install openssh-client -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">apt-get install openssh-server -y
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="6-安装vim">6. 安装vim
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get install vim -y
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="7-安装conda">7. 安装conda
&lt;/h4>&lt;p>&lt;a class="link" href="https://zhuanlan.zhihu.com/p/307923089" target="_blank" rel="noopener"
>https://zhuanlan.zhihu.com/p/307923089&lt;/a>
注意，可能要手动配置环境变量&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index.png"
width="683"
height="23"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index_hu18018924885186202092.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index_hu11055120303891815462.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="2969"
data-flex-basis="7126px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-1.png"
width="1061"
height="316"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-1_hu15996279467176129364.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-1_hu14527054585824512022.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="335"
data-flex-basis="805px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-2.png"
width="722"
height="21"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-2_hu15911958128711493842.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-2_hu10207799075979233041.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="3438"
data-flex-basis="8251px"
>&lt;/p>
&lt;h4 id="8-安装zipunzip">8. 安装zip、unzip
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get install zip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">apt-get install unzip
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="9-解决中文乱码问题">9. 解决中文乱码问题
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">export&lt;/span> &lt;span class="n">LC_ALL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;C.UTF-8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">source&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">bash&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bashrc&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="10-安装sudo">10. 安装sudo
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get install sudo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="docker使用实战-mysql和ubuntu容器">Docker使用实战-MySQL和Ubuntu容器
&lt;/h3>&lt;h4 id="1-安装docker-mysql容器">1. 安装docker mysql容器
&lt;/h4>&lt;h5 id="首先拉取mysql镜像-默认拉取最新版">首先拉取mysql镜像 ，默认拉取最新版
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker pull mysql
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index.png"
width="950"
height="322"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index_hu15883106726238587353.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index_hu11001229380795782989.png 1024w"
loading="lazy"
alt="拉取mysql镜像"
class="gallery-image"
data-flex-grow="295"
data-flex-basis="708px"
>&lt;/p>
&lt;h5 id="然后创建容器">然后创建容器
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run --name mysql_zata -p 3306:3306 -e &lt;span class="nv">MYSQL_ROOT_PASSWORD&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">123456&lt;/span> mysql &lt;span class="c1">#第一个端口是主机的端口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">或
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run --name mysql_zata -p 53306:3306 -e &lt;span class="nv">MYSQL_ROOT_PASSWORD&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">123456&lt;/span> mysql
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>参数&lt;/th>
&lt;th>解释&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>docker run:&lt;/td>
&lt;td>创建Docker容器的命令。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ndash;name mysql_zata&lt;/td>
&lt;td>这个选项用于指定容器的名称，这里将容器命名为&amp;quot;mysql_zata&amp;quot;。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-p 3306:3306: [第一个3306是主机的端口可以改成其他的，第二个3306是容器的端口，也就是要固定的]可以改成如： &lt;br> -p 53306：3306&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>这个选项用于将容器内部的端口映射到宿主机上。具体来说，-p 3306:3306 将容器内部的MySQL数据库服务的端口3306映射到宿主机的端口3306。这样，您可以通过宿主机上的3306端口访问容器内的MySQL服务。&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-e MYSQL_ROOT_PASSWORD=123456:&lt;/td>
&lt;td>这个选项用于设置MySQL数据库的root用户的密码。在这里，密码被设置为&amp;quot;123456&amp;quot;。这是一个环境变量设置，MySQL容器会使用这个密码来授权root用户访问数据库。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>mysql:&lt;/td>
&lt;td>这是要运行的Docker镜像的名称。在这里，它是MySQL镜像，意味着会启动一个MySQL数据库容器。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-1.png"
width="1803"
height="862"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-1_hu9353473650543336541.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-1_hu14472057940714386618.png 1024w"
loading="lazy"
alt="创建容器"
class="gallery-image"
data-flex-grow="209"
data-flex-basis="501px"
>&lt;/p>
&lt;p>现在是前台模式创建的（我也不是很清楚，反正关闭这个窗口服务就会停掉），关闭窗口也没事，可以重新再windows的docker界面重开&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-2.png"
width="1102"
height="183"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-2_hu7652912901109794189.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-2_hu9010311326302050966.png 1024w"
loading="lazy"
alt="docker图形界面运行mysql容器"
class="gallery-image"
data-flex-grow="602"
data-flex-basis="1445px"
>&lt;/p>
&lt;h4 id="2-安装ubuntu容器并配置">2. 安装Ubuntu容器并配置
&lt;/h4>&lt;h5 id="1-拉取镜像-1">1. 拉取镜像
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker pull ubuntu
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="2查看镜像是否拉取成功-1">2.查看镜像是否拉取成功
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker images
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="3-运行容器-1">3. 运行容器
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -itd --name &amp;lt;容器名称&amp;gt; -p &amp;lt;主机端口&amp;gt;:&amp;lt;容器端口&amp;gt; ubuntu
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="4-通过-exec-命令进入-ubuntu-容器-1">4. 通过 exec 命令进入 ubuntu 容器
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> -it &amp;lt;容器名&amp;gt; /bin/bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="5-安装ssh并启动">5. 安装ssh并启动
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get updata
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">apt-get install openssh-client
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">apt-get install openssh-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo service ssh start
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="6-安装vim-1">6. 安装vim
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get install vim
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="7-安装conda-1">7. 安装conda
&lt;/h5>&lt;p>&lt;a class="link" href="https://zhuanlan.zhihu.com/p/307923089" target="_blank" rel="noopener"
>https://zhuanlan.zhihu.com/p/307923089&lt;/a>&lt;/p>
&lt;p>注意，可能要手动配置环境变量（如果能直接用也就不用配置）&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-3.png"
width="499"
height="292"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-3_hu784335540611786281.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/mysql-index-3_hu4687353665301487606.png 1024w"
loading="lazy"
alt="配置环境变量"
class="gallery-image"
data-flex-grow="170"
data-flex-basis="410px"
>&lt;/p>
&lt;h5 id="8-安装zipunzip-1">8. 安装zip、unzip
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get install zip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">apt-get install unzip
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="9-解决中文乱码问题-1">9. 解决中文乱码问题
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">export&lt;/span> &lt;span class="n">LC_ALL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;C.UTF-8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">source&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">bash&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">bashrc&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="10-安装sudo-1">10. 安装sudo
&lt;/h5>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get install sudo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;hr>
&lt;hr>
&lt;hr>
&lt;h3 id="docker使用技巧">Docker使用技巧
&lt;/h3>&lt;h4 id="docker的卷挂载功能">docker的卷挂载功能
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -v &amp;lt;host_path&amp;gt;:&amp;lt;container_path&amp;gt; &amp;lt;image_name&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 需要为每一个路径都添加-v&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -v &amp;lt;host_path1&amp;gt;:&amp;lt;container_path1&amp;gt; -v &amp;lt;host_path2&amp;gt;:&amp;lt;container_path2&amp;gt; ... &amp;lt;image_name&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>windows环境下面的特殊情况（路径反斜杠）
示例： 假设你想将 Windows 上的 C:\data 映射到容器内的 /app/data&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -v /c/data:/app/data -it &amp;lt;image_name&amp;gt; /bin/bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果路径存在空格，需要用&amp;#34;&amp;#34;包起来 如&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -v &lt;span class="s2">&amp;#34;/c/Program Files/my folder:/app&amp;#34;&lt;/span> -it &amp;lt;image_name&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="将镜像推送到远程仓库">将镜像推送到远程仓库
&lt;/h4>&lt;p>推送到公开&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker tag my-app:1.0 myusername/my-app:1.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker push myusername/my-app:1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>到私有&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker login registry.example.com &lt;span class="c1"># 如果是docker hub 只需要 login in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker tag my-app:1.0 registry.example.com/myusername/my-app:1.0 &lt;span class="c1"># 重命名tag&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker push registry.example.com/myusername/my-app:1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="docker-context-教程">Docker Context 教程
&lt;/h4>&lt;h3 id="什么是-docker-context">什么是 Docker Context？
&lt;/h3>&lt;p>在深入了解之前，我们首先需要理解 Docker Context 的概念。简单来说，&lt;strong>Docker Context&lt;/strong> 是 Docker CLI（命令行工具）用来连接和管理不同 Docker 守护进程（Docker daemon）或 Kubernetes 集群的配置集合。&lt;/p>
&lt;p>在 Docker CLI 的早期版本中，我们通常使用 &lt;code>DOCKER_HOST&lt;/code> 环境变量来指定要连接的远程 Docker daemon。这种方式虽然有效，但对于需要频繁切换不同环境（例如：本地开发、远程测试服务器、生产环境等）的开发者来说，管理起来非常不便，因为它要求每次切换时都手动更改环境变量。&lt;/p>
&lt;p>Docker Context 解决了这个问题。它允许你将连接信息（如主机地址、证书等）保存为一个命名的配置，并可以通过简单的命令 (&lt;code>docker context use&lt;/code>) 在这些配置之间快速切换，而无需手动管理环境变量。&lt;/p>
&lt;p>一个 Docker Context 可以包含：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Docker Endpoint&lt;/strong>：Docker 守护进程的地址（例如：&lt;code>tcp://&amp;lt;host&amp;gt;:&amp;lt;port&amp;gt;&lt;/code> 或 &lt;code>ssh://&amp;lt;user&amp;gt;@&amp;lt;host&amp;gt;&lt;/code>）。&lt;/li>
&lt;li>&lt;strong>Kubernetes Endpoint&lt;/strong>：Kubernetes API 服务器的地址。&lt;/li>
&lt;li>&lt;strong>安全认证信息&lt;/strong>：用于连接的证书和密钥。&lt;/li>
&lt;/ul>
&lt;p>默认情况下，当你安装 Docker 后，会自动创建一个名为 &lt;code>default&lt;/code> 的上下文，它指向你本地的 Docker 守护进程。&lt;/p>
&lt;h3 id="为什么使用-docker-context">为什么使用 Docker Context？
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>简化工作流&lt;/strong>：无需频繁设置和取消 &lt;code>DOCKER_HOST&lt;/code> 环境变量。&lt;/li>
&lt;li>&lt;strong>集中管理&lt;/strong>：将所有不同环境的连接配置集中管理。&lt;/li>
&lt;li>&lt;strong>支持多种环境&lt;/strong>：不仅支持远程 Docker daemon，还支持连接到 Kubernetes 集群。&lt;/li>
&lt;li>&lt;strong>安全&lt;/strong>：可以方便地管理和切换不同环境的安全认证信息。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="docker-context-常用命令">Docker Context 常用命令
&lt;/h3>&lt;p>在使用 Docker Context 之前，您需要确保您的 Docker 版本在 19.03 或更高。&lt;/p>
&lt;h4 id="1-查看现有上下文">1. 查看现有上下文
&lt;/h4>&lt;p>&lt;code>docker context ls&lt;/code> 命令用于列出所有已定义的上下文。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>您将看到类似以下的输出：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">NAME&lt;/span> &lt;span class="n">DESCRIPTION&lt;/span> &lt;span class="n">DOCKER&lt;/span> &lt;span class="n">ENDPOINT&lt;/span> &lt;span class="n">KUBERNETES&lt;/span> &lt;span class="n">ENDPOINT&lt;/span> &lt;span class="n">ORCHESTRATOR&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">default&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">Current&lt;/span> &lt;span class="n">DOCKER_HOST&lt;/span> &lt;span class="n">based&lt;/span> &lt;span class="n">configuration&lt;/span> &lt;span class="n">unix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="o">///&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">docker&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sock&lt;/span> &lt;span class="n">swarm&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>NAME&lt;/code>：上下文的名称。&lt;/li>
&lt;li>&lt;code>*&lt;/code>：星号表示当前正在使用的上下文。&lt;/li>
&lt;li>&lt;code>DESCRIPTION&lt;/code>：对上下文的描述。&lt;/li>
&lt;li>&lt;code>DOCKER ENDPOINT&lt;/code>：Docker 守护进程的连接地址。&lt;/li>
&lt;li>&lt;code>KUBERNETES ENDPOINT&lt;/code>：如果上下文用于 Kubernetes，则显示其连接地址。&lt;/li>
&lt;/ul>
&lt;h4 id="2-创建新上下文">2. 创建新上下文
&lt;/h4>&lt;p>&lt;code>docker context create&lt;/code> 命令用于创建一个新的上下文。&lt;/p>
&lt;p>&lt;strong>创建连接到远程 Docker daemon 的上下文&lt;/strong>&lt;/p>
&lt;p>有几种方式可以连接远程 Docker daemon，最常见的是通过 SSH 或 TCP。&lt;/p>
&lt;p>&lt;strong>a) 通过 SSH 连接&lt;/strong>&lt;/p>
&lt;p>这是最推荐和最安全的连接方式。您需要确保：&lt;/p>
&lt;ul>
&lt;li>本地机器已配置好无密码 SSH 登录远程主机。&lt;/li>
&lt;li>远程主机上已安装 Docker，且当前用户有权限执行 &lt;code>docker&lt;/code> 命令（通常通过将用户添加到 &lt;code>docker&lt;/code> 用户组来实现）。&lt;/li>
&lt;/ul>
&lt;!-- end list -->
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context create &amp;lt;context_name&amp;gt; --description &lt;span class="s2">&amp;#34;&amp;lt;description&amp;gt;&amp;#34;&lt;/span> --docker &lt;span class="s2">&amp;#34;host=ssh://&amp;lt;username&amp;gt;@&amp;lt;remote_host&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>示例：&lt;/strong>&lt;/p>
&lt;p>假设您的远程主机 IP 为 &lt;code>192.168.1.100&lt;/code>，用户名为 &lt;code>ubuntu&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context create remote-server --description &lt;span class="s2">&amp;#34;Remote Docker daemon on dev server&amp;#34;&lt;/span> --docker &lt;span class="s2">&amp;#34;host=ssh://ubuntu@192.168.1.100&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>b) 通过 TCP 连接&lt;/strong>&lt;/p>
&lt;p>这种方式需要远程 Docker daemon 暴露在 TCP 端口上，并且通常需要TLS加密来保证安全。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>警告&lt;/strong>：不建议在没有TLS加密的情况下通过公共网络连接。&lt;/li>
&lt;/ul>
&lt;!-- end list -->
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 假设远程 Docker daemon 暴露在 2375 端口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker context create remote-tcp --description &lt;span class="s2">&amp;#34;Insecure remote connection&amp;#34;&lt;/span> --docker &lt;span class="s2">&amp;#34;host=tcp://192.168.1.100:2375&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果使用 TLS 加密&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker context create remote-tls --description &lt;span class="s2">&amp;#34;Secure remote connection&amp;#34;&lt;/span> --docker &lt;span class="s2">&amp;#34;host=tcp://192.168.1.100:2376&amp;#34;&lt;/span> --tls-ca-path &amp;lt;path_to_ca&amp;gt; --tls-cert-path &amp;lt;path_to_cert&amp;gt; --tls-key-path &amp;lt;path_to_key&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>c) 创建连接到 Kubernetes 集群的上下文&lt;/strong>&lt;/p>
&lt;p>如果您已经使用 &lt;code>kubectl&lt;/code> 配置了集群连接，Docker Context 可以自动从您的 &lt;code>.kube/config&lt;/code> 文件中加载这些配置。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context create &amp;lt;context_name&amp;gt; --description &lt;span class="s2">&amp;#34;&amp;lt;description&amp;gt;&amp;#34;&lt;/span> --kubernetes &lt;span class="s2">&amp;#34;config-file=&amp;lt;path_to_kubeconfig&amp;gt;&amp;#34;&lt;/span> --kubernetes &lt;span class="s2">&amp;#34;context=&amp;lt;kube_context_name&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>示例：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context create k8s-cluster --description &lt;span class="s2">&amp;#34;My Kubernetes cluster&amp;#34;&lt;/span> --kubernetes &lt;span class="s2">&amp;#34;context=my-cluster-context&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="3-切换上下文">3. 切换上下文
&lt;/h4>&lt;p>&lt;code>docker context use&lt;/code> 命令用于切换当前活动的上下文。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context use &amp;lt;context_name&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>示例：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 切换到之前创建的远程服务器上下文&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker context use remote-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 验证当前上下文&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker context ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>此时，&lt;code>docker context ls&lt;/code> 的输出会显示 &lt;code>remote-server&lt;/code> 旁边有星号 &lt;code>*&lt;/code>。从现在开始，您所有的 &lt;code>docker&lt;/code> 命令（例如 &lt;code>docker ps&lt;/code>、&lt;code>docker images&lt;/code> 等）都将在远程主机上执行。&lt;/p>
&lt;h4 id="4-在单次命令中使用特定上下文">4. 在单次命令中使用特定上下文
&lt;/h4>&lt;p>如果您只想在某一次命令中使用某个上下文，而不想永久切换，可以使用 &lt;code>--context&lt;/code> 全局选项。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker --context &amp;lt;context_name&amp;gt; &amp;lt;docker_command&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>示例：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在不切换当前上下文的情况下，查看远程服务器上的容器列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker --context remote-server ps -a
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="5-检查上下文详情">5. 检查上下文详情
&lt;/h4>&lt;p>&lt;code>docker context inspect&lt;/code> 命令可以查看某个上下文的详细配置信息。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context inspect &amp;lt;context_name&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>示例：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context inspect remote-server
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="6-更新上下文">6. 更新上下文
&lt;/h4>&lt;p>&lt;code>docker context update&lt;/code> 命令可以修改现有上下文的配置。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context update &amp;lt;context_name&amp;gt; --description &lt;span class="s2">&amp;#34;New description&amp;#34;&lt;/span> --docker &lt;span class="s2">&amp;#34;host=...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="7-移除上下文">7. 移除上下文
&lt;/h4>&lt;p>&lt;code>docker context rm&lt;/code> 命令可以删除一个上下文。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context rm &amp;lt;context_name&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="综合示例从本地切换到远程再切换回来">综合示例：从本地切换到远程，再切换回来
&lt;/h3>&lt;p>这是一个完整的流程，展示了如何利用 Docker Context 简化日常工作。&lt;/p>
&lt;p>&lt;strong>步骤 1：检查本地默认上下文&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 检查默认上下文，确保指向本地&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker context ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出应显示 &lt;code>default *&lt;/code>。&lt;/p>
&lt;p>&lt;strong>步骤 2：创建远程上下文&lt;/strong>&lt;/p>
&lt;p>假设您要连接到 IP 为 &lt;code>192.168.1.100&lt;/code> 的远程服务器。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context create my-remote-host --description &lt;span class="s2">&amp;#34;My development server&amp;#34;&lt;/span> --docker &lt;span class="s2">&amp;#34;host=ssh://user@192.168.1.100&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>步骤 3：切换到远程上下文&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context use my-remote-host
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>此时，您已成功切换到远程环境。&lt;/p>
&lt;p>&lt;strong>步骤 4：在远程主机上执行命令&lt;/strong>&lt;/p>
&lt;p>现在，您可以像操作本地 Docker 一样操作远程 Docker。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看远程主机上正在运行的容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker ps
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在远程主机上拉取一个镜像&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker pull nginx
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在远程主机上运行一个新容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d --name my-nginx -p 80:80 nginx
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这些命令的执行都将在 &lt;code>192.168.1.100&lt;/code> 上完成。&lt;/p>
&lt;p>&lt;strong>步骤 5：切换回本地上下文&lt;/strong>&lt;/p>
&lt;p>当您完成远程操作后，可以轻松切换回本地环境。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context use default
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>步骤 6：验证切换&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker context ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出应再次显示 &lt;code>default *&lt;/code>。&lt;/p>
&lt;p>现在，您的 &lt;code>docker&lt;/code> 命令又将作用于本地机器。&lt;/p>
&lt;p>通过这个简单的教程，您可以看到 Docker Context 如何极大地简化了多 Docker 环境的管理，使其成为一个高效且安全的工具。&lt;/p>
&lt;h4 id="在-windows-上实现-docker-in-docker-dind-终极教程">在 Windows 上实现 Docker in Docker (DinD) 终极教程
&lt;/h4>&lt;p>在 Windows 环境下开发时，我们有时会遇到需要在 Docker 容器内部再次调用 Docker 命令的场景，例如在 Jenkins CI/CD 流水线中构建 Docker 镜像。这个技术通常被称为 &amp;ldquo;Docker in Docker&amp;rdquo; (DinD)。&lt;/p>
&lt;p>本文将详细介绍在 Windows (通过 Docker Desktop + WSL 2) 中实现 DinD 的两种主流方法，分析其优劣，并提供手把手的操作步骤。&lt;/p>
&lt;h5 id="核心概念两种实现方式">核心概念：两种实现方式
&lt;/h5>&lt;ol>
&lt;li>&lt;strong>挂载 Docker Socket (DooD - Docker-out-of-Docker)&lt;/strong>: &lt;strong>(官方推荐)&lt;/strong> 让容器内的 Docker CLI 直接与宿主机的 Docker 守护进程 (Daemon) 通信。这好比在办公室里装一部电话分机，直接使用公司总机的功能。&lt;/li>
&lt;li>&lt;strong>真正的 Docker-in-Docker (DinD)&lt;/strong>: 在容器内运行一个全新的、完全隔离的 Docker 守护进程。这好比在办公室里私建一个小基站，内外通信完全独立。&lt;/li>
&lt;/ol>
&lt;h5 id="结论先行哪种方法最适合你">结论先行：哪种方法最适合你？
&lt;/h5>&lt;p>对于绝大多数场景，&lt;strong>方法一 (挂载 Docker Socket)&lt;/strong> 是最佳选择。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">特性&lt;/th>
&lt;th style="text-align: left">方法一 (挂载 Socket)&lt;/th>
&lt;th style="text-align: left">方法二 (真·DinD)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>推荐度&lt;/strong>&lt;/td>
&lt;td style="text-align: left">⭐⭐⭐⭐⭐ &lt;strong>(强烈推荐)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">⭐⭐ (仅特定场景)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>隔离性&lt;/strong>&lt;/td>
&lt;td style="text-align: left">弱&lt;/td>
&lt;td style="text-align: left">强&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>安全性&lt;/strong>&lt;/td>
&lt;td style="text-align: left">较高 (无需特权模式)&lt;/td>
&lt;td style="text-align: left">低 (必须使用 &lt;code>--privileged&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>性能/资源&lt;/strong>&lt;/td>
&lt;td style="text-align: left">开销极小&lt;/td>
&lt;td style="text-align: left">开销大 (双倍守护进程)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>配置复杂度&lt;/strong>&lt;/td>
&lt;td style="text-align: left">简单&lt;/td>
&lt;td style="text-align: left">复杂&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>镜像缓存&lt;/strong>&lt;/td>
&lt;td style="text-align: left">与宿主机共享&lt;/td>
&lt;td style="text-align: left">独立缓存，占用额外空间&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>典型用例&lt;/strong>&lt;/td>
&lt;td style="text-align: left">CI/CD, 开发环境&lt;/td>
&lt;td style="text-align: left">隔离的 Docker 功能测试&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h5 id="方法一挂载-docker-socket-dood---官方推荐">方法一：挂载 Docker Socket (DooD) - 官方推荐
&lt;/h5>&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-3.png"
width="644"
height="512"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-3_hu9222955544559158541.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-3_hu17625505401200353027.png 1024w"
loading="lazy"
alt="示例效果"
class="gallery-image"
data-flex-grow="125"
data-flex-basis="301px"
>&lt;/p>
&lt;p>这是最简单、高效且安全的方式。&lt;/p>
&lt;h6 id="-原理">✨ 原理
&lt;/h6>&lt;p>通过 &lt;code>-v&lt;/code> 参数将宿主机的 Docker Socket 文件 (&lt;code>/var/run/docker.sock&lt;/code>) 挂载到容器内部。容器内的任何 &lt;code>docker&lt;/code> 命令都会通过这个 Socket 文件被发送到宿主机，由宿主机的 Docker Daemon 执行。&lt;/p>
&lt;h6 id="-优点">✅ 优点
&lt;/h6>&lt;ul>
&lt;li>&lt;strong>配置简单&lt;/strong>：一行命令参数即可搞定。&lt;/li>
&lt;li>&lt;strong>资源高效&lt;/strong>：无需启动额外的 Docker 服务，内存和 CPU 占用极低。&lt;/li>
&lt;li>&lt;strong>镜像共享&lt;/strong>：容器内拉取的镜像，宿主机可以直接使用，避免重复下载，节约时间和磁盘空间。&lt;/li>
&lt;/ul>
&lt;h6 id="-操作步骤">🚀 操作步骤
&lt;/h6>&lt;p>&lt;strong>1. 前提条件&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Windows 10/11 已安装 &lt;strong>Docker Desktop&lt;/strong>。&lt;/li>
&lt;li>Docker Desktop 使用 &lt;strong>WSL 2 后端&lt;/strong> (当前默认设置)。
&lt;ul>
&lt;li>&lt;strong>检查方法&lt;/strong>: 打开 Docker Desktop &amp;gt; &lt;code>Settings&lt;/code> &amp;gt; &lt;code>General&lt;/code>，确保 &lt;code>Use the WSL 2 based engine&lt;/code> 已勾选。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. 运行容器并挂载 Socket&lt;/strong>&lt;/p>
&lt;p>打开 PowerShell 或 CMD，执行以下命令来启动一个带有 Docker CLI 的容器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -it --rm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v /var/run/docker.sock:/var/run/docker.sock &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> docker:latest &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>命令解析&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>docker run -it --rm&lt;/code>: 以交互模式启动一个临时容器，退出后自动删除。&lt;/li>
&lt;li>&lt;code>-v /var/run/docker.sock:/var/run/docker.sock&lt;/code>: &lt;strong>核心命令&lt;/strong>。将宿主机的 Docker Socket 文件挂载到容器内的相同路径。
&lt;blockquote>
&lt;p>&lt;strong>提示&lt;/strong>: 即使在 Windows 系统，也请直接使用 &lt;code>/var/run/docker.sock&lt;/code> 这个 Linux 路径。Docker Desktop 会自动处理好与 WSL 2 之间的路径转换。&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;li>&lt;code>docker:latest&lt;/code>: 使用官方 &lt;code>docker&lt;/code> 镜像，它内置了 Docker 命令行工具 (CLI)。&lt;/li>
&lt;li>&lt;code>sh&lt;/code>: 启动容器内的 shell 环境。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. 在容器内验证&lt;/strong>&lt;/p>
&lt;p>命令执行后，你的终端提示符会变为 &lt;code>/#&lt;/code>，表示已进入容器内部。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>查看宿主机容器&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在容器内部执行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出结果会列出你&lt;strong>宿主机&lt;/strong>上所有正在运行的容器，包括你刚刚启动的这一个。这证明容器已成功连接到宿主机的 Docker 服务。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>运行一个新容器&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在容器内部执行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run --rm hello-world
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你会看到 &lt;code>hello-world&lt;/code> 镜像被成功拉取并运行。此时，你在&lt;strong>宿主机&lt;/strong>上执行 &lt;code>docker images&lt;/code>，也能看到 &lt;code>hello-world&lt;/code> 这个镜像，证明了缓存是共享的。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h5 id="方法二真正的-docker-in-docker-dind---特定场景使用">方法二：真正的 Docker-in-Docker (DinD) - 特定场景使用
&lt;/h5>&lt;blockquote>
&lt;p>&lt;strong>⚠️ 安全警告&lt;/strong>
此方法必须开启 &lt;code>--privileged&lt;/code> (特权)模式，这会打破容器的隔离性，给予容器访问宿主机内核的权限，存在&lt;strong>严重的安全风险&lt;/strong>。请仅在完全信任镜像内容，并确实需要强隔离环境时使用。&lt;/p>
&lt;/blockquote>
&lt;h6 id="-原理-1">✨ 原理
&lt;/h6>&lt;p>使用官方提供的 &lt;code>docker:dind&lt;/code> 镜像，在容器内启动一个完整的、独立的 Docker 守护进程。&lt;/p>
&lt;h6 id="-缺点">❌ 缺点
&lt;/h6>&lt;ul>
&lt;li>&lt;strong>安全风险高&lt;/strong>：&lt;code>--privileged&lt;/code> 模式是危险的。&lt;/li>
&lt;li>&lt;strong>性能开销大&lt;/strong>：双重 Docker Daemon 运行，消耗更多系统资源。&lt;/li>
&lt;li>&lt;strong>双重存储&lt;/strong>：内外镜像是隔离的，同一镜像需要下载两次，占用双倍磁盘空间。&lt;/li>
&lt;/ul>
&lt;h6 id="-操作步骤-1">🚀 操作步骤
&lt;/h6>&lt;p>&lt;strong>1. 启动 DinD 守护进程容器&lt;/strong>&lt;/p>
&lt;p>这个容器专门用来在后台运行独立的 Docker 服务。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run --privileged --name my-dind-daemon -d &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">DOCKER_TLS_CERTDIR&lt;/span>&lt;span class="o">=&lt;/span>/certs &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> docker:24.0-dind
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>命令解析&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>--privileged&lt;/code>: &lt;strong>(高危)&lt;/strong> 授予容器特权。&lt;/li>
&lt;li>&lt;code>--name my-dind-daemon&lt;/code>: 为该容器命名，方便后续连接。&lt;/li>
&lt;li>&lt;code>-d&lt;/code>: 后台运行。&lt;/li>
&lt;li>&lt;code>docker:24.0-dind&lt;/code>: 使用官方的 &lt;code>dind&lt;/code> 专用镜像。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. 启动客户端容器并连接到 DinD&lt;/strong>&lt;/p>
&lt;p>现在启动另一个容器作为客户端，并将其网络连接到刚才的 &lt;code>dind&lt;/code> 容器。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -it --rm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --link my-dind-daemon:docker &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> docker:24.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>命令解析&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>--link my-dind-daemon:docker&lt;/code>: &lt;strong>核心命令&lt;/strong>。将 &lt;code>my-dind-daemon&lt;/code> 容器连接到当前容器，并设置网络别名为 &lt;code>docker&lt;/code>。这样，客户端内的 Docker CLI 就会自动找到名为 &lt;code>docker&lt;/code> 的主机作为其守护进程。&lt;/li>
&lt;li>&lt;code>docker:24.0&lt;/code>: 使用普通的 &lt;code>docker&lt;/code> 镜像作为客户端。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. 在客户端容器内验证&lt;/strong>&lt;/p>
&lt;p>进入客户端容器后，进行验证。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>查看容器&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在客户端容器内执行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出结果应为空。因为它连接的是 &lt;code>my-dind-daemon&lt;/code> 提供的全新、隔离的环境。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>运行一个新容器&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在客户端容器内执行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run --rm hello-world
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>hello-world&lt;/code> 镜像会被下载并运行。这个容器完全存在于 &lt;code>my-dind-daemon&lt;/code> 的环境中，你的宿主机对此毫不知情。在宿主机上执行 &lt;code>docker ps&lt;/code> 或 &lt;code>docker images&lt;/code> 都看不到这个 &lt;code>hello-world&lt;/code>。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h4 id="使用已有容器创建镜像">使用已有容器创建镜像
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker commit container-name new-image-name
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="开启重启ssh服务">开启/重启ssh服务
&lt;/h4>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">service ssh start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">service ssh restart
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="docker-文件传输">docker 文件传输
&lt;/h4>&lt;p>宿主机到容器&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># docker cp 宿主机文件/路径 容器名：容器内路径&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker cp /home/Download/index.html wordpress-lee:/var/www/html
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>容器到宿主机&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># docker cp 容器名：文件/路径 宿主机路径 &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker cp wordpress-lee:/root/example.sh /root
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="修改服务器配置允许通过此服务器进行ssh转发">修改服务器配置允许通过此服务器进行ssh转发
&lt;/h4>&lt;p>进入配置文件，不要cd..然后在vim，直接vim &amp;hellip;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vim /etc/ssh/sshd_config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配置文件内容&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-3.png"
width="1184"
height="1027"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-3_hu12745493966913438140.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-3_hu242959657583519928.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="115"
data-flex-basis="276px"
>&lt;/p>
&lt;p>修改其中的：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-4.png"
width="393"
height="132"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-4_hu11075327025778233217.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-4_hu3492958155437548261.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="297"
data-flex-basis="714px"
>
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-5.png"
width="417"
height="149"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-5_hu6223924596129936722.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-5_hu710651274989886181.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="279"
data-flex-basis="671px"
>&lt;/p>
&lt;p>重启ssh服务&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl"> service ssh restart
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="docker容器添加对外映射端口">docker容器添加对外映射端口
&lt;/h4>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>参考：&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a class="link" href="https://www.cnblogs.com/zhumengke/articles/13525837.html" target="_blank" rel="noopener"
>https://www.cnblogs.com/zhumengke/articles/13525837.html&lt;/a>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>最简单省事方法：将现有的容器打包成镜像，然后在使用新的镜像运行容器时重新指定要映射的端口&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-7.png"
width="1042"
height="311"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-7_hu654794957350065612.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-7_hu8900178969343253002.png 1024w"
loading="lazy"
alt="打包镜像重新运行容器"
class="gallery-image"
data-flex-grow="335"
data-flex-basis="804px"
>&lt;/p>
&lt;h4 id="局域网访问docker容器完整指南">局域网访问Docker容器完整指南
&lt;/h4>&lt;p>这个问题非常常见，是团队协作或使用多台设备进行开发时必备的技能。&lt;/p>
&lt;p>另一台局域网上的电脑（我们称之为&amp;quot;开发机&amp;quot;）要进入运行 Docker 的电脑（我们称之为&amp;quot;主机&amp;quot;）的容器，核心思想是&lt;strong>通过网络暴露容器的服务或端口&lt;/strong>。容器本身是隔离的，你需要&amp;quot;开一个门&amp;quot;让局域网上的其他设备可以访问。&lt;/p>
&lt;p>这里有几种主流的方法，从简单到复杂，适用于不同的开发场景。&lt;/p>
&lt;h5 id="准备工作必须知道主机ip">&lt;strong>准备工作：必须知道主机IP&lt;/strong>
&lt;/h5>&lt;p>首先，你需要在运行 Docker 的主机上获取其局域网 IP 地址。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>在 Windows 上:&lt;/strong> 打开命令提示符（CMD）或 PowerShell，输入 &lt;code>ipconfig&lt;/code>，查找 &amp;ldquo;IPv4 地址&amp;rdquo;。通常是 &lt;code>192.168.x.x&lt;/code> 或 &lt;code>10.x.x.x&lt;/code> 的形式。&lt;/li>
&lt;li>&lt;strong>在 macOS 或 Linux 上:&lt;/strong> 打开终端，输入 &lt;code>ifconfig&lt;/code> 或 &lt;code>ip a&lt;/code>，查找 &lt;code>inet&lt;/code> 后面的地址。&lt;/li>
&lt;/ul>
&lt;p>下文中，我们假设主机的 IP 地址是 &lt;code>192.168.1.100&lt;/code>。请在你的实际操作中替换成你自己的主机 IP。&lt;/p>
&lt;h5 id="方法一端口映射-port-mapping---最常用">&lt;strong>方法一：端口映射 (Port Mapping) - 最常用&lt;/strong>
&lt;/h5>&lt;p>这是最直接、最常见的方法，适用于访问容器内运行的 Web 应用、API 服务、数据库等。&lt;/p>
&lt;p>&lt;strong>原理：&lt;/strong>
将主机的一个端口映射到容器内的一个端口。这样，访问主机的这个端口就等于访问了容器内的对应端口。&lt;/p>
&lt;p>&lt;strong>操作步骤：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>启动容器时添加 &lt;code>-p&lt;/code> 参数：&lt;/strong>
在主机上，当你使用 &lt;code>docker run&lt;/code> 启动容器时，必须使用 &lt;code>-p&lt;/code> 或 &lt;code>--publish&lt;/code> 参数来暴露端口。&lt;/p>
&lt;p>格式为：&lt;code>-p &amp;lt;主机端口&amp;gt;:&amp;lt;容器端口&amp;gt;&lt;/code>&lt;/p>
&lt;p>&lt;strong>关键点：&lt;/strong> 为了让局域网上的其他电脑能访问，主机端口部分必须绑定到 &lt;code>0.0.0.0&lt;/code>，或者干脆省略 IP 地址（默认就是 &lt;code>0.0.0.0&lt;/code>）。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>示例1：运行一个 Nginx Web 服务器&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将主机的 8080 端口映射到容器的 80 端口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 这样局域网内的任何机器都可以通过主机的 8080 端口访问&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d --name my-web -p 8080:80 nginx
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>示例2：运行一个 Python Flask 应用&lt;/strong>
假设你的 Flask 应用在容器的 5000 端口运行。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将主机的 5000 端口映射到容器的 5000 端口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d --name my-app -p 5000:5000 your-python-app-image
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>在开发机上访问：&lt;/strong>
现在，在局域网的另一台开发机上，打开浏览器或使用工具（如 cURL, Postman）访问 &lt;code>http://&amp;lt;主机IP&amp;gt;:&amp;lt;主机端口&amp;gt;&lt;/code>。&lt;/p>
&lt;ul>
&lt;li>对于上面的 Nginx 示例，访问地址是：&lt;code>http://192.168.1.100:8080&lt;/code>&lt;/li>
&lt;li>对于 Flask 应用示例，访问地址是：&lt;code>http://192.168.1.100:5000&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>如果容器已经启动了怎么办？&lt;/strong>
你不能动态地给一个正在运行的容器添加端口映射。你需要停止并删除旧的容器，然后使用新的 &lt;code>-p&lt;/code> 参数重新启动它。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker stop &amp;lt;容器名或ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm &amp;lt;容器名或ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 然后使用上面的 docker run -p ... 命令重新创建&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="方法二通过-ssh-进入容器---获取完整的-shell-环境">&lt;strong>方法二：通过 SSH 进入容器 - 获取完整的 Shell 环境&lt;/strong>
&lt;/h5>&lt;p>如果你需要在容器内部执行命令、调试脚本，就像登录一台远程服务器一样，那么在容器里运行一个 SSH 服务是最佳选择。&lt;/p>
&lt;p>&lt;strong>原理：&lt;/strong>
在你的 Docker 镜像中安装并运行一个 SSH 服务器，然后像方法一那样，将容器的 SSH 端口（默认为 22）映射到主机的一个端口。&lt;/p>
&lt;p>&lt;strong>操作步骤：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>准备一个带 SSH 服务的 Dockerfile：&lt;/strong>
你不能直接在官方的基础镜像（如 &lt;code>ubuntu&lt;/code>）里直接用 SSH，需要先安装。&lt;/p>
&lt;p>下面是一个基于 Ubuntu 的 &lt;code>Dockerfile&lt;/code> 示例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 使用一个基础镜像&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> ubuntu:20.04&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 安装 SSH 服务端和一些常用工具&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apt-get update &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apt-get install -y openssh-server sudo vim curl &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> rm -rf /var/lib/apt/lists/*&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 创建一个用于 SSH 登录的用户，并设置密码&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 注意：在生产环境中，不要用硬编码的密码！&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> useradd -m -s /bin/bash developer &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;developer:yourpassword&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> chpasswd &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> adduser developer sudo&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 允许 root 登录（仅用于开发，不推荐在生产中使用）&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> sed -i &lt;span class="s1">&amp;#39;s/#PermitRootLogin prohibit-password/PermitRootLogin yes/&amp;#39;&lt;/span> /etc/ssh/sshd_config&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 创建 SSH 服务运行所需的目录&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> mkdir /var/run/sshd&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 暴露容器的 22 端口&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">EXPOSE&lt;/span>&lt;span class="s"> 22&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 容器启动时运行 SSH 服务&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;/usr/sbin/sshd&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-D&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>构建并运行镜像：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>在主机上，将上述 &lt;code>Dockerfile&lt;/code> 保存，然后在该目录下执行构建命令：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker build -t my-ssh-container .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>运行容器，并将主机的 2222 端口映射到容器的 22 端口（使用 2222 是为了避免与主机本身的 SSH 服务冲突）：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name dev-env -p 2222:22 my-ssh-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>在开发机上通过 SSH 连接：&lt;/strong>
在你的开发机上，打开终端，使用 &lt;code>ssh&lt;/code> 命令连接。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 使用你创建的 developer 用户登录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ssh developer@192.168.1.100 -p &lt;span class="m">2222&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后输入你设置的密码 (&lt;code>yourpassword&lt;/code>)，就可以进入容器的命令行界面了。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h5 id="方法三使用-vs-code-remote-development---最现代化的开发体验">&lt;strong>方法三：使用 VS Code Remote Development - 最现代化的开发体验&lt;/strong>
&lt;/h5>&lt;p>这是目前最受推崇的开发方式，它结合了本地 IDE 的流畅体验和容器化环境的隔离与一致性。&lt;/p>
&lt;p>&lt;strong>原理：&lt;/strong>
你在本地的 VS Code 上编写代码，但所有的文件操作、终端命令、调试器都运行在远程主机上的容器内部。VS Code 会在容器里安装一个轻量的服务来实现这种无缝连接。&lt;/p>
&lt;p>&lt;strong>操作步骤：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>在主机上：暴露 Docker Daemon&lt;/strong>
这是最关键的一步。你需要让 Docker 守护进程监听一个 TCP 端口，以便远程的 VS Code 可以连接。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>对于 Docker Desktop (Windows/macOS):&lt;/strong>
进入 Settings -&amp;gt; General，勾选 &amp;ldquo;Expose daemon on tcp://localhost:2375 without TLS&amp;rdquo;。&lt;/li>
&lt;li>&lt;strong>对于 Linux:&lt;/strong>
编辑 Docker 的配置文件 &lt;code>/etc/docker/daemon.json&lt;/code> (如果不存在则创建)，添加以下内容：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hosts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;tcp://0.0.0.0:2375&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;unix:///var/run/docker.sock&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>然后重启 Docker 服务：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo systemctl restart docker
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>安全警告：&lt;/strong> 将 Docker Daemon 暴露在网络上存在安全风险，请确保你的局域网是受信任的，并且主机防火墙已正确配置。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>在主机上：确保你的开发容器正在运行。&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 只需要一个普通的开发容器即可，无需特殊配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d --name my-project-container -it your-dev-image bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>-it&lt;/code> 和 &lt;code>bash&lt;/code> 让容器保持运行状态。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>在开发机上：配置 VS Code&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>安装 VS Code。&lt;/li>
&lt;li>在 VS Code 的扩展市场中，搜索并安装 &lt;strong>Remote Development&lt;/strong> 扩展包（由 Microsoft 发布）。&lt;/li>
&lt;li>打开 VS Code 的设置 (Settings JSON)，添加以下配置，告诉它 Docker 主机在哪里：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;docker.host&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tcp://192.168.1.100:2375&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>重启 VS Code。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>在开发机上：连接到容器&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>点击 VS Code 左下角的绿色 &lt;code>&amp;gt;&amp;lt;&lt;/code> 图标，或者按 &lt;code>F1&lt;/code> 输入 &lt;code>Remote-Containers: Attach to Running Container...&lt;/code>。&lt;/li>
&lt;li>VS Code 会列出主机 &lt;code>192.168.1.100&lt;/code> 上所有正在运行的容器。&lt;/li>
&lt;li>选择你的目标容器（例如 &lt;code>my-project-container&lt;/code>）。&lt;/li>
&lt;li>VS Code 会自动在容器内安装所需的服务，并重新加载窗口。完成后，你的 VS Code 就已经&amp;quot;进入&amp;quot;了容器。你可以直接打开容器内的文件夹、使用 VS Code 的集成终端（这个终端就是容器的 shell）、安装语言扩展、进行调试等，一切都像在本地一样。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h5 id="总结与选择">&lt;strong>总结与选择&lt;/strong>
&lt;/h5>&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">方法&lt;/th>
&lt;th style="text-align: left">适用场景&lt;/th>
&lt;th style="text-align: left">优点&lt;/th>
&lt;th style="text-align: left">缺点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>端口映射&lt;/strong>&lt;/td>
&lt;td style="text-align: left">访问 Web 服务、API、数据库等需要端口的应用。&lt;/td>
&lt;td style="text-align: left">简单、直接、最常用。&lt;/td>
&lt;td style="text-align: left">只能访问暴露的服务，无法直接操作容器内部文件系统或执行任意命令。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>SSH 进入容器&lt;/strong>&lt;/td>
&lt;td style="text-align: left">需要完整的命令行权限，进行系统管理、脚本调试。&lt;/td>
&lt;td style="text-align: left">功能强大，像操作一台真实的 Linux 服务器。&lt;/td>
&lt;td style="text-align: left">配置相对复杂，需要在镜像中预置 SSH 服务和用户。&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>VS Code Remote&lt;/strong>&lt;/td>
&lt;td style="text-align: left">现代化的编码、调试、测试一体化开发。&lt;/td>
&lt;td style="text-align: left">体验最佳，无缝集成，兼具本地 IDE 的流畅和容器环境的隔离。&lt;/td>
&lt;td style="text-align: left">需要配置 Docker Daemon，有一定安全风险，依赖 VS Code。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>强烈建议：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>对于&lt;strong>查看和测试 Web 应用&lt;/strong>，使用&lt;strong>方法一&lt;/strong>。&lt;/li>
&lt;li>对于&lt;strong>需要深入容器内部进行复杂操作&lt;/strong>的场景，使用&lt;strong>方法二&lt;/strong>。&lt;/li>
&lt;li>对于&lt;strong>日常编码和开发&lt;/strong>，强烈推荐学习并使用&lt;strong>方法三&lt;/strong>，这是目前最先进、最高效的流程。&lt;/li>
&lt;/ul>
&lt;p>最后，请务必检查&lt;strong>主机的防火墙&lt;/strong>，确保你映射的端口（如 &lt;code>8080&lt;/code>, &lt;code>2222&lt;/code>, &lt;code>2375&lt;/code>）是开放的，允许来自局域网的访问。&lt;/p>
&lt;h4 id="docker设置可用cpu核心数量">docker设置可用CPU核心数量
&lt;/h4>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>参考：&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a class="link" href="https://www.cnblogs.com/sparkdev/p/8052522.html" target="_blank" rel="noopener"
>https://www.cnblogs.com/sparkdev/p/8052522.html&lt;/a>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-8.png"
width="668"
height="69"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-8_hu5883548660596835905.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-8_hu10735190958917900837.png 1024w"
loading="lazy"
alt="CPU数量"
class="gallery-image"
data-flex-grow="968"
data-flex-basis="2323px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-9.png"
width="697"
height="73"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-9_hu16433319545129989747.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-9_hu4415500239632671014.png 1024w"
loading="lazy"
alt="指定CPU"
class="gallery-image"
data-flex-grow="954"
data-flex-basis="2291px"
>&lt;/p>
&lt;h4 id="容器端口转发问题排查和解决方案">容器端口转发问题排查和解决方案
&lt;/h4>&lt;p>在容器中进行端口转发并遇到无法访问的问题，可能涉及多个原因。以下是详细的分析和可能的原因，以及解决方法：&lt;/p>
&lt;h5 id="1-容器端口转发的工作原理">1. &lt;strong>容器端口转发的工作原理&lt;/strong>
&lt;/h5>&lt;p>Docker 的端口转发通过 &lt;code>-p&lt;/code> 或 &lt;code>-P&lt;/code> 参数实现：&lt;/p>
&lt;ul>
&lt;li>&lt;code>-p &amp;lt;主机端口&amp;gt;:&amp;lt;容器端口&amp;gt;&lt;/code>：显式地将主机的某个端口映射到容器的某个端口。例如，&lt;code>-p 5980:5980&lt;/code> 会将主机的 5980 端口映射到容器的 5980 端口。&lt;/li>
&lt;li>&lt;code>-P&lt;/code>（大写）：自动将容器 Dockerfile 中通过 &lt;code>EXPOSE&lt;/code> 指令暴露的端口随机映射到主机的高位端口（通常是 32768 以上的端口）。&lt;/li>
&lt;/ul>
&lt;p>当你使用 &lt;code>-P&lt;/code> 参数时，Docker 会自动分配一个主机端口，而不是直接使用容器内的端口（比如 5980）。因此，访问 &lt;code>http://localhost:5980&lt;/code> 可能失败，因为主机的 5980 端口并未映射到容器的 5980 端口。&lt;/p>
&lt;p>&lt;strong>检查方法&lt;/strong>：
运行以下命令查看端口映射：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker port &amp;lt;容器名称或ID&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出会显示类似 &lt;code>0.0.0.0:49153-&amp;gt;5980/tcp&lt;/code> 的信息，表示容器内的 5980 端口被映射到主机的 49153 端口。你需要访问 &lt;code>http://localhost:49153&lt;/code> 而不是 &lt;code>http://localhost:5980&lt;/code>。&lt;/p>
&lt;p>&lt;strong>解决方法&lt;/strong>：
如果你希望明确映射到主机的 5980 端口，创建容器时使用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -p 5980:5980 &amp;lt;镜像名称&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样容器内的 5980 端口会直接映射到主机的 5980 端口。&lt;/p>
&lt;hr>
&lt;h5 id="2-ssl-配置导致的问题">2. &lt;strong>SSL 配置导致的问题&lt;/strong>
&lt;/h5>&lt;p>你提到后端服务启用了 SSL（HTTPS）。这意味着服务可能在容器内监听的是 HTTPS 协议（&lt;code>https://&lt;/code>），而不是 HTTP 协议（&lt;code>http://&lt;/code>）。如果你尝试访问 &lt;code>http://localhost:5980&lt;/code>，可能会因为协议不匹配而失败。&lt;/p>
&lt;p>&lt;strong>可能原因&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>服务在容器内运行在 HTTPS 模式（默认 443 端口或你配置的 5980 端口），但你尝试用 HTTP 访问。&lt;/li>
&lt;li>SSL 证书配置错误，导致服务无法正常响应。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>检查方法&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>确认服务是否使用 HTTPS：
&lt;ul>
&lt;li>检查你的后端服务配置文件（例如 Spring Boot 的 &lt;code>application.properties&lt;/code> 或 Node.js 的 HTTPS 配置），确认监听的协议和端口。&lt;/li>
&lt;li>如果服务监听的是 HTTPS，访问时需要使用 &lt;code>https://localhost:5980&lt;/code> 而不是 &lt;code>http://localhost:5980&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>检查容器内的服务是否正常运行：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> -it &amp;lt;容器名称或ID&amp;gt; bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>进入容器后，运行 &lt;code>netstat -tuln&lt;/code> 或 &lt;code>ss -tuln&lt;/code> 检查 5980 端口是否在监听。如果没有，说明服务未正确启动。&lt;/li>
&lt;li>检查 SSL 证书：
&lt;ul>
&lt;li>确保 SSL 证书和密钥文件已正确挂载到容器中（通过 &lt;code>-v&lt;/code> 挂载卷或 Dockerfile 复制）。&lt;/li>
&lt;li>确保证书有效（未过期，主机名匹配等）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>解决方法&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>如果服务使用 HTTPS，访问时使用 &lt;code>https://localhost:5980&lt;/code>。&lt;/li>
&lt;li>如果证书有问题，尝试临时禁用 SSL（用于测试），或确保证书正确配置。&lt;/li>
&lt;li>如果你希望继续使用 HTTP，检查服务是否支持 HTTP 模式（例如，修改配置文件将服务切换到 HTTP）。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h5 id="3-主机与容器的网络问题">3. &lt;strong>主机与容器的网络问题&lt;/strong>
&lt;/h5>&lt;p>Docker 容器默认运行在桥接网络（bridge network）中，主机和容器之间的网络通信需要正确配置。&lt;/p>
&lt;p>&lt;strong>可能原因&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>防火墙或安全组阻止了主机的 5980 端口（或随机映射的端口）。&lt;/li>
&lt;li>容器网络模式配置错误（例如使用了 &lt;code>host&lt;/code> 网络模式但未正确配置）。&lt;/li>
&lt;li>服务绑定了错误的网络接口（例如只绑定了 &lt;code>127.0.0.1&lt;/code>，而不是 &lt;code>0.0.0.0&lt;/code>）。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>检查方法&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>确认服务监听的 IP 地址：
&lt;ul>
&lt;li>进入容器，运行 &lt;code>netstat -tuln&lt;/code> 检查服务是否绑定到 &lt;code>0.0.0.0:5980&lt;/code>（接受所有接口的连接）或 &lt;code>127.0.0.1:5980&lt;/code>（仅接受本地连接）。&lt;/li>
&lt;li>如果绑定到 &lt;code>127.0.0.1&lt;/code>，容器外部无法访问。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>检查主机防火墙：
&lt;ul>
&lt;li>在 Linux 上运行 &lt;code>iptables -L&lt;/code> 或 &lt;code>ufw status&lt;/code> 检查是否阻止了 5980 端口。&lt;/li>
&lt;li>在 Windows/Mac 上，检查系统防火墙设置。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>检查 Docker 网络模式：
&lt;ul>
&lt;li>如果你使用了 &lt;code>--network host&lt;/code>，容器直接使用主机的网络，端口映射（&lt;code>-p&lt;/code> 或 &lt;code>-P&lt;/code>）无效，服务端口需要与主机一致。&lt;/li>
&lt;li>如果使用默认桥接网络，确保端口映射正确。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>解决方法&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>确保服务监听 &lt;code>0.0.0.0&lt;/code> 而不是 &lt;code>127.0.0.1&lt;/code>。在后端服务配置中修改监听地址（例如 Spring Boot 的 &lt;code>server.address=0.0.0.0&lt;/code>）。&lt;/li>
&lt;li>开放主机防火墙的相应端口：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo ufw allow &lt;span class="m">5980&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>如果不需要桥接网络，可以尝试使用 &lt;code>--network host&lt;/code> 运行容器：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run --network host &amp;lt;镜像名称&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>但注意这会绕过 Docker 的端口映射机制，容器直接使用主机端口。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h5 id="4-本机运行-vs-容器运行的差异">4. &lt;strong>本机运行 vs. 容器运行的差异&lt;/strong>
&lt;/h5>&lt;p>你提到在本机运行代码可以访问，但在容器中无法访问。以下是可能的原因：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>环境差异&lt;/strong>：
&lt;ul>
&lt;li>本机运行时，服务可能默认监听 HTTP 或绑定到 &lt;code>127.0.0.1&lt;/code>，而容器中可能配置不同。&lt;/li>
&lt;li>容器中的环境变量、配置文件或依赖可能与本机不一致。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>端口冲突&lt;/strong>：
&lt;ul>
&lt;li>如果主机上已有其他服务占用了 5980 端口，Docker 端口映射可能失败。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>证书路径问题&lt;/strong>：
&lt;ul>
&lt;li>容器内的 SSL 证书路径可能与本机不同，导致服务启动失败或无法响应 HTTPS 请求。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>检查方法&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>比较本机和容器的服务配置：
&lt;ul>
&lt;li>检查本机和容器中的配置文件（例如 &lt;code>application.properties&lt;/code> 或 &lt;code>config.json&lt;/code>）。&lt;/li>
&lt;li>确保环境变量（通过 &lt;code>docker run -e&lt;/code> 设置）一致。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>检查主机端口占用：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">netstat -tuln &lt;span class="p">|&lt;/span> grep &lt;span class="m">5980&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>如果 5980 端口已被占用，尝试使用其他端口映射（例如 &lt;code>-p 5981:5980&lt;/code>）。&lt;/li>
&lt;li>检查容器日志：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker logs &amp;lt;容器名称或ID&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>查看服务是否启动成功，是否有 SSL 或端口相关的错误。&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>解决方法&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>确保容器内的配置文件与本机一致。&lt;/li>
&lt;li>如果端口冲突，释放主机上的 5980 端口或使用其他端口映射。&lt;/li>
&lt;li>检查日志并修复启动错误（例如缺少依赖、证书路径错误等）。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h5 id="5-如何调试和解决">5. &lt;strong>如何调试和解决&lt;/strong>
&lt;/h5>&lt;p>按照以下步骤逐步排查：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>确认端口映射&lt;/strong>：
&lt;ul>
&lt;li>使用 &lt;code>docker port &amp;lt;容器名称或ID&amp;gt;&lt;/code> 查看实际映射的端口。&lt;/li>
&lt;li>尝试访问正确的映射端口（例如 &lt;code>http://localhost:&amp;lt;映射端口&amp;gt;&lt;/code> 或 &lt;code>https://localhost:&amp;lt;映射端口&amp;gt;&lt;/code>）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>检查服务状态&lt;/strong>：
&lt;ul>
&lt;li>进入容器，确认服务是否在 5980 端口监听，并检查绑定地址（&lt;code>0.0.0.0&lt;/code> 或 &lt;code>127.0.0.1&lt;/code>）。&lt;/li>
&lt;li>检查日志是否有错误。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>测试 HTTPS&lt;/strong>：
&lt;ul>
&lt;li>使用 &lt;code>curl -k https://localhost:5980&lt;/code>（&lt;code>-k&lt;/code> 忽略证书验证）测试是否能连接。&lt;/li>
&lt;li>如果失败，检查 SSL 配置或尝试 HTTP。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>尝试显式端口映射&lt;/strong>：
&lt;ul>
&lt;li>停止并删除当前容器，重新运行：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker stop &amp;lt;容器名称或ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm &amp;lt;容器名称或ID&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -p 5980:5980 &amp;lt;镜像名称&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>检查网络和防火墙&lt;/strong>：
&lt;ul>
&lt;li>确保主机防火墙允许 5980 端口。&lt;/li>
&lt;li>如果在云服务器上运行，检查安全组规则是否开放 5980 端口。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h5 id="6-总结">6. &lt;strong>总结&lt;/strong>
&lt;/h5>&lt;p>你无法访问 &lt;code>http://localhost:5980&lt;/code> 的主要原因可能是：&lt;/p>
&lt;ul>
&lt;li>使用 &lt;code>-P&lt;/code> 参数导致主机端口不是 5980（而是随机端口）。&lt;/li>
&lt;li>服务使用 HTTPS，但你尝试用 HTTP 访问。&lt;/li>
&lt;li>服务绑定了 &lt;code>127.0.0.1&lt;/code> 而不是 &lt;code>0.0.0.0&lt;/code>，或存在端口冲突。&lt;/li>
&lt;li>SSL 证书配置错误或容器环境与本机不一致。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>推荐步骤&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>使用 &lt;code>docker port&lt;/code> 检查实际映射端口，尝试访问正确的端口。&lt;/li>
&lt;li>确保访问协议正确（HTTP 或 HTTPS）。&lt;/li>
&lt;li>使用显式端口映射 &lt;code>-p 5980:5980&lt;/code> 重启容器。&lt;/li>
&lt;li>检查容器日志和服务配置，确保与本机运行一致。&lt;/li>
&lt;li>如果问题仍未解决，提供以下信息以便进一步分析：
&lt;ul>
&lt;li>你的 &lt;code>docker run&lt;/code> 完整命令。&lt;/li>
&lt;li>容器日志（&lt;code>docker logs &amp;lt;容器ID&amp;gt;&lt;/code>）。&lt;/li>
&lt;li>服务配置文件或代码片段（涉及端口和 SSL 配置的部分）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>如果需要更具体的帮助，请提供上述信息，我可以进一步协助你！&lt;/p>
&lt;h1 id="docker实战">docker实战
&lt;/h1>&lt;h3 id="docker使用实战-容器保存和迁移">Docker使用实战-容器保存和迁移
&lt;/h3>&lt;p>在 Docker 中，如果你想保存一个容器，可以将其保存为镜像（image），然后在需要时基于这个镜像重新创建容器。以下是具体步骤：&lt;/p>
&lt;h4 id="方法一将容器保存为镜像">方法一：将容器保存为镜像
&lt;/h4>&lt;ol>
&lt;li>
&lt;p>&lt;strong>查看正在运行的容器&lt;/strong>
使用以下命令列出当前运行的容器，找到你要保存的容器 ID 或名称：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker ps -a
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>提交容器为镜像&lt;/strong>
使用 &lt;code>docker commit&lt;/code> 命令将容器保存为一个新的镜像。假设你的容器 ID 是 &lt;code>abc123&lt;/code>，你想保存为镜像名称 &lt;code>myimage:latest&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker commit abc123 myimage:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>abc123&lt;/code> 是容器 ID 或容器名称。&lt;/li>
&lt;li>&lt;code>myimage:latest&lt;/code> 是你想保存的新镜像名称和标签。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>验证镜像&lt;/strong>
保存后，可以用以下命令查看新创建的镜像：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker images
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>（可选）保存镜像到文件&lt;/strong>
如果你想将镜像导出为一个 &lt;code>.tar&lt;/code> 文件以便备份或转移到其他机器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker save -o myimage.tar myimage:latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 或者&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将镜像传递到远程服务器 将 your-user@your-server 替换为你的 SSH 登录信息&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker save my-x86-app:1.0 &lt;span class="p">|&lt;/span> ssh your-user@your-server &lt;span class="s1">&amp;#39;docker load&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样会生成一个 &lt;code>myimage.tar&lt;/code> 文件。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>（可选）加载镜像&lt;/strong>
在其他地方使用时，可以通过以下命令加载 &lt;code>.tar&lt;/code> 文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker load -i myimage.tar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h4 id="方法二直接导出容器">方法二：直接导出容器
&lt;/h4>&lt;p>如果你不需要将其转化为镜像，而是想直接保存容器的完整状态（包括文件系统和配置），可以用 &lt;code>docker export&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">export&lt;/span> abc123 &amp;gt; container.tar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>这会将容器导出为一个 &lt;code>.tar&lt;/code> 文件。&lt;/li>
&lt;li>之后可以用 &lt;code>docker import&lt;/code> 导入这个文件为镜像：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker import container.tar myimage:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h4 id="注意事项-1">注意事项
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>&lt;code>docker commit&lt;/code> vs &lt;code>docker export&lt;/code>&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>docker commit&lt;/code> 保存的是容器的运行时状态为一个新镜像，适合需要保留容器修改的情况。&lt;/li>
&lt;li>&lt;code>docker export&lt;/code> 导出的只是文件系统快照，不包括容器的元数据（如 CMD、ENTRYPOINT 等）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>推荐方式&lt;/strong>：通常建议使用 &lt;code>docker commit&lt;/code> 并结合 Dockerfile 来管理镜像，这样更符合 Docker 的最佳实践。&lt;/li>
&lt;/ul>
&lt;h4 id="使用保存的镜像">使用保存的镜像
&lt;/h4>&lt;p>保存为镜像后，随时可以用以下命令基于镜像启动新容器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d myimage:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你有其他具体需求（比如保存到某个地方或自动化脚本），可以告诉我，我再帮你调整方案！&lt;/p>
&lt;h3 id="docker-服务部署教程">Docker 服务部署教程
&lt;/h3>&lt;p>在开始之前，我们先快速理解三个最重要的概念：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Docker&lt;/strong>：一个开源平台，用于开发、发布和运行应用程序。它允许你将应用程序及其所有依赖（库、环境变量、配置文件等）打包到一个标准化的单元中，这个单元被称为“容器”。&lt;/li>
&lt;li>&lt;strong>镜像 (Image)&lt;/strong>：一个只读的模板，包含了运行应用程序所需的所有内容——代码、运行时、库、环境变量和配置文件。你可以把镜像理解为安装程序或一个“蓝图”。&lt;/li>
&lt;li>&lt;strong>容器 (Container)&lt;/strong>：镜像的运行实例。当你运行一个镜像，你就启动了一个容器。容器是独立的、轻量级的，并且在你的主机操作系统上运行，但与主机和其他容器隔离。你可以同时运行同一个镜像的多个容器。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>简单比喻&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;code>Dockerfile&lt;/code> 是菜谱。&lt;/li>
&lt;li>&lt;code>镜像 (Image)&lt;/code> 是做好的、打包在盒子里的预制菜。&lt;/li>
&lt;li>&lt;code>容器 (Container)&lt;/code> 是你把预制菜放进微波炉加热后，正在运行、可以享用的那份饭菜。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="环境准备">环境准备
&lt;/h4>&lt;p>在开始之前，你需要在你的机器上安装 Docker。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Windows / macOS&lt;/strong>: 下载并安装 &lt;strong>Docker Desktop&lt;/strong>。它提供了一个图形化界面和命令行工具。
&lt;ul>
&lt;li>官方下载地址: &lt;code>https://www.docker.com/products/docker-desktop/&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Linux&lt;/strong>: 根据你的发行版（如 Ubuntu, CentOS）按照官方文档进行安装。
&lt;ul>
&lt;li>官方安装指南: &lt;code>https://docs.docker.com/engine/install/&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>安装完成后，打开你的终端（在 Windows 上是 PowerShell 或 WSL2，macOS 上是 Terminal），运行以下命令来验证 Docker 是否安装成功：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker --version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果命令能够成功执行并显示版本信息和一个空的容器列表，说明 Docker 已经准备就绪。&lt;/p>
&lt;hr>
&lt;h4 id="实战演练部署一个-python-web-应用">实战演练：部署一个 Python Web 应用
&lt;/h4>&lt;p>我们将部署一个简单的 Python Flask Web 应用，它会在访问时返回 &amp;ldquo;Hello, Docker!&amp;quot;。&lt;/p>
&lt;h5 id="步骤一准备应用程序">步骤一：准备应用程序
&lt;/h5>&lt;p>首先，在你喜欢的位置创建一个新的项目文件夹，例如 &lt;code>my-docker-app&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir my-docker-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> my-docker-app
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在该文件夹中，创建以下两个文件：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>&lt;code>app.py&lt;/code>&lt;/strong> (我们的 Web 应用程序代码)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">flask&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Flask&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建 Flask 应用实例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Flask&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 定义根路由&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@app.route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">hello&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;Hello, Docker! This is a simple web service.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># host=&amp;#39;0.0.0.0&amp;#39; 让容器外部可以访问&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># port=5000 是应用监听的端口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;__main__&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;0.0.0.0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">port&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5000&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>&lt;code>requirements.txt&lt;/code>&lt;/strong> (列出 Python 依赖)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Flask==2.2.2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>现在你的文件夹结构应该是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">my-docker-app/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── app.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── requirements.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="步骤二编写-dockerfile">步骤二：编写 Dockerfile
&lt;/h5>&lt;p>&lt;code>Dockerfile&lt;/code> 是一个文本文件，它包含了构建 Docker 镜像所需的所有指令。在 &lt;code>my-docker-app&lt;/code> 文件夹中创建名为 &lt;code>Dockerfile&lt;/code> (没有文件扩展名) 的文件。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 步骤 1: 选择一个基础镜像&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 我们选择一个官方的、轻量级的 Python 3.9 镜像&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> python:3.9-slim&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 步骤 2: 在镜像中设置工作目录&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 之后的所有命令都会在这个目录下执行&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 步骤 3: 复制依赖文件到工作目录&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 将 requirements.txt 复制到镜像的 /app 目录中&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> requirements.txt .&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 步骤 4: 安装应用程序的依赖&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 在镜像中运行 pip 命令来安装 Flask&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> pip install --no-cache-dir -r requirements.txt&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 步骤 5: 复制应用程序代码到工作目录&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 将当前目录下的所有文件 (app.py) 复制到镜像的 /app 目录&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> . .&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 步骤 6: 声明容器将监听的端口&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 这只是一个元数据声明，告诉用户这个容器内部的应用会使用 5000 端口&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">EXPOSE&lt;/span>&lt;span class="s"> 5000&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 步骤 7: 定义启动容器时要执行的命令&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 运行我们的 Python 应用&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;python&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;app.py&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="步骤三构建-docker-镜像">步骤三：构建 Docker 镜像
&lt;/h5>&lt;p>现在我们有了应用程序和 &lt;code>Dockerfile&lt;/code>，可以构建我们的镜像了。确保你的终端当前路径在 &lt;code>my-docker-app&lt;/code> 文件夹下。&lt;/p>
&lt;p>运行 &lt;code>docker build&lt;/code> 命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -t my-web-app:v1 给镜像起一个名字 (tag)，格式是 &amp;lt;name&amp;gt;:&amp;lt;version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># . 表示使用当前目录下的 Dockerfile&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker build -t my-web-app:v1 .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-11.png"
width="464"
height="181"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-11_hu16836429575141968351.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-11_hu11469463485972152702.png 1024w"
loading="lazy"
alt="构建"
class="gallery-image"
data-flex-grow="256"
data-flex-basis="615px"
>&lt;/p>
&lt;p>你会看到 Docker 按照 &lt;code>Dockerfile&lt;/code> 中的步骤逐一执行。构建成功后，你可以用以下命令查看你刚刚创建的镜像：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker images
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你应该能在列表中看到 &lt;code>my-web-app&lt;/code> 这个镜像。&lt;/p>
&lt;h5 id="步骤四运行-docker-容器">步骤四：运行 Docker 容器
&lt;/h5>&lt;p>镜像已经构建好了，现在让我们用它来启动一个容器。&lt;/p>
&lt;p>运行 &lt;code>docker run&lt;/code> 命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d -p 8888:5000 --name my-first-container my-web-app:v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>让我们分解一下这个命令：&lt;/p>
&lt;ul>
&lt;li>&lt;code>docker run&lt;/code>: 运行一个容器的命令。&lt;/li>
&lt;li>&lt;code>-d&lt;/code> (detached): 后台运行模式。容器将在后台启动并持续运行。&lt;/li>
&lt;li>&lt;code>-p 8888:5000&lt;/code> (port mapping): 端口映射。这是非常重要的一步。
&lt;ul>
&lt;li>它将&lt;strong>宿主机 (你的电脑)&lt;/strong> 的 &lt;code>8888&lt;/code> 端口映射到&lt;strong>容器&lt;/strong>的 &lt;code>5000&lt;/code> 端口。&lt;/li>
&lt;li>&lt;code>5000&lt;/code> 是我们在 &lt;code>app.py&lt;/code> 和 &lt;code>Dockerfile&lt;/code> 中指定的应用端口。&lt;/li>
&lt;li>&lt;code>8888&lt;/code> 是我们希望通过外部访问的端口。你可以换成其他未被占用的端口，如 &lt;code>80&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;code>--name my-first-container&lt;/code>: 给这个正在运行的容器起一个友好的名字，方便管理。&lt;/li>
&lt;li>&lt;code>my-web-app:v1&lt;/code>: 指定要运行的镜像。&lt;/li>
&lt;/ul>
&lt;h5 id="步骤五验证和管理">步骤五：验证和管理
&lt;/h5>&lt;ol>
&lt;li>
&lt;p>&lt;strong>验证服务&lt;/strong>
打开你的浏览器，访问 &lt;code>http://localhost:8888&lt;/code>。
你应该能看到页面上显示 &amp;ldquo;Hello, Docker! This is a simple web service.&amp;quot;。&lt;/p>
&lt;p>你也可以使用 &lt;code>curl&lt;/code> 命令在终端验证：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://localhost:8888
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>管理容器&lt;/strong>
以下是一些常用的容器管理命令：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>查看正在运行的容器&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>查看容器日志&lt;/strong> (非常适合排查问题)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker logs my-first-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>停止容器&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker stop my-first-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>重新启动已停止的容器&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker start my-first-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>删除容器&lt;/strong> (必须先停止容器才能删除)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker rm my-first-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>删除镜像&lt;/strong> (必须先删除所有使用该镜像的容器才能删除)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker rmi my-web-app:v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h4 id="进阶使用-docker-compose-编排服务">进阶：使用 Docker Compose 编排服务
&lt;/h4>&lt;p>当你的应用变得复杂，比如需要一个 Web 服务和一个数据库服务时，手动管理多个容器会很麻烦。&lt;code>Docker Compose&lt;/code> 就是解决这个问题的工具。它允许你使用一个 &lt;code>YAML&lt;/code> 文件来定义和运行多容器的 Docker 应用程序。&lt;/p>
&lt;h5 id="步骤一安装-docker-compose">步骤一：安装 Docker Compose
&lt;/h5>&lt;p>如果你安装的是 Docker Desktop (Windows/macOS)，那么 Docker Compose 已经包含在内了。你可以通过 &lt;code>docker-compose --version&lt;/code> 或 &lt;code>docker compose version&lt;/code> 来验证。&lt;/p>
&lt;h5 id="步骤二创建-docker-composeyml-文件">步骤二：创建 &lt;code>docker-compose.yml&lt;/code> 文件
&lt;/h5>&lt;p>在你的 &lt;code>my-docker-app&lt;/code> 文件夹中，创建一个名为 &lt;code>docker-compose.yml&lt;/code> 的文件。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># docker-compose.yml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 定义 Compose 文件的版本&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 定义服务&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 我们的 Web 应用服务&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 构建指令&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用当前目录下的 Dockerfile&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 镜像名称&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-web-app-compose:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 容器名称&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-web-app-container&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 端口映射&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8888:5000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 卷挂载 (可选，用于代码热更新)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 将本地代码目录挂载到容器的 /app 目录&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 这样修改本地代码后，服务会自动重启（需要 Flask 开启 debug 模式）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">.:/app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 环境变量&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">FLASK_ENV=development&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 开启 Flask 调试模式&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 假设我们还需要一个 Redis 服务&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 直接使用官方 Redis 镜像&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;redis:alpine&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-redis-cache&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h5 id="步骤三使用-docker-compose-启动服务">步骤三：使用 Docker Compose 启动服务
&lt;/h5>&lt;p>在 &lt;code>my-docker-app&lt;/code> 目录下，运行以下命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 启动所有服务 (-d 表示后台运行)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或者新版本命令&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># docker compose up -d&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Compose 会自动帮你：&lt;/p>
&lt;ol>
&lt;li>构建 &lt;code>web&lt;/code> 服务的镜像（如果尚未构建）。&lt;/li>
&lt;li>拉取 &lt;code>redis&lt;/code> 镜像。&lt;/li>
&lt;li>创建并启动 &lt;code>web&lt;/code> 和 &lt;code>redis&lt;/code> 两个容器，并根据配置建立网络连接。&lt;/li>
&lt;/ol>
&lt;h5 id="步骤四管理-compose-服务">步骤四：管理 Compose 服务
&lt;/h5>&lt;ul>
&lt;li>&lt;strong>查看服务状态&lt;/strong>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker-compose ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>查看日志&lt;/strong>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker-compose logs -f web &lt;span class="c1"># 查看 web 服务的日志&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>停止并删除所有服务&lt;/strong>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker-compose down
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h4 id="总结">总结
&lt;/h4>&lt;p>恭喜你！你已经学会了如何使用 Docker 部署一个单服务的 Web 应用，并了解了如何使用 Docker Compose 管理更复杂的应用场景。&lt;/p>
&lt;p>&lt;strong>核心流程回顾&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>写代码&lt;/strong> (&lt;code>app.py&lt;/code>, &lt;code>requirements.txt&lt;/code>)&lt;/li>
&lt;li>&lt;strong>写配置&lt;/strong> (&lt;code>Dockerfile&lt;/code>)&lt;/li>
&lt;li>&lt;strong>构建镜像&lt;/strong> (&lt;code>docker build&lt;/code>)&lt;/li>
&lt;li>&lt;strong>运行容器&lt;/strong> (&lt;code>docker run&lt;/code> 或 &lt;code>docker-compose up&lt;/code>)&lt;/li>
&lt;/ol>
&lt;p>Docker 的世界非常广阔，接下来你可以继续探索数据持久化（Volumes）、网络（Networking）、以及如何将你的镜像推送到 Docker Hub 与他人共享。&lt;/p>
&lt;h3 id="docker服务部署教程2-使用uv替代pip">Docker服务部署教程2-使用uv替代pip
&lt;/h3>&lt;p>好的，当然可以。使用 &lt;code>uv&lt;/code> 来替代 &lt;code>pip&lt;/code> 进行包管理是一个非常棒的选择，尤其是在 Docker 构建环境中，因为 &lt;code>uv&lt;/code> 的速度极快，可以显著缩短镜像构建的时间。&lt;/p>
&lt;p>改动主要集中在 &lt;code>Dockerfile&lt;/code> 文件中。你需要添加安装 &lt;code>uv&lt;/code> 的步骤，并用 &lt;code>uv pip install&lt;/code> 替换 &lt;code>pip install&lt;/code>。你的应用程序代码 (&lt;code>app.py&lt;/code>) 和依赖列表 (&lt;code>requirements.txt&lt;/code>) &lt;strong>完全不需要改变&lt;/strong>。&lt;/p>
&lt;p>下面是详细的说明和更新后的代码。&lt;/p>
&lt;h4 id="使用-uv-的核心改动">使用 UV 的核心改动
&lt;/h4>&lt;p>&lt;code>uv&lt;/code> 是一个独立的二进制文件，我们需要在 Docker 镜像中先安装它。最直接的方法是使用官方提供的安装脚本。&lt;/p>
&lt;p>更新后的 &lt;code>Dockerfile&lt;/code> 会包含以下几个新步骤：&lt;/p>
&lt;ol>
&lt;li>安装 &lt;code>curl&lt;/code> 工具，以便下载 &lt;code>uv&lt;/code> 安装脚本。&lt;/li>
&lt;li>运行 &lt;code>uv&lt;/code> 的安装脚本。&lt;/li>
&lt;li>将 &lt;code>uv&lt;/code> 的路径添加到环境变量 &lt;code>PATH&lt;/code> 中，以便后续命令可以找到它。&lt;/li>
&lt;li>使用 &lt;code>uv&lt;/code> 来安装依赖。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h4 id="更新后的-dockerfile-使用-uv">更新后的 &lt;code>Dockerfile&lt;/code> (使用 uv)
&lt;/h4>&lt;p>这是修改后的 &lt;code>Dockerfile&lt;/code>。你可以直接用它替换掉原来项目中的 &lt;code>Dockerfile&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Use a Python image with uv pre-installed&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> ghcr.io/astral-sh/uv:python3.12-bookworm-slim&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Set working directory&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Enable bytecode compilation&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">UV_COMPILE_BYTECODE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Copy from the cache instead of linking since it&amp;#39;s a mounted volume&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">UV_LINK_MODE&lt;/span>&lt;span class="o">=&lt;/span>copy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Copy requirements file first for better layer caching&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> requirements.txt .&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Install dependencies using uv with requirements.txt&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> --mount&lt;span class="o">=&lt;/span>&lt;span class="nv">type&lt;/span>&lt;span class="o">=&lt;/span>cache,target&lt;span class="o">=&lt;/span>/root/.cache/uv &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> uv pip install --system -r requirements.txt&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Copy application code&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> . .&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Expose port 5000 (Flask default)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">EXPOSE&lt;/span>&lt;span class="s"> 5000&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Reset the entrypoint, don&amp;#39;t invoke `uv`&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENTRYPOINT&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Run the Flask application&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;python&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;app.py&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>你的整体工作流程几乎不变，只是替换了 &lt;code>Dockerfile&lt;/code> 的内容。
之前使用gemini的教程但是报错了
&lt;img src="https://www.zata.cc/images/index/index-12.png"
loading="lazy"
alt="报错信息"
>
所以最好使用uv官方提供的镜像，具体使用教程可见&lt;/p>
&lt;p>&lt;span style="color:red;">&lt;a class="link" href="https://hellowac.github.io/uv-zh-cn/guides/integration/docker/" target="_blank" rel="noopener"
>在 Docker 中使用 uv&lt;/a>&lt;/span>&lt;/p>
&lt;h4 id="使用-uv-的完整流程">使用 UV 的完整流程
&lt;/h4>&lt;pre>&lt;code>```bash
docker build -t my-web-app:uv-latest .
```
（我们用了 `uv-latest` 这个新标签来区分版本）
在这一步，你会注意到 `RUN uv pip install` 这一层会比之前使用 `pip` 快非常多，尤其是在依赖项很多的情况下。
&lt;/code>&lt;/pre>
&lt;hr>
&lt;ol start="3">
&lt;li>
&lt;p>&lt;strong>运行容器（命令不变）&lt;/strong>
使用新的镜像标签来运行容器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d -p 8888:5000 --name my-uv-container my-web-app:uv-latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/images/index/index-13.png"
loading="lazy"
alt="结果"
>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>验证（方式不变）&lt;/strong>
访问 &lt;code>http://localhost:8888&lt;/code>，一切应该和之前一样正常工作。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h4 id="对于-docker-compose-呢">对于 Docker Compose 呢？
&lt;/h4>&lt;p>&lt;strong>你的 &lt;code>docker-compose.yml&lt;/code> 文件完全不需要任何改动。&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># docker-compose.yml (无需改动)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8888:5000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ... 其他配置&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这就是 Docker 的强大之处。&lt;code>docker-compose.yml&lt;/code> 只关心“做什么”（比如，从当前目录 &lt;code>.&lt;/code> 构建一个镜像），而不关心“怎么做”（具体是使用 &lt;code>pip&lt;/code> 还是 &lt;code>uv&lt;/code> 来安装依赖）。所有的实现细节都被封装在 &lt;code>Dockerfile&lt;/code> 里了。当你运行 &lt;code>docker-compose up&lt;/code> 时，Compose 会自动使用你更新后的 &lt;code>Dockerfile&lt;/code> 来构建镜像。&lt;/p>
&lt;h4 id="总结-1">总结
&lt;/h4>&lt;p>将包管理器从 &lt;code>pip&lt;/code> 切换到 &lt;code>uv&lt;/code>，你只需要：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>更新 &lt;code>Dockerfile&lt;/code>&lt;/strong> 以包含安装和使用 &lt;code>uv&lt;/code> 的指令。&lt;/li>
&lt;li>重新 &lt;strong>&lt;code>build&lt;/code>&lt;/strong> 你的镜像。&lt;/li>
&lt;/ol>
&lt;p>其他所有代码、文件和部署命令都保持原样，流程无缝衔接，同时还能享受到 &lt;code>uv&lt;/code> 带来的极致构建速度。&lt;/p>
&lt;h3 id="docker容器的部署教程3-将容器部署到服务器上">docker容器的部署教程3-将容器部署到服务器上
&lt;/h3>&lt;p>当你创建完 &lt;code>Dockerfile&lt;/code> 之后，部署一个应用通常遵循一个标准化的流程：&lt;strong>构建镜像 -&amp;gt; 推送镜像 -&amp;gt; 在目标环境运行容器&lt;/strong>。&lt;/p>
&lt;p>下面我将为你分解这个过程，并介绍从简单到复杂的不同部署方案。&lt;/p>
&lt;h4 id="核心流程">核心流程
&lt;/h4>&lt;h5 id="第一步构建-docker-镜像-build">第一步：构建 Docker 镜像 (Build)
&lt;/h5>&lt;p>这是将你的 &lt;code>Dockerfile&lt;/code> 和应用程序代码打包成一个标准化的、不可变的“Docker 镜像”的过程。&lt;/p>
&lt;p>在你的 &lt;code>Dockerfile&lt;/code> 所在的目录下，打开终端并运行以下命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># docker build -t &amp;lt;你的镜像名称&amp;gt;:&amp;lt;标签&amp;gt; .&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 示例:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker build -t my-awesome-app:v1.0 .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>docker build&lt;/code>: 构建命令。&lt;/li>
&lt;li>&lt;code>-t my-awesome-app:v1.0&lt;/code>: &lt;code>-t&lt;/code> 参数用来给镜像打上“标签”（tag），格式通常是 &lt;code>image_name:version&lt;/code>。这非常重要，便于版本管理。&lt;/li>
&lt;li>&lt;code>.&lt;/code>: 这个点表示 &lt;code>Dockerfile&lt;/code> 的上下文路径（Context Path）是当前目录。Docker 会把这个目录下的所有文件发送给 Docker 守护进程来帮助构建。&lt;/li>
&lt;/ul>
&lt;p>构建成功后，你可以通过 &lt;code>docker images&lt;/code> 命令查看你本地的所有镜像。&lt;/p>
&lt;h5 id="第二步在本地运行和测试-run-locally">第二步：在本地运行和测试 (Run Locally)
&lt;/h5>&lt;p>在推送到服务器之前，首先在本地运行容器以确保它能正常工作。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># docker run --rm -p &amp;lt;本地端口&amp;gt;:&amp;lt;容器端口&amp;gt; &amp;lt;你的镜像名称&amp;gt;:&amp;lt;标签&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 示例: 假设你的应用在容器内监听 8080 端口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run --rm -p 3000:8080 my-awesome-app:v1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>docker run&lt;/code>: 运行命令。&lt;/li>
&lt;li>&lt;code>--rm&lt;/code>: 容器停止后自动删除，适合测试。&lt;/li>
&lt;li>&lt;code>-p 3000:8080&lt;/code>: &lt;code>-p&lt;/code> 参数用来做端口映射（Port Mapping），将你本机的 &lt;code>3000&lt;/code> 端口映射到容器的 &lt;code>8080&lt;/code> 端口。这样你就可以通过访问 &lt;code>http://localhost:3000&lt;/code> 来访问你的应用了。&lt;/li>
&lt;li>&lt;code>my-awesome-app:v1.0&lt;/code>: 指定要运行的镜像。&lt;/li>
&lt;/ul>
&lt;h5 id="第三步推送镜像到镜像仓库-push-to-registry">第三步：推送镜像到镜像仓库 (Push to Registry)
&lt;/h5>&lt;p>为了在其他机器（例如你的云服务器）上使用这个镜像，你需要将它推送到一个中央存储库，这被称为“镜像仓库”（Image Registry）。&lt;/p>
&lt;p>最常用的公共仓库是 &lt;a class="link" href="https://hub.docker.com/" target="_blank" rel="noopener"
>Docker Hub&lt;/a>。各大云服务商也提供私有仓库，如 Google Artifact Registry (GCR), Amazon Elastic Container Registry (ECR), Azure Container Registry (ACR) 等。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>为镜像打上符合仓库要求的标签&lt;/strong>：
推送前，你需要将镜像重命名为 &lt;code>&amp;lt;你的仓库用户名&amp;gt;/&amp;lt;镜像名称&amp;gt;:&amp;lt;标签&amp;gt;&lt;/code> 的格式。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 格式: docker tag &amp;lt;本地镜像&amp;gt; &amp;lt;仓库用户名&amp;gt;/&amp;lt;新镜像名&amp;gt;:&amp;lt;标签&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 示例 (以 Docker Hub 为例):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker tag my-awesome-app:v1.0 your-dockerhub-username/my-awesome-app:v1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>登录你的镜像仓库&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 登录 Docker Hub&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 按照提示输入你的 Docker Hub 用户名和密码&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 然后这个页面不要关，下面推送的时候打开一个新的终端&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/images/index/index-15.png"
loading="lazy"
alt="保持这个界面"
>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>推送镜像&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 格式: docker push &amp;lt;仓库用户名&amp;gt;/&amp;lt;镜像名&amp;gt;:&amp;lt;标签&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 示例:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker push your-dockerhub-username/my-awesome-app:v1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/images/index/index-14.png"
loading="lazy"
alt="推送好的镜像"
>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h4 id="第四步选择部署方案-deploy">第四步：选择部署方案 (Deploy)
&lt;/h4>&lt;p>现在你的镜像已经准备就绪，可以部署了。以下是几种常见的部署方案，从易到难：&lt;/p>
&lt;hr>
&lt;h5 id="方案一在单台云服务器上手动部署-最简单">方案一：在单台云服务器上手动部署 (最简单)
&lt;/h5>&lt;p>这种方式适合个人项目、测试环境或小型应用。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>准备服务器&lt;/strong>：购买一台云服务器（如 GCP, AWS, Azure, 阿里云等），并确保已安装 Docker。&lt;/li>
&lt;li>&lt;strong>SSH 登录服务器&lt;/strong>：通过 SSH 客户端连接到你的服务器。&lt;/li>
&lt;li>&lt;strong>拉取镜像&lt;/strong>：在服务器上，从你之前推送的仓库中拉取镜像。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker pull your-dockerhub-username/my-awesome-app:v1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>运行容器&lt;/strong>：与在本地运行类似，但在服务器上通常使用 &lt;code>-d&lt;/code> 参数让容器在后台运行。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># -d (detached mode) 表示在后台运行容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d -p 80:8080 --name my-running-app your-dockerhub-username/my-awesome-app:v1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>这里我们将服务器的 &lt;code>80&lt;/code> 端口（HTTP 默认端口）映射到容器的 &lt;code>8080&lt;/code> 端口。现在，你可以通过服务器的 IP 地址直接访问你的应用。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h5 id="方案二使用-docker-compose-部署-推荐用于多容器应用">方案二：使用 Docker Compose 部署 (推荐用于多容器应用)
&lt;/h5>&lt;p>如果你的应用包含多个服务（例如一个 Web 应用 +一个数据库），使用 &lt;code>Docker Compose&lt;/code> 会让管理变得非常简单。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>创建 &lt;code>docker-compose.yml&lt;/code> 文件&lt;/strong>：在你的项目中创建一个 &lt;code>docker-compose.yml&lt;/code> 文件。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">your-dockerhub-username/my-awesome-app:v1.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;80:8080&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 容器挂掉后自动重启&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 如果有数据库，可以这样添加&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># db:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># image: postgres:13&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># environment:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># - POSTGRES_PASSWORD=mysecretpassword&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>部署&lt;/strong>：将 &lt;code>docker-compose.yml&lt;/code> 文件上传到你的服务器，然后在该文件所在的目录运行：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 以后台模式启动所有服务&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果需要更新镜像并重新部署&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-compose pull &lt;span class="c1"># 拉取 yml 文件中定义的所有镜像的最新版&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-compose up -d --force-recreate &lt;span class="c1"># 强制重新创建容器&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h5 id="方案三部署到云平台的容器服务-paasserverless">方案三：部署到云平台的容器服务 (PaaS/Serverless)
&lt;/h5>&lt;p>这种方式可以让你不用管理底层服务器，只需提供容器镜像，平台会自动为你扩缩容和管理。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Google Cloud Run&lt;/strong>：非常简单的无服务器平台。你只需将镜像推送到 Google Artifact Registry，然后在 Cloud Run 上创建一个服务，指向这个镜像即可。平台按需计费，没有请求时不收费。&lt;/li>
&lt;li>&lt;strong>AWS App Runner / Azure Container Apps&lt;/strong>：与 Google Cloud Run 类似的服务。&lt;/li>
&lt;li>&lt;strong>Heroku&lt;/strong>：传统的 PaaS 平台，也支持通过 Docker 部署。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>优点&lt;/strong>：免运维、自动扩缩容、按需付费。
&lt;strong>缺点&lt;/strong>：平台锁定性较强，自定义程度较低。&lt;/p>
&lt;hr>
&lt;h5 id="方案四使用容器编排工具-kubernetes">方案四：使用容器编排工具 (Kubernetes)
&lt;/h5>&lt;p>对于大型、复杂、高可用的生产系统，Kubernetes (K8s) 是事实上的标准。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>它是什么&lt;/strong>：一个强大的系统，用于自动化部署、扩展和管理容器化应用程序。&lt;/li>
&lt;li>&lt;strong>何时使用&lt;/strong>：当你的应用需要高可用性、自动伸缩、滚动更新、服务发现和负载均衡等高级功能时。&lt;/li>
&lt;li>&lt;strong>如何使用&lt;/strong>：
&lt;ol>
&lt;li>学习 Kubernetes 的基本概念（Pod, Deployment, Service 等）。&lt;/li>
&lt;li>编写 Kubernetes 的 YAML 配置文件来定义你的应用部署。&lt;/li>
&lt;li>使用云服务商提供的托管 Kubernetes 服务，如：
&lt;ul>
&lt;li>&lt;strong>Google Kubernetes Engine (GKE)&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Amazon Elastic Kubernetes Service (EKS)&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Azure Kubernetes Service (AKS)&lt;/strong>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>优点&lt;/strong>：极其强大和灵活，是微服务架构的最佳拍档。
&lt;strong>缺点&lt;/strong>：学习曲线非常陡峭，管理复杂。&lt;/p>
&lt;h4 id="如何选择">如何选择？
&lt;/h4>&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">&lt;strong>方案&lt;/strong>&lt;/th>
&lt;th style="text-align: left">&lt;strong>适用场景&lt;/strong>&lt;/th>
&lt;th style="text-align: left">&lt;strong>优点&lt;/strong>&lt;/th>
&lt;th style="text-align: left">&lt;strong>缺点&lt;/strong>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>手动部署&lt;/strong>&lt;/td>
&lt;td style="text-align: left">个人项目、学习、快速原型&lt;/td>
&lt;td style="text-align: left">简单直接，易于理解&lt;/td>
&lt;td style="text-align: left">手动操作多，不易扩展和管理&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>Docker Compose&lt;/strong>&lt;/td>
&lt;td style="text-align: left">多容器的单机应用、开发和测试环境&lt;/td>
&lt;td style="text-align: left">大大简化多容器管理&lt;/td>
&lt;td style="text-align: left">主要面向单机，集群能力弱&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>云平台容器服务&lt;/strong>&lt;/td>
&lt;td style="text-align: left">中小型应用、无服务器爱好者&lt;/td>
&lt;td style="text-align: left">极简运维，自动扩缩容&lt;/td>
&lt;td style="text-align: left">平台锁定，不够灵活&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>Kubernetes&lt;/strong>&lt;/td>
&lt;td style="text-align: left">生产级、大型复杂系统、微服务架构&lt;/td>
&lt;td style="text-align: left">功能强大，高可用，可移植性好&lt;/td>
&lt;td style="text-align: left">学习曲线陡峭，非常复杂&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>对于初学者，我强烈建议你从 &lt;code>方案一&lt;/code> 和 &lt;code>方案二&lt;/code> 开始。&lt;/strong> 当你对容器化部署有了更深入的理解后，再根据项目需求去探索 &lt;code>方案三&lt;/code> 和 &lt;code>方案四&lt;/code>。&lt;/p>
&lt;h3 id="运行容器了但是并没有生效-通过查看日志解决问题">运行容器了但是并没有生效-通过查看日志解决问题
&lt;/h3>&lt;p>我运行容器了，但是访问localhost:8888并没有生效&lt;/p>
&lt;p>好的，这是一个非常常见的问题，别担心，我们一步一步来排查。访问 &lt;code>localhost:8888&lt;/code> 不生效通常有以下几个原因。&lt;/p>
&lt;p>请打开你的终端，按顺序执行下面的命令来定位问题。&lt;/p>
&lt;h4 id="第一步检查容器是否正在运行">&lt;strong>第一步：检查容器是否正在运行&lt;/strong>
&lt;/h4>&lt;p>首先，我们需要确认你的容器是否成功启动并且正在运行。&lt;/p>
&lt;p>运行这个命令，它会列出所有正在运行的容器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>你应该会看到类似这样的输出：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">a1b2c3d4e5f6 my-web-app:v1 &amp;#34;python app.py&amp;#34; 2 minutes ago Up 2 minutes 0.0.0.0:8888-&amp;gt;5000/tcp my-first-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>请检查几件事：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>你的容器在列表里吗？&lt;/strong>
&lt;ul>
&lt;li>&lt;strong>如果不在&lt;/strong>，说明容器启动失败或者已经退出了。请运行 &lt;code>docker ps -a&lt;/code> (这会显示所有容器，包括已停止的)，找到你的容器 (&lt;code>my-first-container&lt;/code>)，然后查看它的 &lt;code>STATUS&lt;/code>。如果显示 &lt;code>Exited&lt;/code>，请直接跳到下面的 &lt;strong>第二步&lt;/strong>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>&lt;code>STATUS&lt;/code> 是否是 &lt;code>Up&lt;/code>？&lt;/strong>
&lt;ul>
&lt;li>如果状态是 &lt;code>Up&lt;/code>，说明容器正在运行，这是个好消息。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>&lt;code>PORTS&lt;/code> 是否正确？&lt;/strong>
&lt;ul>
&lt;li>请仔细检查 &lt;code>PORTS&lt;/code> 这一列。它&lt;strong>必须显示&lt;/strong> &lt;code>0.0.0.0:8888-&amp;gt;5000/tcp&lt;/code> 或类似的映射关系。这表示主机的 8888 端口已成功映射到容器的 5000 端口。如果这里是空的或者不正确，说明你运行 &lt;code>docker run&lt;/code> 命令时 &lt;code>-p&lt;/code> 参数可能有误。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h4 id="第二步查看容器日志">&lt;strong>第二步：查看容器日志&lt;/strong>
&lt;/h4>&lt;p>如果容器没有运行 (状态是 &lt;code>Exited&lt;/code>)，或者虽然在运行但服务依然无法访问，那么最重要的一步就是查看日志。日志会告诉我们应用程序在容器内部发生了什么。&lt;/p>
&lt;p>运行以下命令来获取容器的日志：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker logs my-first-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/images/index/index-12.png"
loading="lazy"
alt="报错日志"
>&lt;/p>
&lt;p>我这里看到这个错误，于是去&lt;a class="link" href="#from-werkzeugurls-import-url_quote-%e9%97%ae%e9%a2%98%e6%a0%b9%e6%ba%90" >from-werkzeugurls-import-url_quote-问题根源&lt;/a>&lt;/p>
&lt;p>&lt;strong>分析日志输出：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>情况A：看到错误信息 (Error)&lt;/strong>
&lt;ul>
&lt;li>如果你在日志中看到任何像 &lt;code>SyntaxError&lt;/code>, &lt;code>ModuleNotFoundError&lt;/code>, &lt;code>ImportError&lt;/code> 或其他 Python 错误堆栈信息，说明是你的 &lt;code>app.py&lt;/code> 代码有问题。请检查代码是否和教程中的完全一致。最常见的错误是 &lt;code>pip install&lt;/code> 失败或者代码缩进错误。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>情况B：看到 Flask 正常的启动信息&lt;/strong>
&lt;ul>
&lt;li>如果你看到类似下面的信息，说明 Flask 应用本身是正常启动的：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> * Serving Flask app &amp;#39;app&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * Running on http://0.0.0.0:5000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * Press CTRL+C to quit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>如果日志正常，但你依然无法访问，问题很可能出在 &lt;strong>端口映射&lt;/strong> 或 &lt;strong>网络/防火墙&lt;/strong> 上。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="第三步常见问题和解决方案汇总">&lt;strong>第三步：常见问题和解决方案汇总&lt;/strong>
&lt;/h4>&lt;p>根据以上检查，这里是问题排查的清单：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>问题：容器启动后立即退出 (&lt;code>Exited&lt;/code>)。&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>原因&lt;/strong>：应用代码有错误。&lt;/li>
&lt;li>&lt;strong>解决&lt;/strong>：运行 &lt;code>docker logs my-first-container&lt;/code> 查看具体错误，修改 &lt;code>app.py&lt;/code> 或 &lt;code>requirements.txt&lt;/code>。修改后，你需要&lt;strong>重新构建镜像&lt;/strong>并&lt;strong>重新运行容器&lt;/strong>。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 停止并删除旧容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm my-first-container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 重新构建镜像&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker build -t my-web-app:v1 .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 重新运行容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d -p 8888:5000 --name my-first-container my-web-app:v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>问题：容器在运行 (&lt;code>Up&lt;/code>)，但 &lt;code>docker ps&lt;/code> 的 &lt;code>PORTS&lt;/code> 列是空的。&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>原因&lt;/strong>：运行容器时忘记了 &lt;code>-p 8888:5000&lt;/code> 参数。&lt;/li>
&lt;li>&lt;strong>解决&lt;/strong>：停止并删除现有容器，然后使用正确的 &lt;code>docker run&lt;/code> 命令重新运行。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker stop my-first-container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm my-first-container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d -p 8888:5000 --name my-first-container my-web-app:v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>问题：容器在运行，端口映射也正确，日志也正常，但就是无法访问。&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>原因1&lt;/strong>：你电脑的 &lt;code>8888&lt;/code> 端口可能被其他程序占用了（比如另一个开发服务）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>解决1&lt;/strong>：尝试换一个主机端口，比如 &lt;code>8889&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker stop my-first-container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm my-first-container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -d -p 8889:5000 --name my-first-container my-web-app:v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后访问 &lt;code>http://localhost:8889&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>原因2&lt;/strong>：防火墙或安全软件。公司电脑或一些安全软件可能会阻止端口访问。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>解决2&lt;/strong>：暂时关闭防火墙或安全软件进行测试。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>原因3 (特别是 Windows 用户)&lt;/strong>：如果你在使用一些旧版本的 Docker 或者网络配置特殊的虚拟机，&lt;code>localhost&lt;/code> 可能无法直接映射。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>解决3&lt;/strong>：尝试使用 &lt;code>127.0.0.1:8888&lt;/code> 访问。如果还不行，可以找到 Docker 虚拟机的 IP 地址来访问。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>请按照这个顺序排查，99% 的问题都能通过检查 &lt;strong>容器状态&lt;/strong> 和 &lt;strong>容器日志&lt;/strong> 来解决。如果你能把 &lt;code>docker ps&lt;/code> 和 &lt;code>docker logs my-first-container&lt;/code> 的输出结果贴出来，我可以给你更精确的诊断。&lt;/p>
&lt;hr>
&lt;h4 id="from-werkzeugurls-import-url_quote-问题根源">from werkzeug.urls import url_quote 问题根源
&lt;/h4>&lt;p>你遇到的错误是 &lt;code>ImportError: cannot import name 'url_quote' from 'werkzeug.urls'&lt;/code>。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>简单解释&lt;/strong>：你的应用程序（Flask）依赖于另一个名为 &lt;code>Werkzeug&lt;/code> 的底层库。你安装的 Flask 版本 (2.2.2) 需要从 &lt;code>Werkzeug&lt;/code> 库中导入一个叫做 &lt;code>url_quote&lt;/code> 的函数。然而，&lt;code>pip&lt;/code> 在安装依赖时，为你安装了一个&lt;strong>非常新&lt;/strong>的 &lt;code>Werkzeug&lt;/code> 版本（3.0 或更高版本），而在这个新版本中，&lt;code>url_quote&lt;/code> 这个函数已经被移除了，从而导致了 &lt;code>ImportError&lt;/code>（导入错误）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>为什么会发生？&lt;/strong>：在你的 &lt;code>requirements.txt&lt;/code> 文件中，你只指定了 &lt;code>Flask==2.2.2&lt;/code>。&lt;code>pip&lt;/code> 在安装它时，会去自动安装 Flask 所需的依赖（比如 Werkzeug），并且通常会选择最新的可用版本。这就导致了 Flask (旧) 和 Werkzeug (新) 之间的不兼容。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h4 id="解决方案">解决方案
&lt;/h4>&lt;p>解决方法很简单：我们需要在 &lt;code>requirements.txt&lt;/code> 中明确指定一个与 &lt;code>Flask==2.2.2&lt;/code> 兼容的 &lt;code>Werkzeug&lt;/code> 版本。Werkzeug 2.x 版本是兼容的。&lt;/p>
&lt;p>请按照以下步骤操作：&lt;/p>
&lt;h5 id="第一步修改-requirementstxt">&lt;strong>第一步：修改 &lt;code>requirements.txt&lt;/code>&lt;/strong>
&lt;/h5>&lt;p>打开你的项目文件夹中的 &lt;code>requirements.txt&lt;/code> 文件，将其内容修改为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Flask==2.2.2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Werkzeug==2.2.2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>为什么要这样做？&lt;/strong>
通过“锁定”或“固定”&lt;code>Werkzeug&lt;/code> 的版本，我们确保了每次构建 Docker 镜像时，安装的都是经过测试、相互兼容的库版本组合。这正是 Docker 实现环境一致性和可复现性的核心思想之一。&lt;/p>
&lt;hr>
&lt;h5 id="第二步停止并删除旧的容器">&lt;strong>第二步：停止并删除旧的容器&lt;/strong>
&lt;/h5>&lt;p>由于之前的容器是基于错误的镜像创建的，我们需要先清理它。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 停止正在运行的容器（如果它还在尝试运行的话）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stop my-first-container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除已停止的容器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker rm my-first-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/images/index/index-13.png"
loading="lazy"
alt="删除容器"
>&lt;/p>
&lt;blockquote>
&lt;p>如果命令提示 &amp;ldquo;No such container&amp;rdquo;，说明容器已经停止并被移除了，这没关系，直接进行下一步即可。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h5 id="第三步重新构建-docker-镜像">&lt;strong>第三步：重新构建 Docker 镜像&lt;/strong>
&lt;/h5>&lt;p>由于我们修改了 &lt;code>requirements.txt&lt;/code>，镜像的构建指令已经发生了变化，所以必须重新构建镜像。Docker 会很智能地利用缓存，只重新执行发生变化的步骤（即 &lt;code>RUN pip install&lt;/code> 之后的步骤）。&lt;/p>
&lt;p>在你的项目文件夹中，运行 &lt;code>build&lt;/code> 命令：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker build -t my-web-app:v1 .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>&lt;code>![重构Docker镜像](images/index/index-14.png)
&lt;/code>&lt;/pre>
&lt;hr>
&lt;h5 id="第四步重新运行容器">&lt;strong>第四步：重新运行容器&lt;/strong>
&lt;/h5>&lt;p>现在，使用新构建的、包含了正确库版本的镜像来启动容器。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d -p 8888:5000 --name my-first-container my-web-app:v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>&lt;img src="https://www.zata.cc/images/index/index-15.png"
loading="lazy"
alt="启动容器"
>&lt;/p>
&lt;h5 id="第五步再次验证">&lt;strong>第五步：再次验证&lt;/strong>
&lt;/h5>&lt;p>等待几秒钟让容器启动，然后再次访问 &lt;code>http://localhost:8888&lt;/code>。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/images/index/index-16.png"
loading="lazy"
alt="问题解决"
>&lt;/p>
&lt;p>这次，你应该就能成功看到 &amp;ldquo;Hello, Docker! This is a simple web service.&amp;rdquo; 的消息了。&lt;/p>
&lt;p>这个错误完美地展示了为什么在 &lt;code>requirements.txt&lt;/code> 中锁定依赖版本是如此重要，尤其是在使用 Docker 创建可复现、无意外的环境时。&lt;/p>
&lt;h3 id="wsl中镜像网络与docker网络冲突使用netsh-winsock-reset修改网络设置之后docker连不上----解决">wsl中镜像网络与docker网络冲突，使用netsh winsock reset修改网络设置之后，docker连不上 &amp;ndash; 解决
&lt;/h3>&lt;p>报错信息
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-11.png"
width="464"
height="181"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-11_hu16836429575141968351.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-11_hu11469463485972152702.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="256"
data-flex-basis="615px"
>&lt;/p>
&lt;p>解决方法，将wsl setting的网络调到默认的nat模式，然后重置网络
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image.png"
width="464"
height="181"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image_hu16836429575141968351.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image_hu11469463485972152702.png 1024w"
loading="lazy"
alt="wsl setting"
class="gallery-image"
data-flex-grow="256"
data-flex-basis="615px"
>
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-1.png"
width="660"
height="620"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-1_hu8898885243080334072.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-1_hu1052763918837491881.png 1024w"
loading="lazy"
alt="重置网络"
class="gallery-image"
data-flex-grow="106"
data-flex-basis="255px"
>&lt;/p>
&lt;p>然后就ok了&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-2.png"
width="1113"
height="702"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-2_hu13279574493942317322.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-2_hu6858458842162574348.png 1024w"
loading="lazy"
alt="网络ok"
class="gallery-image"
data-flex-grow="158"
data-flex-basis="380px"
>&lt;/p>
&lt;h3 id="具有ssl证书的后端在docker环境中出现问题">具有SSL证书的后端在docker环境中出现问题
&lt;/h3>&lt;p>首先，后端服务是放在dokcer容器中 0.0.0.0上的，然后看vscode的映射，最后记得改成https（第一次加载超级慢，而且可能连接失败）
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-4.png"
width="3348"
height="504"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-4_hu1734424590945960189.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-4_hu10606547435160311554.png 1024w"
loading="lazy"
alt="后端服务"
class="gallery-image"
data-flex-grow="664"
data-flex-basis="1594px"
>
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-6.png"
width="3600"
height="780"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-6_hu7078629779953246620.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-6_hu893367478247058634.png 1024w"
loading="lazy"
alt="vscode映射"
class="gallery-image"
data-flex-grow="461"
data-flex-basis="1107px"
>
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-5.png"
width="5104"
height="2742"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-5_hu5225827151997963938.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-5_hu1221934025227137583.png 1024w"
loading="lazy"
alt="在本机https访问"
class="gallery-image"
data-flex-grow="186"
data-flex-basis="446px"
>&lt;/p>
&lt;p>如果使用http就无法访问
&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-8.png"
width="5104"
height="2744"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-8_hu8223364434973822633.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-8_hu1243765635687842984.png 1024w"
loading="lazy"
alt="如果使用http就无法访问"
class="gallery-image"
data-flex-grow="186"
data-flex-basis="446px"
>&lt;/p>
&lt;p>尽量还是不要在容器里面调试https吧&lt;/p>
&lt;p>然后我试了127.0.0.1&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-7.png"
width="5104"
height="2742"
srcset="https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-7_hu9965555966883908243.png 480w, https://www.zata.cc/p/docker%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-7_hu191126954655620681.png 1024w"
loading="lazy"
alt="同样的访问地址不可以"
class="gallery-image"
data-flex-grow="186"
data-flex-basis="446px"
>&lt;/p></description></item><item><title>2-github action 使用</title><link>https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/</link><pubDate>Mon, 03 Mar 2025 00:00:00 +0800</pubDate><guid>https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/</guid><description>&lt;img src="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/images/index/index.png" alt="Featured image of post 2-github action 使用" />&lt;p>参考：&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://github.com/tech-shrimp/GithubActionSample" target="_blank" rel="noopener"
>https://github.com/tech-shrimp/GithubActionSample&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html" target="_blank" rel="noopener"
>https://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html&lt;/a> [LXF,挺有用]&lt;/li>
&lt;/ul>
&lt;h3 id="常用知识">常用知识
&lt;/h3>&lt;h3 id="基础概念">&lt;strong>基础概念&lt;/strong>
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>Workflow（工作流）&lt;/strong>&lt;br>
工作流是一个自动化过程，定义在一或多个 YAML 文件中，存放在 &lt;code>.github/workflows&lt;/code> 目录下。一个项目可以有多个工作流，他们顺序执行，你也可以在一个工作流里面引用执行另一个工作流&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Event（事件）&lt;/strong>&lt;br>
触发工作流的事件，例如 &lt;code>push&lt;/code>（推送代码）、&lt;code>pull_request&lt;/code>（拉取请求）或定时触发。也可以手动执行，&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Job（任务）&lt;/strong>&lt;br>
工作流中的一组步骤（Steps），运行在同一个虚拟机上。多个 Job 可以并行或按顺序执行。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Step（步骤）&lt;/strong>&lt;br>
Job 中的单个操作，例如运行命令或调用一个 Action。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Action（动作）&lt;/strong>&lt;br>
可重用的代码单元，通常由社区或官方提供，简化常见任务。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Runner（运行器）&lt;/strong>&lt;br>
执行工作流的服务器，GitHub 提供托管的 Runner，也支持自托管。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>共享的action&lt;/strong>
很多操作在不同项目里面是类似的，完全可以共享。GitHub 注意到了这一点，想出了一个很妙的点子，允许开发者把每个操作写成独立的脚本文件，存放到代码仓库，使得其他开发者可以引用。&lt;/p>
&lt;p>如果你需要某个 action，不必自己写复杂的脚本，直接引用他人写好的 action 即可，整个持续集成过程，就变成了一个 actions 的组合。这就是 GitHub Actions 最特别的地方。&lt;/p>
&lt;p>GitHub 做了一个官方市场 &lt;a class="link" href="https://github.com/marketplace?type=actions" target="_blank" rel="noopener"
>https://github.com/marketplace?type=actions&lt;/a> ，可以搜索到他人提交的 actions。另外，还有一个 awesome actions 的仓库 &lt;a class="link" href="https://github.com/sdras/awesome-actions" target="_blank" rel="noopener"
>https://github.com/sdras/awesome-actions&lt;/a> ，也可以找到不少 action。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index.jpg"
width="1912"
height="990"
srcset="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index_hu1281008539772498063.jpg 480w, https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index_hu4829532998228493744.jpg 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="193"
data-flex-basis="463px"
>
每个 action 就是一个独立脚本，因此可以做成代码仓库，使用userName/repoName的语法引用 action。比如，actions/setup-node就表示github.com/actions/setup-node这个仓库，它代表一个 action，作用是安装 Node.js。事实上，GitHub 官方的 actions 都放在 github.com/actions 里面。
既然 actions 是代码仓库，当然就有版本的概念，用户可以引用某个具体版本的 action。下面都是合法的 action 引用，用的就是 Git 的指针概念，详见官方文档。https://docs.github.com/en/actions/sharing-automations/creating-actions/about-custom-actions#versioning-your-action&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">actions/setup-node@74bc508 # 指向一个 commit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actions/setup-node@v1.0 # 指向一个标签
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actions/setup-node@master # 指向一个分支
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>仓库密钥和环境密钥的区别，以及组织密钥&lt;/p>
&lt;p>参考：&lt;/p>
&lt;p>&lt;a class="link" href="https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions" target="_blank" rel="noopener"
>https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index-1.jpg"
width="1912"
height="954"
srcset="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index-1_hu4397505417618076307.jpg 480w, https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index-1_hu8228239392819969285.jpg 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="481px"
>&lt;/p>
&lt;p>仓库密钥：（仓库下面的所有分支和标签可用）&lt;/p>
&lt;ul>
&lt;li>仓库密钥是在 GitHub 仓库级别设置的，用于保护仓库的访问权限和敏感信息。&lt;/li>
&lt;li>仓库密钥可以在仓库的“Settings” &amp;gt; “Secrets” &amp;gt; “Actions”中设置。&lt;/li>
&lt;li>仓库密钥只能在仓库内使用，不能跨仓库共享。&lt;/li>
&lt;/ul>
&lt;p>环境密钥：（可设置哪些分支可用）&lt;/p>
&lt;ul>
&lt;li>环境密钥是在 GitHub 分支级别设置的，可以通过设置去设置哪些分支可用&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index-2.jpg"
width="1912"
height="954"
srcset="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index-2_hu14917312720165822342.jpg 480w, https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index-2_hu11430220340789479472.jpg 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="200"
data-flex-basis="481px"
>&lt;/p>
&lt;p>组织密钥：（可以设置哪些仓库可用）&lt;/p>
&lt;p>&lt;code>另外，在工作流里面是打印不出来密钥的&lt;/code>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="构建画爱心为可执行程序">构建画爱心为可执行程序
&lt;/h3>&lt;p>原始代码效果：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index.gif"
width="646"
height="516"
srcset="https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index_hu11815783179957955532.gif 480w, https://www.zata.cc/p/2-github-action-%E4%BD%BF%E7%94%A8/image/index/index_hu12728063288137864306.gif 1024w"
loading="lazy"
alt="画爱心效果"
class="gallery-image"
data-flex-grow="125"
data-flex-basis="300px"
>&lt;/p>
&lt;p>代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 版权https://github.com/royalneverwin/beating-heart&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">tkinter&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="c1"># 导入tkinter库，用于创建GUI界面&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">math&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">sin&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cos&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">pi&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">log&lt;/span> &lt;span class="c1"># 导入数学函数，用于心形计算&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">random&lt;/span> &lt;span class="c1"># 导入随机数模块，用于生成随机点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">time&lt;/span> &lt;span class="c1"># 导入时间模块（当前代码未使用，但保留）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 定义画布的宽度和高度&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CANVAS_WIDTH&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">640&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CANVAS_HEIGHT&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">480&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 计算画布中心坐标&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CANVAS_CENTER_X&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">CANVAS_WIDTH&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CANVAS_CENTER_Y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">CANVAS_HEIGHT&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 心形放大倍数&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">IMAGE_ENLARGE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">11&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">scatter_inside&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">beta&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.15&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 在心形内部生成散点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;使用对数分布在给定点附近生成内部散点&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ratiox&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">beta&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">random&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="c1"># 对数分布计算x方向偏移量，可调整beta值改变分布&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ratioy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">beta&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">random&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="c1"># 对数分布计算y方向偏移量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ratiox&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_X&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 计算x方向的实际偏移&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ratioy&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_Y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 计算y方向的实际偏移&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">dx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">dy&lt;/span> &lt;span class="c1"># 返回散点的新坐标&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">heart_function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">enlarge_ratio&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">float&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">IMAGE_ENLARGE&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 定义心形曲线方程&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;根据参数t生成心形曲线的x,y坐标&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 心形曲线参数方程&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">16&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">sin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">**&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">13&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">cos&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">cos&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">cos&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">cos&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 放大心形&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span> &lt;span class="o">*=&lt;/span> &lt;span class="n">enlarge_ratio&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">y&lt;/span> &lt;span class="o">*=&lt;/span> &lt;span class="n">enlarge_ratio&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 将心形移动到画布中心&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">CANVAS_CENTER_X&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">y&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">CANVAS_CENTER_Y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 返回整数坐标&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">shrink&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ratio&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 向心形中心收缩点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;根据距离中心点的平方反比计算收缩量&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sk_range&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_X&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">**&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_Y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">**&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 平方反比因子&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ratio&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">sk_range&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_X&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># x方向收缩量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ratio&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">sk_range&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_Y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># y方向收缩量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">dx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">dy&lt;/span> &lt;span class="c1"># 返回收缩后的坐标&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">Heart&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">frame&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 初始化心形类&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">points&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">set&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 存储心形曲线上的点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edge_points&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">set&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 存储边缘散点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">inside_points&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">set&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 存储内部散点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">all_points&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span> &lt;span class="c1"># 存储每一帧的所有点坐标&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">build&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2000&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 构建初始点集，可调整点的数量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">frame&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">frame&lt;/span> &lt;span class="c1"># 动画的总帧数&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">f&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">frame&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 预计算每一帧的点位置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">calc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">f&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">random_halo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1000&lt;/span> &lt;span class="c1"># 用于光晕效果的随机点数量（未直接使用）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">build&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">number&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 生成初始点集&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;在心形上生成随机点及其边缘和内部散点&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 在心形曲线上随机生成number个点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">_&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">number&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">t&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">uniform&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">pi&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 随机生成角度t&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">heart_function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 计算心形上的点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">shrink&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 向中心收缩点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">points&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">)))&lt;/span> &lt;span class="c1"># 添加到点集&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 生成边缘散点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">px&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">py&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">points&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">_&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 每个点生成3个边缘散点，可调整数量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">scatter_inside&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">px&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">py&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.05&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 在点附近生成散点，可调整beta值&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edge_points&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 添加到边缘点集&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 生成内部散点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pt_ls&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">points&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 将点集转为列表以便随机选择&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">_&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">4000&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 生成4000个内部散点，可调整数量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">choice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pt_ls&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 随机选择一个基础点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">scatter_inside&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 在其附近生成散点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">inside_points&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 添加到内部点集&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">cal_position&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ratio&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 计算点在跳动时的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;根据距离中心的平方反比计算跳动幅度&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">bt_range&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_X&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">**&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_Y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">**&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 跳动因子&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ratio&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">bt_range&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_X&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># x方向位移&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ratio&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">bt_range&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">CANVAS_CENTER_Y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># y方向位移&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">dx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">dy&lt;/span> &lt;span class="c1"># 返回跳动后的位置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">calc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">frame&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 计算每一帧的点位置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;预计算每帧中所有点的动态位置&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ratio&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">800&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">sin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">frame&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">pi&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 跳动幅度随正弦变化，30fps，可调整&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">all_pts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span> &lt;span class="c1"># 存储当前帧所有点的列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 生成光晕效果&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">halo_radius&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">4&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">6&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">sin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">frame&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">pi&lt;/span>&lt;span class="p">)))&lt;/span> &lt;span class="c1"># 光晕半径随时间变化&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">halo_number&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3000&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">4000&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nb">abs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">frame&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">10&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">pi&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">**&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 光晕点数量&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">heart_halo_point&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">set&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 存储光晕点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">_&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">halo_number&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">t&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">uniform&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">pi&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 随机角度&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">heart_function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">enlarge_ratio&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">11.6&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 生成光晕基础点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">shrink&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">halo_radius&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 向中心收缩&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">heart_halo_point&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1"># 避免重复&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">heart_halo_point&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">14&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">14&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 添加随机偏移&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">y&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">14&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">14&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">choice&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 随机选择点大小&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">all_pts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1"># 添加到点列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 计算心形曲线上的点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">points&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cal_position&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ratio&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 计算跳动位置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 随机点大小，可调整&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">all_pts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 计算边缘点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edge_points&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cal_position&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ratio&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 随机点大小，可调整&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">all_pts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 计算内部点&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">inside_points&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cal_position&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ratio&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">random&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">randint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 随机点大小，可调整&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">all_pts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">all_points&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">frame&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">all_pts&lt;/span> &lt;span class="c1"># 保存当前帧的点数据&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">canvas&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">frame&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 渲染点到画布上&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;在画布上绘制当前帧的所有点&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">all_points&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">frame&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">frame&lt;/span>&lt;span class="p">]:&lt;/span> &lt;span class="c1"># 循环取帧数据&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 绘制矩形点，颜色为粉红色&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">canvas&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_rectangle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">width&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fill&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;#ff7171&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">draw&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Tk&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">canvas&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Canvas&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">heart&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Heart&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">frame&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># 动画绘制函数&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;递归调用实现心形跳动动画&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">canvas&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;all&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 清空画布&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">heart&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">canvas&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">frame&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 绘制当前帧&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 每30毫秒刷新一次，相当于约33fps&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">root&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">after&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">draw&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">canvas&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">heart&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">frame&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;__main__&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1"># 主程序入口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">root&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Tk&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 创建主窗口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">root&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;漂亮宝贝一周年快乐&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 设置窗口标题&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 创建画布，背景黑色&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">canvas&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Canvas&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">bg&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;black&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">height&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">CANVAS_HEIGHT&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">width&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">CANVAS_WIDTH&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">canvas&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pack&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 将画布添加到窗口&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">heart&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Heart&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 创建心形对象，帧数设为20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">draw&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">canvas&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">heart&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 开始动画&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">root&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mainloop&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 进入主循环，显示窗口&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后创建一个.github/workflows文件夹，并在其中创建一个love_heart.yml文件。文件内容如下填写,功能是创建一个windows的可执行程序&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># This workflow will install Python dependencies, run tests and lint with a single version of Python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 这是一个 GitHub Actions 工作流，用于安装 Python 依赖、运行测试和代码检查，使用单一 Python 版本&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 更多信息请参考上述 GitHub 文档链接&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">画爱心Windows版 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 工作流名称，显示在 GitHub Actions 界面上，表示这是一个在 Windows 上构建“画爱心”程序的工作流&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 定义触发工作流的事件&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflow_dispatch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 手动触发事件，允许用户通过 GitHub 界面手动运行此工作流&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">permissions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 设置工作流的权限&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">contents&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">read &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 授予读取仓库内容的权限，确保工作流可以访问代码&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 定义工作流中的任务&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pyinstaller-build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 任务名称，表示使用 PyInstaller 构建可执行文件&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">windows-latest &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定运行环境为最新的 Windows 系统&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 定义任务的具体步骤&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Create Executable &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 步骤名称，表示创建可执行文件&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sayyid5416/pyinstaller@v1 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用社区提供的 PyInstaller Action，版本为 v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 配置 Action 的参数&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">python_ver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.12&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定使用的 Python 版本为 3.12&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;love_heart.py&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定 PyInstaller 的构建配置文件或主 Python 文件为 love_heart.py&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">upload_exe_with_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;love_heart&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定上传的可执行文件名称为 love_heart&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">options&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>--&lt;span class="l">onefile, --name &amp;#34;love_heart&amp;#34;, --windowed, &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># PyInstaller 的构建选项&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --onefile: 将所有内容打包为单个可执行文件（生成 .exe 文件）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --name &amp;#34;love_heart&amp;#34;: 设置输出可执行文件的名称为 love_heart（最终为 love_heart.exe）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --windowed: 生成无控制台窗口的 GUI 应用程序，适合 Windows 上的图形界面程序&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 注意：选项之间用逗号分隔，最后一个选项后不需要逗号，但此处保留原始格式&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="快速入门教程">&lt;strong>快速入门教程&lt;/strong>
&lt;/h3>&lt;h4 id="目标">&lt;strong>目标&lt;/strong>
&lt;/h4>&lt;p>创建一个简单的 GitHub Actions 工作流，在每次推送代码时运行一个脚本，输出 &amp;ldquo;Hello, GitHub Actions!&amp;quot;。&lt;/p>
&lt;h4 id="步骤">&lt;strong>步骤&lt;/strong>
&lt;/h4>&lt;ol>
&lt;li>
&lt;p>&lt;strong>创建仓库&lt;/strong>&lt;br>
在 GitHub 上创建一个新仓库（或使用现有仓库）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>添加工作流文件&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>在仓库根目录下创建文件夹 &lt;code>.github/workflows&lt;/code>。&lt;/li>
&lt;li>在该文件夹中创建一个文件，例如 &lt;code>hello.yml&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>编写工作流文件&lt;/strong>&lt;br>
将以下内容写入 &lt;code>hello.yml&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Hello World Workflow&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 触发条件：在推送代码到 main 分支时运行&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># 定义任务&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">say-hello&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用 GitHub 提供的最新 Ubuntu 虚拟机&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Checkout code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v3&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 检出代码到虚拟机&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Say Hello&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">echo &amp;#34;Hello, GitHub Actions!&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 运行简单的 shell 命令&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>提交文件&lt;/strong>&lt;br>
将 &lt;code>hello.yml&lt;/code> 提交到仓库的 &lt;code>main&lt;/code> 分支。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>查看运行结果&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>转到仓库的 &lt;strong>Actions&lt;/strong> 标签页。&lt;/li>
&lt;li>你会看到名为 &amp;ldquo;Hello World Workflow&amp;rdquo; 的工作流正在运行。&lt;/li>
&lt;li>点击工作流名称，查看日志，确认 &amp;ldquo;Hello, GitHub Actions!&amp;rdquo; 已输出。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="进阶示例">&lt;strong>进阶示例&lt;/strong>
&lt;/h3>&lt;h4 id="运行测试并部署">&lt;strong>运行测试并部署&lt;/strong>
&lt;/h4>&lt;p>假设你有一个 Node.js 项目，想在推送代码时运行测试并部署到服务器。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>工作流文件示例&lt;/strong>&lt;br>
创建 &lt;code>.github/workflows/test-and-deploy.yml&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Test and Deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Checkout code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Setup Node.js&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/setup-node@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">node-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;16&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Install dependencies&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">npm install&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Run tests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">npm test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">needs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 在 test 任务成功后运行&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Checkout code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy to server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> echo &amp;#34;Deploying to server...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> # 这里可以添加实际部署命令，例如通过 SSH&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>添加秘密（Secrets）&lt;/strong>&lt;br>
如果部署需要敏感信息（如 SSH 密钥）：&lt;/p>
&lt;ul>
&lt;li>转到仓库的 &lt;strong>Settings &amp;gt; Secrets and variables &amp;gt; Actions&lt;/strong>。&lt;/li>
&lt;li>点击 &lt;strong>New repository secret&lt;/strong>，添加密钥（如 &lt;code>SSH_KEY&lt;/code>）。&lt;/li>
&lt;li>在工作流中通过 &lt;code>secrets.SSH_KEY&lt;/code> 使用它。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="常用配置">&lt;strong>常用配置&lt;/strong>
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>触发事件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 推送时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 拉取请求时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schedule&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 定时触发（每天凌晨0点）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">cron&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;0 0 * * *&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>设置环境变量&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MY_VAR&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Hello&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">example&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">echo $MY_VAR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 输出 &amp;#34;Hello&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>使用社区 Action&lt;/strong>&lt;br>
示例：使用 &lt;code>actions/cache&lt;/code> 缓存依赖：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Cache node modules&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/cache@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">~/.npm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ runner.os }}-node-${{ hashFiles(&amp;#39;**/package-lock.json&amp;#39;) }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="调试技巧">&lt;strong>调试技巧&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>查看日志&lt;/strong>：在 Actions 标签页点击工作流，展开每个 Job 和 Step 查看详细输出。&lt;/li>
&lt;li>&lt;strong>添加调试步骤&lt;/strong>：在步骤中运行 &lt;code>ls -la&lt;/code>（Linux）或 &lt;code>dir&lt;/code>（Windows）检查文件。&lt;/li>
&lt;li>&lt;strong>使用 GitHub 提供的测试工具&lt;/strong>：在本地安装 &lt;code>act&lt;/code>（需要 Docker）模拟运行工作流。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="常见问题">&lt;strong>常见问题&lt;/strong>
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>工作流没触发？&lt;/strong>&lt;br>
检查 &lt;code>on&lt;/code> 配置是否匹配事件和分支。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>权限不足？&lt;/strong>&lt;br>
在仓库设置中确认 Actions 已启用（Settings &amp;gt; Actions &amp;gt; General）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>需要更多计算资源？&lt;/strong>&lt;br>
考虑使用自托管 Runner。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h3 id="扩展阅读">&lt;strong>扩展阅读&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>官方文档：https://docs.github.com/en/actions&lt;/li>
&lt;li>社区 Actions：https://github.com/marketplace?type=actions&lt;/li>
&lt;/ul>
&lt;h3 id="on字段">on字段
&lt;/h3>&lt;p>在 GitHub Actions 的 &lt;code>on&lt;/code> 字段中，你可以定义触发工作流的事件。&lt;code>workflow_dispatch&lt;/code> 是其中一种触发方式，表示手动触发。除了 &lt;code>workflow_dispatch&lt;/code>，还有许多其他触发事件可以选择，具体取决于你的需求。以下是 &lt;code>on&lt;/code> 可以填写的参数（触发事件类型），以及它们的用途和可能配置项：&lt;/p>
&lt;hr>
&lt;h3 id="on-可填写的触发事件类型">&lt;code>on&lt;/code> 可填写的触发事件类型
&lt;/h3>&lt;h4 id="1-workflow_dispatch手动触发">1. &lt;code>workflow_dispatch&lt;/code>（手动触发）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>作用&lt;/strong>：允许用户通过 GitHub 界面手动触发工作流。&lt;/li>
&lt;li>&lt;strong>可选配置&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>inputs&lt;/code>：定义手动触发时可以输入的参数。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>示例&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflow_dispatch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">inputs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;输入你的名字&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 是否必填&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Anonymous&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 默认值&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;选择版本&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">choice &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 类型可以是 string, boolean, choice 等&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">options&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;v1&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;v2&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>用途&lt;/strong>：适合需要用户干预或自定义参数的场景，例如手动选择构建版本。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="2-push推送事件">2. &lt;code>push&lt;/code>（推送事件）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>作用&lt;/strong>：当代码被推送到指定分支或路径时触发。&lt;/li>
&lt;li>&lt;strong>可选配置&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>branches&lt;/code>：指定触发分支。&lt;/li>
&lt;li>&lt;code>branches-ignore&lt;/code>：忽略特定分支。&lt;/li>
&lt;li>&lt;code>paths&lt;/code>：指定触发文件路径。&lt;/li>
&lt;li>&lt;code>paths-ignore&lt;/code>：忽略特定文件路径。&lt;/li>
&lt;li>&lt;code>tags&lt;/code>：指定触发标签。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>示例&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;feature/*&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 支持通配符&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">paths&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;src/**&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 只在 src 目录下文件变更时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;v*&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>用途&lt;/strong>：适合每次代码提交时自动构建或测试。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="3-pull_request拉取请求事件">3. &lt;code>pull_request&lt;/code>（拉取请求事件）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>作用&lt;/strong>：当拉取请求（PR）被创建、更新或合并时触发。&lt;/li>
&lt;li>&lt;strong>可选配置&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>branches&lt;/code>：目标分支。&lt;/li>
&lt;li>&lt;code>types&lt;/code>：触发的事件类型（如 &lt;code>opened&lt;/code>, &lt;code>synchronize&lt;/code>, &lt;code>closed&lt;/code>）。&lt;/li>
&lt;li>&lt;code>paths&lt;/code>：触发文件路径。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>示例&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">types&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">opened &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># PR 创建时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">synchronize &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># PR 更新时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">paths&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;*.py&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 只在 Python 文件变更时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>用途&lt;/strong>：用于 PR 的自动化检查或构建。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="4-schedule定时触发">4. &lt;code>schedule&lt;/code>（定时触发）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>作用&lt;/strong>：按计划时间触发，使用 cron 表达式。&lt;/li>
&lt;li>&lt;strong>可选配置&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>cron&lt;/code>：定义运行时间（UTC）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>示例&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schedule&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">cron&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;0 0 * * *&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 每天 UTC 00:00 触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">cron&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;*/15 * * * *&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 每 15 分钟触发一次&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>用途&lt;/strong>：适合定时任务，例如每天构建或检查。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="5-release发布事件">5. &lt;code>release&lt;/code>（发布事件）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>作用&lt;/strong>：当 GitHub 发布（release）相关操作发生时触发。&lt;/li>
&lt;li>&lt;strong>可选配置&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>types&lt;/code>：触发类型（如 &lt;code>published&lt;/code>, &lt;code>created&lt;/code>, &lt;code>edited&lt;/code>）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>示例&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">release&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">types&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">published &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 发布时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">prereleased &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 预发布时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>用途&lt;/strong>：用于发布版本时的自动化构建或部署。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="6-issues问题事件">6. &lt;code>issues&lt;/code>（问题事件）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>作用&lt;/strong>：当 GitHub Issues 相关操作发生时触发。&lt;/li>
&lt;li>&lt;strong>可选配置&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>types&lt;/code>：触发类型（如 &lt;code>opened&lt;/code>, &lt;code>closed&lt;/code>, &lt;code>labeled&lt;/code>）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>示例&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issues&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">types&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">opened &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 新建 issue 时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">labeled &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 添加标签时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>用途&lt;/strong>：适合与问题管理相关的自动化任务。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="7-workflow_run工作流运行事件">7. &lt;code>workflow_run&lt;/code>（工作流运行事件）
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>作用&lt;/strong>：当另一个工作流完成时触发当前工作流。&lt;/li>
&lt;li>&lt;strong>可选配置&lt;/strong>：
&lt;ul>
&lt;li>&lt;code>workflows&lt;/code>：指定监听的工作流名称。&lt;/li>
&lt;li>&lt;code>types&lt;/code>：触发类型（如 &lt;code>completed&lt;/code>）。&lt;/li>
&lt;li>&lt;code>branches&lt;/code>：触发分支。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>示例&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflow_run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflows&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Build&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 监听名为 Build 的工作流&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">types&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">completed &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Build 完成后触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>用途&lt;/strong>：用于工作流间的依赖关系，例如先构建再部署。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="8-其他事件">8. 其他事件
&lt;/h4>&lt;ul>
&lt;li>&lt;strong>&lt;code>create&lt;/code>&lt;/strong>：当创建分支或标签时触发。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">create&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;feature/*&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>&lt;code>delete&lt;/code>&lt;/strong>：当删除分支或标签时触发。&lt;/li>
&lt;li>&lt;strong>&lt;code>fork&lt;/code>&lt;/strong>：当仓库被 fork 时触发。&lt;/li>
&lt;li>&lt;strong>&lt;code>watch&lt;/code>&lt;/strong>：当用户点击“Star”时触发。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">watch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">types&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">started &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 用户 star 仓库时触发&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="组合使用">组合使用
&lt;/h4>&lt;p>你可以组合多个事件类型。例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflow_dispatch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，工作流会在推送、拉取请求或手动触发时运行。&lt;/p>
&lt;hr>
&lt;h4 id="参数总结表">参数总结表
&lt;/h4>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>触发事件&lt;/th>
&lt;th>主要配置项&lt;/th>
&lt;th>示例用途&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>workflow_dispatch&lt;/code>&lt;/td>
&lt;td>&lt;code>inputs&lt;/code>&lt;/td>
&lt;td>手动构建，带自定义参数&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>push&lt;/code>&lt;/td>
&lt;td>&lt;code>branches&lt;/code>, &lt;code>paths&lt;/code>, &lt;code>tags&lt;/code>&lt;/td>
&lt;td>代码提交时构建&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>pull_request&lt;/code>&lt;/td>
&lt;td>&lt;code>branches&lt;/code>, &lt;code>types&lt;/code>, &lt;code>paths&lt;/code>&lt;/td>
&lt;td>PR 检查&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>schedule&lt;/code>&lt;/td>
&lt;td>&lt;code>cron&lt;/code>&lt;/td>
&lt;td>定时任务&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>release&lt;/code>&lt;/td>
&lt;td>&lt;code>types&lt;/code>&lt;/td>
&lt;td>发布版本&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>issues&lt;/code>&lt;/td>
&lt;td>&lt;code>types&lt;/code>&lt;/td>
&lt;td>Issue 管理&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>workflow_run&lt;/code>&lt;/td>
&lt;td>&lt;code>workflows&lt;/code>, &lt;code>types&lt;/code>, &lt;code>branches&lt;/code>&lt;/td>
&lt;td>工作流依赖&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>create&lt;/code>&lt;/td>
&lt;td>&lt;code>branches&lt;/code>&lt;/td>
&lt;td>创建分支或标签&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h4 id="针对你的需求">针对你的需求
&lt;/h4>&lt;p>你的工作流目前使用 &lt;code>workflow_dispatch&lt;/code>，适合手动触发构建“画爱心”程序。如果需要其他触发方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>自动构建&lt;/strong>：用 &lt;code>push&lt;/code> 或 &lt;code>pull_request&lt;/code>，在代码变更时触发。&lt;/li>
&lt;li>&lt;strong>定时构建&lt;/strong>：用 &lt;code>schedule&lt;/code>，每天生成新版本。&lt;/li>
&lt;li>&lt;strong>发布版本&lt;/strong>：用 &lt;code>release&lt;/code>，在发布时生成可执行文件。&lt;/li>
&lt;/ul>
&lt;p>如果你想具体调整（例如添加 &lt;code>push&lt;/code> 或 &lt;code>schedule&lt;/code>），可以告诉我，我帮你写出对应配置！&lt;/p></description></item><item><title>VScode使用教程|cursor使用教程</title><link>https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</link><pubDate>Mon, 03 Mar 2025 00:00:00 +0800</pubDate><guid>https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</guid><description>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index.png" alt="Featured image of post VScode使用教程|cursor使用教程" />&lt;p>参考：
&lt;a class="link" href="https://blog.csdn.net/weixin_46474921/article/details/132841711" target="_blank" rel="noopener"
>https://blog.csdn.net/weixin_46474921/article/details/132841711&lt;/a>&lt;/p>
&lt;h2 id="安装及设置">安装及设置
&lt;/h2>&lt;h3 id="1-下载安装">1. 下载安装
&lt;/h3>&lt;p>&lt;a class="link" href="https://code.visualstudio.com/" target="_blank" rel="noopener"
>VScode官网&lt;/a>
注意，这一步最好全部打勾&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index.png"
width="698"
height="571"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index_hu13589672253225225047.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index_hu17280254825820459526.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="122"
data-flex-basis="293px"
>&lt;/p>
&lt;h3 id="2-设置默认terminal为cmd">2. 设置默认terminal为cmd
&lt;/h3>&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-1.png"
width="713"
height="435"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-1_hu11700358320837873619.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-1_hu16803957676085418169.png 1024w"
loading="lazy"
alt="设置terminal"
class="gallery-image"
data-flex-grow="163"
data-flex-basis="393px"
>&lt;/p>
&lt;h3 id="自动fetch远程分支">自动fetch远程分支
&lt;/h3>&lt;p>Git 默认不会自动从远程拉取状态更新。只有当你显式运行 &lt;code>git fetch&lt;/code> 或 &lt;code>git pull&lt;/code> 时，本地仓库才会更新远程分支引用。如果你希望在 VS Code / Cursor 中自动感知远程分支变化，需要开启自动 fetch：&lt;/p>
&lt;p>&lt;strong>设置路径&lt;/strong>：&lt;code>Settings&lt;/code> → 搜索 &lt;code>git.autofetch&lt;/code> → 勾选启用&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-5.png"
width="587"
height="624"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-5_hu10781715172386046384.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-5_hu15959515978697210561.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="94"
data-flex-basis="225px"
>&lt;/p>
&lt;p>建议同时设置自动 fetch 间隔（默认 3 分钟）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;git.autofetch&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;git.autofetchPeriod&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">180&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>注意&lt;/strong>：如果仓库的 remote 名不是默认的 &lt;code>origin&lt;/code>，VS Code 的 Git 插件和 GitHub Pull Requests 插件可能无法正确识别上下文。需要在 &lt;code>settings.json&lt;/code> 中显式配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;githubPullRequests.remotes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;zata&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;origin&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;upstream&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配置完成后执行 &lt;code>Cmd+Shift+P&lt;/code> → &lt;code>Developer: Reload Window&lt;/code> 生效。&lt;/p>
&lt;h3 id="设置文件自动保存">设置文件自动保存
&lt;/h3>&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-4.png"
width="538"
height="187"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-4_hu2159568048718872866.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-4_hu1715730031400021464.png 1024w"
loading="lazy"
alt="设置文件自动保存"
class="gallery-image"
data-flex-grow="287"
data-flex-basis="690px"
>&lt;/p>
&lt;h3 id="vscode右侧的预览窗口设置">vscode右侧的预览窗口设置
&lt;/h3>&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-5.png"
width="1276"
height="1083"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-5_hu17948736909585048686.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-5_hu2407808058440598551.png 1024w"
loading="lazy"
alt="预览窗口"
class="gallery-image"
data-flex-grow="117"
data-flex-basis="282px"
>&lt;/p>
&lt;p>设置方法，在设置里面搜索minimap&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-6.png"
width="1228"
height="855"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-6_hu17946443210414639711.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-6_hu6861750511434885091.png 1024w"
loading="lazy"
alt="设置显示预览窗口"
class="gallery-image"
data-flex-grow="143"
data-flex-basis="344px"
>&lt;/p>
&lt;h3 id="vscode写markdown插入图片时放在指定目录">vscode写markdown插入图片时放在指定目录
&lt;/h3>&lt;p>参考 &lt;a class="link" href="https://juejin.cn/post/7244809769794289721" target="_blank" rel="noopener"
>https://juejin.cn/post/7244809769794289721&lt;/a>&lt;/p>
&lt;p>打开粘贴选项&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/index/PixPin_2025-04-28_10-21-02.png"
width="1005"
height="90"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/index/PixPin_2025-04-28_10-21-02_hu11822395765945102914.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/index/PixPin_2025-04-28_10-21-02_hu16843890278771082285.png 1024w"
loading="lazy"
alt="Edit-Paste As：Enable 勾选"
class="gallery-image"
data-flex-grow="1116"
data-flex-basis="2680px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-7.png"
width="1437"
height="624"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-7_hu16272182988011740176.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-7_hu3191450440497687134.png 1024w"
loading="lazy"
alt="vscode写markdown插入图片时放在指定目录"
class="gallery-image"
data-flex-grow="230"
data-flex-basis="552px"
>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">**/*.md images/&lt;span class="si">${&lt;/span>&lt;span class="nv">documentDirName&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">fileName&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="c1"># 以原始文件名放到 ./assets/&amp;lt;md文件名&amp;gt;/&amp;lt;图片文件名&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">**/*.md images/&lt;span class="si">${&lt;/span>&lt;span class="nv">documentBaseName&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">documentBaseName&lt;/span>&lt;span class="si">}&lt;/span>.&lt;span class="si">${&lt;/span>&lt;span class="nv">fileExtName&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="c1"># 重新以md文件名命名图片名&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">**/*.md images/&lt;span class="si">${&lt;/span>&lt;span class="nv">documentBaseName&lt;/span>&lt;span class="si">}&lt;/span>/image.&lt;span class="si">${&lt;/span>&lt;span class="nv">fileExtName&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="c1"># 以image.png重命名放到images/文件名 文件夹下&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="vscode折叠代码-ctrlk-ctrl0">vscode折叠代码 ctrl+k ctrl+0
&lt;/h3>&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-8.png"
width="1023"
height="737"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-8_hu790163729665904948.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-8_hu16773904108429831075.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="138"
data-flex-basis="333px"
>&lt;/p>
&lt;h3 id="diff-editor-settings">Diff Editor settings
&lt;/h3>&lt;ol>
&lt;li>取消相同的代码被折叠&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image.png"
width="3486"
height="1548"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image_hu15111586778798503878.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image_hu13581365640553951591.png 1024w"
loading="lazy"
alt="相同代码被折叠"
class="gallery-image"
data-flex-grow="225"
data-flex-basis="540px"
>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-1.png"
width="3220"
height="1670"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-1_hu11128250374553231081.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-1_hu7488541314914223334.png 1024w"
loading="lazy"
alt="不打勾不折叠"
class="gallery-image"
data-flex-grow="192"
data-flex-basis="462px"
>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-2.png"
width="3478"
height="1430"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-2_hu3865723076554555921.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-2_hu18287158195393333397.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="243"
data-flex-basis="583px"
>&lt;/p>
&lt;ol start="2">
&lt;li>diff 双栏变一栏&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-3.png"
width="3994"
height="852"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-3_hu1537511311543917897.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-3_hu9718360637936353412.png 1024w"
loading="lazy"
alt="单次设置"
class="gallery-image"
data-flex-grow="468"
data-flex-basis="1125px"
>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-4.png"
width="2998"
height="1650"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-4_hu18099045732909483448.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/image-4_hu14523416898402501905.png 1024w"
loading="lazy"
alt="默认设置"
class="gallery-image"
data-flex-grow="181"
data-flex-basis="436px"
>&lt;/p>
&lt;hr>
&lt;hr>
&lt;hr>
&lt;h2 id="vscode-插件">vscode 插件
&lt;/h2>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">GitLG&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">Office&lt;/span> &lt;span class="err">Viewer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">Markdown&lt;/span> &lt;span class="err">Preview&lt;/span> &lt;span class="err">Mermaid&lt;/span> &lt;span class="err">Support&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>
&lt;p>GitLG
&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/PixPin_2025-10-22_11-54-38.png"
width="1025"
height="973"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/PixPin_2025-10-22_11-54-38_hu6910770450734727336.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/PixPin_2025-10-22_11-54-38_hu4297854560165279744.png 1024w"
loading="lazy"
alt="GitLG"
class="gallery-image"
data-flex-grow="105"
data-flex-basis="252px"
>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>office viewer
&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-1.png"
width="1008"
height="976"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-1_hu2590265366357976703.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-1_hu7616321620605304573.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="103"
data-flex-basis="247px"
>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;code>但是有一个非常严重的问题，就是说如果安装了office viewer 会导致vscode自己的image paste失效&lt;/code>&lt;/p>
&lt;p>不过我发现一个解决方案，就是改下配置，然后不要用ctrl+v粘贴，而是用鼠标右键然后paste
&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-2.png"
width="866"
height="357"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-2_hu4878059622758473231.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index-2_hu16003774483311875165.png 1024w"
loading="lazy"
alt="setting json"
class="gallery-image"
data-flex-grow="242"
data-flex-basis="582px"
>&lt;br>
&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index.gif"
width="1576"
height="1001"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index_hu9055154505544676963.gif 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index_hu4374803445859308386.gif 1024w"
loading="lazy"
alt="paste"
class="gallery-image"
data-flex-grow="157"
data-flex-basis="377px"
>&lt;/p>
&lt;p>当然，如果想支持更多办公文档的查看，那么可以一步到位，直接安装office viewer(Markdown Editor)。但这个插件有一个坑点，就是会更改markdown文件的格式，所以安装之后，可以取消对markdown文件的默认开启方式。方法很简单，只需右键单击一个markdown文件，选择打开方式，在命令栏中选择最下面的为*.md配置默认编辑器，最后点击文本编辑器就可以了。&lt;/p>
&lt;p>此外，这个插件内嵌了一个主题，所以安装之后界面的颜色可能会发生变化，不必惊慌，重新选择一个主题就可以了。&lt;/p>
&lt;ol start="3">
&lt;li>Markdown Preview Mermaid Support（作者：Matt Bierner）
特点：这是下载量最高、最基础的 Mermaid 插件。安装后，它会无缝集成到 VS Code 原生的 Markdown 预览功能中。
用法：在 .md 文件中输入 ```mermaid 代码块，然后点击 VS Code 右上角的“预览”按钮（或快捷键 Ctrl+Shift+V / Cmd+Shift+V），就能直接在右侧看到渲染出的图表。&lt;/li>
&lt;/ol>
&lt;h2 id="遇到的问题和解决方案">遇到的问题和解决方案
&lt;/h2>&lt;h3 id="vscode-一直-reactivatiing-terminals">vscode 一直 reactivatiing terminals
&lt;/h3>&lt;p>这个是由于python扩展找不到虚拟环境的问题，具体可以看
&lt;a class="link" href="https://stackoverflow.com/questions/78886125/vscode-python-extension-loading-forever-saying-reactivating-terminals/78886126#78886126" target="_blank" rel="noopener"
>https://stackoverflow.com/questions/78886125/vscode-python-extension-loading-forever-saying-reactivating-terminals/78886126#78886126&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-9.png"
width="328"
height="118"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-9_hu680122498402490842.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-9_hu13953229298804032371.png 1024w"
loading="lazy"
alt="图片显示reactivatiing terminals"
class="gallery-image"
data-flex-grow="277"
data-flex-basis="667px"
>&lt;/p>
&lt;p>我的解决方法是把python Locator换成js&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-10.png"
width="933"
height="706"
srcset="https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-10_hu1943000486665289403.png 480w, https://www.zata.cc/p/vscode%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8Bcursor%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image/index/index-10_hu3580115981823081284.png 1024w"
loading="lazy"
alt="把python Locator换成js"
class="gallery-image"
data-flex-grow="132"
data-flex-basis="317px"
>&lt;/p>
&lt;h3 id="安装工具包之后桌面cmd窗口可用但是vscodecursor不可用">安装工具包之后，桌面cmd窗口可用，但是vscode/cursor不可用
&lt;/h3>&lt;p>cmd加载成功，但是 cursor ternimal没有生效。
解决办法：完全退出cursor，然后重启cursor&lt;/p>
&lt;h3 id="github-pull-requests-插件一直-loading">GitHub Pull Requests 插件一直 Loading
&lt;/h3>&lt;p>现象：安装 &lt;code>GitHub Pull Requests and Issues&lt;/code> 插件后，VS Code 侧边栏一直处于 loading 状态，无法正常显示当前仓库的 PR。&lt;/p>
&lt;p>优先检查三个点：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git remote -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh auth status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr list --repo OWNER/REPO --state all --limit &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这次遇到的原因是仓库 remote 名不是默认的 &lt;code>origin&lt;/code>，而是自定义的 &lt;code>zata&lt;/code>。VS Code 的 GitHub PR 插件默认主要识别 &lt;code>origin&lt;/code> 和 &lt;code>upstream&lt;/code>，如果仓库使用了其他 remote 名，插件可能找不到 GitHub 仓库上下文，于是一直 loading。&lt;/p>
&lt;p>解决方法：在 VS Code 的 &lt;code>settings.json&lt;/code> 中显式配置插件要识别的 remote 名：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;githubPullRequests.remotes&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;zata&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;origin&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;upstream&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后执行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Cmd+Shift+P
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Developer: Reload Window
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果还是 loading，打开下面这个输出面板看具体报错：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">View -&amp;gt; Output -&amp;gt; GitHub Pull Requests
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外要注意：如果 PR 已经 merge，插件的 open PR 列表里可能不会显示。可以用命令确认：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr list --repo OWNER/REPO --state all --limit &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gh pr view PR_NUMBER --repo OWNER/REPO
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="macos-下按-cmdq-直接退出-vscode">macOS 下按 Cmd+Q 直接退出 VSCode
&lt;/h3>&lt;p>在 macOS 上使用 VSCode / Cursor 时, 按下 &lt;code>Cmd+Q&lt;/code> 会触发 macOS 的&amp;quot;退出应用&amp;quot;快捷键, 直接把整个 VSCode 进程关掉 (而不是仅关闭当前窗口), 容易丢失未保存的内容。&lt;/p>
&lt;p>解决方法是在 VSCode 的快捷键设置里把 &lt;code>Cmd+Q&lt;/code> 绑定的命令移除或改成无害操作:&lt;/p>
&lt;ol>
&lt;li>打开 &lt;code>Cmd+K Cmd+S&lt;/code> (Keyboard Shortcuts) 或者 &lt;code>File → Preferences → Keyboard Shortcuts&lt;/code>。&lt;/li>
&lt;li>搜索 &lt;code>cmd+q&lt;/code> 或 &lt;code>Quit&lt;/code>, 找到 &lt;code>Quit&lt;/code> / &lt;code>workbench.action.quit&lt;/code> 这一项。&lt;/li>
&lt;li>双击该项, 选择 &lt;code>Remove Keybinding&lt;/code> (删除快捷键), 或者改成 &lt;code>Cmd+Q&lt;/code> 之外的其他组合 (比如改成 &lt;code>Cmd+Shift+Q&lt;/code> 之类的)。&lt;/li>
&lt;/ol>
&lt;p>也可以直接编辑 &lt;code>keybindings.json&lt;/code> (命令面板 &lt;code>Preferences: Open Keyboard Shortcuts (JSON)&lt;/code>), 写入:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;cmd+q&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;-workbench.action.quit&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加负号 &lt;code>-&lt;/code> 表示解除该命令的快捷键绑定。保存后重启 VSCode 生效, 此后按 &lt;code>Cmd+Q&lt;/code> 不会再退出 VSCode, 只会由 macOS 提示要不要退出 (或者完全没反应), 避免误触丢工作内容。&lt;/p></description></item><item><title>git&amp;github_tutorial</title><link>https://www.zata.cc/p/gitgithub/</link><pubDate>Fri, 28 Feb 2025 00:00:00 +0000</pubDate><guid>https://www.zata.cc/p/gitgithub/</guid><description>&lt;img src="https://www.zata.cc/p/gitgithub/image/index/index.png" alt="Featured image of post git&amp;github_tutorial" />&lt;h2 id="目录">目录
&lt;/h2>&lt;ul>
&lt;li>
&lt;p>&lt;strong>Git 基础操作&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#git%e5%9f%ba%e6%9c%ac%e4%bd%bf%e7%94%a8" >git基本使用&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%a6%82%e6%9e%9c%e6%83%b3%e8%a6%81%e4%b8%b4%e6%97%b6%e6%9f%a5%e7%9c%8b%e6%9f%90%e6%ac%a1commit%e6%97%b6%e9%a1%b9%e7%9b%ae%e7%9a%84%e5%85%a8%e9%83%a8%e4%bb%a3%e7%a0%81" >如果想要临时查看某次commit时项目的全部代码&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%9c%a8%e6%9c%ac%e5%9c%b0%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83%e6%a3%80%e6%9f%a5%e8%bf%9c%e7%a8%8b%e6%98%af%e5%90%a6%e6%9b%b4%e6%96%b0" >在本地开发环境检查远程是否更新&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>远程仓库操作&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#git-clone-%e8%bf%9c%e7%a8%8b%e9%a1%b9%e7%9b%ae%e5%90%8c%e6%ad%a5%e8%bf%9c%e7%a8%8b%e9%a1%b9%e7%9b%ae%e6%9b%b4%e6%96%b0" >git clone 远程项目,同步远程项目更新&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#git-%e7%89%88%e6%9c%ac%e6%a0%87%e7%ad%be" >git 版本标签&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>分支与合并&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#git-merge%e8%af%a6%e8%a7%a3" >git merge详解&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#git-pullmerge-git-%e5%a4%9a%e4%ba%ba%e5%8d%8f%e4%bd%9c%e7%9a%84%e6%97%b6%e5%80%99%e6%80%8e%e4%b9%88%e8%a7%a3%e5%86%b3%e5%86%b2%e7%aa%81" >git pull merge git 多人协作的时候怎么解决冲突？&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e4%bd%bf%e7%94%a8rebase%e8%b0%83%e6%8d%a2%e6%9c%80%e5%90%8e%e4%b8%a4%e4%b8%aaCommit%e7%9a%84%e9%a1%ba%e5%ba%8f" >使用rebase调换最后两个Commit的顺序&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e8%bd%af%e4%bb%b6%e7%89%88%e6%9c%ac%e7%ae%a1%e7%90%86%e4%b8%8e%e7%83%ad%e4%bf%ae%e5%a4%8d%e6%b5%81%e7%a8%8b" >软件版本管理与热修复流程&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>版本回退与撤销&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#git-reset---hard-head-%e8%af%a6%e8%a7%a3andgit-reset---soft-head-%e8%af%a6%e8%a7%a3" >git reset &amp;ndash;hard HEAD^ 详解&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e6%81%a2%e5%a4%8d%e8%a2%ab-git-%e5%90%88%e5%b9%b6%e8%a6%86%e7%9b%96%e7%9a%84%e6%8f%90%e4%ba%a4%e5%b9%b6%e9%98%b2%e6%ad%a2%e6%9c%aa%e6%9d%a5%e8%a6%86%e7%9b%96" >恢复被 Git 合并覆盖的提交并防止未来覆盖&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%bd%93%e5%89%8d%e6%ad%a3%e5%9c%a8%e8%bf%9b%e8%a1%8c%e4%bb%a3%e7%a0%81%e7%9a%84%e5%bc%80%e5%8f%91%e4%bd%86%e6%98%af%e6%83%b3%e8%a6%81%e7%9c%8b%e5%8e%86%e5%8f%b2commit%e7%9a%84%e9%a1%b9%e7%9b%ae%e5%ae%8c%e6%95%b4%e4%bb%a3%e7%a0%81%e8%80%8c%e5%bd%93%e5%89%8d%e7%9a%84%e5%b7%a5%e4%bd%9c%e5%8c%ba%e4%bf%9d%e8%af%81%e5%8e%9f%e6%a0%b7" >当前正在进行代码的开发，但是想要看历史commit的项目完整代码，而当前的工作区保证原样&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%9c%a8-git-%e4%b8%ad%e5%a6%82%e6%9e%9c%e4%bd%a0%e6%83%b3%e5%9b%9e%e9%80%80%e5%88%b0%e4%b8%8a%e4%b8%80%e4%b8%aa%e7%89%88%e6%9c%ac%e7%bb%a7%e7%bb%ad%e5%bc%80%e5%8f%91%e5%90%8c%e6%97%b6%e4%bf%9d%e7%95%99%e5%b7%b2%e7%bb%8f%e6%8f%90%e4%ba%a4%e5%88%b0-main-%e5%88%86%e6%94%af%e7%9a%84%e6%9c%80%e6%96%b0%e6%8f%90%e4%ba%a4%e5%8f%af%e4%bb%a5%e9%80%9a%e8%bf%87%e5%88%9b%e5%bb%ba%e6%96%b0%e5%88%86%e6%94%af%e5%b9%b6%e5%9b%9e%e9%80%80%e7%9a%84%e6%96%b9%e5%bc%8f%e5%ae%9e%e7%8e%b0" >回退到上一个版本继续开发的方案&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%a4%9a%e8%bd%ae%e4%bb%a3%e7%a0%81%e6%8f%90%e4%ba%a4%e5%90%8e%e6%89%8d%e5%8f%91%e7%8e%b0%e9%87%8d%e5%a4%a7Bug%e5%a6%82%e4%bd%95%e4%bc%98%e9%9b%85%e5%9c%b0%e4%bf%ae%e6%94%b9%e4%b8%8a%e4%b8%8a%e4%b8%aa-Commit" >多轮代码提交后才发现重大Bug？如何优雅地修改“上上个” Commit&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>GitHub 相关&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e5%85%ac%e5%bc%8f-github-github%e4%b8%8d%e6%98%be%e7%a4%bamd%e6%96%87%e4%bb%b6%e4%b8%ad%e7%9a%84%e5%85%ac%e5%bc%8f" >公式 github github不显示md文件中的公式&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#github-%e5%88%86%e6%94%af%e4%bf%9d%e6%8a%a4%e8%a7%84%e5%88%99rulesets%e9%85%8d%e7%bd%ae" >GitHub 分支保护规则（Rulesets）配置&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://mp.weixin.qq.com/s/TEjkisB-s2p-KD_OHS-fnQ" target="_blank" rel="noopener"
>Git 进阶教程(git lfs)：从“版本控制”到“内容追踪”&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>身份验证&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#git-%e4%bd%bf%e7%94%a8ssh%e5%af%86%e9%92%a5%e7%99%bb%e5%bd%95github" >git 使用ssh密钥登录github&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#git-%e4%bd%bf%e7%94%a8token%e7%99%bb%e5%bd%95github-%e5%b9%b6%e6%8b%89%e5%8f%96%e9%a1%b9%e7%9b%ae%e5%a6%82%e6%9e%9c%e7%94%b5%e8%84%91%e4%b8%8a%e5%b7%b2%e7%bb%8f%e7%99%bb%e5%bd%95%e8%bf%87%e4%ba%86%e9%9c%80%e8%a6%81%e6%8a%8a%e8%b4%a6%e6%88%b7%e4%bf%a1%e6%81%af%e6%b8%85%e9%99%a4%e6%8e%89%e5%a6%82%e6%9e%9c%e6%98%af%e7%94%a8ssh%e5%af%86%e9%92%a5%e7%99%bb%e5%bd%95%e7%9a%84%e4%b9%9f%e4%b8%8d%e8%a1%8c%e6%b8%85%e9%99%a4token%e8%b4%a6%e6%88%b7%e4%bf%a1%e6%81%af%e8%af%b7%e7%9c%8b%e4%b8%8b%e9%9d%a2%e6%b8%85%e9%99%a4%e7%94%b5%e8%84%91%e4%b8%8a%e5%b7%b2%e7%bb%8f%e7%99%bb%e5%bd%95%e7%9a%84github%e8%b4%a6%e6%88%b7%e4%bf%a1%e6%81%af" >git 使用token登录github 并拉取项目&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#github-%e8%a6%81%e6%b1%822fa%e8%ae%a4%e8%af%81" >github 要求2FA认证&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#github-%e6%b8%85%e9%99%a4%e7%94%b5%e8%84%91%e4%b8%8a%e5%b7%b2%e7%bb%8f%e7%99%bb%e5%bd%95%e7%9a%84github%e8%b4%a6%e6%88%b7%e4%bf%a1%e6%81%af" >github 清除电脑上已经登录的github账户信息&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#git-config---global---add-safe-directory-%e8%ae%be%e7%bd%ae%e5%ae%89%e5%85%a8%e7%9b%ae%e5%bd%95" >git config &amp;ndash;global &amp;ndash;add safe.directory 设置安全目录&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>问题解决&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#%e8%a7%a3%e5%86%b3mac%e3%80%81linux%e4%b8%8b%e4%bd%bf%e7%94%a8git%e5%91%bd%e4%bb%a4%e6%97%b6%e4%b8%ad%e6%96%87%e4%b9%b1%e7%a0%81%e7%9a%84%e5%8a%9e%e6%b3%95" >解决Mac、linux下使用git命令时中文乱码的办法&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e8%a6%81%e5%b0%86%e8%bf%9c%e7%a8%8b%e4%bb%93%e5%ba%93%e7%9a%84-other-%e5%88%86%e6%94%af%e8%ae%be%e7%bd%ae%e4%b8%ba-main-%e5%88%86%e6%94%af%e5%b9%b6%e5%88%a0%e9%99%a4%e5%8e%9f%e6%9d%a5%e7%9a%84-main-%e5%88%86%e6%94%af%e5%8f%af%e4%bb%a5%e6%8c%89%e7%85%a7%e4%bb%a5%e4%b8%8b%e6%ad%a5%e9%aa%a4%e6%93%8d%e4%bd%9c" >要将远程仓库的 other 分支设置为 main 分支&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e4%bd%bf%e7%94%a8windows%e7%b3%bb%e7%bb%9f%e6%9c%8d%e5%8a%a1%e5%99%a8%e5%81%9a%e8%bf%9c%e7%a8%8b%e5%bc%80%e5%8f%91%e7%a2%b0%e5%88%b0%e7%9a%84%e9%97%ae%e9%a2%98" >使用windows系统服务器做远程开发碰到的问题&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#git-clone-%e5%87%ba%e7%8e%b0%e4%bb%a5%e4%b8%8b%e9%97%ae%e9%a2%98fatal-unable-to-access-xxx-gnutls_handshake-failed-the-tls-connection-was-non-properly-terminated" >git clone 出现TLS连接错误&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#git-push-ssh-connect-to-host-githubcom-port-22-connection-timed-out-fatal-could-not" >git push ssh连接超时问题&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e8%bf%9c%e7%a8%8b%e5%88%86%e6%94%af%e6%98%afv3%e7%89%88%e6%9c%ac%e6%9c%ac%e5%9c%b0%e7%9a%84v2%e7%89%88%e6%9c%ac%e4%bd%86%e6%98%af%e6%9c%ac%e5%9c%b0%e4%bf%ae%e6%94%b9%e4%ba%86%e6%96%87%e4%bb%b6%e8%bf%98%e6%b2%a1%e6%9c%89add%e4%b9%9f%e6%b2%a1%e6%9c%89commit%e5%ba%94%e8%af%a5%e6%80%8e%e4%b9%88%e6%9b%b4%e6%96%b0%e6%9c%ac%e5%9c%b0" >远程分支版本冲突问题&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e7%94%a8git-hooks%e8%a7%a3%e5%86%b3github%e5%a4%a7%e6%96%87%e4%bb%b6%e6%8a%a5%e9%94%99100m%e9%99%90%e5%88%b6%e6%88%9650m%e9%99%90%e5%88%b6%e5%a4%a7%e6%96%87%e4%bb%b650mgit-hooksgit" >用git hooks解决github大文件报错&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e4%ba%91%e6%9c%8d%e5%8a%a1%e5%99%a8%e6%97%a0%e6%b3%95%e8%ae%bf%e9%97%aegithub%e5%af%bc%e8%87%b4git%e5%a4%b1%e8%b4%a5%e6%96%b9%e6%a1%88" >云服务器无法访问Github导致git失败方案&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#git-pull-%e6%97%b6%e5%87%ba%e7%8e%b0-cannot-lock-ref-%e9%94%99%e8%af%af%e7%9a%84%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88" >git pull 时出现 &amp;ldquo;cannot lock ref&amp;rdquo; 错误的解决方案&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#Git-%e6%97%a0%e6%b3%95%e8%af%86%e5%88%ab%e6%96%87%e4%bb%b6%e5%90%8d%e5%a4%a7%e5%b0%8f%e5%86%99%e6%9b%b4%e6%94%b9%e4%b8%80%e6%96%87%e5%bd%bb%e5%ba%95%e8%a7%a3%e5%86%b3%e9%87%8d%e5%91%bd%e5%90%8d%e9%9a%be%e9%a2%98" >解决 Git 未检测文件名大小写变化的问题&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#githook%e8%84%9a%e6%9c%ac%e7%89%88%e6%9c%ac%e6%8e%a7%e5%88%b6%e7%ae%a1%e7%90%86" >githook脚本版本控制管理&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e7%94%a8githooks%e8%a7%a3%e5%86%b3github%e5%a4%a7%e6%96%87%e4%bb%b6%e6%8a%a5%e9%94%99100M%e9%99%90%e5%88%b6%e6%88%9650M%e9%99%90%e5%88%b6-%e5%a4%a7%e6%96%87%e4%bb%b6-50M-githooks-git" >用git hooks解决github大文件报错，100M限制或50M限制|大文件|50M|git hooks|git&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#cannot_lock_ref_error" >error: cannot lock ref &amp;lsquo;refs/remotes/XXX/main&amp;rsquo;: is at XXX&amp;hellip;XXX but expected YYY&amp;hellip;YYY 解决&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e4%b8%80%e4%b8%aa%e7%94%b5%e8%84%91%e6%83%b3%e8%a6%81%e5%90%8c%e6%97%b6%e4%bd%bf%e7%94%a8%e4%b8%a4%e4%b8%aagithub%e8%b4%a6%e5%8f%b7ssh%e7%99%bb%e5%bd%95" >一个电脑想要同时使用两个github账号ssh登录&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>实战&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="#Git%e9%a1%b9%e7%9b%ae%e4%be%9d%e8%b5%96%e7%ae%a1%e7%90%86-Submodule%e4%b8%8eSubtree%e5%ae%9e%e6%88%98%e6%95%99%e7%a8%8b" >Git 项目依赖管理：Submodule 与 Subtree 实战教程&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="#%e5%a6%82%e4%bd%95%e5%b0%86clone%e4%b8%8b%e6%9d%a5%e7%9a%84%e9%a1%b9%e7%9b%ae%e6%8e%a8%e9%80%81%e5%88%b0%e8%87%aa%e5%b7%b1%e7%9a%84%e6%96%b0%e4%bb%93%e5%ba%93%e5%b9%b6%e5%90%8c%e6%ad%a5%e5%8e%9f%e5%a7%8b%e4%bb%93%e5%ba%93%e6%9b%b4%e6%96%b0" >如何将 clone 下来的项目推送到自己的新仓库并同步原始仓库更新&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="git基本使用">git基本使用
&lt;/h2>&lt;p>安装&lt;/p>
&lt;ol>
&lt;li>windows 安装&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-30.png"
width="870"
height="680"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-30_hu10250126752463687867.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-30_hu4927639420476342210.png 1024w"
loading="lazy"
alt="推荐使用便携版"
class="gallery-image"
data-flex-grow="127"
data-flex-basis="307px"
>
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-31.png"
width="1019"
height="575"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-31_hu16225620278392027579.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-31_hu1081102384893208144.png 1024w"
loading="lazy"
alt="bin目录添加到环境变量"
class="gallery-image"
data-flex-grow="177"
data-flex-basis="425px"
>&lt;/p>
&lt;ol start="2">
&lt;li>简单常用命令&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%设置用户签名%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global user.name &lt;span class="o">[&lt;/span>username&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global user.email &lt;span class="o">[&lt;/span>useremail&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%设置对于非ASCII字符的显示方式%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config--global core.quotepath &lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%设置init分支名%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global init.defaultBranch main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch --set-upstream-to&lt;span class="o">=&lt;/span>origin/&amp;lt;远程分支&amp;gt; &amp;lt;本地分支&amp;gt; &lt;span class="c1"># 绑定本地分支和远程分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 设置 HTTP/HTTPS 代理&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global http.proxy http://127.0.0.1:7890
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global https.proxy http://127.0.0.1:7890
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或者设置 SOCKS5 代理（更稳定）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global http.proxy socks5://127.0.0.1:7890
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull --rebase &lt;span class="c1"># 变基拉取&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git stash push --keep-index &lt;span class="c1">#只 stash 未暂存但已跟踪的修改 加-u 参数同时 stash 未跟踪文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git stash pop &lt;span class="c1"># 恢复被 stash 的未暂存部分&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git stash apply
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote prune &amp;lt;remote-name&amp;gt; &lt;span class="c1"># 清理本地仓库中对远程仓库 zata 已删除分支的过时跟踪引用。 并不是删除分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git gc --prune&lt;span class="o">=&lt;/span>now &lt;span class="c1">#清理不必要的文件并优化本地仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%% 设置远端库别名
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote add &amp;lt;别名&amp;gt; &amp;lt;网址&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%% 查看远端库
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%获取远程仓库的所有更新，并删除本地已经不存在的远程分支的跟踪引用，以保持本地仓库与远程仓库的同步
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch --prune
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%查看本地库状态%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%追踪文件，放入暂存区%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add &lt;span class="o">[&lt;/span>filename&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add . &lt;span class="c1">#所有文件放入暂存区&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%撤销追踪%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git restore 【file】
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%删除暂存区的文件%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rm --cache 【filename】
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%提交本地库%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;[提交信息]&amp;#34;&lt;/span> 【文件名（可以不加）】
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%撤销commit提交%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git reset --soft HEAD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%推送远程库
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push &lt;span class="o">[&lt;/span>&amp;lt;远程主机名&amp;gt;&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>&amp;lt;本地分支&amp;gt;:&amp;lt;远程分支&amp;gt;&lt;span class="o">]&lt;/span> &lt;span class="c1">#如果不加分支对应信息就默认上次记录的全部分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin &lt;span class="k">$(&lt;/span>git branch --show-current&lt;span class="k">)&lt;/span> &lt;span class="c1"># 将当前分支推送到远程仓库（通常是 `origin`），并保持相同的分支名&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%拉取远程库%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull &lt;span class="o">[&lt;/span>&amp;lt;远程主机名&amp;gt;&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>&amp;lt;远程分支&amp;gt;:&amp;lt;本地分支&amp;gt;&lt;span class="o">]&lt;/span> &lt;span class="c1"># 从远程仓库（名为 origin）的 main 分支拉取代码并自动与本地的 main 分支合并 ,如果方括号里面的不加就默认拉去上一次的&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull &lt;span class="o">[&lt;/span>&amp;lt;远程主机名&amp;gt;&lt;span class="o">]&lt;/span> --depth &amp;lt;分支深度&amp;gt; &lt;span class="o">[&lt;/span>&amp;lt;远程分支&amp;gt;:&amp;lt;本地分支&amp;gt;&lt;span class="o">]&lt;/span> &lt;span class="c1"># 例如git pull zata --depth 1 hugo:hugo ，用于历史内容过多，只拉取少量历史记录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%查看引用日志版本信息%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git reflog
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%查看详细日志%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%以图形化方式查看本地和远程分支的结构%%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git log --graph --oneline --all --remotes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">%%版本穿梭%% 一般用soft多一点
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git reset --hard 【版本号】
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git log --oneline --graph --all &lt;span class="c1"># 显示所有分支的提交记录建议直接使用gitG&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">--.gitignore--------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">test* 忽略以test开头的文件
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="一些问题解决">一些问题解决
&lt;/h2>&lt;h3 id="git-rebase-冲突解决vs-code-----20260308">Git Rebase 冲突解决（VS Code） &amp;mdash; 20260308
&lt;/h3>&lt;p>当你执行 &lt;code>git rebase &amp;lt;目标分支&amp;gt;&lt;/code> 开始变基后，Git 会尝试将你的提交依次应用到目标分支上。如果在这个过程中发生代码冲突，Git 会自动暂停变基操作，等待你解决冲突。&lt;/p>
&lt;p>此时，VS Code 会清晰地提示你当前状态：&lt;/p>
&lt;ul>
&lt;li>文件列表中被检测到冲突的文件旁边会出现警告标记（⚠️）。&lt;/li>
&lt;li>编辑器底部状态栏会显示“合并冲突”的提示。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>解决冲突的核心步骤分为以下环节：&lt;/strong>&lt;/p>
&lt;p>&lt;strong>第一步：打开并处理冲突文件&lt;/strong>
在编辑器中打开有冲突标记的文件，你会看到类似以下结构的代码块：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">这是你当前分支（正在变基的提交）的代码
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">=======
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">这是来自目标分支（或历史提交中）的代码
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; commit-hash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你需要决定如何整合这些代码。你有两种主要方式来完成这一步：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>使用 VS Code 内置工具&lt;/strong>：直接点击冲突标记区域上方出现的选项按钮。你可以选择“接受当前更改”、“接受传入更改”或“保留双方更改”。如果内置选项不能满足需求，你也可以手动删除所有的 &lt;code>&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code>、&lt;code>=======&lt;/code> 和 &lt;code>&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code> 标记，并编辑成你最终想要的代码。&lt;/li>
&lt;li>&lt;strong>使用 GitLens 扩展&lt;/strong>：如果你安装了 GitLens 扩展，它会提供更加强大和直观的并排对比视图，方便你逐块检查和解决冲突。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>第二步：标记冲突文件为已解决&lt;/strong>
在完成一个文件的所有冲突编辑后，你需要告诉 Git 这个文件的冲突已经处理完毕。&lt;/p>
&lt;ul>
&lt;li>你可以在 VS Code 的源代码管理侧边栏（√图标）中，找到该文件，点击旁边的“+”号或右键选择“暂存更改”。&lt;/li>
&lt;li>或者在终端中执行命令 &lt;code>git add &amp;lt;文件名&amp;gt;&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>第三步：继续变基过程&lt;/strong>
标记完所有已解决的冲突文件后，你需要让 Git 继续执行变基操作。&lt;/p>
&lt;ul>
&lt;li>在 VS Code 中，你可以点击底部状态栏的“继续变基”按钮。&lt;/li>
&lt;li>或者在终端中执行命令 &lt;code>git rebase --continue&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>随后，Git 会尝试应用下一个提交，你可能需要重复上述步骤来解决后续可能出现的冲突，直到所有提交都应用完成。&lt;/p>
&lt;p>&lt;strong>其他有用的命令&lt;/strong>
在变基过程中，你还可以使用以下命令：&lt;/p>
&lt;ul>
&lt;li>&lt;code>git rebase --skip&lt;/code>：跳过当前正在应用的提交（请谨慎使用，这意味着完全丢弃这个提交的更改）。&lt;/li>
&lt;li>&lt;code>git rebase --abort&lt;/code>：完全终止本次变基操作，分支将回退到执行 &lt;code>git rebase&lt;/code> 之前的状态。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>快速流程总结&lt;/strong>
一个典型的 rebase 解决冲突流程可以简化为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">git rebase main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 发生冲突
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 使用 VS Code 编辑文件解决冲突
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase --continue
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 可能重复“编辑-&amp;gt;add-&amp;gt;continue”直到完成
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>最后的重要提示&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>变基过程可能需要解决多个提交引发的冲突，请耐心处理完一个后继续下一个。&lt;/li>
&lt;li>随时可以使用 &lt;code>git status&lt;/code> 命令来查看当前的变基进度和状态。&lt;/li>
&lt;li>在进行变基等重写历史的操作前，创建一个备份分支是一个好习惯：&lt;code>git checkout -b backup-branch&lt;/code>。&lt;/li>
&lt;/ul>
&lt;h3 id="解决maclinux下使用git命令时中文乱码的办法">解决Mac、linux下使用git命令时中文乱码的办法
&lt;/h3>&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-22.png"
width="966"
height="706"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-22_hu7184100398968431327.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-22_hu327649835395407819.png 1024w"
loading="lazy"
alt="中文乱码"
class="gallery-image"
data-flex-grow="136"
data-flex-basis="328px"
>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git config --global core.quotepath &lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="要将远程仓库的-other-分支设置为-main-分支并删除原来的-main-分支可以按照以下步骤操作">要将远程仓库的 &lt;code>other&lt;/code> 分支设置为 &lt;code>main&lt;/code> 分支，并删除原来的 &lt;code>main&lt;/code> 分支，可以按照以下步骤操作：
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>确保本地仓库是最新的&lt;/strong>
首先，确保你的本地仓库与远程仓库同步，并切换到 &lt;code>other&lt;/code> 分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout other
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull origin other
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>将 &lt;code>other&lt;/code> 分支推送到远程的 &lt;code>main&lt;/code> 分支&lt;/strong>
使用 &lt;code>git push&lt;/code> 的 &lt;code>--force&lt;/code> 选项，将 &lt;code>other&lt;/code> 分支的内容强制覆盖远程的 &lt;code>main&lt;/code> 分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push origin other:main --force
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这会将 &lt;code>other&lt;/code> 分支的内容直接推送到远程的 &lt;code>main&lt;/code> 分支，覆盖原有的 &lt;code>main&lt;/code> 分支。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>更新本地仓库&lt;/strong>
在本地仓库中，确保你的 &lt;code>main&lt;/code> 分支与远程的 &lt;code>main&lt;/code> 分支同步：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>删除本地的 &lt;code>other&lt;/code> 分支（可选）&lt;/strong>
如果你不再需要本地的 &lt;code>other&lt;/code> 分支，可以删除它：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git branch -d other
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>验证操作&lt;/strong>
确认远程仓库的分支状态：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch -r
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你应该看到 &lt;code>origin/main&lt;/code> 包含了 &lt;code>other&lt;/code> 分支的内容，且 &lt;code>origin/other&lt;/code> 仍然存在（除非你也想删除它）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>（可选）删除远程的 &lt;code>other&lt;/code> 分支&lt;/strong>
如果 &lt;code>other&lt;/code> 分支不再需要，可以删除远程的 &lt;code>other&lt;/code> 分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push origin --delete other
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h3 id="使用windows系统服务器做远程开发碰到的问题">使用windows系统服务器做远程开发碰到的问题
&lt;/h3>&lt;ul>
&lt;li>
&lt;p>使用哪个版本的git&lt;/p>
&lt;p>感觉这个没有太大关系，不过后面发现便携版本的git也是可用的，感觉以后可以就使用便携版的，毕竟不需要界面&lt;/p>
&lt;/li>
&lt;li>
&lt;p>使用windows远程服务器进行git pull总是卡死&lt;/p>
&lt;p>2025年0617，买了一个腾讯云到2h2G服务器，想用来写hugo博客， 但是在pull代码的时候总是卡死，试了很多次，我以为是服务器性能太烂了，最后发现，如果使用http地址pull的话就没有问题。但是后面又发现使用github的http地址的话会存在无法push的情况，所以我最终给出的解决方案就是：使用github的http地址拉取项目，然后使用ssh地址同步
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-18.png"
width="2576"
height="560"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-18_hu14672072072689613467.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-18_hu17718580601840178516.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="460"
data-flex-basis="1104px"
>&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="如果想要临时查看某次commit时项目的全部代码">如果想要临时查看某次commit时项目的全部代码
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git log --oneline
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout &amp;lt;commit哈希值&amp;gt; &lt;span class="c1"># 查看代码&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout main &lt;span class="c1"># 返回主分支&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="在本地开发环境检查远程是否更新">在本地开发环境检查远程是否更新
&lt;/h3>&lt;p>第一种方法&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">git log --oneline --graph --all
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第二种方法&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 设置跟踪远程分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch --set-upstream-to&lt;span class="o">=&lt;/span>origin/&amp;lt;branch&amp;gt; &amp;lt;branch&amp;gt; &lt;span class="c1"># git branch --set-upstream-to=origin/main main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看是否跟踪&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch -vv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 下载数据&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看是否有更新&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>是否跟踪对比&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-14.png"
width="644"
height="49"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-14_hu3664483552206992883.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-14_hu2295860765431715334.png 1024w"
loading="lazy"
alt="未跟踪"
class="gallery-image"
data-flex-grow="1314"
data-flex-basis="3154px"
>
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-13.png"
width="706"
height="110"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-13_hu4425906168980590616.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-13_hu7726638049079455134.png 1024w"
loading="lazy"
alt="已经跟踪"
class="gallery-image"
data-flex-grow="641"
data-flex-basis="1540px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-15.png"
width="435"
height="96"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-15_hu14661769001706881027.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-15_hu10722412028536629420.png 1024w"
loading="lazy"
alt="可以看见本地和远程分支状态"
class="gallery-image"
data-flex-grow="453"
data-flex-basis="1087px"
>&lt;/p>
&lt;h3 id="git-clone-远程项目同步远程项目更新">git clone 远程项目,同步远程项目更新
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">git fetch origin &lt;span class="c1">#查看远程项目更新&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull origin 远程分支：本地分支
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="git-版本标签">git 版本标签
&lt;/h3>&lt;ol>
&lt;li>什么是版本标签？
标签（Tag） 是 Git 中的一个引用（reference），指向某个特定的提交。
它通常用于标记一个稳定的发布版本，比如 v1 表示第一个正式版本。
在 GitHub 上，标签还会显示在 Releases 页面，便于用户下载或查看。
两种标签类型
轻量标签（Lightweight Tag）：只是一个简单的指针，指向某个提交，不包含额外信息。
附注标签（Annotated Tag）：包含额外元数据（如创建者、日期、描述），更常用于正式发布。&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 假设你已经完成代码更改并提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Finalize version 1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个轻量标签（简单标记当前提交）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git tag v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或者创建一个附注标签（带描述信息，推荐用于正式发布）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git tag -a v1 -m &lt;span class="s2">&amp;#34;Release version 1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看所有本地标签&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git tag
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 推送单个标签到远程仓库（如 GitHub）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 或者推送所有标签到远程仓库&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin --tags
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># （可选）如果需要删除本地标签&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git tag -d v1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># （可选）如果需要删除远程标签&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin --delete v1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="git-merge详解">git merge详解
&lt;/h3>&lt;p>如果没有冲突，Git 会自动完成合并，并创建一个合并提交（merge commit，如果需要的话）。如果有冲突，Git 会暂停合并，提示你解决冲突（后面会讲怎么处理）。&lt;/p>
&lt;ol>
&lt;li>无冲突的合并（Fast-forward）&lt;/li>
&lt;/ol>
&lt;p>如果 main 分支没有额外改动，而 feature 分支只是基于 main 增加了提交，Git 会执行&amp;quot;快进合并&amp;quot;（fast-forward）。这时候历史记录会变成一条直线，看起来像是直接在 main 上开发了一样。&lt;/p>
&lt;ol start="2">
&lt;li>有额外提交的合并（Merge Commit）&lt;/li>
&lt;/ol>
&lt;p>如果 main 和 feature 都有各自的提交，Git 会创建一个新的合并提交，保留两个分支的历史。
命令一样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git merge &amp;lt;分支名&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是，合并后，Git 会自动生成一个 Merge commit，这样，从历史上看，分支信息就非常清楚。&lt;/p>
&lt;ol start="3">
&lt;li>冲突的合并
当两个分支修改了同一文件的同一部分，Git 无法自动决定用哪个版本，就会报冲突。这时你需要手动解决：&lt;/li>
&lt;/ol>
&lt;p>运行 git merge feature 后，Git 会提示冲突文件。
打开这些文件，冲突部分会被标记为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">（main 分支的内容）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">=======
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">（feature 分支的内容）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编辑文件，保留你想要的部分，删除标记。
解决完后，标记文件为已解决：&amp;ldquo;git add &lt;filename>&amp;quot;，然后提交：&amp;ldquo;git commit -m &amp;lsquo;解决冲突&amp;rsquo;&lt;/p>
&lt;ul>
&lt;li>实用技巧
查看合并状态：用 git status 检查当前是否在合并过程中。
中止合并：如果合并出了问题，想放弃，可以用：&lt;/li>
&lt;/ul>
&lt;p>git merge &amp;ndash;abort&lt;/p>
&lt;ul>
&lt;li>指定合并策略：默认情况下 Git 会自动选择合并方式，但你可以用选项调整，比如强制非快进合并：&lt;/li>
&lt;/ul>
&lt;p>git merge &amp;ndash;no-ff feature&lt;/p>
&lt;h3 id="如何解决-fatal-need-to-specify-how-to-reconcile-divergent-branches-错误">如何解决 &amp;ldquo;fatal: Need to specify how to reconcile divergent branches&amp;rdquo; 错误
&lt;/h3>&lt;p>问题根源：什么是“分支分叉”？&lt;/p>
&lt;p>这个错误的核心原因是：&lt;strong>您的本地分支和它所跟踪的远程分支，各自都有了新的、对方不知道的提交。&lt;/strong>&lt;/p>
&lt;p>让我们用一个形象的例子来说明：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>您和您的同事都在 &lt;code>main&lt;/code> 分支上工作。你们最后一次同步时的代码状态是 &lt;code>Commit O&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>之后，您在本地写了新功能，并创建了两个提交 &lt;code>A&lt;/code> 和 &lt;code>B&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>在您提交的这段时间里，您的同事完成了另一个任务，并将他的提交 &lt;code>C&lt;/code> 和 &lt;code>D&lt;/code> 推送（push）到了远程仓库。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>这时，Git 的历史记录就变成了两条独立的路径，即“分叉”了：&lt;/p>
&lt;p>A&amp;mdash;B &amp;lt;&amp;ndash; 您的本地 &lt;code>main&lt;/code> 分支&lt;/p>
&lt;p>/&lt;/p>
&lt;p>&amp;mdash;O&amp;mdash;C&amp;mdash;D &amp;lt;&amp;ndash; 远程 origin/main 分支&lt;/p>
&lt;p>当您执行 &lt;code>git pull&lt;/code> 时，Git 发现它无法简单地“快进”（Fast-forward）来更新您的代码，因为它不知道应该如何处理这两条分叉的路径。为了避免自动操作可能带来的混乱，新版 Git 强制要求您必须明确告诉它您的合并策略。&lt;/p>
&lt;p>二、 核心概念：两种合并策略 Merge vs. Rebase&lt;/p>
&lt;p>要解决分叉问题，您有两种主要的方法：&lt;code>Merge&lt;/code> (合并) 和 &lt;code>Rebase&lt;/code> (变基)。&lt;/p>
&lt;p>方案 A：临时解决本次问题&lt;/p>
&lt;p>您可以只在本次 &lt;code>pull&lt;/code> 命令中指定策略。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>使用 Rebase (推荐)：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull --rebase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这会用变基的方式拉取并应用远程更新。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>使用 Merge：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull --no-rebase &lt;span class="c1"># 或者 git pull --merge&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这会用传统合并的方式，并生成一个合并提交。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>方案 B：永久配置默认行为 (一劳永逸)&lt;/p>
&lt;p>为了避免每次都输入额外参数，您可以为 Git 设置一个全局的默认 &lt;code>pull&lt;/code> 行为。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>将 Rebase 设置为默认 (推荐)：&lt;/strong>&lt;/p>
&lt;p>如果您喜欢干净的线性历史，这是大多数现代开发者的首选。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global pull.rebase &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>将 Merge 设置为默认：&lt;/strong>&lt;/p>
&lt;p>如果您偏爱保留所有合并痕迹的传统方式。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global pull.rebase &lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>更严格的 &lt;code>ff-only&lt;/code> 策略：&lt;/strong>&lt;/p>
&lt;p>还有一个选项是 &lt;code>fast-forward only&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global pull.ff only
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个设置意味着，只有当您的本地分支没有任何新提交时（即可以“快进”时），&lt;code>git pull&lt;/code> 才能成功。如果分支出现分叉，&lt;code>pull&lt;/code> 会直接失败，强制您手动执行 &lt;code>git rebase&lt;/code> 或 &lt;code>git merge&lt;/code>，让您对每一次合并操作都更加谨慎。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="git-pullmerge-git-多人协作的时候怎么解决冲突">|git pull|merge| git 多人协作的时候怎么解决冲突？
&lt;/h3>&lt;p>使用git-pull 然后git-commit 最后git-push&lt;/p>
&lt;p>如果有冲突可以看&lt;a class="link" href="#git-merge%e8%af%a6%e8%a7%a3" >get merge使用&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/image/git@github/git@github.jpg"
width="1316"
height="1301"
srcset="https://www.zata.cc/p/gitgithub/image/git@github/git@github_hu2912269543672264858.jpg 480w, https://www.zata.cc/p/gitgithub/image/git@github/git@github_hu5581466344476847627.jpg 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="101"
data-flex-basis="242px"
>&lt;/p>
&lt;h3 id="使用rebase调换最后两个commit的顺序">使用rebase调换最后两个Commit的顺序
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Markdown" data-lang="Markdown">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh"># Git 教程：使用 rebase 调换最后两个 Commit 的顺序
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">本教程将指导你如何使用 &lt;span class="sb">`git rebase -i`&lt;/span>（交互式 rebase）来调换你本地最后两个 commit 的顺序。## 场景
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">你通过 &lt;span class="sb">`rebase`&lt;/span> 将你的本地修改放在了远程分支的最新 commit 之上，但你现在希望反过来，让你自己的修改在前面，让远程的 commit 成为最后一个。**你当前的状态 (HEAD 指向你的修改):**
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">... -&amp;gt; 远程的最后一个commit -&amp;gt; 你的本地修改 (HEAD)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gs">**你希望达到的状态 (HEAD 指向远程的 commit):**&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">... -&amp;gt; 你的本地修改 -&amp;gt; 远程的最后一个commit (HEAD)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## ⚠️ 重要警告
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">此操作会**重写 Git 历史**。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gs">**请只在尚未将这些 commit 推送（push）到远程仓库时执行此操作。**&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">如果你已经推送了 &lt;span class="sb">`你的本地修改`&lt;/span>，强行修改历史（&lt;span class="sb">`git push --force`&lt;/span>）会给你的协作者带来严重问题。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## 操作步骤
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">### 步骤 1: 启动交互式 Rebase
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">你需要编辑最后两个 commit。运行以下命令来启动交互式 rebase，&lt;span class="sb">`HEAD~2`&lt;/span> 表示“从 HEAD 开始往前数 2 个 commit”。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">```bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase -i HEAD~2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">步骤 2: 在编辑器中编辑 Rebase 列表
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">运行上述命令后，Git 会打开你的默认文本编辑器，显示一个列表。这个列表的顺序是从旧到新（从上到下）。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">它看起来会是这样：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">hash_A&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 远程的最后一个commit 的消息
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">hash_B&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 你的本地修改 的消息
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh"># Rebase ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh"># Commands:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh"># p, pick &amp;lt;commit&amp;gt; = use commit
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh"># ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">hash_A&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 是那个较旧的 commit（远程的 commit）。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">hash_B&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 是那个较新的 commit（你的本地修改）。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">步骤 3: 调换 Commit 顺序
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">你只需要在编辑器中交换这两行的文本顺序，把你的本地修改放到上面（即让它更早发生）：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">修改为：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">hash_B&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 你的本地修改 的消息
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">hash_A&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 远程的最后一个commit 的消息
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">步骤 4: 保存并退出编辑器
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">保存文件并关闭编辑器。Git 会立即按照你指定的新顺序重新应用这两个 commit。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">步骤 5: 验证结果
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">操作完成后，你可以使用 git log 来检查历史记录，确认顺序已经被正确调换。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh"># 查看最后两个 commit
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git log --oneline -2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">你现在应该会看到 远程的最后一个commit 的消息 在最上面（最新）的位置。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">附录：如何在编辑器中快速调换行顺序
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">在步骤 3 中，你不需要手动剪切和粘贴。几乎所有编辑器都支持“移动当前行”的快捷键（将光标放在该行任意位置即可）。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">常用 GUI 编辑器
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Visual Studio Code (VS Code):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Windows / Linux: Alt + ↑ (上移) / Alt + ↓ (下移)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">macOS: Option + ↑ (上移) / Option + ↓ (下移)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Sublime Text:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Windows / Linux: Ctrl + Shift + ↑ (上移) / Ctrl + Shift + ↓ (下移)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">macOS: Cmd + Ctrl + ↑ (上移) / Cmd + Ctrl + ↓ (下移)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Notepad++:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Ctrl + Shift + ↑ (上移) / Ctrl + Shift + ↓ (下移)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">常用终端编辑器 (Git 默认)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Vim / Vi (最常见的默认编辑器):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">按 Esc 键确保你处于普通模式 (Normal Mode)。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">将光标移动到你想移动的行。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">输入 ddp：dd (剪切当前行) + p (粘贴到下一行)，效果是与下一行交换。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">输入 ddkP：dd (剪切) + k (上移) + P (粘贴到上一行)，效果是与上一行交换。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">(在你的场景中，光标在第二行 pick &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">hash_B&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> ... 上时，按 ddkP 即可)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nano (较简单的编辑器):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nano 没有直接的“移动行”快捷键，你必须使用剪切和粘贴：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">将光标放在要移动的行上。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">按 Ctrl + K (剪切该行)。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">将光标移动到目标位置（例如，移动到第一行）。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">按 Ctrl + U (粘贴该行)。
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="公式--github--github不显示md文件中的公式">|公式 | github | github不显示md文件中的公式
&lt;/h3>&lt;p>&lt;code>第一种方法：使用github推荐的公式写法（推荐）&lt;/code>
在写md的时候，使用&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$` 和 `$
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>来包围行内公式&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">```math
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">和
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ```
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>来包围单行公式，如下&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image.png"
width="1348"
height="145"
srcset="https://www.zata.cc/p/gitgithub/images/index/image_hu2980433107614408744.png 480w, https://www.zata.cc/p/gitgithub/images/index/image_hu11750368063988626786.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="929"
data-flex-basis="2231px"
>&lt;/p>
&lt;p>&lt;code>第二种方法： 插件&lt;/code>&lt;/p>
&lt;p>但是还是会有一部分显示不正确
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-1.png"
width="1015"
height="293"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-1_hu993762255634323821.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-1_hu1583790060066129962.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="346"
data-flex-basis="831px"
>&lt;/p>
&lt;p>解决方法：安装插件：https://chrome.google.com/webstore/detail/mathjax-plugin-for-github/ioemnmodlmafdkllaclgeombjnmnbima/related
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-2.png"
width="358"
height="59"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-2_hu14824977159200266602.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-2_hu10950753139731083826.png 1024w"
loading="lazy"
alt="插件名"
class="gallery-image"
data-flex-grow="606"
data-flex-basis="1456px"
>&lt;/p>
&lt;h3 id="git-使用ssh密钥登录github">git 使用ssh密钥登录github
&lt;/h3>&lt;p>&lt;code> 注意一个密钥只能登录一个github，如果你想要在新的github账户上面添加旧的ssh密钥，就会报错&lt;/code>
准备工作：本地先下载安装好git，注册并登陆github账号
在注册好后的github账号中先创建一个仓库
在本地git创建SSH key
第一步：设置全局的用户名和邮箱（这样主要是不用总是重填）&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">git&lt;/span> &lt;span class="n">config&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">global&lt;/span> &lt;span class="n">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span> &lt;span class="err">&amp;#39;用户名&amp;#39;&lt;/span> &lt;span class="c1">// 切换到你的github用户名上
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">git&lt;/span> &lt;span class="n">config&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">global&lt;/span> &lt;span class="n">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">email&lt;/span> &lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">github账号注册用的邮箱&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第二步：生成密钥，有可能提示没有这个库，需要安装一下，去搜索一下&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">ssh&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">keygen&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">t&lt;/span> &lt;span class="n">rsa&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">C&lt;/span> &lt;span class="s">&amp;#34;你的邮箱地址&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三步：查看自己本地的密钥，执行以下命令&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">ls&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">al&lt;/span> &lt;span class="o">~/&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ssh&lt;/span> &lt;span class="c1">// 查看本机是否有秘钥文件 没有秘钥文件生成秘钥文件
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">cd&lt;/span> &lt;span class="o">~/&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ssh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ls&lt;/span> &lt;span class="c1">// 查看 .ssh 中有什么文件
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">cat&lt;/span> &lt;span class="n">id_rsa&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pub&lt;/span> &lt;span class="c1">// 查看本地的公钥
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">cat&lt;/span> &lt;span class="n">id_rsa&lt;/span> &lt;span class="c1">// 查看本地的私钥
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="err">（如果是&lt;/span>&lt;span class="n">windows系统可以直接到C盘的对应用户名文件夹下的&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ssh文件夹中查看&lt;/span>&lt;span class="err">）&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第四步：将公钥复制粘贴到github上&lt;/p>
&lt;blockquote>
&lt;p>github上点击头像，点击Settings，进入后点击 SSH and GPS keys，接着点击 New SSH key
将公钥粘贴在key输入框那里，Title则随便输入可以就行&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-3.png"
width="1327"
height="571"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-3_hu1087538447768357301.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-3_hu6470134803155830141.png 1024w"
loading="lazy"
alt="github ssh"
class="gallery-image"
data-flex-grow="232"
data-flex-basis="557px"
>&lt;/p>
&lt;h3 id="git-使用token登录github-并拉取项目-如果电脑上已经登录过了需要把账户信息清除掉如果是用ssh密钥登录的也不行清除token账户信息请看下面清除电脑上已经登录的github账户信息">&lt;code>git 使用token登录github 并拉取项目&lt;/code> （如果电脑上已经登录过了，需要把账户信息清除掉。&lt;strong>如果是用ssh密钥登录的也不行&lt;/strong>，清除token账户信息请看下面&amp;quot;清除电脑上已经登录的github账户信息&amp;rdquo;）
&lt;/h3>&lt;table>
&lt;tr>
&lt;td >
&lt;img src="images/index/image-4.png" >图1 打开设置
&lt;/td>
&lt;td >
&lt;img src="images/index/image-5.png" >图2 开发者设置
&lt;/td>
&lt;td >
&lt;img src="images/index/image-6.png" >图3 点击Token
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td >
&lt;img src="images/index/image-7.png" >图4 生成classic Token
&lt;/td>
&lt;td >
&lt;img src="images/index/image-8.png" >图5 设置命名和权限
&lt;/td>
&lt;/tr>
&lt;/table>
&lt;p>&lt;strong>最后一步：&lt;/strong>
在新设备上pull或者push的时候，会让你登录，有两种方式，一种是账号密码，另一种就算token，选择token，然后粘贴上一步生成的token就可以了&lt;/p>
&lt;h3 id="github-要求2fa认证">github 要求2FA认证
&lt;/h3>&lt;p>今天上线的时候突然发现，github要求我设置2FA认证，不然就不能登录，手机号我必然是没有的，只能用安全密钥控制
具体的github说明在这里：https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication&lt;/p>
&lt;p>一句话说，就是需要用TOPT密钥管理工具生成动态密码，然后输入动态密码登录，然后第一次登录会给你一个github-recovery-codes.txt,如果更换设备需要使用这个恢复码登录新设备，然后登录新设备之后也是会给你一个新的恢复码&lt;/p>
&lt;p>如果是第一次登录，看到github要求进行2FA，可以安装如下 chrome插件：Github 2FA（去chrome商店搜索）&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-12.png"
width="1035"
height="813"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-12_hu7404912787645360837.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-12_hu13790825795733162414.png 1024w"
loading="lazy"
alt="Github 2FA应用图标"
class="gallery-image"
data-flex-grow="127"
data-flex-basis="305px"
>&lt;/p>
&lt;p>然后你回到github页面刷新，在上面图片红框的那个位置就会出现一个30s的密码，输入就可以确认，确认好之后会让你下周恢复密钥，记得下载保存&lt;/p>
&lt;h3 id="github-清除电脑上已经登录的github账户信息">github 清除电脑上已经登录的github账户信息
&lt;/h3>&lt;p>step1： 进入控制面板点击用户账户&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-9.png"
width="1123"
height="546"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-9_hu8136748358710120527.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-9_hu13854892858309720935.png 1024w"
loading="lazy"
alt="控制面板-用户账户"
class="gallery-image"
data-flex-grow="205"
data-flex-basis="493px"
>&lt;/p>
&lt;p>step2：管理windows凭证&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-10.png"
width="923"
height="546"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-10_hu11284426244296711471.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-10_hu739297296554875556.png 1024w"
loading="lazy"
alt="windows凭证"
class="gallery-image"
data-flex-grow="169"
data-flex-basis="405px"
>&lt;/p>
&lt;p>step3：删除github相关的凭证&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-11.png"
width="868"
height="275"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-11_hu9565429108820901483.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-11_hu15691339718458112265.png 1024w"
loading="lazy"
alt="删除github凭证"
class="gallery-image"
data-flex-grow="315"
data-flex-basis="757px"
>&lt;/p>
&lt;h3 id="git-config---global---add-safedirectory-设置安全目录">git config &amp;ndash;global &amp;ndash;add safe.directory 设置安全目录
&lt;/h3>&lt;p>命令用途
&lt;code>git config --global --add safe.directory&lt;/code> 命令用于将指定的目录添加到 Git 的安全目录列表中，解决 Git 安全机制导致的仓库访问问题。&lt;/p>
&lt;p>问题背景
从 Git 2.35.2 开始，Git 引入了更严格的安全机制，默认情况下会拒绝访问由其他用户拥有的目录中的 Git 仓库。这通常发生在以下情况：&lt;/p>
&lt;ul>
&lt;li>在 WSL (Windows Subsystem for Linux) 环境中&lt;/li>
&lt;li>在共享目录或挂载的目录中&lt;/li>
&lt;li>在 Docker 容器中访问宿主机目录&lt;/li>
&lt;li>在多用户系统中&lt;/li>
&lt;/ul>
&lt;p>命令语法&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git config --global --add safe.directory &amp;lt;directory-path&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用示例&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 添加单个目录到安全目录列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --add safe.directory /code/GRT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 添加当前目录到安全目录列表&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --add safe.directory &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 添加多个目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --add safe.directory /path/to/repo1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --add safe.directory /path/to/repo2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>查看当前安全目录列表&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看所有安全目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --get-all safe.directory
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查看所有全局配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --list &lt;span class="p">|&lt;/span> grep safe.directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>删除安全目录&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除特定的安全目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --unset-all safe.directory /code/GRT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除所有安全目录配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global --unset-all safe.directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常见错误信息
当遇到安全目录问题时，Git 会显示类似以下的错误：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">fatal: detected dubious ownership in repository at &amp;#39;/code/GRT&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">To add an exception for this directory, call:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> git config --global --add safe.directory /code/GRT
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安全注意事项&lt;/p>
&lt;ol>
&lt;li>&lt;strong>谨慎添加目录&lt;/strong>：只添加你信任的目录，避免添加系统关键目录&lt;/li>
&lt;li>&lt;strong>使用绝对路径&lt;/strong>：建议使用绝对路径而不是相对路径&lt;/li>
&lt;li>&lt;strong>定期检查&lt;/strong>：定期检查安全目录列表，删除不再需要的目录&lt;/li>
&lt;li>&lt;strong>环境隔离&lt;/strong>：在不同环境中使用不同的安全目录配置&lt;/li>
&lt;/ol>
&lt;p>替代方案
如果不想使用全局配置，也可以：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>使用本地配置&lt;/strong>：在特定仓库中使用 &lt;code>--local&lt;/code> 而不是 &lt;code>--global&lt;/code>&lt;/li>
&lt;li>&lt;strong>修改目录权限&lt;/strong>：确保目录的所有权正确&lt;/li>
&lt;li>&lt;strong>使用 Git 环境变量&lt;/strong>：设置 &lt;code>GIT_SAFE_DIRECTORIES&lt;/code> 环境变量&lt;/li>
&lt;/ol>
&lt;p>验证配置
配置完成后，可以验证是否生效：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 进入目标目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /code/GRT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 尝试执行 Git 命令&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果没有错误信息，说明配置成功&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="github-分支保护规则rulesets配置">GitHub 分支保护规则（Rulesets）配置
&lt;/h3>&lt;p>在团队协作中，保护主分支（如 &lt;code>main&lt;/code>）是非常重要的一项最佳实践。GitHub 提供了 &lt;strong>Rulesets&lt;/strong>（规则集）功能，可以灵活地配置分支保护策略，而无需启用组织级别的付费功能。&lt;/p>
&lt;p>下面介绍两种常见的配置方案：&lt;/p>
&lt;p>&lt;strong>方案一：极简保护（防删 + 防强制推送）&lt;/strong>&lt;/p>
&lt;p>适用于个人仓库或小型项目，仅开启最核心的两项保护：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Restrict deletions&lt;/strong> — 限制删除匹配的分支，防止误删主分支。&lt;/li>
&lt;li>&lt;strong>Block force pushes&lt;/strong> — 阻止强制推送，避免历史被覆盖。&lt;/li>
&lt;/ul>
&lt;p>配置截图如下：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-32.png"
width="1072"
height="1806"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-32_hu6418344265277619561.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-32_hu16847788663928288651.png 1024w"
loading="lazy"
alt="极简分支保护"
class="gallery-image"
data-flex-grow="59"
data-flex-basis="142px"
>&lt;/p>
&lt;p>&lt;strong>方案二：严格保护（推荐用于团队项目）&lt;/strong>&lt;/p>
&lt;p>适用于多人协作的项目，要求更规范的代码合入流程：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Restrict deletions&lt;/strong> — 限制删除分支。&lt;/li>
&lt;li>&lt;strong>Require a pull request before merging&lt;/strong> — 必须通过 Pull Request 才能合并代码。&lt;/li>
&lt;li>&lt;strong>Require status checks to pass&lt;/strong> — 要求指定的 CI 检查通过后才能合并。例如：
&lt;ul>
&lt;li>&lt;code>Python Quality Gate&lt;/code>&lt;/li>
&lt;li>&lt;code>Frontend Build&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Block force pushes&lt;/strong> — 阻止强制推送。&lt;/li>
&lt;/ul>
&lt;p>配置截图如下：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-33.png"
width="1072"
height="3033"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-33_hu11611640907184866084.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-33_hu16811386978142832023.png 1024w"
loading="lazy"
alt="严格分支保护"
class="gallery-image"
data-flex-grow="35"
data-flex-basis="84px"
>&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>提示&lt;/strong>：Rulesets 对私有仓库完全免费即可使用。对于团队项目，建议至少开启「Require a pull request before merging」和「Require status checks to pass」，以保证代码质量和审计追踪。&lt;/p>
&lt;/blockquote>
&lt;h3 id="git-clone-出现以下问题fatal-unable-to-access-xxx-gnutls_handshake-failed-the-tls-connection-was-non-properly-terminated">&lt;code>git clone 出现以下问题：fatal: unable to access XXX: gnutls_handshake() failed: The TLS connection was non-properly terminated.&lt;/code>
&lt;/h3>&lt;p>&lt;code>git clone 出现以下问题：fatal: unable to access 'https://github.com/QwenLM/Qwen.git/': gnutls_handshake() failed: The TLS connection was non-properly terminated.&lt;/code>&lt;/p>
&lt;p>可以参考stack overflow里面的&lt;a class="link" href="https://stackoverflow.com/questions/68801315/gnutls-handshake-failed-the-tls-connection-was-non-properly-terminated-while" target="_blank" rel="noopener"
>Stack&lt;/a>
我是执行下面两行代码解决（ubuntu系统）&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">apt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">get&lt;/span> &lt;span class="n">update&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">apt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">get&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">curl&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="git-push-ssh-connect-to-host-githubcom-port-22-connection-timed-out-fatal-could-not">&lt;code>[git push] ssh: connect to host github.com port 22: Connection timed out fatal: Could not....&lt;/code>
&lt;/h3>&lt;p>最有可能是防火墙问题，可以参考github官方的解决方法 用443端口解决
&lt;a class="link" href="https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port" target="_blank" rel="noopener"
>https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&lt;/a>&lt;/p>
&lt;p>第一步,用以下代码测试443端口是否可用&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">ssh&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">T&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="mi">443&lt;/span> &lt;span class="n">git&lt;/span>&lt;span class="err">@&lt;/span>&lt;span class="n">ssh&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">github&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">com&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果出现这样就是可用的，那么恭喜，十拿九稳了！
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-16.png"
width="551"
height="48"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-16_hu17354657446761016326.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-16_hu6545698333154276200.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="1147"
data-flex-basis="2755px"
>
用如下ssh地址替代原来的&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">git&lt;/span> &lt;span class="n">clone&lt;/span> &lt;span class="nl">ssh&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="c1">//git@ssh.github.com:443/YOUR-USERNAME/YOUR-REPOSITORY.git
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意是替代，直接用原来的地址加端口是不行的。
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-17.png"
width="551"
height="148"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-17_hu9065490677750730365.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-17_hu15926033294578801456.png 1024w"
loading="lazy"
alt="alt text"
class="gallery-image"
data-flex-grow="372"
data-flex-basis="893px"
>&lt;/p>
&lt;p>拿下！&lt;/p>
&lt;h3 id="远程分支是v3版本本地的v2版本但是本地修改了文件还没有add也没有commit应该怎么更新本地">&lt;code>远程分支是v3版本，本地的v2版本，但是本地修改了文件还没有add也没有commit，应该怎么更新本地？&lt;/code>
&lt;/h3>&lt;p>&lt;strong>1. 第一步：首先保存你的本地修改：&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>git stash &lt;/code>&lt;/p>
&lt;p>这会将你当前的修改暂时保存起来&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>2. 第二步：拉取远程更新&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>git fetch origin&lt;/code>&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 第三步：更新本地分支到远程版本&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>git merge FETCH_HEAD&lt;/code>&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>4. 第四步，恢复你之前的本地修改&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>cpp git stash pop &lt;/code>&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>5. 如果在恢复 stash 时遇到冲突，你需要手动解决这些冲突。解决完冲突后，你可以:&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>添加解决冲突后的文件 git add .&lt;/p>
&lt;p>提交你的修改 git commit -m &amp;ldquo;your commit message&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>提示：&lt;/strong>&lt;/p>
&lt;p>如果你想在应用 stash 前查看保存了什么内容，可以用 git stash list 查看 stash 列表
如果你想查看具体改动，可以用 git stash show -p
如果合并时遇到问题，随时可以用 git status 查看当前状态
如果想放弃当前操作回到起点，可以用 git reset &amp;ndash;hard HEAD（注意这会丢失未提交的修改）&lt;/p>
&lt;h3 id="用githooks解决github大文件报错100m限制或50m限制-大文件-50m-githooks-git">用githooks解决github大文件报错100M限制或50M限制-大文件-50M-githooks-git
&lt;/h3>&lt;p>如果你已经commit了大文件，并且报错了，可以使用下面方法，基本上都起作用&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt-get install git-filter-repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 删除指定的大文件（路径必须完全匹配）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git filter-repo --path &lt;span class="s2">&amp;#34;路径地址&amp;#34;&lt;/span> --invert-paths
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果还没有提交&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 把大文件取消追踪&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rm --cached &lt;span class="s2">&amp;#34;path/to/large/file&amp;#34;&lt;/span> &lt;span class="c1"># 引号括起来的是大文件地址&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 把大文件加入.gitignore&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;path/to/large/file&amp;#34;&lt;/span> &amp;gt;&amp;gt; .gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 更改提交 （当然不commit直接git push XXX 也是可以的）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Update .gitignore to exclude large files&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push XXX main:main --force
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果已经在过去的几次commit提交了，并且知道在哪个commit开始出现，可以使用以下方法&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 撤销最近的提交，但保留更改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git reset --soft HEAD~1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果大文件在更早的提交（比如倒数第二次），用 HEAD~2 或具体哈希：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># git reset --soft abc123^ # abc123 是包含大文件的提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 移除大文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rm --cached &lt;span class="s2">&amp;#34;大文件名&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3.重新提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Remove large file XXX&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. 强制推送&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push XXX main:main --force
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我写了一个git hooks 文件，在commit大于设定size的文件的时候，拦截commit，并且把大文件名写入.gitignore ，同时从缓存区中移除大文件
&lt;strong>进行版本控制的时候，经常会由于大文件导致上传github出问题，并且版本回退也比较麻烦。
实际上我们很少代码文件会超过50M，而往往是由于数据文件过大导致错误，这些数据文件往往我们可能都不需要进行版本控制&lt;/strong>
&lt;font color = "red" size = 5>于是，不如直接通过脚本，默认不对这些大文件进行版本控制
&lt;/font>&lt;/p>
&lt;p>代码 （文件名设置为pre-commit，防在.git/hooks目录下，注意文件名要一致，这涉及到git hooks的逻辑，不做过多解释）&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-py" data-lang="py">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#!/usr/bin/env python3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 我在2025年之前用的是 #!/bin/python,但是报错了，改成了上面的内容&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">subprocess&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">os&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;__main__&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">files&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">check_output&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;git&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;diff&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;--cached&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;--name-status&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">encoding&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;utf-8&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">files&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\t&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">file&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\t&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;D&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">file_path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">file&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\t&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">file_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getsize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size_in_mb&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">file_size&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1024&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">size_in_mb&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.gitignore&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;a&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">encoding&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;utf-8&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">f&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_path&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Remove the file from the Git index (staged area)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">check_output&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;git&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;rm&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;--cached&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file_path&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Removed &lt;/span>&lt;span class="si">{}&lt;/span>&lt;span class="s2"> from stage&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_path&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CalledProcessError&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Error removing file from stage: &lt;/span>&lt;span class="si">{}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Reset the file to unstage changes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">check_output&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;git&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;reset&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file_path&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CalledProcessError&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Error while resetting file: &lt;/span>&lt;span class="si">{}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;The file is larger than 50MB and has been added to .gitignore. Please confirm and recommit!&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file_path&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">exit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/index.png"
width="705"
height="404"
srcset="https://www.zata.cc/p/gitgithub/images/index/index_hu17052794815196557193.png 480w, https://www.zata.cc/p/gitgithub/images/index/index_hu1959225404414415857.png 1024w"
loading="lazy"
class="gallery-image"
data-flex-grow="174"
data-flex-basis="418px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/index-1.png"
width="734"
height="840"
srcset="https://www.zata.cc/p/gitgithub/images/index/index-1_hu12338691074657616501.png 480w, https://www.zata.cc/p/gitgithub/images/index/index-1_hu1304271098367262708.png 1024w"
loading="lazy"
alt="演示"
class="gallery-image"
data-flex-grow="87"
data-flex-basis="209px"
>&lt;/p>
&lt;p>&lt;strong>下面可以配置全局Git 钩子&lt;/strong>&lt;/p>
&lt;p>LINUX平台&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建全局 Git 钩子目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir -p ~/.git-template/hooks
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建并编辑 pre-commit 钩子脚本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &amp;gt; ~/.git-template/hooks/pre-commit &lt;span class="s">&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">MAX_SIZE=$((50 * 1024 * 1024)) # 50MB in bytes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># 获取暂存区所有文件的列表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">files=$(git diff --cached --name-only --diff-filter=ACM)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for file in $files; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # 检查文件是否存在
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [ -f &amp;#34;$file&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # 获取文件大小（字节）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> size=$(stat -f%z &amp;#34;$file&amp;#34; 2&amp;gt;/dev/null || stat -c%s &amp;#34;$file&amp;#34; 2&amp;gt;/dev/null)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [ &amp;#34;$size&amp;#34; -gt &amp;#34;$MAX_SIZE&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;错误：文件 &amp;#39;$file&amp;#39; 超过 50MB（大小：$((size / 1024 / 1024))MB）&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> exit 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">exit 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 赋予执行权限&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x ~/.git-template/hooks/pre-commit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 配置 Git 使用全局钩子模板&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global init.templatedir ~/.git-template
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># （可选）为现有仓库手动复制钩子（替换 /path/to/your/repo 为实际路径）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># cp ~/.git-template/hooks/pre-commit /path/to/your/repo/.git/hooks/pre-commit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># chmod +x /path/to/your/repo/.git/hooks/pre-commit&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>WINDOWS平台&lt;/p>
&lt;p>需要 POWERSHELL 因为环境变量&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 创建全局 Git 钩子目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$templateDir&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$env:USERPROFILE&lt;/span>&lt;span class="s2">\.git-template\hooks&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">New-Item&lt;/span> &lt;span class="n">-ItemType&lt;/span> &lt;span class="n">Directory&lt;/span> &lt;span class="n">-Force&lt;/span> &lt;span class="n">-Path&lt;/span> &lt;span class="nv">$templateDir&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 创建 pre-commit 钩子脚本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$hookPath&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$templateDir&lt;/span>&lt;span class="s2">\pre-commit&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Set-Content&lt;/span> &lt;span class="n">-Path&lt;/span> &lt;span class="nv">$hookPath&lt;/span> &lt;span class="n">-Value&lt;/span> &lt;span class="sh">@&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">MAX_SIZE=$((50 * 1024 * 1024)) # 50MB in bytes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"># 获取暂存区所有文件的列表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">files=$(git diff --cached --name-only --diff-filter=ACM)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">for file in $files; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> # 检查文件是否存在
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> if [ -f &amp;#34;$file&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> # 获取文件大小（字节）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> size=$(wc -c &amp;lt; &amp;#34;$file&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> if [ &amp;#34;$size&amp;#34; -gt &amp;#34;$MAX_SIZE&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> echo &amp;#34;错误：文件 &amp;#39;$file&amp;#39; 超过 50MB（大小：$((size / 1024 / 1024))MB）&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> exit 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">exit 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sh">&amp;#39;@&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 赋予执行权限（Windows 下无需 chmod，但确保 Git Bash 支持）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 配置 Git 使用全局钩子模板&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">git&lt;/span> &lt;span class="n">config&lt;/span> &lt;span class="p">-&lt;/span>&lt;span class="n">-global&lt;/span> &lt;span class="n">init&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="py">templatedir&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$env:USERPROFILE&lt;/span>&lt;span class="s2">\.git-template&amp;#34;&lt;/span> &lt;span class="c"># 如果你不用变量，那么就赋绝对路径&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># （可选）为现有仓库手动复制钩子（将以下路径替换为实际仓库路径）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># $repoPath = &amp;#34;C:\path\to\your\repo&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Copy-Item -Path $hookPath -Destination &amp;#34;$repoPath\.git\hooks\pre-commit&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>也可以按照下面进行手动配置&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>首先创建一个文件夹，然后git init，可以手动将上面代码粘贴修改pre-commit文件&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/index-3.png"
width="886"
height="796"
srcset="https://www.zata.cc/p/gitgithub/images/index/index-3_hu4356868991785405460.png 480w, https://www.zata.cc/p/gitgithub/images/index/index-3_hu14436102453899749230.png 1024w"
loading="lazy"
alt="配置文件夹"
class="gallery-image"
data-flex-grow="111"
data-flex-basis="267px"
>&lt;/p>
&lt;ol start="2">
&lt;li>配置全局钩子，需要绝对路径&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/index-2.png"
width="997"
height="160"
srcset="https://www.zata.cc/p/gitgithub/images/index/index-2_hu15037366000381273471.png 480w, https://www.zata.cc/p/gitgithub/images/index/index-2_hu13621154539024177249.png 1024w"
loading="lazy"
alt="配置全局"
class="gallery-image"
data-flex-grow="623"
data-flex-basis="1495px"
>&lt;/p>
&lt;ol start="3">
&lt;li>可以测试一下，原理就是git init的时候，把设置的这些配置复制一份&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/index-4.png"
width="984"
height="707"
srcset="https://www.zata.cc/p/gitgithub/images/index/index-4_hu18398455187137645033.png 480w, https://www.zata.cc/p/gitgithub/images/index/index-4_hu18373867733037030671.png 1024w"
loading="lazy"
alt="创建的新本地git仓库"
class="gallery-image"
data-flex-grow="139"
data-flex-basis="334px"
>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/index-5.png"
width="1113"
height="968"
srcset="https://www.zata.cc/p/gitgithub/images/index/index-5_hu15228099333374333631.png 480w, https://www.zata.cc/p/gitgithub/images/index/index-5_hu649624552783475551.png 1024w"
loading="lazy"
alt="测试大文件"
class="gallery-image"
data-flex-grow="114"
data-flex-basis="275px"
>&lt;/p>
&lt;h3 id="cannot_lock_ref_error">cannot_lock_ref_error
&lt;/h3>&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/index-6.png"
width="1114"
height="209"
srcset="https://www.zata.cc/p/gitgithub/images/index/index-6_hu6141658079254375775.png 480w, https://www.zata.cc/p/gitgithub/images/index/index-6_hu8272317630638439298.png 1024w"
loading="lazy"
alt="cannot_lock_ref_error"
class="gallery-image"
data-flex-grow="533"
data-flex-basis="1279px"
>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">root@ce4322c051c6:/codes/dataAnalysisModels# git pull
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remote: Enumerating objects: 193, &lt;span class="k">done&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remote: Counting objects: 100% &lt;span class="o">(&lt;/span>193/193&lt;span class="o">)&lt;/span>, &lt;span class="k">done&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remote: Compressing objects: 100% &lt;span class="o">(&lt;/span>178/178&lt;span class="o">)&lt;/span>, &lt;span class="k">done&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remote: Total &lt;span class="m">191&lt;/span> &lt;span class="o">(&lt;/span>delta 10&lt;span class="o">)&lt;/span>, reused &lt;span class="m">188&lt;/span> &lt;span class="o">(&lt;/span>delta 10&lt;span class="o">)&lt;/span>, pack-reused &lt;span class="m">0&lt;/span> &lt;span class="o">(&lt;/span>from 0&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Receiving objects: 100% &lt;span class="o">(&lt;/span>191/191&lt;span class="o">)&lt;/span>, 115.48 MiB &lt;span class="p">|&lt;/span> 149.00 KiB/s, &lt;span class="k">done&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Resolving deltas: 100% &lt;span class="o">(&lt;/span>10/10&lt;span class="o">)&lt;/span>, completed with &lt;span class="m">2&lt;/span> &lt;span class="nb">local&lt;/span> objects.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">error: cannot lock ref &lt;span class="s1">&amp;#39;refs/remotes/zata/main&amp;#39;&lt;/span>: is at 801efa1db3242f2be077de0d34e4eb29af0a5751 but expected aab6c46b69629454c3ad76e5a030157169eabb6e
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">From github.com:zata-zhangtao/dataAnalysisAndModeling_In-GY
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ! aab6c46..801efa1 main -&amp;gt; zata/main &lt;span class="o">(&lt;/span>unable to update &lt;span class="nb">local&lt;/span> ref&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我的解决方法是&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch --prune
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="云服务器无法访问github导致git失败方案">云服务器无法访问Github导致git失败方案
&lt;/h3>&lt;p>可以先去这个网站，哪些ip可用&lt;/p>
&lt;p>&lt;a class="link" href="https://ping.chinaz.com/github.com" target="_blank" rel="noopener"
>https://ping.chinaz.com/github.com&lt;/a>&lt;/p>
&lt;p>然后要修改服务器端的HOSTS&lt;/p>
&lt;p>例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">20.205.243.166 github.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">20.205.243.166 raw.githubusercontent.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>hosts位置在 /etc/hosts&lt;/p>
&lt;h2 id="知识点">知识点
&lt;/h2>&lt;h3 id="github-actions-密钥管理全指南repositoryenvironment-与-organization-secrets-的区别与最佳实践">GitHub Actions 密钥管理全指南：Repository、Environment 与 Organization Secrets 的区别与最佳实践
&lt;/h3>&lt;p>在配置 GitHub Actions CI/CD 流水线时，管理敏感信息（API Key、数据库密码、部署 Token）是重中之重。GitHub 提供了多种存储 Secrets（密钥）的方式，最常见的是 &lt;strong>Repository Secrets&lt;/strong>，但你可能也注意到了 &lt;strong>Environment Secrets&lt;/strong> 和 &lt;strong>Organization Secrets&lt;/strong>。&lt;/p>
&lt;p>它们到底有什么区别？如果我想让一个变量在几十个仓库里通用，又该怎么办？这篇文章将为你彻底理清这三者的关系、优先级和最佳应用场景。&lt;/p>
&lt;p>&lt;strong>一、Repository Secrets vs. Environment Secrets&lt;/strong>&lt;/p>
&lt;p>这是最基础的两个概念，决定了你的密钥是在“仓库级别”可见，还是在“特定部署环境”下可见。&lt;/p>
&lt;p>&lt;strong>1. Repository Secrets (仓库级密钥)&lt;/strong>&lt;/p>
&lt;p>这是最常用的类型。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>位置&lt;/strong>：&lt;code>Settings&lt;/code> -&amp;gt; &lt;code>Secrets and variables&lt;/code> -&amp;gt; &lt;code>Actions&lt;/code> -&amp;gt; &lt;code>Repository secrets&lt;/code>&lt;/li>
&lt;li>&lt;strong>作用域&lt;/strong>：整个代码仓库。仓库内的任何 Workflow、任何分支、任何 Job 都可以直接读取。&lt;/li>
&lt;li>&lt;strong>适用场景&lt;/strong>：
&lt;ul>
&lt;li>通用的 CI 工具 Token（如 Codecov, SonarQube）。&lt;/li>
&lt;li>构建阶段需要的非敏感于环境的凭证（如拉取私有 Docker 镜像的 Token）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. Environment Secrets (环境级密钥)&lt;/strong>&lt;/p>
&lt;p>这是为 CD（持续部署）设计的更高级功能。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>位置&lt;/strong>：&lt;code>Settings&lt;/code> -&amp;gt; &lt;code>Environments&lt;/code> -&amp;gt; 选择环境（如 &lt;code>production&lt;/code>） -&amp;gt; &lt;code>Environment secrets&lt;/code>&lt;/li>
&lt;li>&lt;strong>作用域&lt;/strong>：仅限在 Workflow YAML 中明确指定了该环境的 Job。&lt;/li>
&lt;li>&lt;strong>核心优势&lt;/strong>：
&lt;ol>
&lt;li>&lt;strong>部署保护（Deployment Protection Rules）&lt;/strong>：你可以设置 &lt;strong>Required reviewers (人工审批)&lt;/strong>。例如，Workflow 运行到部署生产环境的步骤时会暂停，直到管理员点击&amp;quot;Approve&amp;rdquo;，代码才能拿到 Environment Secret 并继续执行。&lt;/li>
&lt;li>&lt;strong>同名变量覆盖&lt;/strong>：允许开发环境和生产环境使用同一个变量名（如 &lt;code>DB_HOST&lt;/code>），但值不同。&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>代码示例：如何使用 Environment Secret&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deploy-prod&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 关键点：必须声明 environment，才能读取到该环境下的 Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">production &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 如果 production 环境里定义了 API_KEY，这里读到的就是生产环境的 Key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">API_KEY&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.API_KEY }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./deploy.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. 对比总结&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">特性&lt;/th>
&lt;th style="text-align: left">Repository Secrets&lt;/th>
&lt;th style="text-align: left">Environment Secrets&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>可见性&lt;/strong>&lt;/td>
&lt;td style="text-align: left">所有 Job 可见&lt;/td>
&lt;td style="text-align: left">仅声明 &lt;code>environment: xxx&lt;/code> 的 Job 可见&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>审批机制&lt;/strong>&lt;/td>
&lt;td style="text-align: left">不支持&lt;/td>
&lt;td style="text-align: left">&lt;strong>支持人工审批、等待计时器等&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>优先级&lt;/strong>&lt;/td>
&lt;td style="text-align: left">较低&lt;/td>
&lt;td style="text-align: left">&lt;strong>最高&lt;/strong> (覆盖同名 Repository Secret)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>典型用途&lt;/strong>&lt;/td>
&lt;td style="text-align: left">单元测试、Lint 检查、构建镜像&lt;/td>
&lt;td style="text-align: left">生产环境数据库密码、AWS 生产证书&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;p>&lt;strong>二、进阶痛点：我想定义所有仓库都能用的变量怎么办？&lt;/strong>&lt;/p>
&lt;p>当你拥有 20 个微服务仓库，且都需要使用同一个 NPM Token 或者 Slack 通知 Webhook URL 时，在每个仓库里重复配置 Repository Secrets 会让人崩溃。&lt;/p>
&lt;p>这时你需要 &lt;strong>Organization Secrets (组织级密钥)&lt;/strong>。&lt;/p>
&lt;p>&lt;strong>1. 什么是 Organization Secrets？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>位置&lt;/strong>：组织主页 -&amp;gt; &lt;code>Settings&lt;/code> -&amp;gt; &lt;code>Secrets and variables&lt;/code> -&amp;gt; &lt;code>Actions&lt;/code>。&lt;/li>
&lt;li>&lt;strong>前提&lt;/strong>：你的仓库必须属于一个 Organization（组织账号），个人账号不支持此功能。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. 强大的访问策略 (Access Policy)&lt;/strong>&lt;/p>
&lt;p>在创建组织密钥时，你可以控制它的传播范围：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>All repositories&lt;/strong>：组织下所有（现有和未来新建的）仓库都能自动读取。&lt;/li>
&lt;li>&lt;strong>Private repositories only&lt;/strong>：仅私有仓库可用（防止开源项目泄露公司 Token）。&lt;/li>
&lt;li>&lt;strong>Selected repositories&lt;/strong>：手动勾选特定仓库可用（&lt;strong>推荐&lt;/strong>，遵循最小权限原则）。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. Organization Variables (组织级变量)&lt;/strong>&lt;/p>
&lt;p>除了加密的 Secrets，GitHub 还允许在组织级定义 &lt;strong>Variables&lt;/strong>（明文变量）。&lt;/p>
&lt;ul>
&lt;li>适用于非敏感配置，如 &lt;code>API_BASE_URL&lt;/code>、&lt;code>COMPANY_NAME&lt;/code>。&lt;/li>
&lt;li>在代码中通过 &lt;code>${{ vars.MY_VAR }}&lt;/code> 访问。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>三、终极问题：同名变量的优先级（覆盖逻辑）&lt;/strong>&lt;/p>
&lt;p>如果我在 Organization、Repository 和 Environment 里都定义了同一个名字的变量（比如 &lt;code>DATABASE_URL&lt;/code>），GitHub 会读取哪一个？&lt;/p>
&lt;p>遵循 &lt;strong>“越具体，优先级越高”&lt;/strong> 的原则：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Environment Secrets&lt;/strong> (最高优先级) 👑&lt;/li>
&lt;li>&lt;strong>Repository Secrets&lt;/strong> (中等优先级)&lt;/li>
&lt;li>&lt;strong>Organization Secrets&lt;/strong> (最低优先级)&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>实战场景：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>你在 &lt;strong>组织级&lt;/strong> 设置了 &lt;code>SLACK_WEBHOOK&lt;/code> 用于日常通知。&lt;/li>
&lt;li>突然有个特定仓库 &lt;code>Project-X&lt;/code> 需要发送通知到另一个独立的 Slack 频道。&lt;/li>
&lt;li>你只需在 &lt;code>Project-X&lt;/code> 的 &lt;strong>Repository secrets&lt;/strong> 里新建一个同名的 &lt;code>SLACK_WEBHOOK&lt;/code>。&lt;/li>
&lt;li>GitHub 会自动优先使用仓库级的配置，而不会影响组织内其他 99 个仓库。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>四、最佳实践决策清单&lt;/strong>&lt;/p>
&lt;p>在创建变量前，请对照此清单选择存储位置：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>这个变量是所有（或多个）项目通用的吗？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>是 -&amp;gt; &lt;strong>Organization Secrets&lt;/strong> (如：公司通用的 NPM Token, Slack Webhook)。&lt;/li>
&lt;li>否 -&amp;gt; 往下看。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>这个变量涉及“生产环境”或“高风险”操作吗？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>是（如 AWS Prod Key, 生产库密码）-&amp;gt; &lt;strong>Environment Secrets&lt;/strong> (并开启人工审批保护)。&lt;/li>
&lt;li>否 -&amp;gt; 往下看。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>这个变量需要在不同的环境（Dev/Test/Prod）有不同的值吗？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>是 -&amp;gt; &lt;strong>Environment Secrets&lt;/strong> (在不同环境里设同名变量)。&lt;/li>
&lt;li>否 -&amp;gt; 往下看。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>默认选项：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>上述情况都不是 -&amp;gt; &lt;strong>Repository Secrets&lt;/strong>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;p>&lt;strong>结语&lt;/strong>&lt;/p>
&lt;p>掌握这三个层级的 Secrets 管理，不仅能让你的 GitHub Actions Workflow 代码更简洁（由 &lt;code>if/else&lt;/code> 地狱转变为统一变量名），还能大幅提升 CI/CD 流程的安全性。&lt;/p>
&lt;p>&lt;strong>Tip&lt;/strong>: 如果你是个人开发者账号，无法使用 Organization Secrets，建议创建一个免费的 Organization 并将仓库 Transfer 进去，以便享受全局变量管理的便利。&lt;/p>
&lt;h3 id="将python包发布到github并通过pip安装的教程">将Python包发布到GitHub并通过pip安装的教程
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>创建Python包结构&lt;/strong>&lt;br>
构建以下目录结构：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">your_package/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── your_package/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── __init__.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── your_module.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── setup.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── README.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── LICENSE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── requirements.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>编写关键文件&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>setup.py&lt;/strong> 示例：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">setuptools&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">setup&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">find_packages&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">setup&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;your_package_name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">version&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;0.1.0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">packages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">find_packages&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">install_requires&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;requests&amp;gt;=2.25.1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">author&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Your Name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">author_email&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;your.email@example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;A short description of your package&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">long_description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;README.md&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">long_description_content_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;text/markdown&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;https://github.com/yourusername/your_package&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">classifiers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;Programming Language :: Python :: 3&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;License :: OSI Approved :: MIT License&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>your_package/&lt;strong>init&lt;/strong>.py&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">__version__&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;0.1.0&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>初始化Git并上传到GitHub&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Initial commit&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote add origin https://github.com/yourusername/your_package.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch -M main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push -u origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>创建GitHub Release（可选，但推荐）&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>访问GitHub仓库页面，点击&amp;quot;Releases&amp;quot; -&amp;gt; &amp;ldquo;Create a new release&amp;rdquo;&lt;/li>
&lt;li>输入版本号（如v0.1.0），发布&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>通过pip安装&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>直接从GitHub安装：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install git+https://github.com/yourusername/your_package.git
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>指定分支：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install git+https://github.com/yourusername/your_package.git@branch_name
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>指定版本（需要有release）：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install git+https://github.com/yourusername/your_package.git@v0.1.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>发布到PyPI（可选，允许标准pip安装）&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>安装工具：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install twine build
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>构建包：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">python -m build
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>上传到PyPI：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">twine upload dist/*
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>之后可使用：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install your_package_name
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>注意事项&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>确保&lt;code>setup.py&lt;/code>信息准确&lt;/li>
&lt;li>设置清晰的GitHub仓库描述和topics&lt;/li>
&lt;li>使用MIT或其他合适的许可证&lt;/li>
&lt;li>编写详细的README.md，包含安装和使用说明&lt;/li>
&lt;li>发布到PyPI需注册账号并配置API token&lt;/li>
&lt;/ul>
&lt;h3 id="git-reset---hard-head-详解andgit-reset---soft-head-详解">git-reset&amp;mdash;hard-head-详解andgit-reset&amp;mdash;soft-head-详解
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git reset --hard HEAD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>效果：&lt;/p>
&lt;p>撤销所有未提交的更改（包括工作目录和暂存区）。
HEAD 指向的提交成为当前状态，之前的修改全部丢失（不可恢复，除非有其他备份）。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git reset --soft HEAD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>功能：将当前分支的指针重置到 HEAD 指向的提交，但保留工作目录和暂存区的所有更改。
效果：&lt;/p>
&lt;p>撤销最近的提交（将 HEAD 指针移到当前提交），但修改的内容仍保留在工作目录或暂存区。
可以重新调整或重新提交这些更改。&lt;/p>
&lt;p>下面我将逐个参数解释这条命令的含义：&lt;/p>
&lt;p>命令分解&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>&lt;code>git reset&lt;/code>&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>git reset&lt;/code> 是 Git 用来重置当前分支的 HEAD（当前分支指针）到指定状态的命令。&lt;/li>
&lt;li>它可以影响 Git 的三个主要区域：
&lt;ul>
&lt;li>&lt;strong>工作目录（Working Directory）&lt;/strong>：你当前编辑的文件。&lt;/li>
&lt;li>&lt;strong>暂存区（Staging Area/Index）&lt;/strong>：通过 &lt;code>git add&lt;/code> 添加的文件。&lt;/li>
&lt;li>&lt;strong>提交历史（Commit History）&lt;/strong>：Git 仓库中的提交记录。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>&lt;code>--hard&lt;/code>&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>--hard&lt;/code> 是 &lt;code>git reset&lt;/code> 的一个选项，指定重置的模式。&lt;/li>
&lt;li>它表示&lt;strong>完全重置&lt;/strong>，不仅会移动 HEAD 指针，还会：
&lt;ul>
&lt;li>重置工作目录中的文件内容，使其与指定的提交状态一致。&lt;/li>
&lt;li>清空暂存区的内容。&lt;/li>
&lt;li>丢弃所有未提交的更改（包括工作目录和暂存区的修改）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>简单来说，&lt;code>--hard&lt;/code> 会让你的工作目录、暂存区和提交历史完全恢复到指定的提交状态，&lt;strong>不可恢复已丢弃的更改&lt;/strong>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>&lt;code>HEAD^&lt;/code>&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>HEAD&lt;/code> 是 Git 中的一个指针，指向当前分支的最新提交。&lt;/li>
&lt;li>&lt;code>^&lt;/code> 是一个相对引用，表示&amp;quot;当前 HEAD 的上一个提交&amp;quot;（即父提交）。&lt;/li>
&lt;li>因此，&lt;code>HEAD^&lt;/code> 表示当前分支最新提交的上一个提交。&lt;/li>
&lt;li>如果当前分支的提交历史是 &lt;code>A &amp;lt;- B &amp;lt;- C&lt;/code>（C 是 HEAD），那么 &lt;code>HEAD^&lt;/code> 指向 &lt;code>B&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>整体含义
&lt;code>git reset --hard HEAD^&lt;/code> 的作用是：&lt;/p>
&lt;ul>
&lt;li>将当前分支的 HEAD 指针移动到上一个提交（&lt;code>HEAD^&lt;/code>）。&lt;/li>
&lt;li>重置工作目录和暂存区，使其与 &lt;code>HEAD^&lt;/code> 指向的提交状态完全一致。&lt;/li>
&lt;li>&lt;strong>丢弃&lt;/strong>当前 HEAD 提交（最新提交）以及工作目录和暂存区的所有未提交更改。&lt;/li>
&lt;/ul>
&lt;p>示例场景
假设你的提交历史如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">A &amp;lt;- B &amp;lt;- C (HEAD)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="githook脚本版本控制管理">githook脚本版本控制管理
&lt;/h3>&lt;p>在 Git 项目中，&lt;code>.githooks&lt;/code> 目录中的钩子（hook）脚本默认是不会被 Git 版本控制系统自动纳入版本管理的，因为 &lt;code>.githooks&lt;/code> 目录通常被视为本地配置的一部分。为了将 Git 钩子脚本保留到项目中并与团队共享，你需要采取一些额外的步骤。以下是具体的方法：&lt;/p>
&lt;p>方法一：将钩子脚本纳入版本控制&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>将 &lt;code>.githooks&lt;/code> 目录重命名或移动到项目中&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>默认情况下，Git 钩子存储在 &lt;code>.git/hooks&lt;/code> 目录中，这些文件不会被 Git 跟踪。你可以将钩子脚本移动到项目的一个自定义目录（例如 &lt;code>githooks&lt;/code> 或 &lt;code>hooks&lt;/code>），并纳入版本控制。&lt;/li>
&lt;li>示例：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir githooks
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mv .git/hooks/pre-commit githooks/pre-commit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>配置 Git 使用自定义钩子目录&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>使用以下命令告诉 Git 使用项目中的自定义钩子目录：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git config core.hooksPath githooks
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">cd&lt;/span> githooks
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 查找所有文件 (f) 并赋予执行权限 &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">find . -type f -exec chmod +x &lt;span class="o">{}&lt;/span> &lt;span class="se">\;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>这会让 Git 使用 &lt;code>githooks&lt;/code> 目录中的钩子脚本，而不是默认的 &lt;code>.git/hooks&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>将钩子脚本提交到版本控制&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>将 &lt;code>githooks&lt;/code> 目录添加到 Git 版本控制：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add githooks
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Add git hooks to project&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>团队成员同步配置&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>其他团队成员在克隆或拉取项目后，需要手动运行 &lt;code>git config core.hooksPath githooks&lt;/code> 来启用自定义钩子路径。或者，你可以通过脚本自动设置。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>方法二：使用脚本自动安装钩子
为了让团队成员无需手动配置 &lt;code>core.hooksPath&lt;/code>，你可以在项目中添加一个安装脚本，自动将钩子脚本复制到 &lt;code>.git/hooks&lt;/code> 目录。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>创建安装脚本&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>在项目根目录创建一个脚本（例如 &lt;code>install-hooks.sh&lt;/code>）：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>cp githooks/* .git/hooks/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x .git/hooks/*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Git hooks installed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>这个脚本会将 &lt;code>githooks&lt;/code> 目录中的钩子复制到 &lt;code>.git/hooks&lt;/code> 目录，并确保它们具有可执行权限。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>添加到版本控制&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>将 &lt;code>install-hooks.sh&lt;/code> 和 &lt;code>githooks&lt;/code> 目录提交到 Git：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add githooks install-hooks.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Add git hooks and install script&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>运行安装脚本&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>团队成员在克隆项目后，运行以下命令来安装钩子：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./install-hooks.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>方法三：使用 Git 模板目录
如果你希望钩子脚本在所有新项目中自动生效，可以配置 Git 的全局模板目录：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>创建全局钩子模板&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>复制默认的 Git 钩子模板到自定义目录：(如果你还没有自定义模板目录，可以通过下面的代码创建一个)
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git init --template&lt;span class="o">=&lt;/span>/path/to/custom-template
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>在 &lt;code>/path/to/custom-template/hooks&lt;/code> 中添加你的钩子脚本。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>也可以配置全局模板路径&lt;/strong>就不用第一步了：&lt;/p>
&lt;ul>
&lt;li>设置 Git 的全局模板路径：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git config --global init.templateDir /path/to/custom-template
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>之后，任何新初始化的 Git 仓库都会使用这个模板。&lt;/li>
&lt;li>具体的使用和介绍可见shou 配置钩子脚本，请查看&lt;a class="link" href="#%e7%94%a8githooks%e8%a7%a3%e5%86%b3github%e5%a4%a7%e6%96%87%e4%bb%b6%e6%8a%a5%e9%94%99100M%e9%99%90%e5%88%b6%e6%88%9650M%e9%99%90%e5%88%b6-%e5%a4%a7%e6%96%87%e4%bb%b6-50M-githooks-git" >用git hooks解决github大文件报错，100M限制或50M限制|大文件|50M|git hooks|git&lt;/a>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>注意事项&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>这种方法适合个人开发环境，但不适合团队项目，因为模板目录是本地的，无法直接共享。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>方法四：使用工具管理钩子
可以使用一些工具来简化 Git 钩子的管理和共享，例如：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Husky&lt;/strong>（适用于 Node.js 项目）：&lt;/p>
&lt;ul>
&lt;li>如果你的项目是 Node.js 项目，可以使用 Husky 来管理 Git 钩子。安装 Husky 后，它会自动管理 &lt;code>.git/hooks&lt;/code> 目录，并在 &lt;code>package.json&lt;/code> 中定义钩子脚本。&lt;/li>
&lt;li>安装：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npm install husky --save-dev
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>配置（在 &lt;code>package.json&lt;/code> 中）：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;husky&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hooks&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pre-commit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;echo &amp;#39;Running pre-commit hook&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>pre-commit&lt;/strong>（适用于 Python 项目）：&lt;/p>
&lt;ul>
&lt;li>如果是 Python 项目，可以使用 &lt;code>pre-commit&lt;/code> 框架来管理钩子。创建一个 &lt;code>.pre-commit-config.yaml&lt;/code> 文件，定义钩子脚本，并提交到版本控制。&lt;/li>
&lt;li>安装：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install pre-commit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pre-commit install
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>注意事项&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>权限问题&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>确保钩子脚本具有可执行权限（&lt;code>chmod +x githooks/*&lt;/code>）。&lt;/li>
&lt;li>在 Windows 系统上，可能需要额外处理文件权限问题。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>跨平台兼容性&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>如果团队成员使用不同操作系统（例如 Windows 和 Linux），确保钩子脚本是跨平台的（例如，使用 Bash 脚本或 Python 脚本）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>文档说明&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>在项目的 &lt;code>README.md&lt;/code> 中添加说明，告诉团队成员如何启用钩子（例如运行 &lt;code>install-hooks.sh&lt;/code> 或设置 &lt;code>core.hooksPath&lt;/code>）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>避免覆盖本地钩子&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>如果直接覆盖 &lt;code>.git/hooks&lt;/code>，可能会覆盖团队成员的本地钩子配置。使用 &lt;code>core.hooksPath&lt;/code> 或脚本复制的方式更安全。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>总结
最推荐的方式是将钩子脚本放入项目中的 &lt;code>githooks&lt;/code> 目录，提交到版本控制，并通过脚本或 &lt;code>git config core.hooksPath&lt;/code> 自动配置。结合工具如 Husky 或 pre-commit 可以进一步简化管理。根据项目类型和团队习惯选择合适的方法。&lt;/p>
&lt;ul>
&lt;li>当前 HEAD 指向提交 &lt;code>C&lt;/code>。&lt;/li>
&lt;li>执行 &lt;code>git reset --hard HEAD^&lt;/code> 后：
&lt;ul>
&lt;li>HEAD 移动到 &lt;code>B&lt;/code>（&lt;code>HEAD^&lt;/code>）。&lt;/li>
&lt;li>提交 &lt;code>C&lt;/code> 从当前分支的提交历史中移除（但可能仍存在于 Git 的对象数据库中，直到被垃圾回收）。&lt;/li>
&lt;li>工作目录和暂存区的内容恢复到提交 &lt;code>B&lt;/code> 的状态。&lt;/li>
&lt;li>任何未提交的更改（工作目录或暂存区）都会被永久删除。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>新的提交历史变为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">A &amp;lt;- B (HEAD)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意事项&lt;/p>
&lt;ul>
&lt;li>&lt;strong>数据丢失风险&lt;/strong>：&lt;code>--hard&lt;/code> 会永久删除未提交的更改和指定的提交（&lt;code>HEAD&lt;/code> 到 &lt;code>HEAD^&lt;/code> 之间的提交）。在执行前，建议使用 &lt;code>git status&lt;/code> 检查是否有未提交的更改，或者用 &lt;code>git log&lt;/code> 确认提交历史。&lt;/li>
&lt;li>&lt;strong>备份建议&lt;/strong>：如果不确定是否需要丢弃更改，可以先用 &lt;code>git branch backup&lt;/code> 创建一个备份分支，以保留当前 HEAD 的状态。&lt;/li>
&lt;li>&lt;strong>远程仓库影响&lt;/strong>：如果当前分支已经推送到远程仓库（如 GitHub），执行 &lt;code>git reset --hard HEAD^&lt;/code> 后需要用 &lt;code>git push --force&lt;/code> 强制推送，这可能会影响其他协作者，需谨慎操作。&lt;/li>
&lt;/ul>
&lt;p>总结&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>git reset&lt;/code>&lt;/strong>: 重置 HEAD 到指定状态。&lt;/li>
&lt;li>&lt;strong>&lt;code>--hard&lt;/code>&lt;/strong>: 完全重置，丢弃工作目录和暂存区的更改。&lt;/li>
&lt;li>&lt;strong>&lt;code>HEAD^&lt;/code>&lt;/strong>: 指向当前 HEAD 的上一个提交。&lt;/li>
&lt;/ul>
&lt;p>这条命令的总体效果是&amp;quot;撤销最近一次提交并恢复到上一个提交的状态，同时丢弃所有未提交的更改&amp;quot;。如果你只是想撤销提交但保留更改，可以考虑使用 &lt;code>git reset --soft HEAD^&lt;/code> 或其他命令（如 &lt;code>git revert&lt;/code>）。&lt;/p>
&lt;h2 id="实战----使用">实战 &amp;ndash; 使用
&lt;/h2>&lt;h3 id="软件版本管理与热修复流程">软件版本管理与热修复流程
&lt;/h3>&lt;p>这是一个非常经典的软件版本管理问题。在真实的开发场景中非常常见，通常被称为**“热修复”（Hotfix）或“补丁发布”（Patch Release）**。&lt;/p>
&lt;p>为了在不影响 0.6.0 开发进度的情况下修复 0.5.0 的 bug，你需要基于 0.5.0 的代码拉取一个新分支，修复 bug 后发布 0.5.1，然后再把修复代码同步回 main 分支。&lt;/p>
&lt;p>以下是标准的处理流程（假设你使用的是 Git）：&lt;/p>
&lt;p>&lt;strong>第一步：找到 0.5.0 的代码并拉取新分支&lt;/strong>&lt;/p>
&lt;p>通常你在发布 0.5.0 时会打一个标签（Tag），或者你能找到发布 0.5.0 时的那个 Commit 记录。我们需要基于这个点创建一个专门用于修复和发布的分支（比如叫 hotfix-0.5.1 或 release-0.5.x）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 假设你的 0.5.0 标签名为 v0.5.0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout -b hotfix-0.5.1 v0.5.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 如果你没有打标签，可以找到当时的 commit hash（比如 a1b2c3d）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout -b hotfix-0.5.1 a1b2c3d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>第二步：在新分支上修复 Bug 并修改版本号&lt;/strong>&lt;/p>
&lt;p>现在你的代码状态已经完全回退到了 0.5.0 发布时的样子。在这里修复那个 bug，并将项目配置文件中的版本号从 0.5.0 修改为 0.5.1（如果你有 package.json、pom.xml 等文件的话）。提交你的代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;fix: 修复了 0.5.0 版本的严重 bug&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;chore: 升级版本号至 0.5.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>第三步：发布 0.5.1&lt;/strong>&lt;/p>
&lt;p>现在你可以基于这个 hotfix-0.5.1 分支去打包、测试并发布产品了。发布完成后，别忘了打上新标签：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git tag v0.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin v0.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin hotfix-0.5.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>到这一步，你的用户已经可以拿到没有 bug 的 0.5.1 版本了，而你的 main 分支毫无影响。&lt;/p>
&lt;p>&lt;strong>第四步：将修复代码同步回 main 分支（极其重要！）&lt;/strong>&lt;/p>
&lt;p>这一步绝对不能忘！否则你以后发布 0.6.0 时，这个 bug 还会再次出现（也就是所谓的回归 bug）。你有两种常见的方式将修复同步回 main：&lt;/p>
&lt;p>&lt;strong>方式 A：合并分支（Merge）—— 适合 Git Flow 等标准工作流&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git merge hotfix-0.5.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 如果有冲突，解决冲突后提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意：这可能会把 0.5.1 的版本号更改也合并进来，你可能需要在 main 分支再次修改版本号回 0.6.0-dev。&lt;/p>
&lt;p>&lt;strong>方式 B：摘取提交（Cherry-pick）—— 很多团队更喜欢这种，因为它更干净&lt;/strong>&lt;/p>
&lt;p>如果你在 hotfix-0.5.1 分支上只有一两个修复 bug 的 Commit，你可以只把修复 bug 的代码拿过来，而不拿修改版本号的代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 1. 先用 git log 看一下你修复 bug 的那个 commit 的 hash 值（比如 f8e9d0c）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 2. 切换回 main 分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># 3. 将修复代码“摘”过来&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git cherry-pick f8e9d0c
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>总结流程图&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> (0.5.0) -----------------------------------&amp;gt; (0.6.0) [main 分支]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | ^
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | | (同步修复代码 Merge 或 Cherry-pick)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> +---&amp;gt;[修 bug] ---&amp;gt; (发布 0.5.1) --------------+ [hotfix 分支]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>通过这种方式，既保证了 0.5.0 用户的可用性，又让 main 分支可以继续安心开发 0.6.0 的新功能。&lt;/p>
&lt;p>&lt;strong>关于 Main 分支的历史记录问题&lt;/strong>&lt;/p>
&lt;p>这个问题非常关键！答案是：这取决于你用哪种方式（Merge 还是 Cherry-pick）把代码同步回 main。准确地说，你关心的“记录”分为两种：修复 Bug 的代码记录（比如提交信息：&amp;ldquo;fix: 修复了 xxx&amp;rdquo;）和 0.5.1 的发布记录（比如提交信息：&amp;ldquo;chore: 升级版本号至 0.5.1&amp;rdquo; 以及 v0.5.1 的标签 Tag）。&lt;/p>
&lt;p>&lt;strong>情况一：如果你使用的是 Cherry-pick（摘取）—— 推荐方式&lt;/strong>&lt;/p>
&lt;p>如果你采用的是 cherry-pick，你只把“修复 Bug&amp;quot;的那个 commit 拿到了 main 分支。在 main 的历史中：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>能看见：&lt;/strong> 修复 Bug 的记录（但 Git 会为它生成一个新的 Commit Hash）。&lt;/li>
&lt;li>&lt;strong>看不见：&lt;/strong> 任何关于&amp;quot;0.5.1&amp;quot;字眼的记录（包括改版本号的 commit 和 0.5.1 的 tag 都不会在 main 上出现）。&lt;/li>
&lt;/ul>
&lt;p>结果：main 分支的历史非常干净，看起来就像是在开发 0.6.0 的过程中顺手修了一个 bug，历史记录是一条直线向前。&lt;/p>
&lt;p>Cherry-pick 后的 main 记录看起来像这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> * 9b8c7d6 (HEAD -&amp;gt; main) fix: 修复了导致崩溃的严重 bug &amp;lt;-- 这是 cherry-pick 过来的新 commit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * 5a4b3c2 feat: 0.6.0 的新功能 B
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * 4z3y2x1 feat: 0.6.0 的新功能 A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * a1b2c3d (tag: v0.5.0) release: 发布 0.5.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>情况二：如果你使用的是 Merge（合并）&lt;/strong>&lt;/p>
&lt;p>如果你直接在 main 分支执行 &lt;code>git merge hotfix-0.5.1&lt;/code>。在 main 的历史中：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>能看见：&lt;/strong> 0.5.1 分支上的所有记录，原封不动地出现在了这里。&lt;/li>
&lt;li>&lt;strong>能看见：&lt;/strong> &amp;ldquo;chore: 升级版本号至 0.5.1&amp;rdquo; 的提交。&lt;/li>
&lt;li>&lt;strong>能看见：&lt;/strong> 一个额外的合并记录（比如 &amp;ldquo;Merge branch &amp;lsquo;hotfix-0.5.1&amp;rsquo; into main&amp;rdquo;）。&lt;/li>
&lt;/ul>
&lt;p>结果：你的 main 分支历史里会混入 0.5.1 的发布痕迹。如果在合并时遇到了代码冲突（比如 main 的版本号已经改成了 0.6.0-dev，而 hotfix 改成了 0.5.1），你还需要手动解决冲突，保留 0.6.0-dev。&lt;/p>
&lt;p>Merge 后的 main 记录看起来像这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> * d4e5f6g (HEAD -&amp;gt; main) Merge branch &amp;#39;hotfix-0.5.1&amp;#39; into main &amp;lt;-- 合并节点
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |\
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | * f8e9d0c (tag: v0.5.1, hotfix-0.5.1) chore: 升级版本号至 0.5.1 &amp;lt;-- 0.5.1 的痕迹进来了
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | * e7d8c9b fix: 修复了导致崩溃的严重 bug
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * | 5a4b3c2 feat: 0.6.0 的新功能 B
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * | 4z3y2x1 feat: 0.6.0 的新功能 A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> * a1b2c3d (tag: v0.5.0) release: 发布 0.5.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>总结与建议&lt;/strong>&lt;/p>
&lt;p>如果你不想在 main 分支里看到 0.5.1 这种历史版本号的“噪音”，只想要修复 bug 的代码，那么极其建议使用 &lt;strong>cherry-pick&lt;/strong>。&lt;/p>
&lt;p>这是大型开源项目（比如 Vue, React）和大多数商业公司的标准做法：&lt;/p>
&lt;ol>
&lt;li>main 分支永远只看前方（0.6.0、0.7.0）。&lt;/li>
&lt;li>旧版本（0.5.0）出了问题，在旧版本对应的分支（hotfix-0.5.x）上修补并发布 0.5.1。&lt;/li>
&lt;li>把修补代码像“摘樱桃”一样（cherry-pick）摘回 main 分支。&lt;/li>
&lt;/ol>
&lt;p>这样 main 里就看不见 0.5.1 的版本发布记录，但 Bug 确实被修好了。&lt;/p>
&lt;h3 id="变基合并多个提交">变基合并多个提交
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 例:变基合并最近2个提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase -i HEAD~2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个命令会打开你的默认文本编辑器（通常是 Vim、VSCode 或 Nano），显示类似以下内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick c2b3a4d 实现用户登录API
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick d3c4b5a 添加输入验证
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick e4d5c6b 移除调试日志
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick f5e6d7a 修复拼写错误
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 变基 b1a2b3c..f5e6d7a 到 b1a2b3c（4个提交）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 命令:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># p, pick &amp;lt;提交&amp;gt; = 使用提交
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># r, reword &amp;lt;提交&amp;gt; = 使用提交，但编辑提交信息
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># e, edit &amp;lt;提交&amp;gt; = 使用提交，但停下来修改提交
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># s, squash &amp;lt;提交&amp;gt; = 使用提交，但合并到前一个提交
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># f, fixup &amp;lt;提交&amp;gt; = 类似于&amp;#34;squash&amp;#34;，但丢弃提交信息
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># x, exec &amp;lt;命令&amp;gt; = 使用shell运行命令（其余行）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># b, break = 在此处停止（稍后用&amp;#39;git rebase --continue&amp;#39;继续）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># d, drop &amp;lt;提交&amp;gt; = 删除提交
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># l, label &amp;lt;标签&amp;gt; = 用名称标记当前HEAD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># t, reset &amp;lt;标签&amp;gt; = 重置HEAD到标签
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># m, merge [-C &amp;lt;提交&amp;gt; | -c &amp;lt;提交&amp;gt;] &amp;lt;标签&amp;gt; [# &amp;lt;单行信息&amp;gt;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># . 创建合并提交，使用原合并提交的信息（如果没有原合并提交，则使用单行信息）
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们需要将后面的3个提交&amp;quot;压缩&amp;quot;（squash）到第一个提交中。编辑文件如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick c2b3a4d 实现用户登录API
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash d3c4b5a 添加输入验证
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash e4d5c6b 移除调试日志
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash f5e6d7a 修复拼写错误
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>保存并关闭编辑器。Git 会开始处理这些提交，然后再次打开编辑器让你编辑合并后的提交信息。&lt;/p>
&lt;h3 id="第四步编写合并后的提交信息">第四步：编写合并后的提交信息
&lt;/h3>&lt;p>你会看到一个新的编辑器窗口，显示所有被合并提交的信息：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"># 这是一个合并了4个提交的组合提交。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 第一个提交信息：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">实现用户登录API
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 这是第二个提交信息：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">添加输入验证
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 这是第三个提交信息：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">移除调试日志
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 这是第四个提交信息：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">修复拼写错误
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 请输入新的提交信息，以说明此组合提交的更改。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 以&amp;#39;#&amp;#39;开头的行将被忽略，空信息会中止提交。
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>删除所有内容，编写一个新的、更有意义的提交信息&lt;/p>
&lt;p>保存并关闭编辑器，Git 就会完成变基操作。&lt;/p>
&lt;h3 id="多轮代码提交后才发现重大bug如何优雅地修改上上个-commit">多轮代码提交后才发现重大Bug？如何优雅地修改“上上个” Commit
&lt;/h3>&lt;p>这是一个为你整理好的博客笔记模板，标题和结构都优化过了，适合发布在 CSDN、掘金、Notion 或个人博客上。&lt;/p>
&lt;p>&lt;strong>📌 问题场景&lt;/strong>&lt;/p>
&lt;p>我在开发中遇到了一个棘手的情况：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>位置&lt;/strong>：倒数第三个 Commit (&lt;code>HEAD~2&lt;/code>) 提交的代码有问题。&lt;/li>
&lt;li>&lt;strong>需求&lt;/strong>：我需要对这个 Commit 进行&lt;strong>大规模的修改/重构&lt;/strong>。&lt;/li>
&lt;li>&lt;strong>工具&lt;/strong>：VS Code。&lt;/li>
&lt;/ol>
&lt;p>直接提交一个新的 Fix Commit 虽然简单，但会让 Git 历史变得脏乱。作为有追求的工程师，我希望历史记录干净整洁。本文记录了两种处理方案：&lt;strong>&amp;ldquo;手术刀模式 (Rebase)&amp;rdquo;&lt;/strong> 和 &lt;strong>&amp;ldquo;推土机模式 (Reset)&amp;rdquo;&lt;/strong>。&lt;/p>
&lt;p>&lt;strong>🛡️ 核心前提：建立安全沙盒&lt;/strong>&lt;/p>
&lt;p>&lt;strong>在修改 Git 历史前，永远不要直接在主分支（如 main/dev）上操作！&lt;/strong>
一旦操作失误（如变基失败），很难恢复。&lt;/p>
&lt;p>&lt;strong>正确做法&lt;/strong>：新建一个临时分支进行&amp;quot;实验&amp;quot;。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout -b fix-old-commit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 所有的修改都在这个分支上进行，确认无误后再合并回主分支或强推。&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>方案一：手术刀模式 (Interactive Rebase)&lt;/strong>&lt;/p>
&lt;p>&lt;strong>适用场景&lt;/strong>：只想精准修复那个错误的 Commit，且&lt;strong>必须保留&lt;/strong>它后面提交的 Commit 记录（例如后面的提交是别人写的，或者与当前修改无关）。&lt;/p>
&lt;p>&lt;strong>操作步骤&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>启动变基&lt;/strong>：
在 VS Code 终端输入命令，回溯到前 3 个版本：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase -i HEAD~3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>&lt;strong>标记要修改的 Commit&lt;/strong>：
Git 会弹出一个编辑器（或 &lt;code>git-rebase-todo&lt;/code> 文件），找到那个有问题的 Commit，将行首的 &lt;code>pick&lt;/code> 修改为 &lt;strong>&lt;code>edit&lt;/code>&lt;/strong>。&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>&lt;code>pick&lt;/code> -&amp;gt; &lt;code>edit&lt;/code>&lt;/p>
&lt;/blockquote>
&lt;ol start="3">
&lt;li>
&lt;p>&lt;strong>修改代码&lt;/strong>：
保存并关闭文件后，Git 会暂停在那个 Commit 的时间点。
此时在 VS Code 中进行大规模的代码修改。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>提交修正&lt;/strong>：
改完后，暂存文件并修补当前 Commit：&lt;/p>
&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit --amend --no-edit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="5">
&lt;li>&lt;strong>完成变基&lt;/strong>：
应用后续的提交，回到现代：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;em>(如果有冲突，在 VS Code 中解冲突 -&amp;gt; add -&amp;gt; continue 即可)&lt;/em>&lt;/p>
&lt;p>&lt;strong>方案二：推土机模式 (Reset &amp;ndash;soft)&lt;/strong>&lt;/p>
&lt;p>&lt;strong>适用场景&lt;/strong>：觉得最近这几次提交都很乱，或者逻辑强相关。不如&lt;strong>把最近的 3 个 Commit 撤销掉，捏成一个完美的 Commit&lt;/strong>。&lt;/p>
&lt;p>&lt;strong>操作步骤&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>&lt;strong>软重置&lt;/strong>：
将指针回退 3 步，但&lt;strong>保留文件修改&lt;/strong>在暂存区：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git reset --soft HEAD~3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>
&lt;p>&lt;strong>状态变化&lt;/strong>：
此时，过去 3 次提交的历史记录消失了，但所有的代码变动都还在，并且处于 &lt;strong>Staged (暂存区/绿色)&lt;/strong> 状态。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>修改与重构&lt;/strong>：
直接在现在的代码基础上进行大规模修改、修复 Bug。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>提交新版本&lt;/strong>：&lt;/p>
&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;refactor: 重构核心逻辑（合并了之前的多次尝试）&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>⚖️ 方案对比总结&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>特性&lt;/th>
&lt;th>方案一：Rebase -i (变基)&lt;/th>
&lt;th>方案二：Reset &amp;ndash;soft (重置)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>形象比喻&lt;/strong>&lt;/td>
&lt;td>&lt;strong>精准手术&lt;/strong> 🔪&lt;/td>
&lt;td>&lt;strong>推土机&lt;/strong> 🚜&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>历史记录&lt;/strong>&lt;/td>
&lt;td>保持原有提交数量，只修改其中一个&lt;/td>
&lt;td>将多个历史提交合并为一个新提交&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>后续提交&lt;/strong>&lt;/td>
&lt;td>保留后续 Commit 的独立性&lt;/td>
&lt;td>后续 Commit 也会被合并掉&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>冲突风险&lt;/strong>&lt;/td>
&lt;td>可能会遇到多次冲突（每个后续 Commit 都可能冲突）&lt;/td>
&lt;td>&lt;strong>无冲突&lt;/strong>（因为是一次性提交）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>推荐指数&lt;/strong>&lt;/td>
&lt;td>⭐⭐⭐⭐ (团队协作且需保留历史时)&lt;/td>
&lt;td>⭐⭐⭐⭐⭐ (个人开发或整理烂代码时)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>💡 VS Code 插件推荐&lt;/strong>&lt;/p>
&lt;p>如果你不习惯命令行，推荐安装 &lt;strong>Git Graph&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Rebase&lt;/strong>: 右键父节点 -&amp;gt; &lt;code>Rebase current branch on this commit&lt;/code> -&amp;gt; 勾选 &lt;code>Interactive&lt;/code>。&lt;/li>
&lt;li>&lt;strong>Reset&lt;/strong>: 右键目标节点 -&amp;gt; &lt;code>Reset current branch to this commit&lt;/code> -&amp;gt; 选择 &lt;code>Soft&lt;/code>。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>最后提示&lt;/strong>：如果你修改的分支已经 Push 到了远程且有同事在协作，请务必与同事沟通，因为修改历史记录需要 &lt;code>git push -f&lt;/code> (强制推送)，这可能会覆盖同事的代码！&lt;/p>
&lt;/blockquote>
&lt;h3 id="如何彻底删除-git-历史记录中的大文件">如何彻底删除 Git 历史记录中的大文件
&lt;/h3>&lt;p>本教程旨在解决一个常见问题：当你在 &lt;code>git push&lt;/code> 时，即使已经删除了某个大文件，GitHub 依然提示 &lt;code>GH001: Large files detected&lt;/code> 错误，导致推送失败。&lt;/p>
&lt;p>问题场景&lt;/p>
&lt;p>你执行了 &lt;code>git rm a_large_file.pkl&lt;/code> 并创建了一个新的 commit，但在推送时仍然看到类似下面的报错：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">remote: error: File your_project/a_large_file.pkl is 107.01 MB&lt;span class="p">;&lt;/span> this exceeds GitHub&lt;span class="err">&amp;#39;&lt;/span>s file size limit of 100.00 MB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remote: error: GH001: Large files detected.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">! &lt;span class="o">[&lt;/span>remote rejected&lt;span class="o">]&lt;/span> main -&amp;gt; main &lt;span class="o">(&lt;/span>pre-receive hook declined&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-20.png"
width="966"
height="596"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-20_hu7271088009656506584.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-20_hu5124425688743671859.png 1024w"
loading="lazy"
alt="问题"
class="gallery-image"
data-flex-grow="162"
data-flex-basis="388px"
>&lt;/p>
&lt;p>&lt;strong>原因是：&lt;/strong> Git 会保存每一次的提交记录。虽然你在最新的提交中删除了该文件，但它依然存在于仓库的过往历史中。推送时，GitHub 会检查所有历史记录，发现这个超大文件后便会拒绝接收。&lt;/p>
&lt;p>要解决此问题，必须从 Git 的历史记录中将该文件彻底清除。&lt;/p>
&lt;hr>
&lt;p>推荐方案：使用 &lt;code>git-filter-repo&lt;/code> (更简单、更快速)&lt;/p>
&lt;p>&lt;code>git-filter-repo&lt;/code> 是 Git 官方现在推荐用来清理历史记录的工具，它比 Git 的原生命令更高效且易于使用。&lt;/p>
&lt;p>1. 安装 &lt;code>git-filter-repo&lt;/code>&lt;/p>
&lt;p>如果尚未安装，请先执行安装。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>macOS (使用 Homebrew):&lt;/strong>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew install git-filter-repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>对于其他系统，请参考其&lt;a class="link" href="https://www.google.com/search?q=https://github.com/newren/git-filter-repo/blob/main/INSTALL.md" target="_blank" rel="noopener"
>官方安装文档&lt;/a>。&lt;/li>
&lt;/ul>
&lt;p>2. 从历史记录中删除文件&lt;/p>
&lt;p>在你的本地仓库根目录运行以下命令。&lt;strong>此操作会重写历史记录&lt;/strong>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将 &amp;#34;path/to/your/large_file.pkl&amp;#34; 替换为你的大文件实际路径&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git filter-repo --path &lt;span class="s2">&amp;#34;path/to/your/large_file.pkl&amp;#34;&lt;/span> --invert-paths
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这条命令会自动处理所有分支和标签，从中移除对指定文件的所有引用。&lt;/p>
&lt;p>3. 强制推送到远程仓库&lt;/p>
&lt;p>由于本地历史已被重写，你需要强制推送来覆盖远程仓库的历史。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push --force
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>备选方案：使用纯 Git 命令 &lt;code>git filter-branch&lt;/code>&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-21.png"
width="966"
height="2382"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-21_hu12267635484686840794.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-21_hu18328967835013978016.png 1024w"
loading="lazy"
alt="git filter-branch"
class="gallery-image"
data-flex-grow="40"
data-flex-basis="97px"
>&lt;/p>
&lt;p>如果你不想安装任何新工具，可以使用 Git 内置的 &lt;code>filter-branch&lt;/code> 命令。&lt;/p>
&lt;p>&lt;strong>警告：&lt;/strong> 此命令非常复杂且速度慢，操作前强烈建议&lt;strong>备份你的整个项目文件夹&lt;/strong>。&lt;/p>
&lt;p>1. 执行历史重写命令&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将 &amp;#34;path/to/your/large_file.pkl&amp;#34; 替换为你的大文件实际路径&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git filter-branch --force --index-filter &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>&lt;span class="s1">&amp;#39;git rm --cached --ignore-unmatch &amp;#34;path/to/your/large_file.pkl&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>--prune-empty --tag-name-filter cat -- --all
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>--ignore-unmatch&lt;/code>: 确保在不包含该文件的历史 commit 上命令不会报错。&lt;/li>
&lt;li>此命令执行速度可能很慢，请耐心等待。&lt;/li>
&lt;/ul>
&lt;p>2. 清理仓库并回收空间&lt;/p>
&lt;p>&lt;code>filter-branch&lt;/code> 会留下备份。运行以下命令以彻底清除旧数据并压缩仓库。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git reflog expire --expire&lt;span class="o">=&lt;/span>now --all
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git gc --prune&lt;span class="o">=&lt;/span>now --aggressive
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>3. 强制推送&lt;/p>
&lt;p>同样，你需要强制推送来更新远程仓库。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push --force
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>未来建议：使用 Git LFS 管理大文件&lt;/p>
&lt;p>为了从根源上避免此类问题，当项目中必须包含大文件时，应使用 &lt;strong>Git Large File Storage (LFS)&lt;/strong>。&lt;/p>
&lt;p>Git LFS 会将大文件存储在专门的服务器上，而在你的仓库中只保留一个轻量级的指针文件，从而使仓库保持小巧和快速。&lt;/p>
&lt;p>LFS 快速上手&lt;/p>
&lt;ol>
&lt;li>&lt;strong>安装 LFS 客户端&lt;/strong>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># macOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew install git-lfs
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>在仓库中启用 LFS&lt;/strong> (每个项目只需执行一次)
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git lfs install
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>追踪指定类型的文件&lt;/strong> (例如，所有 &lt;code>.pkl&lt;/code> 和 &lt;code>.onnx&lt;/code> 文件)
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git lfs track &lt;span class="s2">&amp;#34;*.pkl&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git lfs track &lt;span class="s2">&amp;#34;*.onnx&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>提交 &lt;code>.gitattributes&lt;/code> 文件&lt;/strong>
&lt;code>git lfs track&lt;/code> 命令会创建一个 &lt;code>.gitattributes&lt;/code> 文件，确保将它添加到版本控制中。
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add .gitattributes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Configure Git LFS to track large files&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>之后，你就可以像平常一样 &lt;code>git add&lt;/code> 和 &lt;code>git commit&lt;/code> 大文件了，LFS 会自动处理它们。&lt;/li>
&lt;/ol>
&lt;h3 id="如何优雅地处理不再使用的-github-仓库">如何优雅地处理不再使用的 GitHub 仓库
&lt;/h3>&lt;p>当一个项目长期不用，但又不想彻底删除时，你有以下三种方法可以将其&amp;quot;隐藏&amp;quot;起来，同时保留代码。&lt;/p>
&lt;p>方案一：归档仓库 (Archive) - ⭐最推荐&lt;/p>
&lt;p>这是 GitHub 官方设计的最佳方案，用于封存项目。&lt;/p>
&lt;p>&lt;strong>效果:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>仓库从你的主页列表消失。&lt;/li>
&lt;li>项目变为只读，无法再推送新代码。&lt;/li>
&lt;li>&lt;strong>完整保留&lt;/strong>所有代码、提交历史、Issues、PRs、Wiki 和 Star。&lt;/li>
&lt;li>可以随时一键&amp;quot;取消归档&amp;quot;来恢复项目。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>操作步骤:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>进入仓库页面，点击 &lt;code>Settings&lt;/code> (设置)。&lt;/li>
&lt;li>在 &lt;code>General&lt;/code> (常规) 标签页，拉到最下方的 &lt;code>Danger Zone&lt;/code> (危险区域)。&lt;/li>
&lt;li>点击 &lt;code>Archive this repository&lt;/code> (归档这个仓库) 并确认。&lt;/li>
&lt;/ol>
&lt;p>方案二：设为私有仓库 (Make Private)&lt;/p>
&lt;p>如果只是不想让公众看到，但自己还可能修改。&lt;/p>
&lt;p>&lt;strong>效果:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>仓库从公开主页消失，只有你和协作者可见。&lt;/li>
&lt;li>所有功能（推送、提交）完全正常。&lt;/li>
&lt;li>&lt;strong>注意：&lt;/strong> 仓库在你自己的仓库列表中依然可见。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>操作步骤:&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>进入仓库 &lt;code>Settings&lt;/code> -&amp;gt; &lt;code>General&lt;/code> -&amp;gt; &lt;code>Danger Zone&lt;/code>。&lt;/li>
&lt;li>点击 &lt;code>Change repository visibility&lt;/code> (更改仓库可见性)。&lt;/li>
&lt;li>选择 &lt;code>Make private&lt;/code> (设为私有) 并确认。&lt;/li>
&lt;/ol>
&lt;p>方案三：作为另一项目的分支 (不推荐)&lt;/p>
&lt;p>将旧仓库的历史合并到另一个项目中，然后删除旧仓库。这是一种复杂且有损的操作。&lt;/p>
&lt;p>&lt;strong>效果:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>代码和提交历史被合并到新项目的一个分支上。&lt;/li>
&lt;li>&lt;strong>警告：&lt;/strong> 将&lt;strong>永久丢失&lt;/strong>旧仓库所有的 Issues、PRs、Wiki 等宝贵记录。&lt;/li>
&lt;li>会使主项目的历史变得复杂。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>操作步骤 (命令行):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 进入你的主项目目录&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /path/to/main-project
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 添加旧仓库为临时远程源&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote add old_repo https://github.com/user/old-repo.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 拉取旧仓库数据&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch old_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. 基于旧仓库历史创建新分支 (假设其主分支为 main)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git switch -c archive/old-project old_repo/main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 5. 推送新分支到主项目&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push -u origin archive/old-project
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 6. 删除临时远程源&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote remove old_repo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 7. 去 GitHub 网站上手动删除 old-repo 仓库&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>总结对比&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align: left">方法&lt;/th>
&lt;th style="text-align: left">优点&lt;/th>
&lt;th style="text-align: left">缺点&lt;/th>
&lt;th style="text-align: left">推荐度&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>归档 (Archive)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">保留所有记录、操作简单、可逆&lt;/td>
&lt;td style="text-align: left">项目只读&lt;/td>
&lt;td style="text-align: left">⭐⭐⭐⭐⭐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>设为私有 (Private)&lt;/strong>&lt;/td>
&lt;td style="text-align: left">不公开、可继续编辑&lt;/td>
&lt;td style="text-align: left">仍在自己列表显示&lt;/td>
&lt;td style="text-align: left">⭐⭐⭐⭐&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align: left">&lt;strong>作为分支合并&lt;/strong>&lt;/td>
&lt;td style="text-align: left">物理上整合代码&lt;/td>
&lt;td style="text-align: left">&lt;strong>丢失Issues/PRs等记录&lt;/strong>、操作复杂&lt;/td>
&lt;td style="text-align: left">⭐&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>结论：&lt;/strong> 对于&amp;quot;长期不用但想完整保留&amp;quot;的场景，请始终选择&lt;strong>归档 (Archive)&lt;/strong>。&lt;/p>
&lt;h3 id="一个电脑想要同时使用两个github账号ssh登录">一个电脑想要同时使用两个github账号ssh登录
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>第一步，生成另一个账号专用的ssh密钥和公钥，然后把公钥复制到那个账号的github里面&lt;/p>
&lt;/li>
&lt;li>
&lt;p>配置客户端以使用正确账号&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>vim ~/.ssh/config&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># zata-zhangtao 账号专用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host github.com-zata
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> HostName github.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> User git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> IdentityFile ~/.ssh/id_ed25519_zata
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> IdentitiesOnly yes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="3">
&lt;li>然后更新 remote URL ，使用自定义的Host&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 例如原本是： git@github.com:zata-zhangtao/ZataTree.git ，改成&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote set-url zata git@github.com-zata:zata-zhangtao/ZataTree.git
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="恢复被-git-合并覆盖的提交并防止未来覆盖">恢复被 Git 合并覆盖的提交并防止未来覆盖
&lt;/h3>&lt;p>问题背景
主分支（&lt;code>main&lt;/code>）的修改在合并（如 &lt;code>zata_ssh/hugo&lt;/code>）时被覆盖，可能是快速合并或 &lt;code>ort&lt;/code> 策略自动选择远程分支内容导致。&lt;/p>
&lt;p>恢复被覆盖的提交&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>查看历史&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git reflog main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>找到合并前的提交（如 &lt;code>b510d7b&lt;/code>）。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>恢复提交&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git reset --hard b510d7b
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>备份分支&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git branch main-backup main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>推送更改（谨慎）&lt;/strong>：
如果已推送到远程，需强制推送：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push --force
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>警告&lt;/strong>：提前通知团队，强制推送会影响远程历史。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>重新合并（避免覆盖）&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>拉取远程分支&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch zata_ssh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>非快速合并&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git merge --no-ff zata_ssh/hugo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>若有冲突，手动解决：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add &amp;lt;file&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>或使用变基&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout zata_ssh/hugo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git merge zata_ssh/hugo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>预防未来覆盖&lt;/p>
&lt;ul>
&lt;li>&lt;strong>禁用快速合并&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git config --global merge.ff &lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git config --global pull.ff only
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>预览差异&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git diff main zata_ssh/hugo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>测试合并&lt;/strong>：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout -b temp-merge
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git merge zata_ssh/hugo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>注意事项&lt;/p>
&lt;ul>
&lt;li>检查合并提交（&lt;code>61d17c8&lt;/code>, &lt;code>14e36c9&lt;/code>）的文件变化：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git show 61d17c8
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>确认 &lt;code>main&lt;/code> 跟踪分支：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git branch -vv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h3 id="当前正在进行代码的开发但是想要看历史commit的项目完整代码而当前的工作区保证原样">当前正在进行代码的开发，但是想要看历史commit的项目完整代码，而当前的工作区保证原样
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 临时保存当前工作目录中的未提交更改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git stash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 切换到指定的 commit（替换 &amp;lt;commit id&amp;gt; 为实际的 commit 哈希值，例如 b2dfde96604dcce732aefccc4c7b4dc1fc8b161a）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout &amp;lt;commit id&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 返回到原始分支（替换 &amp;lt;branch name&amp;gt; 为实际的分支名，例如 main）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git checkout &amp;lt;branch name&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 恢复之前保存的更改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git stash pop
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="在-git-中如果你想回退到上一个版本继续开发同时保留已经提交到-main-分支的最新提交可以通过创建新分支并回退的方式实现">在 Git 中，如果你想回退到上一个版本继续开发，同时保留已经提交到 &lt;code>main&lt;/code> 分支的最新提交，可以通过创建新分支并回退的方式实现。
&lt;/h3>&lt;p>以下是一个推荐的方案和详细教程，基于 Git 的最佳实践，确保操作安全且保留所有历史记录。&lt;/p>
&lt;hr>
&lt;ul>
&lt;li>方案一(推荐)：&lt;/li>
&lt;/ul>
&lt;ol>
&lt;li>基于当前代码创建一个新分支，在新分支上介绍功能修改情况&lt;/li>
&lt;li>main分支回退到上一个版本&lt;/li>
&lt;li>接着在main分支上进行开发&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git branch &amp;lt;branchName&amp;gt; &lt;span class="c1"># 创建一个新分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git log --oneline &lt;span class="c1"># 查看历史commit，方便下面的切换&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git reset --hard &amp;lt;HEAD^ 或者 commitId&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>方案二：&lt;/li>
&lt;/ul>
&lt;ol>
&lt;li>直接在main分支上使用revert方法进行回退&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git revert HEAD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这会创建一个新的提交，撤销本次更改，恢复上一步状态，但保留所有提交历史。然后可以继续在 &lt;code>main&lt;/code> 分支上开发。
&lt;img src="https://www.zata.cc/p/gitgithub/images/index/image-19.png"
width="362"
height="232"
srcset="https://www.zata.cc/p/gitgithub/images/index/image-19_hu16817798853553054885.png 480w, https://www.zata.cc/p/gitgithub/images/index/image-19_hu3042514927919901623.png 1024w"
loading="lazy"
alt="revert"
class="gallery-image"
data-flex-grow="156"
data-flex-basis="374px"
>&lt;/p>
&lt;h3 id="假设你当前在main分支暂存区有文件file1txt和file2txt想保存到新分支feature-branch">假设你当前在&lt;code>main&lt;/code>分支，暂存区有文件&lt;code>file1.txt&lt;/code>和&lt;code>file2.txt&lt;/code>，想保存到新分支&lt;code>feature-branch&lt;/code>
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout -b feature-branch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Add file1 and file2 to feature branch&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin feature-branch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="排查-git-push失败问题">排查 Git push失败问题
&lt;/h3>&lt;p>当 &lt;code>git push&lt;/code> 失败并提示 &lt;code>Updates were rejected because the remote contains work that you do not have locally&lt;/code>，可以按照以下步骤查看远程仓库中的更改并解决问题：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>获取远程更改&lt;/strong>&lt;br>
运行 &lt;code>git fetch origin&lt;/code> 下载远程仓库的最新状态，不影响本地工作目录。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>比较本地与远程分支&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>查看提交历史：&lt;code>git log --oneline --graph --all&lt;/code>&lt;br>
显示本地 &lt;code>main&lt;/code> 和远程 &lt;code>origin/main&lt;/code> 的提交差异。&lt;/li>
&lt;li>查看文件差异：&lt;code>git diff --name-only origin/main main&lt;/code>&lt;br>
列出远程和本地分支间更改的文件。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>检查远程独有提交&lt;/strong>&lt;br>
使用 &lt;code>git log main..origin/main --oneline&lt;/code> 查看远程 &lt;code>origin/main&lt;/code> 中独有的提交。&lt;br>
查看具体提交内容：&lt;code>git show &amp;lt;commit-hash&amp;gt;&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>合并远程更改&lt;/strong>&lt;br>
运行 &lt;code>git pull origin main&lt;/code> 合并远程更改到本地。如有冲突，手动解决后提交。&lt;br>
然后推送：&lt;code>git push origin main&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>谨慎使用强制推送&lt;/strong>&lt;br>
若确定覆盖远程更改，运行 &lt;code>git push origin main --force&lt;/code>（注意：可能丢失他人工作，仅在确认安全时使用）。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>建议&lt;/strong>：优先检查远程更改（&lt;code>git fetch&lt;/code> 和 &lt;code>git log&lt;/code>），根据需要合并（&lt;code>git pull&lt;/code>）或强制推送。&lt;/p>
&lt;h3 id="推送当前分支到远程仓库并保持分支名">推送当前分支到远程仓库并保持分支名
&lt;/h3>&lt;p>要将当前分支推送到远程仓库并保持当前分支的名字，可以按照以下步骤操作：&lt;/p>
&lt;p>步骤&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>确认当前分支&lt;/strong>：
确保你位于正确的分支。运行以下命令查看当前分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git branch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当前分支会有一个 &lt;code>*&lt;/code> 标记，例如 &lt;code>* 0710display-prototype3&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>推送当前分支到远程仓库&lt;/strong>：
使用以下命令将当前分支推送到远程仓库（通常是 &lt;code>origin&lt;/code>），并保持相同的分支名：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push origin &lt;span class="k">$(&lt;/span>git branch --show-current&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>git branch --show-current&lt;/code> 会返回当前分支的名称（例如 &lt;code>0710display-prototype3&lt;/code>）。&lt;/li>
&lt;li>&lt;code>git push origin &amp;lt;branch-name&amp;gt;&lt;/code> 将当前分支推送到远程仓库的同名分支。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>设置上游分支（如果需要）&lt;/strong>：
如果这是第一次推送当前分支到远程仓库，Git 可能会提示当前分支没有上游分支（&lt;code>no upstream branch&lt;/code>）。你可以通过以下命令推送并同时设置上游分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push --set-upstream origin &lt;span class="k">$(&lt;/span>git branch --show-current&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或者简写为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push -u origin &lt;span class="k">$(&lt;/span>git branch --show-current&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这会将当前分支推送到远程仓库，并将远程分支设置为当前分支的上游分支，方便后续直接使用 &lt;code>git push&lt;/code> 或 &lt;code>git pull&lt;/code>。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>示例
假设你的当前分支是 &lt;code>0710display-prototype3&lt;/code>，你可以运行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push origin 0710display-prototype3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或者更通用地：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push origin &lt;span class="k">$(&lt;/span>git branch --show-current&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果需要设置上游分支：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push --set-upstream origin 0710display-prototype3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意事项&lt;/p>
&lt;ul>
&lt;li>&lt;strong>检查远程仓库&lt;/strong>：
确保远程仓库（如 &lt;code>origin&lt;/code>）已配置正确。运行 &lt;code>git remote -v&lt;/code> 查看远程仓库的 URL。&lt;/li>
&lt;li>&lt;strong>工作区状态&lt;/strong>：
确保你已提交所有更改（&lt;code>git commit&lt;/code>），否则运行 &lt;code>git status&lt;/code> 检查是否有未提交的更改。&lt;/li>
&lt;li>&lt;strong>分支名冲突&lt;/strong>：
如果远程仓库已存在同名分支，推送可能会失败（例如，提示需要拉取或强制推送）。可以先运行 &lt;code>git fetch origin&lt;/code> 检查远程分支状态，或者使用 &lt;code>git push --force&lt;/code>（谨慎使用，仅当你确定要覆盖远程分支）。&lt;/li>
&lt;/ul>
&lt;p>验证推送
推送完成后，运行以下命令确认远程分支已创建：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch -r
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你应该能看到 &lt;code>origin/0710display-prototype3&lt;/code> 在远程分支列表中。&lt;/p>
&lt;p>如果遇到任何错误（例如权限问题或冲突），请分享错误信息，我可以帮你进一步排查！&lt;/p>
&lt;h3 id="git-pull-时出现-cannot-lock-ref-错误的解决方案">git pull 时出现 &amp;ldquo;cannot lock ref&amp;rdquo; 错误的解决方案
&lt;/h3>&lt;p>当执行 &lt;code>git pull&lt;/code> 时出现类似以下错误：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">error: cannot lock ref &amp;#39;refs/remotes/zata/hugo&amp;#39;: unable to resolve reference &amp;#39;refs/remotes/zata/hugo&amp;#39;: reference broken
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个错误通常是因为本地的 Git 引用缓存与远程仓库状态不一致导致的。&lt;/p>
&lt;p>问题原因&lt;/p>
&lt;ol>
&lt;li>&lt;strong>远程分支被强制更新&lt;/strong>：远程仓库的分支可能被强制推送（&lt;code>git push --force&lt;/code>），导致本地缓存的提交哈希值与远程最新的提交哈希值不匹配。&lt;/li>
&lt;li>&lt;strong>本地引用损坏或不一致&lt;/strong>：本地的 &lt;code>.git/refs/remotes/&lt;/code> 文件可能被锁定、损坏，或者由于网络中断等原因未正确更新。&lt;/li>
&lt;li>&lt;strong>Git 缓存问题&lt;/strong>：Git 的引用日志或跟踪分支信息可能出现了问题，导致无法正常同步。&lt;/li>
&lt;/ol>
&lt;p>解决方法
以下是几种解决方法，建议按顺序尝试：&lt;/p>
&lt;p>&lt;strong>方法 1：清理并重新拉取&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch --prune
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>git fetch --prune&lt;/code> 会更新本地对远程分支的跟踪信息，并删除远程已不存在的分支引用。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>方法 2：强制更新本地引用&lt;/strong>
如果确认远程分支的最新状态是你需要的，可以强制更新本地的远程分支引用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git update-ref refs/remotes/zata/hugo origin/hugo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>git update-ref&lt;/code> 手动将本地的远程分支引用更新为远程分支的最新状态。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>方法 3：删除并重置本地跟踪分支&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">rm .git/refs/remotes/zata/hugo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>手动删除本地引用文件，然后重新获取远程分支信息并拉取。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>方法 4：检查锁文件&lt;/strong>
错误信息提到&amp;quot;cannot lock ref&amp;quot;，可能是因为存在锁文件导致引用无法更新：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">rm .git/refs/remotes/zata/hugo.lock
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>如果存在锁文件，删除它以解除锁定。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>方法 5：彻底清理并重新克隆（最后手段）&lt;/strong>
如果以上方法都无效，且你不介意丢失本地未提交的更改：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ..
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rm -rf ZataTree
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git clone https://github.com/zata-zhangtao/ZataTree.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ZataTree
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>注意&lt;/strong>：在执行此操作前，请备份所有本地未提交的更改（例如通过 &lt;code>git stash&lt;/code> 或复制工作目录）。&lt;/li>
&lt;/ul>
&lt;p>预防措施&lt;/p>
&lt;ol>
&lt;li>&lt;strong>避免强制推送&lt;/strong>：如果多人协作，尽量避免对公共分支使用 &lt;code>git push --force&lt;/code>，改用 &lt;code>git push --force-with-lease&lt;/code> 以减少冲突。&lt;/li>
&lt;li>&lt;strong>定期清理&lt;/strong>：定期运行 &lt;code>git fetch --prune&lt;/code> 清理无效的远程分支引用。&lt;/li>
&lt;li>&lt;strong>检查网络&lt;/strong>：确保网络连接稳定，避免 &lt;code>git pull&lt;/code> 中断导致引用不一致。&lt;/li>
&lt;/ol>
&lt;h3 id="git-无法识别文件名大小写更改一文彻底解决重命名难题">Git 无法识别文件名大小写更改？一文彻底解决重命名难题
&lt;/h3>&lt;blockquote>
&lt;p>适用场景：你在 Windows 或 macOS 上将 &lt;code>readme.md&lt;/code> 改为 &lt;code>Readme.md&lt;/code>，但 &lt;code>git status&lt;/code> 却毫无反应——别慌，这不是 Git 的 bug，而是文件系统的“锅”。本文教你如何正确处理 Git 中的大小写重命名。&lt;/p>
&lt;/blockquote>
&lt;p>你执行了如下操作：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mv readme.md Readme.md
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后运行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>结果 Git &lt;strong>完全没反应&lt;/strong>，仿佛什么都没发生。即使你强制 &lt;code>git add Readme.md&lt;/code>，Git 可能还会报错：“文件已存在”或“路径冲突”。&lt;/p>
&lt;p>为什么？&lt;/p>
&lt;p>Git 本身是支持文件名大小写的，但它会&lt;strong>尊重底层操作系统的文件系统行为&lt;/strong>：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>操作系统&lt;/th>
&lt;th>文件系统&lt;/th>
&lt;th>是否区分大小写&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Linux&lt;/td>
&lt;td>ext4, XFS 等&lt;/td>
&lt;td>✅ 是&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>macOS&lt;/td>
&lt;td>APFS / HFS+（默认）&lt;/td>
&lt;td>❌ 否&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Windows&lt;/td>
&lt;td>NTFS&lt;/td>
&lt;td>❌ 否&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>在 macOS 和 Windows 上，&lt;code>readme.md&lt;/code> 和 &lt;code>Readme.md&lt;/code> 被视为&lt;strong>同一个文件&lt;/strong>。因此，当你重命名时，文件系统认为“文件没变”，Git 也就检测不到任何修改。&lt;/p>
&lt;blockquote>
&lt;p>💡 这不是 Git 的缺陷，而是为了兼容性做出的设计选择。&lt;/p>
&lt;/blockquote>
&lt;p>✅ 正确解决方案：使用 &lt;code>git mv&lt;/code> 分两步走&lt;/p>
&lt;p>由于直接重命名会被文件系统“吞掉”，我们需要&lt;strong>绕过文件系统的限制&lt;/strong>。最可靠的方法是：&lt;strong>先改成一个临时名字，再改成目标名字&lt;/strong>。&lt;/p>
&lt;p>步骤示例&lt;/p>
&lt;p>假设你想把 &lt;code>login.js&lt;/code> 改为 &lt;code>Login.js&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 第一步：重命名为临时文件（名字不能和原名仅大小写不同）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git mv login.js temp_login.js
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 第二步：从临时文件重命名为目标文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git mv temp_login.js Login.js
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 提交更改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;Rename login.js to Login.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>✅ 这样 Git 就会正确记录为一次 &lt;strong>重命名操作&lt;/strong>，而不是“删除 + 新增”，保持历史连续性。&lt;/p>
&lt;p>⚠️ 常见误区&lt;/p>
&lt;p>❌ 直接在文件管理器中改名&lt;/p>
&lt;ul>
&lt;li>在 Finder（macOS）或资源管理器（Windows）里右键重命名 → Git 无法感知。&lt;/li>
&lt;li>结果：Git 认为原文件被删除，新文件未跟踪。&lt;/li>
&lt;/ul>
&lt;p>❌ 强行设置 &lt;code>core.ignorecase false&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git config core.ignorecase &lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>虽然能让 Git “假装”区分大小写，但在不支持的系统上会导致状态混乱（比如误报文件丢失）。&lt;/li>
&lt;li>&lt;strong>不推荐&lt;/strong>，尤其在团队协作中可能引发更多问题。&lt;/li>
&lt;/ul>
&lt;p>🛠️ 高级技巧：批量重命名脚本（可选）&lt;/p>
&lt;p>如果你有多个文件需要改名，可以写个小脚本：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># rename_case.sh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">old&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;myfile.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">new&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;MyFile.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git mv &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$old&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;temp_&lt;/span>&lt;span class="nv">$$&lt;/span>&lt;span class="s2">.&lt;/span>&lt;span class="nv">$old&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git mv &lt;span class="s2">&amp;#34;temp_&lt;/span>&lt;span class="nv">$$&lt;/span>&lt;span class="s2">.&lt;/span>&lt;span class="nv">$old&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$new&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Renamed &lt;/span>&lt;span class="nv">$old&lt;/span>&lt;span class="s2"> → &lt;/span>&lt;span class="nv">$new&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>运行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">chmod +x rename_case.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./rename_case.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;code>$&lt;/code>$ 是进程 ID，确保临时文件名唯一。&lt;/p>
&lt;/blockquote>
&lt;p>🌐 跨平台协作建议&lt;/p>
&lt;p>如果你的团队同时使用 Windows、macOS 和 Linux，请遵守以下规范：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>避免仅靠大小写区分文件名&lt;/strong>
❌ 不要同时存在 &lt;code>User.js&lt;/code> 和 &lt;code>user.js&lt;/code>&lt;/li>
&lt;li>&lt;strong>统一命名风格&lt;/strong>
✅ 推荐全小写 + 下划线/短横线：&lt;code>user_profile.js&lt;/code> 或 &lt;code>user-profile.js&lt;/code>&lt;/li>
&lt;li>&lt;strong>重命名一律用 &lt;code>git mv&lt;/code>&lt;/strong>
即使在 Linux 上，也养成好习惯&lt;/li>
&lt;/ol>
&lt;p>🔚 总结&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>问题&lt;/th>
&lt;th>原因&lt;/th>
&lt;th>解决方案&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Git 不识别大小写重命名&lt;/td>
&lt;td>文件系统不区分大小写（Win/macOS）&lt;/td>
&lt;td>✅ 用 &lt;code>git mv&lt;/code> 分两步重命名&lt;br>✅ 避免仅靠大小写区分文件&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>记住：&lt;strong>Git 是忠实的记录者，但它的“眼睛”受限于操作系统&lt;/strong>。理解这一点，就能从容应对各种“诡异”行为。&lt;/p>
&lt;p>📌 小贴士：下次遇到类似问题，先问自己——“我的文件系统真的支持这个操作吗？”&lt;/p>
&lt;p>&lt;strong>作者&lt;/strong>：你的名字
&lt;strong>标签&lt;/strong>：#Git #开发技巧 #跨平台开发 #版本控制
&lt;strong>发布时间&lt;/strong>：2026年1月16日&lt;/p>
&lt;h3 id="如何将clone下来的项目推送到自己的新仓库并同步原始仓库更新">如何将clone下来的项目推送到自己的新仓库并同步原始仓库更新
&lt;/h3>&lt;p>当你 &lt;code>clone&lt;/code> 一个项目，你的本地仓库默认会有一个名为 &lt;code>origin&lt;/code> 的远程连接，指向你克隆的那个仓库地址。现在你既想保留自己的修改，又想同步原始仓库的更新，并最终将所有代码推送到你自己的一个全新仓库。&lt;/p>
&lt;p>下面是详细的步骤，分为两种情况：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>方案一：简单直接，将推送目标&amp;quot;切换&amp;quot;到你的新仓库。&lt;/strong>&lt;/li>
&lt;li>&lt;strong>方案二：最佳实践，同时管理&amp;quot;原始仓库&amp;quot;和&amp;quot;你的仓库&amp;quot;两个远程地址。&lt;/strong> (推荐)&lt;/li>
&lt;/ul>
&lt;p>前提：创建你自己的远程仓库&lt;/p>
&lt;hr>
&lt;p>无论使用哪种方案，你都需要先在代码托管平台（如 GitHub、Gitee、GitLab）上创建一个&lt;strong>空的&lt;/strong>新仓库。&lt;/p>
&lt;p>&lt;strong>非常重要：&lt;/strong> 创建仓库时，&lt;strong>不要&lt;/strong>勾选&amp;quot;使用README文件初始化仓库&amp;quot;、&amp;ldquo;添加 .gitignore&amp;rdquo; 或 &amp;ldquo;选择许可证&amp;rdquo;，以避免产生不兼容的提交历史。&lt;/p>
&lt;p>创建好之后，复制新仓库的 URL 地址（HTTPS 或 SSH 格式），比如 &lt;code>https://github.com/your-username/your-new-repo.git&lt;/code>。&lt;/p>
&lt;hr>
&lt;p>方案一：切换远程仓库地址 (简单直接)&lt;/p>
&lt;p>这个方案直接把本地仓库的 &lt;code>origin&lt;/code> 指向从原始仓库地址修改为你的新仓库地址。&lt;/p>
&lt;p>第一步：提交你的本地更改&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;这里写下你的提交信息，比如：添加了xx功能&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第二步：同步原始仓库的更新&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git pull --rebase origin main &lt;span class="c1"># 如果你的分支不是 main，请替换为实际分支名&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>git pull --rebase&lt;/code> 会先拉取原始仓库的新提交，再把你本地的提交&amp;quot;重新播放&amp;quot;在最前面，保持提交历史线性。&lt;/li>
&lt;li>如果遇到冲突，解决后用 &lt;code>git add &amp;lt;文件名&amp;gt;&lt;/code> 标记解决，然后 &lt;code>git rebase --continue&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>第三步：更改远程仓库 &lt;code>origin&lt;/code> 的 URL&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git remote -v &lt;span class="c1"># 查看当前远程仓库地址&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote set-url origin &amp;lt;你自己的新仓库URL&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote -v &lt;span class="c1"># 再次确认&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第四步：推送到你自己的仓库&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push -u origin main &lt;span class="c1"># -u 建立本地分支与远程分支的追踪关系&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;p>方案二：添加新的远程仓库 (最佳实践)&lt;/p>
&lt;p>这个方案保留了原始仓库的连接（通常命名为 &lt;code>upstream&lt;/code>），同时添加你自己的仓库作为新的远程连接（通常还叫 &lt;code>origin&lt;/code>）。这样做的好处是，未来你还可以随时方便地从原始仓库拉取更新。&lt;/p>
&lt;p>第一步和第二步：同方案一&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;你的提交信息&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git pull --rebase origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三步：重命名原始仓库并添加你自己的仓库&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git remote rename origin upstream &lt;span class="c1"># 原始仓库重命名为 upstream&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote add origin &amp;lt;你自己的新仓库URL&amp;gt; &lt;span class="c1"># 添加你自己的仓库为 origin&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git remote -v &lt;span class="c1"># 检查远程仓库配置&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>此时你应该能看到：&lt;/p>
&lt;ul>
&lt;li>&lt;code>origin&lt;/code> 指向你的新仓库地址（可读写）。&lt;/li>
&lt;li>&lt;code>upstream&lt;/code> 指向你最初克隆的那个原始仓库地址（通常是只读的）。&lt;/li>
&lt;/ul>
&lt;p>第四步：推送到你自己的仓库&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git push -u origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>未来的工作流&lt;/p>
&lt;ul>
&lt;li>推送自己的修改：&lt;code>git push origin main&lt;/code>&lt;/li>
&lt;li>同步原始项目的更新：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git fetch upstream
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase upstream/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>常见问题与注意事项&lt;/p>
&lt;ul>
&lt;li>&lt;strong>分支名不是 main？&lt;/strong>
&lt;ul>
&lt;li>如果你的分支名是 &lt;code>master&lt;/code> 或其他，请将命令中的 &lt;code>main&lt;/code> 替换为实际分支名。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>遇到冲突怎么办？&lt;/strong>
&lt;ul>
&lt;li>Git 会提示你解决冲突。解决后 &lt;code>git add &amp;lt;文件&amp;gt;&lt;/code>，然后 &lt;code>git rebase --continue&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>推送时遇到权限问题？&lt;/strong>
&lt;ul>
&lt;li>检查你是否有新仓库的写权限，或 SSH/Token 配置是否正确。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>未来如何同步原始仓库的更新？&lt;/strong>
&lt;ul>
&lt;li>只需 &lt;code>git fetch upstream&lt;/code>，然后 &lt;code>git rebase upstream/main&lt;/code>。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul></description></item></channel></rss>