<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Claude Code - DJJ</title><link>https://blog.pdjjq.org/tags/claude-code/</link><description>反抗吧，朋友！</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Mon, 05 Oct 2026 12:55:00 +0800</lastBuildDate><atom:link href="https://blog.pdjjq.org/tags/claude-code/index.xml" rel="self" type="application/rss+xml"/><item><title>Claude Code Mods：把 Agent 的工作流程变成可编程接口 by GPT-6-Astra</title><link>https://blog.pdjjq.org/post/claude-code-mods.html</link><pubDate>Mon, 05 Oct 2026 12:55:00 +0800</pubDate><guid>https://blog.pdjjq.org/post/claude-code-mods.html</guid><description>&lt;p>给 Claude 一份 Skill，可以教它怎么做代码审查。接一个 MCP，可以让它查询公司的工单系统。&lt;/p>
&lt;p>但如果我想在它执行工具之前改一下参数，在每轮回答结束后展示一个面板，或者把团队的规则直接接进执行流程，这就涉及 Claude Code 自己怎么工作了。&lt;/p>
&lt;p>Mods 开放的正是这一层：&lt;strong>用 JavaScript / TypeScript，参与 Claude Code 处理 Prompt、工具调用、会话和界面的过程。&lt;/strong>&lt;/p>
&lt;p>读完文档后，我最感兴趣的是它的接入方式。很多行为都经过一条事件处理链，Mod 可以在链上观察、修改输入，也可以直接返回结果。写过 Web 中间件的话，这套模型会很熟悉。&lt;/p>
&lt;blockquote>
&lt;p>本文按 &lt;strong>2026 年 10 月 5 日&lt;/strong>可查的官方资料整理。Mods 从 v2.1.287 起默认开启，在线 Reference 当前描述的是 v2.1.289。涉及接口时，以本机 Claude Code 生成的类型声明为准；涉及内部实现的猜测，会明确标出。&lt;a href="https://code.claude.com/docs/en/plugins/mods/reference">版本与接口说明&lt;/a>、&lt;a href="https://code.claude.com/docs/en/plugins/mods/troubleshoot#your-version-is-older-than-21287">启用条件&lt;/a>&lt;/p>&lt;/blockquote>
&lt;h2 id="plugin-负责装mod-负责跑">Plugin 负责装，Mod 负责跑&lt;a class="heading-anchor" href="#plugin-%e8%b4%9f%e8%b4%a3%e8%a3%85mod-%e8%b4%9f%e8%b4%a3%e8%b7%91" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>先把几个名字放回各自的位置。&lt;/p>
&lt;p>&lt;strong>Plugin 是扩展包。&lt;/strong> 安装、版本、依赖、分发，都围绕它进行。一个包里可以放 Skill、Agent、传统 Hook、MCP/LSP 配置，也可以放一段长期参与事件处理的 JS/TS 代码。&lt;/p>
&lt;p>这段代码叫 &lt;strong>hooks module&lt;/strong>。按照官方定义，包含这类模块的 Plugin 就叫 &lt;strong>Mod&lt;/strong>。&lt;a href="https://code.claude.com/docs/en/plugins/overview">Plugin 概览&lt;/a>&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/01fd6be0e1ee4555e03deeceaaf0782b.ba0a568ff0b5377a8d70fed2435166b25850dbca707bff8bb54a1b7ab04c25f9.svg"
alt="Plugin 的组件关系：包含 hooks module 的 Plugin 就是 Mod"
loading="lazy"
decoding="async" width="570" height="694">
&lt;figcaption>Plugin 的组件关系：包含 hooks module 的 Plugin 就是 Mod&lt;/figcaption>
&lt;/figure>&lt;p>所以开发一个 Mod，不需要再学一套安装系统。它仍然是一个 Plugin，也可以和 Skill、MCP 一起发给别人。&lt;/p>
&lt;p>还有一个容易混淆的词：&lt;strong>codemod&lt;/strong>。它通常指批量改源码的工具，比如把旧 API 替换成新 API。Claude Code Mod 的主要处理对象则是 Agent 的运行过程。两者可以配合，但职责不同。&lt;/p>
&lt;h2 id="核心机制一次工具调用要经过哪些人">核心机制：一次工具调用，要经过哪些人&lt;a class="heading-anchor" href="#%e6%a0%b8%e5%bf%83%e6%9c%ba%e5%88%b6%e4%b8%80%e6%ac%a1%e5%b7%a5%e5%85%b7%e8%b0%83%e7%94%a8%e8%a6%81%e7%bb%8f%e8%bf%87%e5%93%aa%e4%ba%9b%e4%ba%ba" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>先看一段完整的 Hook：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">register&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tool.call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Bash&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&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="p">{&lt;/span> &lt;span class="nx">to&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;debug&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="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&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="p">{&lt;/span> &lt;span class="nx">to&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;debug&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="nx">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三个参数各司其职：&lt;/p>
&lt;ul>
&lt;li>&lt;code>$&lt;/code>：Claude Code 提供的能力，比如写日志、读文件、运行进程。&lt;/li>
&lt;li>&lt;code>e&lt;/code>：这一次事件的数据。对 Bash 调用来说，里面有工具名和命令等字段。&lt;/li>
&lt;li>&lt;code>next&lt;/code>：继续往后执行，并取回结果。&lt;/li>
&lt;/ul>
&lt;p>执行到 &lt;code>await next(e)&lt;/code> 时，当前 Hook 把控制权交出去。后面的 Mod 运行完，Claude Code 完成原本的处理，结果才返回这里。接下来才会打印第二条日志。&lt;/p>
&lt;p>有两个 Mod 时，正常执行路径大致如下：&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/8301ffebda48102fc056cb07dc8e86f3.23a1501022f06d37bfc76160dc367e233c4f72a48a5868642741aa734d9ae0fb.svg"
alt="事件逐层进入，结果反向返回：简化的中间件调用顺序"
loading="lazy"
decoding="async" width="631" height="520">
&lt;figcaption>事件逐层进入，结果反向返回：简化的中间件调用顺序&lt;/figcaption>
&lt;/figure>&lt;p>这就是常说的「洋葱模型」：调用一层层进去，结果一层层回来。图里省略了策略分组等排序规则，但保留了最关键的调用关系。&lt;/p>
&lt;p>它也解释了 Mod 的三种基本用法：&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>await next(e)&lt;/code> 前后处理&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>改一下输入&lt;/td>
&lt;td>&lt;code>next({ ...e, 某个字段: 新值 })&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>自己回答，不再往后执行&lt;/td>
&lt;td>按该事件要求的格式直接返回结果&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>这里有两个细节。第一，事件对象是深度冻结的数据，修改时应该复制一份。第二，每种事件的返回值有自己的约定，不能随便返回一个对象就指望它生效。例如 &lt;code>tool.call&lt;/code> 可以返回拒绝原因，而 &lt;code>ui.render&lt;/code> 返回的是界面树。&lt;a href="https://code.claude.com/docs/en/plugins/mods/reference#the-hook-function">Hook 与事件约定&lt;/a>&lt;/p>
&lt;p>&lt;strong>Mod 处在实际调用路径上。&lt;/strong> 因此，它能改变接下来发生的事情，这比只订阅「工具已经执行完了」这样的通知更深入。&lt;/p>
&lt;h2 id="events拦截的位置不同能改变的事情就不同">Events：拦截的位置不同，能改变的事情就不同&lt;a class="heading-anchor" href="#events%e6%8b%a6%e6%88%aa%e7%9a%84%e4%bd%8d%e7%bd%ae%e4%b8%8d%e5%90%8c%e8%83%bd%e6%94%b9%e5%8f%98%e7%9a%84%e4%ba%8b%e6%83%85%e5%b0%b1%e4%b8%8d%e5%90%8c" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>事件多，真正需要弄清的是它们处在执行流程的哪一步。拿一个「记录文件改动，再帮助用户审查」的扩展来说，工具执行、模型回答和用户点击按钮，是三个不同的时刻。&lt;/p>
&lt;h3 id="toolcall-管执行toolcheck-管权限判断">&lt;code>tool.call&lt;/code> 管执行，&lt;code>tool.check&lt;/code> 管权限判断&lt;a class="heading-anchor" href="#toolcall-%e7%ae%a1%e6%89%a7%e8%a1%8ctoolcheck-%e7%ae%a1%e6%9d%83%e9%99%90%e5%88%a4%e6%96%ad" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>这两个事件很容易混用：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>事件&lt;/th>
&lt;th>你面对的是什么&lt;/th>
&lt;th>适合做什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>tool.describe&lt;/code>&lt;/td>
&lt;td>Claude 看到的工具说明&lt;/td>
&lt;td>调整工具描述&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tool.call&lt;/code>&lt;/td>
&lt;td>工具名和实际参数&lt;/td>
&lt;td>改参数、拒绝执行、替换结果、在执行前后做事&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tool.check&lt;/code>&lt;/td>
&lt;td>工具调用的权限判断&lt;/td>
&lt;td>根据当前状态调整 &lt;code>allow / ask / deny&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>在 &lt;code>tool.call&lt;/code> 中，Bash 参数直接位于 &lt;code>e.command&lt;/code>；在 &lt;code>tool.check&lt;/code> 中，它位于 &lt;code>e.input.command&lt;/code>。后者的 &lt;code>next(e)&lt;/code> 返回权限决定，并不执行工具。&lt;a href="https://code.claude.com/docs/en/plugins/mods/reference#tools">工具事件及字段&lt;/a>&lt;/p>
&lt;p>因此，「修改完成后记录这个文件」应该放在 &lt;code>tool.call&lt;/code> 的 &lt;code>await next(e)&lt;/code> 后面，而且要检查结果里的 &lt;code>deny&lt;/code> 和 &lt;code>isError&lt;/code>。收到了一次 &lt;code>Edit&lt;/code> 请求，不代表文件真的被改过。&lt;/p>
&lt;p>筛选事件也不用全部挤进一个大 &lt;code>if&lt;/code>。&lt;code>on&lt;/code> 的第二个参数可以按字段匹配，值可以是字符串、候选数组或正则。例如 &lt;code>{ tool: [&amp;quot;Edit&amp;quot;, &amp;quot;Write&amp;quot;] }&lt;/code> 就只接收这两类调用。多个字段同时出现时，需要全部满足。&lt;/p>
&lt;h3 id="改用户输入和给模型补上下文是两种产品行为">改用户输入，和给模型补上下文，是两种产品行为&lt;a class="heading-anchor" href="#%e6%94%b9%e7%94%a8%e6%88%b7%e8%be%93%e5%85%a5%e5%92%8c%e7%bb%99%e6%a8%a1%e5%9e%8b%e8%a1%a5%e4%b8%8a%e4%b8%8b%e6%96%87%e6%98%af%e4%b8%a4%e7%a7%8d%e4%ba%a7%e5%93%81%e8%a1%8c%e4%b8%ba" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>假设用户输入「帮我审查这些改动」，扩展希望补充审查范围。&lt;/p>
&lt;p>修改 &lt;code>prompt.submit&lt;/code> 的 &lt;code>text&lt;/code>，会改变对话里显示的用户消息；追加 &lt;code>context&lt;/code>，则保留用户原话，把额外信息交给 Claude。选哪个，取决于你希望用户看到什么。&lt;a href="https://code.claude.com/docs/en/plugins/mods/events#rewrite-or-add-to-a-prompt">Prompt 事件&lt;/a>&lt;/p>
&lt;p>例如下面这个片段只补充审查要求，并保留前面其他 Mod 已添加的上下文：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;prompt.submit&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">text&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">includes&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="k">return&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&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="nx">next&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">e&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">context&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">context&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="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;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>e.context&lt;/code> 很关键。几个 Mod 一起工作时，后来的扩展不应该顺手抹掉前面的内容。类似地，&lt;code>prompt.section&lt;/code> 面对的是系统提示词的一个命名片段，&lt;code>prompt.context&lt;/code> 面对的是会话初始上下文，&lt;code>skill.prompt&lt;/code> 面对的是展开后的 Skill 文本。它们影响的位置不同，不能都按「每次用户按下回车」理解。&lt;/p>
&lt;h3 id="一轮回答可以包含多次模型请求">一轮回答，可以包含多次模型请求&lt;a class="heading-anchor" href="#%e4%b8%80%e8%bd%ae%e5%9b%9e%e7%ad%94%e5%8f%af%e4%bb%a5%e5%8c%85%e5%90%ab%e5%a4%9a%e6%ac%a1%e6%a8%a1%e5%9e%8b%e8%af%b7%e6%b1%82" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>用户发一个 Prompt，Claude 可能先请求模型、调用工具，再带着工具结果请求模型。这个完整过程是一轮 turn，其中每次模型请求是一个 step。&lt;/p>
&lt;p>&lt;code>turn.start&lt;/code> 和 &lt;code>turn.complete&lt;/code> 适合统计整轮行为；&lt;code>turn.step&lt;/code> 适合观察单次模型请求及其流式结果。后者必须用 async generator，才能把中间响应继续传出去：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;turn.step&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&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="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">yield&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">usage&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`本次输出 token：&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">usage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">output_tokens&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">to&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;debug&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="k">return&lt;/span> &lt;span class="nx">result&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>yield*&lt;/code> 在这里有实际意义：它一边转发响应流，一边等最终结果。如果只按普通 Promise 的思路写，就丢掉了这个事件的流式约定。统计时还要注意 &lt;code>e.agentId&lt;/code>，否则可能把子 Agent 的请求也算进主对话。&lt;code>turn.complete&lt;/code> 也会在用户中断时触发，要用 &lt;code>isAborted&lt;/code> 区分。&lt;a href="https://code.claude.com/docs/en/plugins/mods/events#follow-a-turn">Turn 生命周期&lt;/a>&lt;/p>
&lt;h3 id="多个-mod-同时存在时顺序就是行为的一部分">多个 Mod 同时存在时，顺序就是行为的一部分&lt;a class="heading-anchor" href="#%e5%a4%9a%e4%b8%aa-mod-%e5%90%8c%e6%97%b6%e5%ad%98%e5%9c%a8%e6%97%b6%e9%a1%ba%e5%ba%8f%e5%b0%b1%e6%98%af%e8%a1%8c%e4%b8%ba%e7%9a%84%e4%b8%80%e9%83%a8%e5%88%86" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>前面的 Mod 能先看输入、后看结果，还能决定后面的 Mod 是否执行。因而一个日志扩展放在脱敏扩展前面还是后面，可能决定它读到的是原始数据还是脱敏数据。&lt;/p>
&lt;p>当前的分组顺序可以简化成：&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/b1b201ae2019536cdc84d628992aa731.99fa0dd5179df89044204b2b103ad2613762ecc8b2331e533d7133a68cd0d22c.svg"
alt="Mod 的分组顺序：越靠前，越能控制后面的调用路径"
loading="lazy"
decoding="async" width="307" height="892">
&lt;figcaption>Mod 的分组顺序：越靠前，越能控制后面的调用路径&lt;/figcaption>
&lt;/figure>&lt;p>用户 Mod 还会运行在自己声明依赖的 Mod 之前。传统 Hook 也有位置：组织管理的 &lt;code>PreToolUse&lt;/code> 先运行，其阻止结果具有优先级；其他来源的 &lt;code>PreToolUse&lt;/code> 位于 &lt;code>tool.call&lt;/code> 链深入宿主后的处理流程中。一个提前返回的 Mod，可能让后者根本没有机会运行。&lt;a href="https://code.claude.com/docs/en/plugins/mods/events#the-order-mods-run-in">排序规则&lt;/a>&lt;/p>
&lt;p>出错时的行为同样重要：没有 &lt;code>.catch&lt;/code> 的 Hook 如果在调用 &lt;code>next&lt;/code> 前失败，会被跳过；如果 &lt;code>next&lt;/code> 已经完成，则保留已经取得的结果，不会自动再执行一次工具。需要失败时拒绝的前置检查，应通过注册结果的 &lt;code>.catch(...)&lt;/code> 明确返回拒绝原因。已经执行完的副作用，不会因为外层后来报错就被撤销。&lt;/p>
&lt;p>这也影响「等待用户确认」的实现。用 &lt;code>$.ui.ask&lt;/code> 等待，等待时间不计入普通 Hook 自身预算；自己挂起一个 Promise 等按钮，可能耗尽预算，最后 Hook 被跳过。确认后再 &lt;code>next(e)&lt;/code>，仍然会继续 Claude Code 的正常权限检查。&lt;a href="https://code.claude.com/docs/en/plugins/mods/events">确认与错误处理&lt;/a>&lt;/p>
&lt;h2 id="更有意思的地方mod-调-api也会经过-mod">更有意思的地方：Mod 调 API，也会经过 Mod&lt;a class="heading-anchor" href="#%e6%9b%b4%e6%9c%89%e6%84%8f%e6%80%9d%e7%9a%84%e5%9c%b0%e6%96%b9mod-%e8%b0%83-api%e4%b9%9f%e4%bc%9a%e7%bb%8f%e8%bf%87-mod" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>如果只是前后加几个回调，设计还不算特别。&lt;/p>
&lt;p>Mods 更有意思的一点是：&lt;strong>它自己调用宿主 API，也会产生事件。&lt;/strong>&lt;/p>
&lt;p>例如，一个 Mod 想读文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">read&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;.env&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个调用会变成 &lt;code>fs.read&lt;/code> 事件。排在它前面的策略 Mod，可以检查路径，决定继续、修改请求，或者拒绝访问。&lt;a href="https://code.claude.com/docs/en/plugins/mods/api#reach-files-processes-and-the-network">文件、进程与网络访问&lt;/a>&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/7c49dae95066d437fa3c68296d6660fa.b6c152d38976568ac382171a49df9fe0ae401b1f76283185a7ef21e650499e14.svg"
alt="Mod 的 API 调用，也能被前层策略 Mod 拦截"
loading="lazy"
decoding="async" width="408" height="635">
&lt;figcaption>Mod 的 API 调用，也能被前层策略 Mod 拦截&lt;/figcaption>
&lt;/figure>&lt;p>同样的思路也适用于进程、网络和模型调用。这样，企业可以在普通 Mod 前面放一层策略，约束后面的扩展能做什么。&lt;/p>
&lt;p>还有两个比较底层的入口：&lt;code>plugin.register&lt;/code> 可以在模块加载时检查它声明使用的事件和 API；&lt;code>engine.create&lt;/code> 可以调整提供给 Mod 的 API。它们让策略能够介入「加载什么代码」和「交给它什么能力」这两个时刻。&lt;a href="https://code.claude.com/docs/en/plugins/mods/reference#other-mods">其他 Mod 相关事件&lt;/a>&lt;/p>
&lt;p>从设计上看，&lt;code>$&lt;/code> 像一个由宿主管理的能力入口。扩展需要资源时先经过它，宿主就有机会统一检查、记录和处理取消，也更容易在测试时替换成假实现。&lt;/p>
&lt;h2 id="api事件决定什么时候做-决定能做什么">API：事件决定什么时候做，&lt;code>$&lt;/code> 决定能做什么&lt;a class="heading-anchor" href="#api%e4%ba%8b%e4%bb%b6%e5%86%b3%e5%ae%9a%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e5%81%9a-%e5%86%b3%e5%ae%9a%e8%83%bd%e5%81%9a%e4%bb%80%e4%b9%88" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>有了事件，扩展知道「现在发生了什么」；有了 API，它才能主动创建命令、查询外部系统、调用模型，或者把结果放进界面。&lt;/p>
&lt;h3 id="command-给人用tool-给-claude-用">Command 给人用，Tool 给 Claude 用&lt;a class="heading-anchor" href="#command-%e7%bb%99%e4%ba%ba%e7%94%a8tool-%e7%bb%99-claude-%e7%94%a8" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>同一个功能，可以有两种入口。比如查询改动清单：&lt;/p>
&lt;ul>
&lt;li>用户输入 &lt;code>/change-desk&lt;/code> 打开面板，这是 command。&lt;/li>
&lt;li>Claude 调用 &lt;code>changed_files&lt;/code> 取得清单，这是 tool。&lt;/li>
&lt;/ul>
&lt;p>两者都在 &lt;code>session.start&lt;/code> 注册，但后续分别由 &lt;code>command.run&lt;/code> 和 &lt;code>tool.call&lt;/code> 处理。Claude Code 会等启动 Hook 完成，再接受首个 Prompt，因此应该在这里把入口准备好。&lt;a href="https://code.claude.com/docs/en/plugins/mods/api#add-a-command-or-a-tool">注册 API&lt;/a>&lt;/p>
&lt;p>工具需要 JSON Schema 描述输入。名为 &lt;code>change-desk&lt;/code> 的插件注册 &lt;code>changed_files&lt;/code> 后，Claude 看到的名字是 &lt;code>mcp__change-desk__changed_files&lt;/code>。这个名字借用了 MCP 工具的命名形式，但实现仍然在 Mod 内，不代表你启动了一个 MCP server。&lt;/p>
&lt;p>命令返回 &lt;code>{ text }&lt;/code> 时，文字既显示在对话中，也会被 Claude 读到；只打开面板时返回 &lt;code>{}&lt;/code> 即可。这和普通 UI 日志的语义不一样。&lt;/p>
&lt;h3 id="模型调用独立小任务和结合当前对话的任务">模型调用：独立小任务和结合当前对话的任务&lt;a class="heading-anchor" href="#%e6%a8%a1%e5%9e%8b%e8%b0%83%e7%94%a8%e7%8b%ac%e7%ab%8b%e5%b0%8f%e4%bb%bb%e5%8a%a1%e5%92%8c%e7%bb%93%e5%90%88%e5%bd%93%e5%89%8d%e5%af%b9%e8%af%9d%e7%9a%84%e4%bb%bb%e5%8a%a1" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>&lt;code>$.model.complete&lt;/code> 适合给定输入就能完成的分类、摘要和判断；&lt;code>$.model.fork&lt;/code> 适合结合当前对话回答一个额外问题。选择它们之前，先想清楚任务需要哪些上下文。&lt;a href="https://code.claude.com/docs/en/plugins/mods/api#call-a-model">模型 API&lt;/a>&lt;/p>
&lt;p>例如「这张工单属于 bug 还是 feature」，可以把工单单独交给 &lt;code>complete&lt;/code>；「结合刚才这轮修改，列出还没验证的假设」，才需要当前对话。&lt;/p>
&lt;p>模型没有正常回答时，&lt;code>complete&lt;/code> 可能返回 &lt;code>isAnswered: false&lt;/code>，而不是抛异常；组织策略拒绝请求等情况又可能直接 reject。因此调用方要同时处理返回状态和异常。它使用当前会话的凭据和用量，不能把后台调用当成没有成本的本地函数。&lt;/p>
&lt;h3 id="后台任务显示状态与启动新一轮对话要分开">后台任务：显示状态，与启动新一轮对话要分开&lt;a class="heading-anchor" href="#%e5%90%8e%e5%8f%b0%e4%bb%bb%e5%8a%a1%e6%98%be%e7%a4%ba%e7%8a%b6%e6%80%81%e4%b8%8e%e5%90%af%e5%8a%a8%e6%96%b0%e4%b8%80%e8%bd%ae%e5%af%b9%e8%af%9d%e8%a6%81%e5%88%86%e5%bc%80" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>一个 CI 监控扩展，可以用 &lt;code>$.clock.every&lt;/code> 定时查询检查状态，再调用 &lt;code>$.ui.status&lt;/code> 更新提示。这个过程不需要让 Claude 每分钟说一次「还在跑」。&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/892756142d5b0bbb8ba8fca10099bdc5.d28246e812ce7bfd1cbcecfa7d5498c3709db5b5d192b47395dfa0fc19f54d3b.svg"
alt="后台监控的两种出口：更新界面，或在满足条件时启动新的 Agent 回合"
loading="lazy"
decoding="async" width="559" height="683">
&lt;figcaption>后台监控的两种出口：更新界面，或在满足条件时启动新的 Agent 回合&lt;/figcaption>
&lt;/figure>&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>$.ui.status&lt;/code>、&lt;code>$.ui.toast&lt;/code>&lt;/td>
&lt;td>更新状态行或临时通知，不启动 turn&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>$.ui.log&lt;/code>&lt;/td>
&lt;td>记录给用户看的日志，Claude 不读取这条日志&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>$.prompt.fill&lt;/code>&lt;/td>
&lt;td>填入可编辑的输入框草稿，等用户发送&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>$.prompt.submit&lt;/code>&lt;/td>
&lt;td>提交 Prompt，等待会话空闲后开始新的 turn&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>这里有个容易造成互相等待的写法：在正在处理的 turn 中 &lt;code>await $.prompt.submit(...)&lt;/code>。提交操作要等当前回合结束才能开始，而当前 Hook 又在等它。应把启动下一轮的工作放到合适的后台任务中，避免这种等待关系。&lt;a href="https://code.claude.com/docs/en/plugins/mods/api#run-work-in-the-background">后台任务及通知&lt;/a>&lt;/p>
&lt;p>timer 会在模块 reload 时停止，新模块可以重新注册。实际做 CI 监控时，我还会记录上次处理的状态，只在新的失败出现时触发一次，避免轮询把同一个问题不断送给 Claude。&lt;/p>
&lt;h3 id="宿主-io名字像熟悉的-api返回值未必一样">宿主 I/O：名字像熟悉的 API，返回值未必一样&lt;a class="heading-anchor" href="#%e5%ae%bf%e4%b8%bb-io%e5%90%8d%e5%ad%97%e5%83%8f%e7%86%9f%e6%82%89%e7%9a%84-api%e8%bf%94%e5%9b%9e%e5%80%bc%e6%9c%aa%e5%bf%85%e4%b8%80%e6%a0%b7" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>&lt;code>$.process.run([&amp;quot;git&amp;quot;, &amp;quot;status&amp;quot;, &amp;quot;--short&amp;quot;])&lt;/code> 接收参数数组，不经过 shell。管道和重定向不会因为写进某个参数就自动生效。进程正常退出时需要检查 &lt;code>exitCode&lt;/code>；启动失败或超时则要处理异常。&lt;/p>
&lt;p>&lt;code>$.http.fetch&lt;/code> 返回已经读取完的 &lt;code>{ status, ok, headers, text }&lt;/code>，不能照搬浏览器里 &lt;code>await response.json()&lt;/code> 的用法。相对文件路径按会话工作目录解释，也不能默认当成插件自身目录。&lt;a href="https://code.claude.com/docs/en/plugins/mods/api#reach-files-processes-and-the-network">宿主 I/O 约定&lt;/a>&lt;/p>
&lt;p>&lt;code>$.session.send&lt;/code> 则能向其他会话或子 Agent 发消息，但成功只表示消息已排入投递队列，不表示对方完成了工作。如果要做协作流程，需要自己设计任务 ID、回复和失败处理。收到的发送者名称也不应直接当成可信身份。&lt;a href="https://code.claude.com/docs/en/plugins/mods/api#send-and-receive-messages-between-sessions">会话消息&lt;/a>&lt;/p>
&lt;h2 id="它到底运行在哪里">它到底运行在哪里&lt;a class="heading-anchor" href="#%e5%ae%83%e5%88%b0%e5%ba%95%e8%bf%90%e8%a1%8c%e5%9c%a8%e5%93%aa%e9%87%8c" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>公开资料能确认几件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>安装的 Mods 共享一个 hooks worker thread。&lt;/strong>&lt;/li>
&lt;li>加载日志里会出现 &lt;code>worker, environment 2, tier user&lt;/code> 这样的信息。&lt;/li>
&lt;li>hooks module 没有 Node.js API，也没有直接文件、网络访问能力；定时器要用 &lt;code>$.clock&lt;/code>，不能直接调用 &lt;code>setTimeout&lt;/code>。&lt;/li>
&lt;/ol>
&lt;p>这些来自官方的 &lt;a href="https://code.claude.com/docs/en/plugins/mods/api#reach-files-processes-and-the-network">API 文档&lt;/a>和&lt;a href="https://code.claude.com/docs/en/plugins/mods/troubleshoot#read-the-debug-log">故障排查文档&lt;/a>。&lt;/p>
&lt;p>把线索放在一起，可以画出下面这个模型。&lt;strong>共享 worker 是事实；worker 内部的独立执行环境、代理与通信方式，是根据公开行为做出的推测。&lt;/strong>&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/70fb8e419a85e7dec8c6c2bb411cb118.5cfe9e64ca0e2ef475ae119e31ee707a501c93d736846778ba13723af0ab1611.svg"
alt="运行时推测图：共享 worker 已确认，虚线框内的具体隔离方式未公开"
loading="lazy"
decoding="async" width="590" height="525">
&lt;figcaption>运行时推测图：共享 worker 已确认，虚线框内的具体隔离方式未公开&lt;/figcaption>
&lt;/figure>&lt;p>我倾向于认为，宿主在 worker 里为不同 Mod 建立执行环境，再通过 &lt;code>$&lt;/code> 把文件、进程和模型等能力接出去。&lt;/p>
&lt;p>但证据只到这里。一个 &lt;code>environment&lt;/code> 编号，并不能证明它就是某种 VM 的 realm，更不能证明每个 Mod 有独立的内存隔离。它用的是哪种 JS 引擎、哪种 TS 转译器、怎样传递消息，官方没有说明。&lt;/p>
&lt;p>静态分析也类似。&lt;code>claude plugin validate&lt;/code> 能列出代码使用的事件、API 和环境变量；官方还要求某些名字写成字符串字面量。这说明它会分析源码。使用 JS/TS 解析器来完成这件事很合理，但直接断言底层是 Babel、SWC 或 TypeScript Compiler，就超出了证据。&lt;a href="https://code.claude.com/docs/en/plugins/mods/admin#review-what-a-mod-can-do">静态能力检查&lt;/a>&lt;/p>
&lt;p>对使用者更实际的影响是：&lt;strong>别在 Hook 里做长时间占用 CPU 的工作。&lt;/strong>&lt;/p>
&lt;p>大家共用 worker，一个永远不让出执行权的循环可能拖住整个线程。官方说明，如果 worker 连续三次崩溃且无法归因，当前会话会卸载所有非内置 Mod，包括组织安装的 Mod。&lt;a href="https://code.claude.com/docs/en/plugins/mods/troubleshoot#it-crashed-the-hooks-worker">worker 故障处理&lt;/a>&lt;/p>
&lt;h2 id="interfaceui-本身也在这条事件链里">Interface：UI 本身，也在这条事件链里&lt;a class="heading-anchor" href="#interfaceui-%e6%9c%ac%e8%ba%ab%e4%b9%9f%e5%9c%a8%e8%bf%99%e6%9d%a1%e4%ba%8b%e4%bb%b6%e9%93%be%e9%87%8c" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>前面讲的是 Agent 怎么执行工作。另一半能力是：用户怎样看到这些工作，又怎样参与其中。&lt;/p>
&lt;p>Claude Code 把允许扩展绘制的地方称为 &lt;strong>render site&lt;/strong>。每次需要生成这些位置的内容，会触发 &lt;code>ui.render&lt;/code>；Mod 返回一棵元素树，宿主负责把它画出来。&lt;a href="https://code.claude.com/docs/en/plugins/mods/interface">界面机制&lt;/a>&lt;/p>
&lt;h3 id="可以画在哪里">可以画在哪里&lt;a class="heading-anchor" href="#%e5%8f%af%e4%bb%a5%e7%94%bb%e5%9c%a8%e5%93%aa%e9%87%8c" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>最常见的三个入口，是自己打开的 &lt;code>Pane&lt;/code>、输入框上方共享的 &lt;code>AbovePrompt&lt;/code>，以及 Claude Code 原有的界面行。&lt;/p>
&lt;p>下面是宽终端中的语义位置图，不是精确比例的界面截图：&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/e39d925c15b7c7a800d5b6d57604628e.92304d9b11b4a5105bb4bb2d694d372885120cb2edd4984198161a26a1d6590a.svg"
alt="终端中的扩展位置：独立面板、共享提示区，以及可参与渲染的原有内容"
loading="lazy"
decoding="async" width="526" height="477">
&lt;figcaption>终端中的扩展位置：独立面板、共享提示区，以及可参与渲染的原有内容&lt;/figcaption>
&lt;/figure>&lt;p>&lt;code>Pane&lt;/code> 的位置由宿主布局决定：宽屏全屏终端可以停靠在对话旁边，其他情况下可能放在输入框上方。写代码时不应假定它始终是一块固定宽度的侧栏。&lt;/p>
&lt;p>而且，「用户主动打开」与「扩展自己弹出来」的待遇不同。前者在窄终端也能显示；后者会受到可用空间限制。调用 &lt;code>$.ui.open&lt;/code> 后，应检查 &lt;code>isPlaced&lt;/code>，不要把 Promise 成功返回等同于用户已经看到了面板。&lt;/p>
&lt;h3 id="渲染不是改-dom而是回答一次绘制请求">渲染不是改 DOM，而是回答一次绘制请求&lt;a class="heading-anchor" href="#%e6%b8%b2%e6%9f%93%e4%b8%8d%e6%98%af%e6%94%b9-dom%e8%80%8c%e6%98%af%e5%9b%9e%e7%ad%94%e4%b8%80%e6%ac%a1%e7%bb%98%e5%88%b6%e8%af%b7%e6%b1%82" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>一个渲染 Hook 里最重要的字段是：&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>e.component&lt;/code>&lt;/td>
&lt;td>正在绘制 &lt;code>Pane&lt;/code>、&lt;code>Spinner&lt;/code>，还是其他位置&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>e.requestId&lt;/code>&lt;/td>
&lt;td>具体是哪一个实例，例如哪个面板&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>e.surface&lt;/code>&lt;/td>
&lt;td>当前是终端还是 Desktop&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>e.props&lt;/code>&lt;/td>
&lt;td>该位置提供的数据和可用空间&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>$.ui.resolve(e)&lt;/code>&lt;/td>
&lt;td>取得当前 surface 可以使用的元素构造函数&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>渲染自己的面板时，先判断 &lt;code>e.requestId&lt;/code>。不是自己的，就 &lt;code>return next(e)&lt;/code>；否则会误接管别人的面板。&lt;/p>
&lt;p>同一位置上，还可以选择三种处理方式：改 &lt;code>e.props&lt;/code> 后交给 &lt;code>next&lt;/code>，沿用宿主的绘制；直接返回自己的树，替换原有内容；或者把 &lt;code>await next(e)&lt;/code> 的结果包进自己的 &lt;code>Box&lt;/code>，在旁边增加内容。&lt;a href="https://code.claude.com/docs/en/plugins/mods/interface#change-what-claude-code-already-draws">修改现有界面&lt;/a>&lt;/p>
&lt;p>例如，在共享提示区增加一行说明，同时保留后面的扩展：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;ui.render&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">component&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;AbovePrompt&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">Box&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Text&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&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">rest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&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="nx">Box&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">flexDirection&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;column&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">children&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">rest&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Text&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">children&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;输入 /change-desk 查看改动清单&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="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>Text&lt;/code>，后面的 Mod 就没机会画了。&lt;strong>共享区域不会自动把所有扩展的内容拼在一起，组合需要作者明确写出来。&lt;/strong>&lt;/p>
&lt;p>宿主原有的内容还可能以 &lt;code>{ type: &amp;quot;engine&amp;quot;, ref }&lt;/code> 引用返回。可以保留、包裹它，不能假定里面是一棵可任意拆改的普通元素树。&lt;/p>
&lt;p>权限确认框不属于开放的 render site。&lt;code>AskUserQuestion&lt;/code> 虽然能扩展，也要求保留宿主问题界面的引用，并受放置规则约束。这些限制说明：开放 UI 不是允许扩展随意接管每一种交互。&lt;/p>
&lt;h3 id="点击按钮也要经过事件链">点击按钮，也要经过事件链&lt;a class="heading-anchor" href="#%e7%82%b9%e5%87%bb%e6%8c%89%e9%92%ae%e4%b9%9f%e8%a6%81%e7%bb%8f%e8%bf%87%e4%ba%8b%e4%bb%b6%e9%93%be" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>&lt;code>Button&lt;/code> 有 &lt;code>onPress&lt;/code>，&lt;code>Input&lt;/code> 有 &lt;code>onInput / onSubmit&lt;/code>，&lt;code>Select&lt;/code> 有 &lt;code>onSelect&lt;/code>。这些回调看起来像普通前端代码，但调用链仍然经过宿主：&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/f8c25e7270cfd1898e6101a1987663c4.d7fd5261360679dbd37c94857bb20c9349d9059a7d1e56ae0ae70c1c31826243.svg"
alt="一次 UI 交互：宿主分发事件，经过 Hook 后才调用控件回调"
loading="lazy"
decoding="async" width="235" height="694">
&lt;figcaption>一次 UI 交互：宿主分发事件，经过 Hook 后才调用控件回调&lt;/figcaption>
&lt;/figure>&lt;p>这有两个后果。第一，其他有相应访问机会的 Mod 可能在回调之前看到输入内容，甚至改写它。第二，输入框按下 Enter 只是执行 &lt;code>onSubmit&lt;/code>，不会自动启动 Claude 的新回合；是否调用 &lt;code>$.prompt.submit&lt;/code>，由你的代码决定。&lt;/p>
&lt;p>控件的 &lt;code>key&lt;/code> 用于识别具体交互目标，测试也可以据此点击。列表里的按钮应有稳定、唯一的 key，不能让两个动作共用一个名字。&lt;/p>
&lt;p>键盘焦点则由宿主管理。面板没有焦点时，按键通常继续进入用户输入框。&lt;code>focus: true&lt;/code> 是请求焦点，不意味着可以抢走用户正在输入的文字；&lt;code>autoFocus&lt;/code> 选择的是面板内部的控件。Tab、方向键、Esc 等也有宿主自己的导航规则。&lt;a href="https://code.claude.com/docs/en/plugins/mods/interface#respond-to-presses-and-typing">交互与焦点&lt;/a>&lt;/p>
&lt;h3 id="数据改了界面不一定马上知道">数据改了，界面不一定马上知道&lt;a class="heading-anchor" href="#%e6%95%b0%e6%8d%ae%e6%94%b9%e4%ba%86%e7%95%8c%e9%9d%a2%e4%b8%8d%e4%b8%80%e5%ae%9a%e9%a9%ac%e4%b8%8a%e7%9f%a5%e9%81%93" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>&lt;code>ui.render&lt;/code> 的结果是一次快照。普通模块变量改变后，宿主不会自动推断哪些界面依赖它，需要调用 &lt;code>$.ui.invalidate(&amp;quot;ui.render&amp;quot;)&lt;/code>。使用 &lt;code>$.state&lt;/code> 时，依赖关系由宿主记录，读过相关状态的位置会自动重绘。&lt;/p>
&lt;p>反过来，也不要在 render 里修改状态。渲染负责读取数据并描述界面；更新应放在控件回调或其他事件中。网络请求同样适合放到事件或后台任务里，再把结果写进状态。否则一次调整窗口宽度，也可能意外再请求一遍服务。&lt;/p>
&lt;p>重绘还会被合并和限频。连续写入的中间值未必都显示出来，所以不能靠「画过这条信息」充当业务操作的可靠确认。&lt;a href="https://code.claude.com/docs/en/plugins/mods/interface#redraw-a-site">重绘机制&lt;/a>&lt;/p>
&lt;h2 id="gallery这套-ui-能做到哪一步">Gallery：这套 UI 能做到哪一步&lt;a class="heading-anchor" href="#gallery%e8%bf%99%e5%a5%97-ui-%e8%83%bd%e5%81%9a%e5%88%b0%e5%93%aa%e4%b8%80%e6%ad%a5" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>只看 &lt;code>Box&lt;/code>、&lt;code>Text&lt;/code> 很容易以为它只能画两行文字。&lt;a href="https://code.claude.com/docs/en/plugins/mods/gallery">官方 Gallery&lt;/a> 更适合用来判断：一个实际功能，能不能用已有元素拼出来。&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>Box&lt;/code>、&lt;code>Text&lt;/code>&lt;/td>
&lt;td>安排清单、摘要和状态；提供行列布局、间距、边框和文字样式&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Markdown&lt;/code>、&lt;code>Link&lt;/code>&lt;/td>
&lt;td>展示审查说明，以及关联 issue、文档链接&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Code&lt;/code>&lt;/td>
&lt;td>展示带高亮的源码，或 unified diff&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Button&lt;/code>、&lt;code>Input&lt;/code>、&lt;code>Select&lt;/code>&lt;/td>
&lt;td>选择文件、填写审查要求、切换过滤条件&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Raster&lt;/code>、&lt;code>Svg&lt;/code>、&lt;code>Image&lt;/code>&lt;/td>
&lt;td>用图形展示分布、热力图或图片，按 surface 选择&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Client&lt;/code>&lt;/td>
&lt;td>把需要动画、键盘或指针输入的局部界面交给单独模块&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="diff-已经是现成的展示能力">Diff 已经是现成的展示能力&lt;a class="heading-anchor" href="#diff-%e5%b7%b2%e7%bb%8f%e6%98%af%e7%8e%b0%e6%88%90%e7%9a%84%e5%b1%95%e7%a4%ba%e8%83%bd%e5%8a%9b" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>&lt;code>Code&lt;/code> 不只是代码高亮。设置 &lt;code>format: &amp;quot;diff&amp;quot;&lt;/code>，就可以把 unified diff 交给宿主绘制，包括增删行的视觉区分：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">Code&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">format&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;diff&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">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;@@ -1 +1 @@\n-const retries = 1\n+const retries = 3&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>因此，一个迁移预览工具可以专心准备 diff 数据、选择范围和处理确认，不必从头实现一个终端 diff renderer。代码着色与主题也能沿用宿主的表现。&lt;a href="https://code.claude.com/docs/en/plugins/mods/gallery#show-code-and-changes">Gallery 的代码与差异示例&lt;/a>&lt;/p>
&lt;p>不过，展示 diff 和计算 diff 是两件事。&lt;code>Code&lt;/code> 不负责读取 Git，也不证明转换正确。前面的 Mod / 确定性工具分工，在 UI 层仍然适用。&lt;/p>
&lt;h3 id="输入控件提供了小型工作台所需的交互">输入控件提供了小型工作台所需的交互&lt;a class="heading-anchor" href="#%e8%be%93%e5%85%a5%e6%8e%a7%e4%bb%b6%e6%8f%90%e4%be%9b%e4%ba%86%e5%b0%8f%e5%9e%8b%e5%b7%a5%e4%bd%9c%e5%8f%b0%e6%89%80%e9%9c%80%e7%9a%84%e4%ba%a4%e4%ba%92" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>按钮、单行输入框、选择器，再加上行列布局，已经能组合出一个文件审查面板。比如上面放筛选框，中间放清单，下面放「填入审查请求」按钮。&lt;/p>
&lt;p>这里需要管理的是状态，而不只是摆几个控件：筛选条件改变后哪些行应该留下，重绘时输入值是否保留，提交以后是否清空。&lt;code>Input&lt;/code> 的 &lt;code>value&lt;/code> 是本次绘制给它的内容，如果每次都传空字符串，重绘可能把输入恢复为空；想保留草稿，就应该保存输入值。&lt;a href="https://code.claude.com/docs/en/plugins/mods/gallery#take-input">Gallery 的输入控件&lt;/a>&lt;/p>
&lt;h3 id="终端和-desktop-有共同能力也有不同能力">终端和 Desktop 有共同能力，也有不同能力&lt;a class="heading-anchor" href="#%e7%bb%88%e7%ab%af%e5%92%8c-desktop-%e6%9c%89%e5%85%b1%e5%90%8c%e8%83%bd%e5%8a%9b%e4%b9%9f%e6%9c%89%e4%b8%8d%e5%90%8c%e8%83%bd%e5%8a%9b" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>普通文本、布局和控件可以共用，但图形元素不能一概而论。当前文档中，&lt;code>Raster&lt;/code> 和 &lt;code>Image&lt;/code> 用于终端，&lt;code>Svg&lt;/code> 用于 Desktop。代码应按 &lt;code>e.surface&lt;/code> 分支，或者提供文字版替代，避免某个 surface 只剩空面板。&lt;a href="https://code.claude.com/docs/en/plugins/mods/reference#elements">元素支持范围&lt;/a>&lt;/p>
&lt;p>&lt;code>Raster&lt;/code> 适合字符网格、热力图这类内容。更新现有网格时，&lt;code>$.ui.blit&lt;/code> 可以只重画那一块，省掉重新执行整个 &lt;code>ui.render&lt;/code> 的过程。它解决的是局部绘制问题，不是给 Hook 增加一套浏览器 Canvas。&lt;/p>
&lt;h3 id="client复杂交互可以有自己的界面模块">&lt;code>Client&lt;/code>：复杂交互可以有自己的界面模块&lt;a class="heading-anchor" href="#client%e5%a4%8d%e6%9d%82%e4%ba%a4%e4%ba%92%e5%8f%af%e4%bb%a5%e6%9c%89%e8%87%aa%e5%b7%b1%e7%9a%84%e7%95%8c%e9%9d%a2%e6%a8%a1%e5%9d%97" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>对于动画、拖动和密集输入，每次都绕回 hooks worker 并重建整个面板，并不合适。&lt;code>Client&lt;/code> 提供了另一条路径：hooks module 返回一个指向界面模块的元素，由界面模块处理局部绘制和输入。&lt;/p>
&lt;p>公开类型声明里的 &lt;code>ClientModule&lt;/code> 和 &lt;code>ClientSurface&lt;/code> 进一步说明了这个分工：界面模块有局部状态、帧时钟、键盘和指针监听；它没有 &lt;code>$&lt;/code>，需要业务能力时，通过 &lt;code>surface.post(data)&lt;/code> 发回所属插件的 &lt;code>ui.message&lt;/code>。后者可以返回新的 props。&lt;a href="https://github.com/anthropics/claude-code/blob/main/mods/types/claude-code.d.ts">公开类型声明&lt;/a>&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/4f2f873bfcdcc5c8ce40ce0144942cb9.31684813424902111fa2c7b521345e1ae2cf29553ba7bdba4e77abf48bb0c4be.svg"
alt="Client 的职责边界：局部交互留在界面模块，业务能力仍由 hooks module 执行"
loading="lazy"
decoding="async" width="557" height="496">
&lt;figcaption>Client 的职责边界：局部交互留在界面模块，业务能力仍由 hooks module 执行&lt;/figcaption>
&lt;/figure>&lt;p>这不是往面板里塞一个任意网页。它仍然遵守宿主提供的元素和运行环境约束。对于普通清单和几个按钮，常规 &lt;code>ui.render&lt;/code> 已经足够；确实需要局部高频交互时，再考虑 &lt;code>Client&lt;/code>。&lt;/p>
&lt;h2 id="能力越深入越要看清权限边界">能力越深入，越要看清权限边界&lt;a class="heading-anchor" href="#%e8%83%bd%e5%8a%9b%e8%b6%8a%e6%b7%b1%e5%85%a5%e8%b6%8a%e8%a6%81%e7%9c%8b%e6%b8%85%e6%9d%83%e9%99%90%e8%be%b9%e7%95%8c" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>没有 Node API，不代表 Mod 被关进了操作系统沙箱。&lt;/p>
&lt;p>&lt;strong>官方明确说明，Mods 没有被 sandbox 隔离，最终以当前用户的权限访问文件、进程和网络。&lt;/strong> &lt;code>$&lt;/code> 让调用变得可管理，但本身不等于操作系统级的权限隔离。&lt;/p>
&lt;p>举一个具体例子：你设置 &lt;code>Read(.env)&lt;/code> 为 deny，限制的是 Claude 的工具调用。Mod 自己仍可能通过 &lt;code>$.fs.read&lt;/code> 读取这个文件，或者通过 &lt;code>$.process&lt;/code> 启动一个能读取它的程序。&lt;a href="https://code.claude.com/docs/en/plugins/mods/admin#know-what-happens-by-default">企业管理文档&lt;/a>&lt;/p>
&lt;p>这也不意味着现有规则全部失效。在启用内置 guard 的受管理环境中，deny 规则和组织管理的 Hook 有相应保护。需要分清的是：&lt;strong>约束 Claude 的工具调用，与约束扩展自己的资源访问，是两条不同的路径。&lt;/strong>&lt;/p>
&lt;p>因此，安装第三方 Mod 时，应该按可执行程序审查。尤其要看文件、环境变量、进程和网络访问，以及它是否会替用户批准工具调用。企业需要限制这些行为时，可以控制允许加载的 Mod，再通过前层策略拦截 API；有更强隔离要求时，还要落实到运行账户或操作系统环境。&lt;/p>
&lt;p>另外，普通 Hook 出错可能被跳过，worker 出问题也可能导致 Mod 被卸载。一个承担强制安全职责的策略，必须验证这些故障场景下的行为，不能只测试正常情况下是否能拦住请求。&lt;/p>
&lt;h2 id="状态放哪取决于你希望它活多久">状态放哪，取决于你希望它活多久&lt;a class="heading-anchor" href="#%e7%8a%b6%e6%80%81%e6%94%be%e5%93%aa%e5%8f%96%e5%86%b3%e4%ba%8e%e4%bd%a0%e5%b8%8c%e6%9c%9b%e5%ae%83%e6%b4%bb%e5%a4%9a%e4%b9%85" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>一旦扩展开始画界面、统计调用次数，就会遇到状态问题。&lt;/p>
&lt;p>Mods 提供的三个位置很容易记：&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;/td>
&lt;td>当前模块实例；reload 后重建&lt;/td>
&lt;td>临时缓存、短期计算结果&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>$.state&lt;/code>&lt;/td>
&lt;td>当前会话，可跨模块 reload；&lt;code>/clear&lt;/code>、&lt;code>/resume&lt;/code>、&lt;code>/branch&lt;/code> 会重置&lt;/td>
&lt;td>会话状态、界面交互&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>$.store&lt;/code>&lt;/td>
&lt;td>跨会话保存，同一机器上该插件的会话共享&lt;/td>
&lt;td>用户偏好、小型持久缓存&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>$.state&lt;/code> 还有响应式行为：某个界面渲染时读了状态，之后状态改变，会触发相关位置重绘。因此，不必每改一次状态，都手动通知所有界面。&lt;a href="https://code.claude.com/docs/en/plugins/mods/interface">状态与持久化&lt;/a>&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/f2d580e72572e406310ed9c4519c2990.a9c9f9c7284fc442d0128c6b21d3d638d74a5ddd48809cff38d6fb8129f704ba.svg"
alt="响应式状态：读取建立依赖，更新触发相关界面重绘"
loading="lazy"
decoding="async" width="293" height="694">
&lt;figcaption>响应式状态：读取建立依赖，更新触发相关界面重绘&lt;/figcaption>
&lt;/figure>&lt;p>这和 atom、signal 的使用体验接近。但仅凭 API 形状，推不出内部采用了哪个前端状态库。&lt;/p>
&lt;p>使用时还需要把状态写进 &lt;code>PluginState&lt;/code> 类型声明，再由 manifest 的 &lt;code>types&lt;/code> 指向该文件。&lt;code>atom({ plugin: &amp;quot;...&amp;quot;, key: &amp;quot;...&amp;quot; }, 默认值)&lt;/code> 里的名字必须是字面量，校验器才能从源码识别。后面的完整示例会把这几个文件一起列出来。&lt;/p>
&lt;p>会话重置有个特别容易漏掉的细节：&lt;code>/clear&lt;/code>、&lt;code>/resume&lt;/code>、&lt;code>/branch&lt;/code> 会重置 &lt;code>$.state&lt;/code>，但不会重新触发 Mod 的 &lt;code>session.start&lt;/code>。如果状态需要从 &lt;code>$.store&lt;/code> 恢复，还要处理 &lt;code>classic.SessionStart&lt;/code> 的相应 &lt;code>source&lt;/code>，其中 branch 对应 &lt;code>fork&lt;/code>。否则初始化时恢复了一次，用户清空对话后却回到了默认值。&lt;a href="https://code.claude.com/docs/en/plugins/mods/interface#load-a-saved-value-again-after-clear">重置后的状态恢复&lt;/a>&lt;/p>
&lt;p>&lt;code>$.store&lt;/code> 则有一个实际的坑：&lt;strong>读取后再写入，不是原子操作。&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Number&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">store&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">??&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">store&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">count&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两个会话同时读到 &lt;code>10&lt;/code>，各自加一，再分别写回 &lt;code>11&lt;/code>。明明执行了两次加一，最终却只增加一次。&lt;/p>
&lt;p>官方明确记录了这种竞争。因此，把它用于偏好设置和少量缓存很合适；要做跨会话计数、锁或者事务，就该考虑外部数据库或服务了。&lt;a href="https://code.claude.com/docs/en/plugins/mods/interface#save-from-more-than-one-session">多会话写入说明&lt;/a>&lt;/p>
&lt;h2 id="ast-转换交给擅长它的工具">AST 转换，交给擅长它的工具&lt;a class="heading-anchor" href="#ast-%e8%bd%ac%e6%8d%a2%e4%ba%a4%e7%bb%99%e6%93%85%e9%95%bf%e5%ae%83%e7%9a%84%e5%b7%a5%e5%85%b7" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>如果你想把整个仓库从旧 SDK 迁移到新 SDK，Mod 能帮忙组织流程，但它没有内置 &lt;code>$.ast.parse()&lt;/code> 这样的接口。&lt;/p>
&lt;p>当前公开 API 主要处理事件、文本、工具结果和界面树，没有提供语言级 AST 重写引擎。&lt;a href="https://code.claude.com/docs/en/plugins/mods/reference#mods-api-methods">API 总表&lt;/a>&lt;/p>
&lt;p>真正的源码转换，可以交给 &lt;code>jscodeshift&lt;/code>、基于 &lt;code>ts-morph&lt;/code> 的 CLI、编译器或语言服务。Mod 负责选择转换、收集参数、展示进度，再把执行结果带回 Claude Code。&lt;/p>
&lt;p>我会把这类任务拆成下面这样：&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/8481dc3a6270bff720ae0748a6cdfbd0.7c85495cacc13596539acbfae3f6cd09cf698657ca75067dfb5d62144fe15bf3.svg"
alt="建议的代码迁移流程：Mod 组织过程，确定性工具转换与验证源码"
loading="lazy"
decoding="async" width="321" height="892">
&lt;figcaption>建议的代码迁移流程：Mod 组织过程，确定性工具转换与验证源码&lt;/figcaption>
&lt;/figure>&lt;p>这是一种建议的工程流程，不是 Mods 自动提供的功能。预演、回滚和转换规则，都需要由转换器或集成代码实现。&lt;/p>
&lt;p>这样分工后，LLM 可以帮助判断迁移范围、理解失败、审查结果；重复的语法替换交给可测试的程序。转换器最好还满足幂等性：同一份代码跑第二次，不应该又产生一批变化。&lt;/p>
&lt;p>即使不用 AST，也可以用同样的方法选择扩展机制：&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>给 Claude 一套知识或操作流程&lt;/td>
&lt;td>Skill&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>连接外部服务，并供多个客户端使用&lt;/td>
&lt;td>MCP&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>编辑后固定运行一次 formatter&lt;/td>
&lt;td>settings hook&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>批量执行确定的源码变换&lt;/td>
&lt;td>codemod / 编译器工具&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>改 Prompt、工具流程，或增加有状态的界面&lt;/td>
&lt;td>Mod&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>把这些能力一起安装、升级和分发&lt;/td>
&lt;td>Plugin&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>需要跨事件的状态、动态决策和界面交互时，Mod 的价值会更明显。只有一条「编辑后执行格式化」的规则，传统 Hook 通常已经够用。&lt;/p>
&lt;h2 id="把它们串起来做一个改动清单面板">把它们串起来：做一个改动清单面板&lt;a class="heading-anchor" href="#%e6%8a%8a%e5%ae%83%e4%bb%ac%e4%b8%b2%e8%b5%b7%e6%9d%a5%e5%81%9a%e4%b8%80%e4%b8%aa%e6%94%b9%e5%8a%a8%e6%b8%85%e5%8d%95%e9%9d%a2%e6%9d%bf" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>前面分别讲了 Events、API、Interface 和 Gallery。下面用一个小功能把它们接起来：&lt;strong>记录本会话通过 Edit / Write 成功改过的文件，让用户查看，也让 Claude 能查询。&lt;/strong>&lt;/p>
&lt;p>它包含四条路径：&lt;/p>
&lt;figure class="d2-diagram">
&lt;img
src="https://blog.pdjjq.org/d2/520ad977fdb567086615034d654d8d7b.678dc47708d19db8bec739bf4c8c258c648210fb9f25e24725890f6d2d22c564.svg"
alt="同一份会话状态，连接工具事件、用户面板和 Claude 可调用的工具"
loading="lazy"
decoding="async" width="635" height="908">
&lt;figcaption>同一份会话状态，连接工具事件、用户面板和 Claude 可调用的工具&lt;/figcaption>
&lt;/figure>&lt;p>这个例子故意把「展示给人」「提供给模型」「启动下一轮工作」分开。面板中的清单不会自动进入模型上下文；Claude 要通过工具查询，或者用户发送填好的审查请求。&lt;/p>
&lt;h3 id="文件结构与声明">文件结构与声明&lt;a class="heading-anchor" href="#%e6%96%87%e4%bb%b6%e7%bb%93%e6%9e%84%e4%b8%8e%e5%a3%b0%e6%98%8e" aria-label="本节链接">#&lt;/a>
&lt;/h3>&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">change-desk/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── .claude-plugin/plugin.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── hooks/hooks.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── hooks/register.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── types/index.d.ts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── tests/register.test.ts
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.claude-plugin/plugin.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;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&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;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.1.0&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;author&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;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;DJJ&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;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;查看当前会话通过 Edit/Write 改过的文件&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;types&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;./types/index.d.ts&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>hooks/hooks.json&lt;/code> 指向入口，路径相对于这个 JSON 文件：&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;modules&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;./register.ts&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>types/index.d.ts&lt;/code> 声明本插件的会话状态：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">declare&lt;/span> &lt;span class="nx">module&lt;/span> &lt;span class="s2">&amp;#34;claude-code&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">interface&lt;/span> &lt;span class="nx">PluginState&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;change-desk&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">files&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">[]&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="事件工具与界面放进同一个模块">事件、工具与界面放进同一个模块&lt;a class="heading-anchor" href="#%e4%ba%8b%e4%bb%b6%e5%b7%a5%e5%85%b7%e4%b8%8e%e7%95%8c%e9%9d%a2%e6%94%be%e8%bf%9b%e5%90%8c%e4%b8%80%e4%b8%aa%e6%a8%a1%e5%9d%97" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>下面是完整的 &lt;code>hooks/register.ts&lt;/code>。留意三处连接：成功的工具调用写入状态，面板读取状态建立依赖，按钮点击时再读取最新清单。代码没有在 &lt;code>ui.render&lt;/code> 中写状态，也不用手动 invalidate。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">atom&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">read&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">update&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s2">&amp;#34;claude-code&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="kr">const&lt;/span> &lt;span class="nx">files&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">atom&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">plugin&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;files&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">register&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;session.start&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">register&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="o">:&lt;/span> &lt;span class="s2">&amp;#34;changed_files&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">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;列出当前会话通过 Edit/Write 成功修改过的文件；不是 Git diff&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">inputSchema&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">properties&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{},&lt;/span> &lt;span class="nx">additionalProperties&lt;/span>: &lt;span class="kt">false&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">command&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">register&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="o">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&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">description&lt;/span>&lt;span class="o">:&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="nx">immediate&lt;/span>: &lt;span class="kt">true&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="k">return&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tool.call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Edit&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Write&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">deny&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">isError&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">files&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">old&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">old&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">includes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">file_path&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">old&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="p">[...&lt;/span>&lt;span class="nx">old&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">file_path&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="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="k">return&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tool.call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;mcp__change-desk__changed_files&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">result&lt;/span>: &lt;span class="kt">JSON.stringify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">await&lt;/span> &lt;span class="nx">read&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">files&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&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;command.run&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">command&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">open&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="o">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;本轮会话的改动&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">focus&lt;/span>: &lt;span class="kt">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">closeOnEscape&lt;/span>: &lt;span class="kt">true&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="k">return&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;ui.render&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">component&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Pane&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">requestId&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s2">&amp;#34;change-desk&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&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="p">{&lt;/span> &lt;span class="nx">Box&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Text&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Button&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&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">current&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">read&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">files&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">return&lt;/span> &lt;span class="nx">Box&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">flexDirection&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;column&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">gap&lt;/span>: &lt;span class="kt">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">children&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Text&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">children&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="sb">`已记录 &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">current&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> 个文件`&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">current&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">path&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="nx">Text&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">children&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">path&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">current&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">20&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">Text&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">children&lt;/span>&lt;span class="o">:&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="p">})]&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="p">[]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">...(&lt;/span>&lt;span class="nx">current&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">Button&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="o">:&lt;/span> &lt;span class="s2">&amp;#34;review&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">label&lt;/span>&lt;span class="o">:&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="nx">onPress&lt;/span>: &lt;span class="kt">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">latest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">read&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">files&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">filled&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">prompt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">text&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;\n请审查这些文件的改动：\n&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">latest&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;\n&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">mode&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;append&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">filled&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">isFilled&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">$&lt;/span>&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="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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})]&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当 &lt;code>Edit / Write&lt;/code> 成功返回，清单去重后更新，已打开的面板自动重绘。&lt;code>changed_files&lt;/code> 工具则直接返回同一份状态，不需要再扫一遍仓库。&lt;/p>
&lt;p>这里的「改动清单」有明确范围：只统计这两个工具成功返回的文件，不代表 Git 当前 diff，也不会自动发现 Bash、外部编辑器或其他工具造成的改动。同一文件后来被恢复，也仍会留在清单中。若产品需要的是「当前未提交差异」，应该像官方 &lt;code>diff&lt;/code> Mod 一样查询 Git。&lt;/p>
&lt;p>清单放在 &lt;code>$.state&lt;/code>，所以模块热加载后仍然保留，会话清空或切换后重置。这正好符合它作为当前会话辅助信息的用途，无需持久化到所有会话共享的 store。&lt;/p>
&lt;h3 id="测试不只看返回值还要真的驱动按钮">测试不只看返回值，还要真的驱动按钮&lt;a class="heading-anchor" href="#%e6%b5%8b%e8%af%95%e4%b8%8d%e5%8f%aa%e7%9c%8b%e8%bf%94%e5%9b%9e%e5%80%bc%e8%bf%98%e8%a6%81%e7%9c%9f%e7%9a%84%e9%a9%b1%e5%8a%a8%e6%8c%89%e9%92%ae" aria-label="本节链接">#&lt;/a>
&lt;/h3>&lt;p>&lt;code>tests/register.test.ts&lt;/code> 覆盖去重、失败分支、入口注册，并在 &lt;code>terminal&lt;/code> 和 &lt;code>desktop&lt;/code> 上分别挂载界面、观察重绘、点击按钮。&lt;code>ui.mount&lt;/code> 测的是元素树和交互协议，不是终端或 Desktop 的像素截图。&lt;a href="https://code.claude.com/docs/en/plugins/mods/test">界面测试方法&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">test&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s2">&amp;#34;claude-code/testing&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="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">surface&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;terminal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;desktop&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="kr">as&lt;/span> &lt;span class="kr">const&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">surface&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">: 成功编辑刷新面板，点击按钮只填写草稿`&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">on&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tool.call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;ok&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="kd">let&lt;/span> &lt;span class="nx">filledText&lt;/span> &lt;span class="o">=&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">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;prompt.fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">filledText&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">text&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="nx">isFilled&lt;/span>: &lt;span class="kt">true&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mount&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">plugin&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">surface&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">component&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Pane&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">requestId&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&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">props&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Changes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">isFocused&lt;/span>: &lt;span class="kt">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">bodyColumns&lt;/span>: &lt;span class="kt">60&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">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">await&lt;/span> &lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">text&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;已记录 0 个文件&amp;#34;&lt;/span> &lt;span class="p">})).&lt;/span>&lt;span class="nx">toBeDefined&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Write&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">file_path&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;src/app.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;example&amp;#34;&lt;/span> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Write&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">file_path&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;src/app.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;example 2&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">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">await&lt;/span> &lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">text&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;已记录 1 个文件&amp;#34;&lt;/span> &lt;span class="p">})).&lt;/span>&lt;span class="nx">toBeDefined&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">list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;mcp__change-desk__changed_files&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">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">list&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;[&amp;#34;src/app.ts&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">press&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;review&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">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filledText&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;\n请审查这些文件的改动：\nsrc/app.ts&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">ui&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">unmount&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;被拒绝或失败的调用不计入清单&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">on&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tool.call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">file_path&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s2">&amp;#34;denied.ts&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">deny&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;blocked&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;failed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">isError&lt;/span>: &lt;span class="kt">true&lt;/span> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Write&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">file_path&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;denied.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;example&amp;#34;&lt;/span> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Write&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">file_path&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;failed.ts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;example&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">list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">tool&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;mcp__change-desk__changed_files&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">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">list&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&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="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;启动时注册入口，命令打开自己的面板&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">on&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;session.start&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="nx">e&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">registered&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">[]&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="kd">let&lt;/span> &lt;span class="nx">opened&lt;/span> &lt;span class="o">=&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">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tool.register&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">registered&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&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="nx">value&lt;/span>: &lt;span class="kt">undefined&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="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;command.register&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">registered&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&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="nx">value&lt;/span>: &lt;span class="kt">undefined&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="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;ui.open&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">opened&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&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="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">isPlaced&lt;/span>: &lt;span class="kt">true&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">start&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">cwd&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;/example-repo&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">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">registered&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toEqual&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s2">&amp;#34;changed_files&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;change-desk&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">$&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">command&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">run&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">command&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;change-desk&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="o">:&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="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">opened&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;change-desk&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>测试中的工具和输入框由 stub 回答，因此不会改真实文件，也不会启动模型请求。面板的按钮回调、状态更新和事件链则会实际运行。这里的 &lt;code>on&lt;/code> stub 也接收 &lt;code>($, e)&lt;/code>，不要把第一个参数误当成事件数据。&lt;/p>
&lt;p>在 &lt;code>change-desk&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">claude plugin validate --strict ./change-desk
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">claude plugin &lt;span class="nb">test&lt;/span> ./change-desk
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">claude --debug-file ./mod-debug.log --plugin-dir ./change-desk
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>前两个命令检查结构和行为；最后一个用于进入实际会话，执行 &lt;code>/change-desk&lt;/code> 检查显示与键盘操作。上面的完整代码已在 Claude Code &lt;strong>v2.1.289&lt;/strong> 通过严格校验和 &lt;strong>4 项测试&lt;/strong>。使用 &lt;code>--plugin-dir&lt;/code> 开发时可以热加载；加载失败会保留上一个可工作版本。&lt;a href="https://code.claude.com/docs/en/plugins/mods/troubleshoot">调试与热加载&lt;/a>&lt;/p>
&lt;h2 id="写到实际项目里我会注意这些事">写到实际项目里，我会注意这些事&lt;a class="heading-anchor" href="#%e5%86%99%e5%88%b0%e5%ae%9e%e9%99%85%e9%a1%b9%e7%9b%ae%e9%87%8c%e6%88%91%e4%bc%9a%e6%b3%a8%e6%84%8f%e8%bf%99%e4%ba%9b%e4%ba%8b" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>&lt;strong>让 Hook 保持轻量。&lt;/strong> 普通 Hook 的自身执行预算是 10 秒，&lt;code>prompt.edit&lt;/code> 只有 50 毫秒。这里的「自身执行」不包含等待 &lt;code>next&lt;/code> 和多数 Mods API 的时间，但 &lt;code>$.clock.sleep&lt;/code> 是例外。耗时 I/O 可以异步等待，大量 CPU 计算应交给外部程序；长任务还要处理 &lt;code>next.signal&lt;/code> 的取消信号。&lt;a href="https://code.claude.com/docs/en/plugins/mods/reference#limits">时间与容量限制&lt;/a>&lt;/p>
&lt;p>&lt;strong>不要让持久化状态无限增长。&lt;/strong> 当前 &lt;code>$.store&lt;/code> 总容量是 4 MiB；&lt;code>$.fs.read/write&lt;/code> 单文件也有限制。它们适合扩展日常工作，不适合直接承担大型数据处理。普通 JSON store 也不应该被当成专门的密钥库。&lt;/p>
&lt;p>&lt;strong>把高频路径上的模型调用算清楚。&lt;/strong> 每次用户输入、每次工具调用都额外请求一次模型，会带来成本和延迟。能从事件数据确定的事，就直接算；确实需要模型判断时，再选择合适的触发时机。修改 Prompt 时，也尽量避免反复塞入无用的时间戳和随机内容。&lt;/p>
&lt;p>&lt;strong>升级时同时检查 Claude Code 和 Plugin。&lt;/strong> Mod 没有独立的版本系统，代码随 Plugin 发布。Plugin 支持依赖版本范围，但范围约束不等于锁死到某个版本；需要复现的团队应保留经过验证的版本组合。&lt;a href="https://code.claude.com/docs/en/plugins/dependencies">依赖机制&lt;/a>&lt;/p>
&lt;p>CI 可以从 &lt;code>claude plugin validate&lt;/code> 和 &lt;code>claude plugin test&lt;/code> 开始。涉及真实文件、外部进程和源码转换时，再补相应的集成测试，以及转换前后样例、类型检查和幂等性检查。通过模拟测试，只能说明事件逻辑符合预期，不能替代真实环境验证。&lt;/p>
&lt;h2 id="值得关注的是agent-的执行过程开始开放了">值得关注的是，Agent 的执行过程开始开放了&lt;a class="heading-anchor" href="#%e5%80%bc%e5%be%97%e5%85%b3%e6%b3%a8%e7%9a%84%e6%98%afagent-%e7%9a%84%e6%89%a7%e8%a1%8c%e8%bf%87%e7%a8%8b%e5%bc%80%e5%a7%8b%e5%bc%80%e6%94%be%e4%ba%86" aria-label="本节链接">#&lt;/a>
&lt;/h2>&lt;p>想继续研究，可以先看 Anthropic 已公开的两个例子：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/anthropics/claude-code/tree/main/mods/diff">&lt;code>diff&lt;/code>&lt;/a>：观察如何把工具事件、文件状态和界面连接起来。&lt;/li>
&lt;li>&lt;a href="https://github.com/anthropics/claude-code/tree/main/mods/sec-default">&lt;code>sec-default&lt;/code>&lt;/a>：观察策略怎样介入其他 Mod，以及受管理配置怎样受到保护。&lt;/li>
&lt;/ul>
&lt;p>它们分别展示了日常功能和策略控制这两种用途。对我来说，这比再列几十个 API 名字更能说明 Mods 的价值。&lt;/p>
&lt;p>过去，定制 Agent 往往集中在 Prompt 和工具列表。现在，输入怎样进入模型、工具怎样被调用、结果怎样展示，这些过程也开始成为可编程的接口。&lt;/p>
&lt;p>我更期待的是它和现有工具的组合：Skill 承载知识，MCP 连接外部系统，确定性的程序负责转换和验证，Mod 把这些能力接入 Agent 的工作过程，再用 Plugin 一起分发。&lt;/p>
&lt;p>每一部分都有清楚的职责，整个扩展才更容易理解、测试，也更容易在出问题时找到原因。&lt;/p></description></item></channel></rss>