<?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/tags/%E7%BB%88%E7%AB%AF%E4%B8%8E%E7%BC%96%E8%BE%91%E5%99%A8/</link><description>Recent content in 终端与编辑器 on 扎塔-Zata</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>Example Person</copyright><lastBuildDate>Wed, 30 Sep 2026 22:21:05 +0800</lastBuildDate><atom:link href="https://www.zata.cc/tags/%E7%BB%88%E7%AB%AF%E4%B8%8E%E7%BC%96%E8%BE%91%E5%99%A8/index.xml" rel="self" type="application/rss+xml"/><item><title>VelaTerm 深度调研：把 iTerm2 和 Codex 装进同一个窗口的 ADE</title><link>https://www.zata.cc/p/velaterm-ade-deep-dive/</link><pubDate>Wed, 30 Sep 2026 20:00:00 +0800</pubDate><guid>https://www.zata.cc/p/velaterm-ade-deep-dive/</guid><description>&lt;p>如果你同时跑着几个 Claude Code、Codex，标签页早就堆成了一排乱码，分不清哪个 Agent 在干活、哪个在等你批准——VelaTerm 就是冲这个场景来的。&lt;/p>
&lt;p>它的官方公式很简单：&lt;strong>VelaTerm = iTerm2 + Codex&lt;/strong>。像 Codex 一样管理智能体会话，像 iTerm2 一样分屏使用终端，两者放进同一个原生应用，还能推到浏览器和手机上继续用。它给自己起的名字是 &lt;strong>ADE（Agent Development Environment）&lt;/strong>——&amp;ldquo;不只是终端，也不只是 IDE&amp;rdquo;。&lt;/p>
&lt;p>需要先声明：本文是一轮&lt;strong>基于公开资料的深度调研&lt;/strong>（官网、GitHub 仓库、Release notes、第三方评测，检索时间 2026-09-30），不是像之前 &lt;a class="link" href="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/" >Herdr 那篇&lt;/a>那样的本机实测。所有功能描述以官方文档为准，未经我亲手验证的地方会在行文中说明。&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>多智能体开发环境（ADE）：终端 + 编程 Agent 会话管理&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>官网&lt;/td>
&lt;td>&lt;a class="link" href="https://velaterm.com/zh-CN" target="_blank" rel="noopener"
>velaterm.com&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>仓库&lt;/td>
&lt;td>&lt;a class="link" href="https://github.com/vlinx-io/VelaTerm" target="_blank" rel="noopener"
>github.com/vlinx-io/VelaTerm&lt;/a>（TypeScript 为主）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>出品方&lt;/td>
&lt;td>VLINX Software&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>许可证&lt;/td>
&lt;td>MIT（2026-08-11 公开源码，仓库创建于 2026-08-09）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>最新版本&lt;/td>
&lt;td>v0.2.6（2026-09-30 发布，就是今天）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GitHub 数据&lt;/td>
&lt;td>⭐ 240 · fork 27 · open issues 66（2026-09-30 查询）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>平台&lt;/td>
&lt;td>macOS（Apple Silicon/Intel）、Windows（x64/arm64）、Linux（x86_64/aarch64）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Shell&lt;/td>
&lt;td>macOS zsh；Windows PowerShell / Git Bash（完整版内置）/ WSL；Linux bash&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>价格&lt;/td>
&lt;td>免费，MIT 开源，无付费层&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>几个第三方渠道的补充信息：&lt;a class="link" href="http://www.myaiexp.com/en/items/dev-tools/velaterm" target="_blank" rel="noopener"
>myaiexp&lt;/a> 和 &lt;a class="link" href="https://huntscreens.com/zh/products/velaterm" target="_blank" rel="noopener"
>HuntScreens&lt;/a> 都把它归入&amp;quot;多代理开发环境&amp;quot;类目；&lt;a class="link" href="https://honeystax.com/p/velaterm" target="_blank" rel="noopener"
>Honeystax&lt;/a> 在 2026 年 8 月中给出的评估是&amp;quot;GitHub 健康 57/100、无安全策略、当时 8 个 open issues、风险 42/100&amp;quot;——两个月后的今天 issues 涨到了 66 个，项目迭代极快（见第六节），但治理配套还没跟上。&lt;/p>
&lt;hr>
&lt;h2 id="二它到底是什么一个窗口里的三层东西">二、它到底是什么：一个窗口里的三层东西
&lt;/h2>&lt;p>理解 VelaTerm 最快的方式，是把它的窗口拆成三层看：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>底层：真实终端。&lt;/strong> 每个会话都是一个真实 PTY，切走之后进程继续在后台跑。支持递归分屏（任意面板向右/向下拆，嵌套无深度限制）、命令与路径补全、&lt;code>vopen&lt;/code> 把文件打开成标签页（带语法高亮、Markdown 渲染、图片查看）。&lt;/li>
&lt;li>&lt;strong>中间层：Agent 会话。&lt;/strong> 九种编程 Agent 作为&amp;quot;一等会话&amp;quot;运行——Claude Code、Codex、OpenCode、Copilot、Cursor、Antigravity、Cline、Pi 等。每个 Agent 有实时状态（working / pending / ready 等）、支持会话恢复和自定义启动参数。&lt;/li>
&lt;li>&lt;strong>顶层：协作与组织。&lt;/strong> 项目 → 分组 → 嵌套子分组 → 会话的树形结构，配搜索、重命名、归档；Agent 之间可以互相搜索对话、发消息、派生子和任务拆分（这是它和普通终端的分水岭，第四节细讲）。&lt;/li>
&lt;/ol>
&lt;p>同一个 Agent 会话有&lt;strong>两种视图&lt;/strong>：会话视图把 Agent 的计划、改动、命令、测试结果渲染成一条对话流；切到终端视图就直接操作 Agent 自己的 TUI。这个&amp;quot;对话/终端双视图&amp;quot;是它区别于 tmux 系工具的一个显性特征。&lt;/p>
&lt;p>另外两个日常顺手的点：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>信息面板&lt;/strong>：Agent 干活时，右栏实时显示订阅额度（plan quota）、上下文占用、Token 用量和系统负载——额度还剩多少不用去服务商后台查。&lt;/li>
&lt;li>&lt;strong>通知系统&lt;/strong>：会话状态变化走树中的脉冲点、状态栏计数器和桌面通知三路，Agent 卡住等输入时不用逐个窗口巡检。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="三功能全景速览">三、功能全景速览
&lt;/h2>&lt;p>按官方 README 的板块整理：&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>真实 PTY、递归分屏、命令/路径建议（Tab 补全）、后台持久化&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent 会话&lt;/td>
&lt;td>九种 Agent 一等会话、实时状态、恢复、自定义启动参数&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>会话组织&lt;/td>
&lt;td>项目/分组/嵌套子分组/会话的无限层级树、搜索、归档&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>视图&lt;/td>
&lt;td>会话视图（对话流）⇄ 终端视图（原生 TUI）随时切换&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>内置编辑器&lt;/td>
&lt;td>WYSIWYG Markdown 编辑器、代码编辑器、图片查看器；桌面端还有内置浏览器&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Git 集成&lt;/td>
&lt;td>每会话显示分支、领先/落后提交数、改动量&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>代码审计&lt;/td>
&lt;td>内置 Codex Security 工作流，用本机已登录的 Codex 或 Claude Code 跑全仓库/目录/工作树审计，可导出 Markdown/JSON 报告（实验性）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>知识库&lt;/td>
&lt;td>会话知识库 + 本地 Markdown 笔记库，统一搜索；Agent 可用 &lt;code>vkb&lt;/code> 查询&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>远程&lt;/td>
&lt;td>SSH 连接、HTTPS 端到端加密浏览器访问、iOS/Android 原生 App（官方标注 coming soon）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>其他&lt;/td>
&lt;td>亮暗主题跟随系统、界面多语言完整翻译、游戏中心（Pixel Wing，键鼠/触控/手柄皆可）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>游戏中心这种东西出现在&amp;quot;开发工具&amp;quot;里多少有点彩蛋性质，但也说明团队的定位是&amp;quot;开发者长时间待着的工作空间&amp;quot;，而不只是一个终端模拟器。&lt;/p>
&lt;hr>
&lt;h2 id="四重头戏多智能体协作">四、重头戏：多智能体协作
&lt;/h2>&lt;p>这是 VelaTerm 与&amp;quot;带分屏的终端&amp;quot;真正拉开差距的部分，也是它敢自称 ADE 的底气。四组能力：&lt;/p>
&lt;h3 id="41-vspawn一条命令派生子会话">4.1 &lt;code>vspawn&lt;/code>：一条命令派生子会话
&lt;/h3>&lt;p>Agent 在干活途中可以把旁支任务交给一个子会话：选 Agent、选模型、选推理强度，需要隔离就给它分配独立 worktree。子会话出现在父会话下方，进度在树里直接可见——本质上是把&amp;quot;多 Agent 编排&amp;quot;从脚本层面搬进了会话树。&lt;/p>
&lt;h3 id="42-跨会话通信vsearch--vrefer--vtell">4.2 跨会话通信：&lt;code>vsearch&lt;/code> / &lt;code>vrefer&lt;/code> / &lt;code>vtell&lt;/code>
&lt;/h3>&lt;p>不同 Agent 的会话之间（Claude、Codex、OpenCode、Pi 等）可以互相查：&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>vsearch&lt;/code>&lt;/td>
&lt;td>搜索所有会话的对话内容&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vrefer&lt;/code>&lt;/td>
&lt;td>读取某段对话，或直接就它提问&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vtell&lt;/code>&lt;/td>
&lt;td>给其他会话发消息；加 &lt;code>--steer&lt;/code> 可插入对方&lt;strong>正在进行的回合&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>想象一个典型场景：Claude 会话在改认证代码，可以 &lt;code>vsearch&lt;/code> 找到之前某个 Codex 会话讨论过的数据库约定，&lt;code>vrefer&lt;/code> 向它追问细节，再 &lt;code>vtell&lt;/code> 通知另一个会话配合调整——Agent 之间的上下文不再靠人复制粘贴搬运。&lt;/p>
&lt;h3 id="43-plan--execute一个大任务一组会话">4.3 Plan / Execute：一个大任务，一组会话
&lt;/h3>&lt;p>规划/执行模式把大任务拆给多个会话协作，流程分三步：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>规划&lt;/strong>：规划会话（planner）制定方案、拆分任务；规划与执行可分别配置 Agent、模型和推理强度。&lt;/li>
&lt;li>&lt;strong>执行&lt;/strong>：&lt;code>vflow&lt;/code> 提出拆分方案，&lt;strong>你逐项调整并确认后&lt;/strong>才开始执行；每个执行会话（executor）在独立 worktree 里并行做一件事，作为规划会话的子会话显示在树中。&lt;/li>
&lt;li>&lt;strong>验收&lt;/strong>：&lt;code>vtell --report&lt;/code> 把执行结果回传给规划会话逐项验收，未达标的任务退回原执行会话返工，上下文完整保留。&lt;/li>
&lt;/ol>
&lt;p>值得注意的是设计取舍：&lt;strong>拆分方案需要人工审批&lt;/strong>，不是全自动发车；worktree 可以每个会话独立、也可以全员共享同一棵。这比&amp;quot;一键 Agent 军团&amp;quot;类产品保守，但在真实项目里更容易控制爆炸半径。&lt;/p>
&lt;h3 id="44-vkb会话里的结论沉淀成知识">4.4 &lt;code>vkb&lt;/code>：会话里的结论沉淀成知识
&lt;/h3>&lt;ul>
&lt;li>&lt;code>vkb memories&lt;/code> — 查会话知识库（把有价值的会话整理进条目，附带来源）&lt;/li>
&lt;li>&lt;code>vkb notes&lt;/code> — 查本地 Markdown 笔记库（带标签、收藏、回收站）&lt;/li>
&lt;li>&lt;code>vkb explore&lt;/code> — 查代码图谱，&lt;strong>在本机完成、不调用 AI 模型&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>三者统一搜索。等于给 Agent 配了一个&amp;quot;动手前先查档案&amp;quot;的入口，也让散落在历史会话里的结论可以复用。&lt;/p>
&lt;hr>
&lt;h2 id="五远程与移动三种接管方式">五、远程与移动：三种接管方式
&lt;/h2>&lt;p>远程是 VelaTerm 宣传里占比很大的一块：&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>SSH&lt;/td>
&lt;td>应用内直连远程机器，连接前确认主机指纹&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>浏览器&lt;/td>
&lt;td>一键开启 HTTPS 服务，端到端加密，任意设备打开 URL 就是完整桌面 UI，无需安装&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>手机&lt;/td>
&lt;td>iOS / Android 原生 App，扫码配对、推送通知、直接回复 Agent；官网标注&amp;quot;coming very soon&amp;quot;&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>还有账号化的&lt;strong>配对链接&lt;/strong>和&lt;strong>设备列表&lt;/strong>管理；v0.2.0 起支持共享项目/会话——通过出站隧道把宿主的真实界面转发到共享 URL，权限限定在被授权的项目或会话范围内。&lt;/p>
&lt;p>对&amp;quot;在服务器上跑长任务、地铁上用手机瞄一眼进度、顺手批准一个操作&amp;quot;这种工作流，这条链路是完整的。端到端加密这一点官方反复强调（&amp;ldquo;传输链路上没有可读的明文&amp;rdquo;），显然是在回应&amp;quot;把终端搬到浏览器&amp;quot;必然引发的安全疑虑。&lt;/p>
&lt;hr>
&lt;h2 id="六技术架构与版本节奏">六、技术架构与版本节奏
&lt;/h2>&lt;h3 id="61-技术栈">6.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>桌面外壳&lt;/td>
&lt;td>&lt;strong>Tauri 2&lt;/strong>（Rust 后端 + 系统 WebView）；另有一个 Electron 外壳并存（&lt;code>electron/&lt;/code> 目录）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PTY&lt;/td>
&lt;td>&lt;code>portable-pty&lt;/code>（来自 wezterm）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>前端&lt;/td>
&lt;td>React 19 + TypeScript + Vite&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>终端渲染&lt;/td>
&lt;td>xterm.js（fit / web-links / search / image / unicode11 插件）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>状态管理&lt;/td>
&lt;td>Zustand&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>持久化&lt;/td>
&lt;td>SQLite（rusqlite，内置编译）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>样式&lt;/td>
&lt;td>Tailwind v4，主题基于 CSS 变量&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>选 Tauri 2 而不是 Electron 做主壳，换来的是&amp;quot;安装包仅数 MB、不捆绑 Chromium、启动快、多终端并发依然流畅&amp;quot;（官方口径；第三方介绍一致）。它解决的痛点很实际——这类会话管理器如果自己就是内存大户，跑十几个 Agent 会话就先把自己压垮了。&lt;/p>
&lt;h3 id="62-版本节奏快得不正常">6.2 版本节奏：快得不正常
&lt;/h3>&lt;ul>
&lt;li>2026-08-09 仓库创建，08-11 公开源码&lt;/li>
&lt;li>v0.1.x 一路迭代到 108+&lt;/li>
&lt;li>v0.2.0（9 月中）：远程访问成体系（SSH/URL/指纹确认/扫码/账号登录）、实验性代码审计、本地知识库、Plan-and-Execute 会话&lt;/li>
&lt;li>v0.2.2（09-15）：用量限额后自动续跑（Claude/Codex 5 小时/周限额重置后自动恢复，默认关）、Windows 一键安装 OpenCode/Grok/Crush、会话知识库归档与全文搜索&lt;/li>
&lt;li>v0.2.3（09-24）→ v0.2.4（09-26）→ v0.2.5（09-28）→ &lt;strong>v0.2.6（09-30）&lt;/strong>：差不多两天一个版本&lt;/li>
&lt;/ul>
&lt;p>v0.2.6 重点是远程连接体验（指纹确认、配对链接、账号设备选择）和对话视图分页加载。这个发版密度在个人/小团队开源项目里属于第一梯队——好处是反馈闭环极快，坏处是每两天升一次级，用户跟不上，文档也容易滞后于代码。&lt;/p>
&lt;hr>
&lt;h2 id="七成熟度与风险说清楚">七、成熟度与风险，说清楚
&lt;/h2>&lt;p>调研不能只报喜。汇总几条需要注意的点：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>项目非常年轻。&lt;/strong> 开源至今不到两个月，还处在 0.2.x 阶段，API、数据格式、行为都可能有破坏性变化。&lt;/li>
&lt;li>&lt;strong>社区规模尚小。&lt;/strong> 240 star（截至 2026-09-30）说明还在早期采用阶段；66 个 open issues 相对这个体量不算少。&lt;/li>
&lt;li>&lt;strong>治理配套缺位。&lt;/strong> Honeystax 8 月的评估提到仓库&lt;strong>没有 security policy&lt;/strong>；远程访问涉及&amp;quot;把终端界面推到网络上&amp;quot;这种高危场景，安全响应机制的重要性远高于普通终端工具。虽然端到端加密的设计方向是对的，但&amp;quot;无安全策略&amp;quot;和&amp;quot;快节奏发版&amp;quot;组合在一起，建议&lt;strong>不要把远程访问端口直接暴露到公网&lt;/strong>，至少先放在可信局域网/VPN 内。&lt;/li>
&lt;li>&lt;strong>本文未经实测。&lt;/strong> 所有功能描述来自官方材料，&amp;ldquo;九种 Agent 一等会话&amp;quot;&amp;ldquo;端到端加密&amp;quot;这些关键声明的实际成色，要以你自己的试用为准。&lt;/li>
&lt;li>&lt;strong>手机 App 还在路上。&lt;/strong> 官网标注 coming soon，现在移动端走浏览器布局（自动重排为触控 UI）。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="八竞品对比">八、竞品对比
&lt;/h2>&lt;p>这个赛道今年明显热起来了，各家切入点不同：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>工具&lt;/th>
&lt;th>形态&lt;/th>
&lt;th>切入点&lt;/th>
&lt;th>和 VelaTerm 的差异&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>VelaTerm&lt;/strong>&lt;/td>
&lt;td>原生 GUI 应用（Tauri 2）&lt;/td>
&lt;td>ADE：会话树 + 双视图 + 跨会话协作 + 远程/移动&lt;/td>
&lt;td>唯一同时做到&amp;quot;GUI 会话树 + 内置协作命令 + 浏览器/手机接管&amp;quot;的&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a class="link" href="https://herdr.dev" target="_blank" rel="noopener"
>Herdr&lt;/a>（本站有&lt;a class="link" href="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/" >实测长文&lt;/a>）&lt;/td>
&lt;td>Rust 单二进制 TUI&lt;/td>
&lt;td>tmux 的 Agent 化改造：语义状态 + socket 控制平面 + 插件体系&lt;/td>
&lt;td>无 GUI、无内置浏览器/编辑器；但 API 面更完整、状态语义更严谨，适合活在终端里的人&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>tmux / Warp&lt;/td>
&lt;td>传统终端&lt;/td>
&lt;td>tmux 是会话持久化原语；Warp 是现代化终端&lt;/td>
&lt;td>不感知 Agent 状态，协作要自己拼&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Emdash&lt;/td>
&lt;td>桌面应用&lt;/td>
&lt;td>用 Git worktree 隔离并行 Agent&lt;/td>
&lt;td>偏&amp;quot;并行实验场&amp;rdquo;，无终端分屏和远程接管&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>cmux&lt;/td>
&lt;td>macOS 终端&lt;/td>
&lt;td>Ghostty 基座 + 垂直标签 + Agent 通知&lt;/td>
&lt;td>macOS 独占，功能面窄&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Superset / Herdr 类 runtime&lt;/td>
&lt;td>CLI&lt;/td>
&lt;td>在隔离 worktree 里批量养 Agent&lt;/td>
&lt;td>面向脚本化/无人值守场景&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>一句话选型：&lt;/p>
&lt;ul>
&lt;li>要&lt;strong>图形界面、会话树、手机上看进度&lt;/strong> → VelaTerm 是当前完成度最高的选择&lt;/li>
&lt;li>要&lt;strong>严谨的 Agent 状态语义和可编程控制平面&lt;/strong>、且愿意住在终端里 → Herdr&lt;/li>
&lt;li>只是想要会话不丢 → tmux 依然是答案&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="九适合谁">九、适合谁
&lt;/h2>&lt;p>&lt;strong>适合：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>同时跑多个编程 Agent，标签页已经管理不动的人&lt;/li>
&lt;li>想让 Agent 之间互相查资料、发消息、拆任务，而不是自己当传话筒的人&lt;/li>
&lt;li>需要在服务器跑长任务、随时用浏览器/手机接管的人&lt;/li>
&lt;li>Windows 用户注意：这是少数把 Windows 当一等公民对待的同类项目（内置 Git Bash、x64/arm64 双架构、安装器处理了 npm wrapper 这类 Windows 特有坑）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>不适合 / 再等等：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>追求生产环境稳定性、受不了两天一个版本的人&lt;/li>
&lt;li>需要完整插件生态的人（Herdr 的插件市场更成熟）&lt;/li>
&lt;li>对&amp;quot;终端数据上网络&amp;quot;零容忍、又不打算细看安全配置的人&lt;/li>
&lt;li>纯终端原教旨主义者——你会更想要 Herdr 或干脆 tmux&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="参考">参考
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://velaterm.com/zh-CN" target="_blank" rel="noopener"
>velaterm.com&lt;/a> —— 官网与用户手册（下载、入门指南、AI 智能体会话、远程开发、更新日志）&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/vlinx-io/VelaTerm" target="_blank" rel="noopener"
>github.com/vlinx-io/VelaTerm&lt;/a> —— 源码（MIT），README 有中文版与技术栈说明&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/vlinx-io/VelaTerm/releases" target="_blank" rel="noopener"
>Releases&lt;/a> —— v0.1.x → v0.2.6 完整变更记录&lt;/li>
&lt;li>&lt;a class="link" href="http://www.myaiexp.com/en/items/dev-tools/velaterm" target="_blank" rel="noopener"
>myaiexp：VelaTerm&lt;/a> / &lt;a class="link" href="https://huntscreens.com/zh/products/velaterm" target="_blank" rel="noopener"
>HuntScreens：VelaTerm&lt;/a> —— 第三方产品收录与功能摘要&lt;/li>
&lt;li>&lt;a class="link" href="https://honeystax.com/p/velaterm" target="_blank" rel="noopener"
>Honeystax：VelaTerm&lt;/a> —— 仓库健康度评估（2026-08-15 快照）&lt;/li>
&lt;li>本站关联阅读：&lt;a class="link" href="https://www.zata.cc/p/herdr-ai-agent-terminal-runtime/" >Herdr 详解：给 AI Agent 用的终端运行时&lt;/a> —— 同赛道另一条路线的本机实测&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>信息时效&lt;/strong>：本文数据检索于 2026-09-30（GitHub 数据、版本号均为此日快照）。项目迭代极快，阅读时请以官网与仓库的最新状态为准。&lt;/p>
&lt;/blockquote></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>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>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></channel></rss>