<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>工具 - 标签 - 豆浆小屋</title><link>https://soybeanmilk.netlify.app/tags/%E5%B7%A5%E5%85%B7/</link><description>工具 - 标签 - 豆浆小屋</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><copyright>This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.</copyright><lastBuildDate>Fri, 07 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://soybeanmilk.netlify.app/tags/%E5%B7%A5%E5%85%B7/" rel="self" type="application/rss+xml"/><item><title>插件机制的本质：扩展点 + 回调</title><link>https://soybeanmilk.netlify.app/%E6%8F%92%E4%BB%B6%E6%9C%BA%E5%88%B6%E7%9A%84%E6%9C%AC%E8%B4%A8/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><author><name>作者</name></author><guid>https://soybeanmilk.netlify.app/%E6%8F%92%E4%BB%B6%E6%9C%BA%E5%88%B6%E7%9A%84%E6%9C%AC%E8%B4%A8/</guid><description><![CDATA[<h2 id="简要回答" class="headerLink">
    <a href="#%e7%ae%80%e8%a6%81%e5%9b%9e%e7%ad%94" class="header-mark"></a>1 简要回答</h2><p>所有插件机制本质相同：<strong>宿主留扩展点（接口），插件注册回调函数，宿主在关键时刻调用回调</strong>。配置文件覆盖、Obsidian 插件、游戏 Mod 全是这个套路。</p>
<h2 id="详细回答" class="headerLink">
    <a href="#%e8%af%a6%e7%bb%86%e5%9b%9e%e7%ad%94" class="header-mark"></a>2 详细回答</h2><h3 id="1-配置覆盖原理" class="headerLink">
    <a href="#1-%e9%85%8d%e7%bd%ae%e8%a6%86%e7%9b%96%e5%8e%9f%e7%90%86" class="header-mark"></a>2.1 1. 配置覆盖原理</h3><p>以 opencode 模型名显示为例：opencode 启动时读取配置并与模型数据合并（数据在 <code>~/.cache/opencode/models.json</code>），选择器渲染时取合并后的 <code>model.name</code>。所以在 <code>opencode.jsonc</code> 里写：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;provider&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;opencode-go&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;models&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;deepseek-v4-flash&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;...价格...&#34;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>就能覆盖显示名。改配置 → 改数据 → UI 自动变，没碰任何程序代码。</p>
<h3 id="2-插件机制的通用四步" class="headerLink">
    <a href="#2-%e6%8f%92%e4%bb%b6%e6%9c%ba%e5%88%b6%e7%9a%84%e9%80%9a%e7%94%a8%e5%9b%9b%e6%ad%a5" class="header-mark"></a>2.2 2. 插件机制的通用四步</h3><ol>
<li><strong>扩展点</strong>：宿主在关键流程留接口（消息发送前、工具执行后、模型显示时…）</li>
<li><strong>注册</strong>：插件把函数注册到挂钩点（宿主保存回调列表，不写死行为）</li>
<li><strong>调用</strong>：宿主在合适时机遍历回调列表依次调用</li>
<li><strong>公开 API</strong>：插件只能通过宿主开放的 API 交互，拿不到内部实现</li>
</ol>
<h3 id="3-c-类比" class="headerLink">
    <a href="#3-c-%e7%b1%bb%e6%af%94" class="header-mark"></a>2.3 3. C++ 类比</h3><ul>
<li>接口 + 虚函数：<code>class IPlugin { virtual void onLoad() = 0; }</code>，插件实现接口</li>
<li>Windows：DLL 动态加载（<code>LoadLibrary</code>）</li>
<li>设计模式：观察者模式 / 回调函数指针</li>
</ul>
<h3 id="4-插件的写法" class="headerLink">
    <a href="#4-%e6%8f%92%e4%bb%b6%e7%9a%84%e5%86%99%e6%b3%95" class="header-mark"></a>2.4 4. 插件的写法</h3><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">client</span><span class="p">,</span> <span class="nx">project</span><span class="p">,</span> <span class="nx">$</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">config</span><span class="o">:</span> <span class="p">(</span><span class="nx">cfg</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="cm">/* 配置加载时 */</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;tool.execute.before&#34;</span><span class="o">:</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">input</span><span class="p">,</span> <span class="nx">output</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="cm">/* 工具执行前 */</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;chat.message&#34;</span><span class="o">:</span> <span class="p">(</span><span class="nx">input</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="cm">/* 每来一条消息 */</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">event</span><span class="o">:</span> <span class="p">(</span><span class="nx">input</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="cm">/* 所有事件 */</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>加载：<code>plugin: [&quot;npm包名&quot;, &quot;./本地.ts&quot;]</code>，或放 <code>.opencode/plugin/</code> 自动发现。宿主启动时调用导出函数拿到 hooks，之后事件发生时调用对应 hook。</p>
<h3 id="5-对照obsidian-插件" class="headerLink">
    <a href="#5-%e5%af%b9%e7%85%a7obsidian-%e6%8f%92%e4%bb%b6" class="header-mark"></a>2.5 5. 对照：Obsidian 插件</h3><p><code>manifest.json</code> 声明 → Obsidian 扫描插件目录 → 实例化 <code>class extends Plugin</code> 并调用 <code>onLoad()</code> → 在 onLoad 里注册 ribbon 图标、状态栏、命令。和 <code>export default function</code> 是同一件事的两种写法：<strong>宿主调用插件注册的回调</strong>。</p>
<h2 id="知识扩展" class="headerLink">
    <a href="#%e7%9f%a5%e8%af%86%e6%89%a9%e5%b1%95" class="header-mark"></a>3 知识扩展</h2><ul>
<li>事件总线：宿主发布事件，插件订阅（<code>event</code> hook 就是这个）</li>
<li>生命周期：发现插件 → 加载模块 → 拿 hooks → 绑定事件 → 卸载时清理</li>
<li>宿主不直接引用插件类型，通过约定的接口/函数签名解耦——插件和插件之间互不感知</li>
</ul>
]]></description></item></channel></rss>