<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>数字旗手</title>
  
  <subtitle>数字化、智能化、智能体化</subtitle>
  <link href="http://qixinbo.github.io/atom.xml" rel="self"/>
  
  <link href="http://qixinbo.github.io/"/>
  <updated>2026-04-12T14:29:51.901Z</updated>
  <id>http://qixinbo.github.io/</id>
  
  <author>
    <name>Xin-Bo Qi(亓欣波)</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>跟着☤Hermes Agent学AI智能体设计和开发</title>
    <link href="http://qixinbo.github.io/2026/04/12/hermes-0/"/>
    <id>http://qixinbo.github.io/2026/04/12/hermes-0/</id>
    <published>2026-04-12T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.901Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Hermes-Agent-核心教程：从零开始开发强大的-Python-AI-应用"><a href="#Hermes-Agent-核心教程：从零开始开发强大的-Python-AI-应用" class="headerlink" title="Hermes Agent 核心教程：从零开始开发强大的 Python AI 应用"></a>Hermes Agent 核心教程：从零开始开发强大的 Python AI 应用</h1><blockquote><p><strong>写在前面：关于 Hermes Agent</strong></p><p>Hermes Agent 是由知名 AI 研究机构 <strong>Nous Research</strong> 开发的一款具备高度自我进化能力的开源智能体系统。它旨在打破传统大模型“阅后即焚”的单一对话框限制，让 AI 真正“生活”在开发者的终端、团队的通讯软件以及自动化的流水线中。</p><p><strong>核心亮点：</strong></p><ul><li><strong>闭环学习与长期记忆</strong>：它不仅能执行任务，还能在跨会话中持久化记忆，甚至从经验中自主创建和优化专属技能。</li><li><strong>全平台无缝接入</strong>：内置强大的消息网关，一套代码即可接入 CLI、Telegram、Discord、Slack 等平台，实现跨设备状态同步。</li><li><strong>模型无关与热切换</strong>：不被单一厂商锁定，支持随时无缝切换底层大模型（如 OpenAI、Anthropic、本地大模型等）。</li><li><strong>多智能体协作与沙盒隔离</strong>：支持生成隔离的子智能体（Subagents）并行处理复杂任务，并在 Docker、SSH 等多种安全沙盒中执行代码。</li></ul><p>本教程将带你由浅入深，掌握如何将其作为 Python 库使用，打造你自己的强大 AI 应用。</p></blockquote><hr><h2 id="1-环境准备"><a href="#1-环境准备" class="headerlink" title="1. 环境准备"></a>1. 环境准备</h2><p>在开始之前，请确保你已经激活了项目的虚拟环境。</p><p>对于配置模型，<strong>Hermes Agent 采用了全新的配置管理机制：</strong></p><blockquote><p>⚠️ <strong>重要提示：</strong><br>过去很多 AI 工具喜欢通过终端 <code>export OPENAI_BASE_URL</code> 或在 <code>.env</code> 中设置 <code>LLM_MODEL</code> 等环境变量来控制模型。但在 Hermes Agent 最新版本中，<strong>这些旧的环境变量已被完全废弃，即使在终端 export 也不会生效！</strong><br>Hermes 的核心配置来源是统一的配置文件：<code>~/.hermes/config.yaml</code>（或者通过代码显式传参）。</p></blockquote><p><strong>配置第三方中转 API（Custom Provider）的正确姿势：</strong></p><p>如果你希望全局使用第三方中转站（使得 CLI 和代码调用都默认生效），最推荐的做法是直接修改 <code>~/.hermes/config.yaml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在 ~/.hermes/config.yaml 中配置：</span></span><br><span class="line"><span class="attr">model:</span></span><br><span class="line">  <span class="attr">default:</span> <span class="string">gpt-5.4-mini</span>                       <span class="comment"># 你的模型名称</span></span><br><span class="line">  <span class="attr">provider:</span> <span class="string">custom</span>                            <span class="comment"># 必须声明 provider 为 custom</span></span><br><span class="line">  <span class="attr">base_url:</span> <span class="string">https://api.your-proxy.com/v1</span>     <span class="comment"># 中转站的 Base URL</span></span><br><span class="line">  <span class="attr">api_key:</span> <span class="string">your-api-key-here</span>                  <span class="comment"># 中转站的 API Key</span></span><br></pre></td></tr></table></figure><p>或者使用交互式命令自动配置：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">hermes model</span><br><span class="line"><span class="comment"># 选择 &quot;Custom endpoint (self-hosted / VLLM / etc.)&quot;</span></span><br><span class="line"><span class="comment"># 按照提示输入 Base URL、API Key 和模型名称即可。</span></span><br></pre></td></tr></table></figure></p><p>当然，你也可以完全在代码中动态覆盖这些配置（见下一节）。</p><hr><h2 id="2-基础篇：最简单的单次对话-Single-turn-Chat"><a href="#2-基础篇：最简单的单次对话-Single-turn-Chat" class="headerlink" title="2. 基础篇：最简单的单次对话 (Single-turn Chat)"></a>2. 基础篇：最简单的单次对话 (Single-turn Chat)</h2><p>使用 Hermes 最简单的方法是调用 <code>chat()</code> 方法 —— 传入一条消息，返回一段字符串文本。它会在内部自动处理完整的对话循环（包括工具调用、重试等）。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="comment"># 初始化 Agent</span></span><br><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,        <span class="comment"># 指定模型</span></span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,           <span class="comment"># 明确指定为自定义端点</span></span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>, <span class="comment"># 代码中显式指定第三方中转站</span></span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>, <span class="comment"># 代码中显式指定 API Key</span></span><br><span class="line">    quiet_mode=<span class="literal">True</span>,             <span class="comment"># 开启静默模式，不在控制台打印 CLI 动画和终端输出</span></span><br><span class="line">    skip_memory=<span class="literal">True</span>,            <span class="comment"># 不持久化对话历史</span></span><br><span class="line">    skip_context_files=<span class="literal">True</span>      <span class="comment"># 不读取默认的 AGENTS.md 上下文文件</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 chat 方法进行简单对话</span></span><br><span class="line">response = agent.chat(<span class="string">&quot;用一句话解释一下什么是 Python 装饰器？&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;AI 回复:&quot;</span>, response)</span><br></pre></td></tr></table></figure><blockquote><p><strong>⚠️ 注意：</strong> 将 Hermes 嵌入到你自己的代码中时，请务必设置 <code>quiet_mode=True</code>。否则，Agent 会打印 CLI 的加载动画和进度指示器，这会扰乱你的应用输出。</p></blockquote><hr><h2 id="3-进阶篇：完全对话控制与多轮对话-Full-Conversation-Control"><a href="#3-进阶篇：完全对话控制与多轮对话-Full-Conversation-Control" class="headerlink" title="3. 进阶篇：完全对话控制与多轮对话 (Full Conversation Control)"></a>3. 进阶篇：完全对话控制与多轮对话 (Full Conversation Control)</h2><p>如果需要对对话有更多的控制权，请直接使用 <code>run_conversation()</code>。它不仅返回最终的文本回复，还会返回完整的消息历史记录和元数据字典。</p><h3 id="3-1-获取完整上下文与自定义-System-Prompt"><a href="#3-1-获取完整上下文与自定义-System-Prompt" class="headerlink" title="3.1 获取完整上下文与自定义 System Prompt"></a>3.1 获取完整上下文与自定义 System Prompt</h3><p>你可以通过 <code>run_conversation()</code> 传入临时的 <code>system_message</code>，覆盖该次调用的系统提示词：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    quiet_mode=<span class="literal">True</span>,</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">result = agent.run_conversation(</span><br><span class="line">    user_message=<span class="string">&quot;解释一下什么是快速排序&quot;</span>,</span><br><span class="line">    system_message=<span class="string">&quot;你是一个计算机科学导师。请使用生活中的简单比喻来解释。&quot;</span>,</span><br><span class="line">    task_id=<span class="string">&quot;my-task-1&quot;</span>, <span class="comment"># 用于环境隔离的任务 ID</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;最终回复:&quot;</span>, result[<span class="string">&quot;final_response&quot;</span>])</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;总共交互的消息数: <span class="subst">&#123;<span class="built_in">len</span>(result[<span class="string">&#x27;messages&#x27;</span>])&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><p>返回的字典包含：</p><ul><li><strong><code>final_response</code></strong> — Agent 的最终文本回复</li><li><strong><code>messages</code></strong> — 完整的消息历史（包含 system, user, assistant, tool calls）</li><li><strong><code>task_id</code></strong> — 用于虚拟机隔离的任务标识符</li></ul><h3 id="3-2-保持多轮对话状态与持久化机制"><a href="#3-2-保持多轮对话状态与持久化机制" class="headerlink" title="3.2 保持多轮对话状态与持久化机制"></a>3.2 保持多轮对话状态与持久化机制</h3><p>Hermes 的对话历史状态保存有两种模式：</p><h4 id="模式-A：纯内存传递（适合无状态-API）"><a href="#模式-A：纯内存传递（适合无状态-API）" class="headerlink" title="模式 A：纯内存传递（适合无状态 API）"></a>模式 A：纯内存传递（适合无状态 API）</h4><p>要在多个回合之间在纯内存中保持对话状态，只需将上一次返回的 <code>messages</code> 历史记录重新传入 <code>conversation_history</code> 参数即可：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    quiet_mode=<span class="literal">True</span>,</span><br><span class="line">    skip_memory=<span class="literal">True</span>, <span class="comment"># 关闭自动持久化</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 第一轮对话</span></span><br><span class="line">result1 = agent.run_conversation(<span class="string">&quot;你好，我叫 Alice，我最喜欢的颜色是蓝色。&quot;</span>)</span><br><span class="line">history = result1[<span class="string">&quot;messages&quot;</span>]</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;第一轮回复:&quot;</span>, result1[<span class="string">&quot;final_response&quot;</span>])</span><br><span class="line"></span><br><span class="line"><span class="comment"># 第二轮对话 —— Agent 能从 history 中回忆起上下文</span></span><br><span class="line">result2 = agent.run_conversation(</span><br><span class="line">    <span class="string">&quot;你还记得我叫什么名字，喜欢什么颜色吗？&quot;</span>,</span><br><span class="line">    conversation_history=history,</span><br><span class="line">)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;第二轮回复:&quot;</span>, result2[<span class="string">&quot;final_response&quot;</span>]) <span class="comment"># &quot;你叫 Alice，喜欢蓝色。&quot;</span></span><br></pre></td></tr></table></figure><h4 id="模式-B：SQLite-自动持久化存储（适合-CLI-聊天机器人）"><a href="#模式-B：SQLite-自动持久化存储（适合-CLI-聊天机器人）" class="headerlink" title="模式 B：SQLite 自动持久化存储（适合 CLI/聊天机器人）"></a>模式 B：SQLite 自动持久化存储（适合 CLI/聊天机器人）</h4><p>如果你想让对话记录<strong>持久化到本地硬盘</strong>，即使程序重启也能继续聊天，你只需要在初始化 <code>AIAgent</code> 时传入一个固定的 <code>session_id</code>。</p><p>Hermes 底层使用了 <code>hermes_state.py</code> 提供的 <code>SessionDB</code>。它会将所有的对话消息自动存入 <code>~/.hermes/state.db</code> (SQLite 数据库) 中。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="comment"># 初始化时设定 session_id，并确保没有设置 skip_memory=True</span></span><br><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    quiet_mode=<span class="literal">True</span>,</span><br><span class="line">    session_id=<span class="string">&quot;my_tutorial_session&quot;</span>,  <span class="comment"># 关键：指定会话 ID</span></span><br><span class="line">    <span class="comment"># skip_memory=False, # 默认就是 False，允许读写 SQLite</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只要指定了 session_id，AIAgent 会在每次对话后自动将历史追加到 SQLite 数据库中。</span></span><br><span class="line">res1 = agent.run_conversation(<span class="string">&quot;我最喜欢的水果是苹果。&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;回复:&quot;</span>, res1[<span class="string">&quot;final_response&quot;</span>])</span><br><span class="line"></span><br><span class="line"><span class="comment"># --- 假设在这里程序崩溃重启 ---</span></span><br><span class="line"><span class="comment"># 下次运行同样的脚本，传入相同的 session_id，Hermes 会自动从 SQLite 中加载历史记录</span></span><br><span class="line">res2 = agent.run_conversation(<span class="string">&quot;我最喜欢的水果是什么？&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;回复:&quot;</span>, res2[<span class="string">&quot;final_response&quot;</span>]) <span class="comment"># &quot;你最喜欢的水果是苹果。&quot;</span></span><br></pre></td></tr></table></figure><hr><h2 id="4-配置篇：工具权限与运行轨迹"><a href="#4-配置篇：工具权限与运行轨迹" class="headerlink" title="4. 配置篇：工具权限与运行轨迹"></a>4. 配置篇：工具权限与运行轨迹</h2><h3 id="4-1-开启或禁用工具-Configuring-Tools"><a href="#4-1-开启或禁用工具-Configuring-Tools" class="headerlink" title="4.1 开启或禁用工具 (Configuring Tools)"></a>4.1 开启或禁用工具 (Configuring Tools)</h3><p>你可以通过 <code>enabled_toolsets</code>（白名单）或 <code>disabled_toolsets</code>（黑名单）来控制 Agent 有权使用哪些工具集：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 仅启用网络工具（浏览、搜索），适合做纯粹的“研究助手”</span></span><br><span class="line">agent_web_only = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    enabled_toolsets=[<span class="string">&quot;web&quot;</span>],</span><br><span class="line">    quiet_mode=<span class="literal">True</span>,</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启用所有工具，但禁用终端执行权限（适合在不安全的共享环境中运行）</span></span><br><span class="line">agent_no_terminal = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    disabled_toolsets=[<span class="string">&quot;terminal&quot;</span>],</span><br><span class="line">    quiet_mode=<span class="literal">True</span>,</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="4-2-保存对话轨迹-Saving-Trajectories-与临时人设"><a href="#4-2-保存对话轨迹-Saving-Trajectories-与临时人设" class="headerlink" title="4.2 保存对话轨迹 (Saving Trajectories)与临时人设"></a>4.2 保存对话轨迹 (Saving Trajectories)与临时人设</h3><p>如果你需要收集数据用于模型微调，可以开启轨迹保存。同时，使用 <code>ephemeral_system_prompt</code> 可以设定专属人设，且该设定<strong>不会</strong>污染保存的轨迹数据：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    ephemeral_system_prompt=<span class="string">&quot;你是一个 SQL 专家。只回答与数据库相关的问题。&quot;</span>,</span><br><span class="line">    save_trajectories=<span class="literal">True</span>, <span class="comment"># 开启后，对话将以 ShareGPT 格式保存到 trajectory_samples.jsonl</span></span><br><span class="line">    quiet_mode=<span class="literal">True</span>,</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">response = agent.chat(<span class="string">&quot;我该怎么写一个 JOIN 查询？&quot;</span>)</span><br></pre></td></tr></table></figure><hr><h2 id="5-架构篇：并发场景下的多用户隔离机制"><a href="#5-架构篇：并发场景下的多用户隔离机制" class="headerlink" title="5. 架构篇：并发场景下的多用户隔离机制"></a>5. 架构篇：并发场景下的多用户隔离机制</h2><p>当你准备将 Hermes Agent 接入 FastAPI、Discord Bot 或 Telegram 机器人时，<strong>多用户隔离</strong>是绕不开的话题。</p><p>Hermes 提供了两个层级的多用户隔离方案：</p><h3 id="5-1-代码层级的软隔离-Session-ID"><a href="#5-1-代码层级的软隔离-Session-ID" class="headerlink" title="5.1 代码层级的软隔离 (Session ID)"></a>5.1 代码层级的软隔离 (Session ID)</h3><p>在同一个 Python 进程中，如果你只是希望让模型能够分别记住不同用户的对话，你只需要为不同的用户请求分配不同的 <code>session_id</code>。</p><p>⚠️ <strong>核心规则：</strong> <code>AIAgent</code> 实例维护着自身的对话历史、工具会话和迭代计数器，<strong>它不是线程安全的</strong>。因此，<strong>必须为每个并发任务或每个用户请求创建一个新的 <code>AIAgent</code> 实例</strong>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> concurrent.futures</span><br><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">handle_user_message</span>(<span class="params">user_id: <span class="built_in">str</span>, message: <span class="built_in">str</span></span>):</span></span><br><span class="line">    <span class="comment"># 为每个并发请求实例化独立的 AIAgent 以保证线程安全</span></span><br><span class="line">    agent = AIAgent(</span><br><span class="line">        model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">        provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">        base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">        api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">        session_id=<span class="string">f&quot;user_session_<span class="subst">&#123;user_id&#125;</span>&quot;</span>,  <span class="comment"># 动态 session_id 隔离不同用户记忆</span></span><br><span class="line">        quiet_mode=<span class="literal">True</span></span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">return</span> agent.chat(message)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 模拟多用户并发请求</span></span><br><span class="line">users_requests = &#123;</span><br><span class="line">    <span class="string">&quot;u1001&quot;</span>: <span class="string">&quot;你好，我是用户A&quot;</span>,</span><br><span class="line">    <span class="string">&quot;u1002&quot;</span>: <span class="string">&quot;你好，我是用户B&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> concurrent.futures.ThreadPoolExecutor(max_workers=<span class="number">2</span>) <span class="keyword">as</span> executor:</span><br><span class="line">    <span class="comment"># 并发执行</span></span><br><span class="line">    futures = &#123;</span><br><span class="line">        executor.submit(handle_user_message, uid, msg): uid </span><br><span class="line">        <span class="keyword">for</span> uid, msg <span class="keyword">in</span> users_requests.items()</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> future <span class="keyword">in</span> concurrent.futures.as_completed(futures):</span><br><span class="line">        uid = futures[future]</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;用户 <span class="subst">&#123;uid&#125;</span> 的专属回复: <span class="subst">&#123;future.result()&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="5-2-物理层级的硬隔离-Profiles"><a href="#5-2-物理层级的硬隔离-Profiles" class="headerlink" title="5.2 物理层级的硬隔离 (Profiles)"></a>5.2 物理层级的硬隔离 (Profiles)</h3><p>如果你不仅需要隔离对话历史，还需要<strong>为不同用户配置完全不同的 API Key、不同的模型、不同的系统提示词（人设）、甚至不同的可用工具集</strong>，你需要使用 Hermes 的 <strong>Profiles（配置文件）</strong> 功能。</p><p>每一个 Profile 都拥有独立的 <code>~/.hermes/profiles/&lt;name&gt;/</code> 目录。</p><p>你可以通过 CLI 预先创建 Profile：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hermes profile create alice</span><br><span class="line">hermes profile create bob</span><br></pre></td></tr></table></figure></p><p>然后在 Python 代码中，你可以在初始化 <code>AIAgent</code> 之前，通过修改环境变量 <code>HERMES_HOME</code> 来让当前进程或 Agent 绑定到特定的 Profile 目录下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">run_agent_for_profile</span>(<span class="params">profile_name: <span class="built_in">str</span>, message: <span class="built_in">str</span></span>):</span></span><br><span class="line">    <span class="comment"># 动态切换当前执行上下文的 HERMES_HOME 到指定的 profile 目录</span></span><br><span class="line">    profile_dir = Path.home() / <span class="string">&quot;.hermes&quot;</span> / <span class="string">&quot;profiles&quot;</span> / profile_name</span><br><span class="line">    os.environ[<span class="string">&quot;HERMES_HOME&quot;</span>] = <span class="built_in">str</span>(profile_dir)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 此时初始化的 Agent 会自动读取对应 profile 目录下的 config.yaml, .env 和 state.db</span></span><br><span class="line">    agent = AIAgent(</span><br><span class="line">        quiet_mode=<span class="literal">True</span></span><br><span class="line">        <span class="comment"># 不需要再传 model 和 key，它会自动从该用户的专属配置中读取</span></span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">return</span> agent.chat(message)</span><br></pre></td></tr></table></figure><hr><h2 id="6-实战集成示例-Integration-Examples"><a href="#6-实战集成示例-Integration-Examples" class="headerlink" title="6. 实战集成示例 (Integration Examples)"></a>6. 实战集成示例 (Integration Examples)</h2><h3 id="6-1-FastAPI-接口端点"><a href="#6-1-FastAPI-接口端点" class="headerlink" title="6.1 FastAPI 接口端点"></a>6.1 FastAPI 接口端点</h3><p>将 Hermes 包装成一个 HTTP API 服务：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> fastapi <span class="keyword">import</span> FastAPI</span><br><span class="line"><span class="keyword">from</span> pydantic <span class="keyword">import</span> BaseModel</span><br><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line">app = FastAPI()</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ChatRequest</span>(<span class="params">BaseModel</span>):</span></span><br><span class="line">    message: <span class="built_in">str</span></span><br><span class="line">    model: <span class="built_in">str</span> = <span class="string">&quot;gpt-5.4-mini&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/chat&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">chat</span>(<span class="params">request: ChatRequest</span>):</span></span><br><span class="line">    <span class="comment"># 每次请求实例化一个全新无状态的 Agent</span></span><br><span class="line">    agent = AIAgent(</span><br><span class="line">        model=request.model,</span><br><span class="line">        provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">        base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">        api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">        quiet_mode=<span class="literal">True</span>,</span><br><span class="line">        skip_context_files=<span class="literal">True</span>,</span><br><span class="line">        skip_memory=<span class="literal">True</span>,</span><br><span class="line">    )</span><br><span class="line">    response = agent.chat(request.message)</span><br><span class="line">    <span class="keyword">return</span> &#123;<span class="string">&quot;response&quot;</span>: response&#125;</span><br></pre></td></tr></table></figure><h3 id="6-2-CI-CD-自动化代码审查流水线"><a href="#6-2-CI-CD-自动化代码审查流水线" class="headerlink" title="6.2 CI/CD 自动化代码审查流水线"></a>6.2 CI/CD 自动化代码审查流水线</h3><p>利用 Agent 在 CI/CD 流程中自动进行 Code Review：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/env python3</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;CI step: auto-review a PR diff.&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">import</span> subprocess</span><br><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取最新的代码变更</span></span><br><span class="line">diff = subprocess.check_output([<span class="string">&quot;git&quot;</span>, <span class="string">&quot;diff&quot;</span>, <span class="string">&quot;main...HEAD&quot;</span>]).decode()</span><br><span class="line"></span><br><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    quiet_mode=<span class="literal">True</span>,</span><br><span class="line">    skip_context_files=<span class="literal">True</span>,</span><br><span class="line">    skip_memory=<span class="literal">True</span>,</span><br><span class="line">    disabled_toolsets=[<span class="string">&quot;terminal&quot;</span>, <span class="string">&quot;browser&quot;</span>], <span class="comment"># CI 环境下关闭敏感工具</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">review = agent.chat(</span><br><span class="line">    <span class="string">f&quot;请审查以下 PR 的代码变更，检查是否存在 bug、安全漏洞或代码风格问题：\n\n<span class="subst">&#123;diff&#125;</span>&quot;</span></span><br><span class="line">)</span><br><span class="line"><span class="built_in">print</span>(review)</span><br></pre></td></tr></table></figure><h3 id="6-3-从零开发自定义工具"><a href="#6-3-从零开发自定义工具" class="headerlink" title="6.3 从零开发自定义工具"></a>6.3 从零开发自定义工具</h3><p>开发自定义工具非常简单，只需调用 <code>tools.registry.registry.register</code> 将其注册到全局系统中。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">from</span> tools.registry <span class="keyword">import</span> registry, tool_result</span><br><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="comment"># 1. 编写业务逻辑函数</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_weather</span>(<span class="params">location: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">    data = &#123;<span class="string">&quot;location&quot;</span>: location, <span class="string">&quot;weather&quot;</span>: <span class="string">&quot;晴朗&quot;</span>, <span class="string">&quot;temp&quot;</span>: <span class="string">&quot;25°C&quot;</span>&#125;</span><br><span class="line">    <span class="comment"># 工具必须返回 JSON 字符串（可以通过自带的 tool_result 辅助函数包裹）</span></span><br><span class="line">    <span class="keyword">return</span> json.dumps(tool_result(success=<span class="literal">True</span>, data=data), ensure_ascii=<span class="literal">False</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 编写符合 OpenAI Function Calling 标准的 Schema</span></span><br><span class="line">WEATHER_SCHEMA = &#123;</span><br><span class="line">    <span class="string">&quot;type&quot;</span>: <span class="string">&quot;function&quot;</span>,</span><br><span class="line">    <span class="string">&quot;function&quot;</span>: &#123;</span><br><span class="line">        <span class="string">&quot;name&quot;</span>: <span class="string">&quot;get_weather&quot;</span>,</span><br><span class="line">        <span class="string">&quot;description&quot;</span>: <span class="string">&quot;获取指定城市的天气信息&quot;</span>,</span><br><span class="line">        <span class="string">&quot;parameters&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">            <span class="string">&quot;properties&quot;</span>: &#123;</span><br><span class="line">                <span class="string">&quot;location&quot;</span>: &#123;<span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>, <span class="string">&quot;description&quot;</span>: <span class="string">&quot;城市名称，如：北京、上海&quot;</span>&#125;</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="string">&quot;required&quot;</span>: [<span class="string">&quot;location&quot;</span>],</span><br><span class="line">        &#125;,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 将工具注册到系统中</span></span><br><span class="line">registry.register(</span><br><span class="line">    name=<span class="string">&quot;get_weather&quot;</span>,           <span class="comment"># 工具名称</span></span><br><span class="line">    toolset=<span class="string">&quot;weather_tools&quot;</span>,      <span class="comment"># 归属的工具集名称</span></span><br><span class="line">    schema=WEATHER_SCHEMA,        <span class="comment"># 描述文件</span></span><br><span class="line">    handler=<span class="keyword">lambda</span> args, **kw: get_weather(args.get(<span class="string">&quot;location&quot;</span>)), </span><br><span class="line">    emoji=<span class="string">&quot;🌤️&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 在 AIAgent 中启用你的自定义工具集</span></span><br><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    enabled_toolsets=[<span class="string">&quot;weather_tools&quot;</span>],</span><br><span class="line">    quiet_mode=<span class="literal">False</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(agent.chat(<span class="string">&quot;今天北京的天气怎么样？&quot;</span>))</span><br></pre></td></tr></table></figure><hr><h2 id="7-高阶篇：复杂任务编排与子智能体委托-Agent-to-Agent-通信"><a href="#7-高阶篇：复杂任务编排与子智能体委托-Agent-to-Agent-通信" class="headerlink" title="7. 高阶篇：复杂任务编排与子智能体委托 (Agent-to-Agent 通信)"></a>7. 高阶篇：复杂任务编排与子智能体委托 (Agent-to-Agent 通信)</h2><p>当面对超大型任务（例如“帮我从头开发一个贪吃蛇游戏并运行测试”）时，单线程执行往往容易遇到上下文瓶颈。Hermes 支持一种受控的 <strong>A2A (Agent-to-Agent)</strong> 协作模式——即<strong>子智能体委托机制 (Subagent Delegation)</strong>。</p><p>当开启 <code>delegate</code> 工具集时，主 Agent 可以化身为“项目经理”，它会为不同的子任务创建隔离的 Child AIAgent 进行并行或串行的任务攻坚。</p><h3 id="A2A-机制的核心特性："><a href="#A2A-机制的核心特性：" class="headerlink" title="A2A 机制的核心特性："></a>A2A 机制的核心特性：</h3><ul><li><strong>上下文隔离</strong>：子 Agent 会获得全新的对话会话和专属任务 ID，主 Agent 的庞大上下文不会拖慢子 Agent 的执行速度。</li><li><strong>防止递归失控</strong>：系统硬编码了最大委派深度（<code>MAX_DEPTH = 2</code>），即子 Agent 无法再创建孙 Agent，且被禁止向人类提问（<code>clarify</code> 工具被禁用），确保它们默默打工不打扰用户。</li><li><strong>并行处理</strong>：主 Agent 可以一次性派发多个任务，系统会自动利用线程池并发运行多个子 Agent。</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    enabled_toolsets=[<span class="string">&quot;delegate&quot;</span>, <span class="string">&quot;terminal&quot;</span>, <span class="string">&quot;file_tools&quot;</span>], </span><br><span class="line">    quiet_mode=<span class="literal">False</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 主 Agent 发现任务复杂后，会自主调用 delegate 工具，通过 A2A 协议派生子 Agent 完成代码编写和测试</span></span><br><span class="line">agent.chat(<span class="string">&quot;请帮我写一个 Python 贪吃蛇游戏，保存到 snake.py 中，然后运行并修复可能存在的 bug。&quot;</span>)</span><br></pre></td></tr></table></figure><p>此外，Hermes Agent 还可以接入 <code>auxiliary_client.py</code> 辅助客户端来支持<strong>视觉多模态</strong>识别，以及通过 <code>cron/</code> 目录下的调度器实现自动化定时任务，这些都是其作为生产级 AI 框架的核心优势。</p><hr><h2 id="8-前沿探索：支持标准-A2A-Protocol-Agent2Agent"><a href="#8-前沿探索：支持标准-A2A-Protocol-Agent2Agent" class="headerlink" title="8. 前沿探索：支持标准 A2A Protocol (Agent2Agent)"></a>8. 前沿探索：支持标准 A2A Protocol (Agent2Agent)</h2><p>最近，由 Linux 基金会主导的 <a href="https://a2a-protocol.org">A2A Protocol</a> (Agent-to-Agent) 成为了跨框架智能体协作的开放标准。与前文提到的 Hermes 内部的 <code>delegate</code>（父子智能体委派）不同，标准的 A2A 协议允许 <strong>Hermes Agent 与完全不同的框架（如 LangGraph, CrewAI 等）构建的外部智能体进行去中心化的通信</strong>。</p><p>虽然目前 Hermes 官方暂未原生内置 A2A 协议，但得益于其高度可扩展的工具注册表（Tool Registry）和网关（Gateway）架构，将其作为一个插件接入是非常容易的！</p><h3 id="概念验证-1：作为-A2A-发起方-Caller"><a href="#概念验证-1：作为-A2A-发起方-Caller" class="headerlink" title="概念验证 1：作为 A2A 发起方 (Caller)"></a>概念验证 1：作为 A2A 发起方 (Caller)</h3><p>如果你想让 Hermes 能够调用外部支持 A2A 协议的 Agent（例如一个专门用于金融分析的 LangGraph 智能体），你可以轻松地为其编写一个自定义工具：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">from</span> tools.registry <span class="keyword">import</span> registry, tool_result</span><br><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="comment"># 假设你已经安装了官方的 A2A Python SDK (pip install a2a-python)</span></span><br><span class="line"><span class="comment"># import a2a</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">call_external_a2a_agent</span>(<span class="params">agent_address: <span class="built_in">str</span>, task_query: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;通过 A2A 协议向外部智能体发送任务并等待结果&quot;&quot;&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;正在通过 A2A 协议联系外部智能体 [<span class="subst">&#123;agent_address&#125;</span>]...&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># --- 以下为伪代码，展示与 A2A SDK 的集成逻辑 ---</span></span><br><span class="line">    <span class="comment"># client = a2a.Client()</span></span><br><span class="line">    <span class="comment"># response = client.send_message(</span></span><br><span class="line">    <span class="comment">#     to=agent_address,</span></span><br><span class="line">    <span class="comment">#     content=&#123;&quot;task&quot;: task_query&#125;</span></span><br><span class="line">    <span class="comment"># )</span></span><br><span class="line">    <span class="comment"># result = response.get_result()</span></span><br><span class="line">    <span class="comment"># ---------------------------------------------</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 模拟外部 Agent 的返回</span></span><br><span class="line">    result = <span class="string">f&quot;来自 <span class="subst">&#123;agent_address&#125;</span> 的分析结果：已完成关于 &#x27;<span class="subst">&#123;task_query&#125;</span>&#x27; 的任务。&quot;</span></span><br><span class="line">    <span class="keyword">return</span> json.dumps(tool_result(success=<span class="literal">True</span>, data=&#123;<span class="string">&quot;result&quot;</span>: result&#125;), ensure_ascii=<span class="literal">False</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 注册 A2A 通信工具</span></span><br><span class="line">registry.register(</span><br><span class="line">    name=<span class="string">&quot;call_a2a_agent&quot;</span>,</span><br><span class="line">    toolset=<span class="string">&quot;a2a_tools&quot;</span>,</span><br><span class="line">    schema=&#123;</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;function&quot;</span>,</span><br><span class="line">        <span class="string">&quot;function&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;name&quot;</span>: <span class="string">&quot;call_a2a_agent&quot;</span>,</span><br><span class="line">            <span class="string">&quot;description&quot;</span>: <span class="string">&quot;通过标准 A2A 协议将特定领域的任务委派给外部智能体&quot;</span>,</span><br><span class="line">            <span class="string">&quot;parameters&quot;</span>: &#123;</span><br><span class="line">                <span class="string">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">                <span class="string">&quot;properties&quot;</span>: &#123;</span><br><span class="line">                    <span class="string">&quot;agent_address&quot;</span>: &#123;<span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>, <span class="string">&quot;description&quot;</span>: <span class="string">&quot;外部智能体的 A2A 寻址地址&quot;</span>&#125;,</span><br><span class="line">                    <span class="string">&quot;task_query&quot;</span>: &#123;<span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>, <span class="string">&quot;description&quot;</span>: <span class="string">&quot;需要外部智能体完成的任务详情&quot;</span>&#125;</span><br><span class="line">                &#125;,</span><br><span class="line">                <span class="string">&quot;required&quot;</span>: [<span class="string">&quot;agent_address&quot;</span>, <span class="string">&quot;task_query&quot;</span>],</span><br><span class="line">            &#125;,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    handler=<span class="keyword">lambda</span> args, **kw: call_external_a2a_agent(args.get(<span class="string">&quot;agent_address&quot;</span>), args.get(<span class="string">&quot;task_query&quot;</span>)),</span><br><span class="line">    emoji=<span class="string">&quot;📡&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动支持 A2A 协议的 Hermes 主控 Agent</span></span><br><span class="line">agent = AIAgent(</span><br><span class="line">    model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">    provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">    base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">    api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">    enabled_toolsets=[<span class="string">&quot;a2a_tools&quot;</span>, <span class="string">&quot;web_search&quot;</span>],</span><br><span class="line">    quiet_mode=<span class="literal">False</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">agent.chat(<span class="string">&quot;我需要一份专业的市场分析报告。请通过 A2A 协议联系地址为 &#x27;a2a://finance-expert-agent&#x27; 的智能体，让它帮你生成报告，然后你再结合最新的网络搜索结果发给我。&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="概念验证-2：作为-A2A-接收方-Server"><a href="#概念验证-2：作为-A2A-接收方-Server" class="headerlink" title="概念验证 2：作为 A2A 接收方 (Server)"></a>概念验证 2：作为 A2A 接收方 (Server)</h3><p>除了主动调用其他 Agent，你也可以将 Hermes 包装成一个 A2A 协议的服务端，监听并处理来自外部框架（如 CrewAI 或 AutoGen）发送的 A2A 请求。这在 Hermes 的架构中相当于构建一个自定义的 Gateway Platform 适配器。</p><p>以下是最简单的伪代码实现思路，展示了如何监听 A2A 请求、将请求内容交给 Hermes <code>AIAgent</code> 处理，再将结果通过 A2A 协议返回：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> run_agent <span class="keyword">import</span> AIAgent</span><br><span class="line"></span><br><span class="line"><span class="comment"># 假设安装了官方的 A2A Python SDK (pip install a2a-python)</span></span><br><span class="line"><span class="comment"># import a2a</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">handle_incoming_a2a_task</span>(<span class="params">request</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;处理外部 Agent 发送来的 A2A 请求&quot;&quot;&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;收到来自 <span class="subst">&#123;request.sender&#125;</span> 的任务：<span class="subst">&#123;request.content[<span class="string">&#x27;task&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 实例化一个 Hermes Agent 来处理该任务</span></span><br><span class="line">    agent = AIAgent(</span><br><span class="line">        model=<span class="string">&quot;gpt-5.4-mini&quot;</span>,</span><br><span class="line">        provider=<span class="string">&quot;custom&quot;</span>,</span><br><span class="line">        base_url=<span class="string">&quot;https://api.your-proxy.com/v1&quot;</span>,</span><br><span class="line">        api_key=<span class="string">&quot;your-api-key-here&quot;</span>,</span><br><span class="line">        quiet_mode=<span class="literal">True</span>,</span><br><span class="line">        <span class="comment"># 可以根据 request.sender 动态设置 session_id 以隔离不同 Agent 的记忆</span></span><br><span class="line">        session_id=<span class="string">f&quot;a2a_session_<span class="subst">&#123;request.sender&#125;</span>&quot;</span></span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 核心：将 A2A 请求转换为 Hermes 的内部聊天指令</span></span><br><span class="line">    result = agent.chat(request.content[<span class="string">&quot;task&quot;</span>])</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 将 Hermes 的回复通过 A2A 协议返回给发送方</span></span><br><span class="line">    <span class="comment"># return request.reply(content=&#123;&quot;result&quot;: result&#125;)</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;任务完成，回复给 <span class="subst">&#123;request.sender&#125;</span>：<span class="subst">&#123;result&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">start_a2a_server</span>():</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;启动 A2A 监听服务&quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># client = a2a.Client(address=&quot;a2a://hermes-research-agent&quot;)</span></span><br><span class="line">    <span class="comment"># client.on_message(handle_incoming_a2a_task)</span></span><br><span class="line">    <span class="comment"># print(&quot;Hermes A2A 节点已启动，监听地址: a2a://hermes-research-agent&quot;)</span></span><br><span class="line">    <span class="comment"># client.start()</span></span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    start_a2a_server()</span><br></pre></td></tr></table></figure><p>正如你所见，A2A 协议（用于 Agent 间通信）与 MCP 协议（用于 Agent 访问工具资源）是完美的互补组合。随着生态的成熟，你完全可以通过上述方式将 Hermes 接入到由无数异构 AI 智能体组成的“互联网”中！</p><hr><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>通过本教程，你已经掌握了如何使用 Python 库的方式调用 <code>hermes-agent</code>，从单次/多轮对话控制，到安全地进行并发处理和工具定制。它既可以作为你的本地终端 Copilot，也能轻易集成到 FastAPI 后端、Discord Bot 甚至自动化 CI/CD 流水线中！</p>]]></content>
    
    
    <summary type="html">Hermes Agent 核心教程：从零开始开发强大的 Python AI 应用
写在前面：关于 Hermes Agent

Hermes Agent 是由知名 AI 研究机构 Nous Research 开发的一款具备高度自我进化能力的开源智能体系统。它旨在打破传统大模型“阅后即焚”的单一对话框限制，让 AI 真正“生活”在开发者的终端、团队的通讯软件以及自动化的流水线中。

核心亮点：

 * 闭环学习与长期记忆：它不仅能执行任务，还能在跨会话中持久化记忆，甚至从经验中自主创建和优化专属技能。
 * 全平台无缝接入：内置强大的消息网关，一套代码即可接入 CLI、Telegram、Disco</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着😺NanoBot学AI智能体设计和开发6：nanobot的记忆长啥样</title>
    <link href="http://qixinbo.github.io/2026/04/11/nanobot-6/"/>
    <id>http://qixinbo.github.io/2026/04/11/nanobot-6/</id>
    <published>2026-04-11T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="深入解析-nanobot-的记忆机制：让-AI-拥有“活”的记忆"><a href="#深入解析-nanobot-的记忆机制：让-AI-拥有“活”的记忆" class="headerlink" title="深入解析 nanobot 的记忆机制：让 AI 拥有“活”的记忆"></a>深入解析 nanobot 的记忆机制：让 AI 拥有“活”的记忆</h1><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>在 AI Agent 的长期交互中，记忆管理一直是个痛点。nanobot 的记忆机制建立在一个非常优雅的理念之上：<strong>记忆应该是鲜活的，但不应该是混乱的（Memory should feel alive, but it should not feel chaotic）。</strong></p><p>好的记忆不应该是一堆笔记的无序堆砌，而是一个安静的注意力系统。它能敏锐地注意到什么值得保留，优雅地放弃不再需要的内容，并将经历转化为平静、持久和有用的知识。本文将结合 nanobot 的实际代码（主要集中在 <code>nanobot/agent/memory.py</code>），深入拆解这套记忆系统的设计与实现。</p><hr><h2 id="记忆的分层架构：各司其职的存储介质"><a href="#记忆的分层架构：各司其职的存储介质" class="headerlink" title="记忆的分层架构：各司其职的存储介质"></a>记忆的分层架构：各司其职的存储介质</h2><p>nanobot 并没有把所有记忆都塞进一个巨大的文件里，而是根据不同类型记忆的生命周期和用途，将其拆分到了不同的层级和文件中：</p><ol><li><strong>短期记忆 (Short-term)</strong>：<ul><li>存在于内存中的 <code>session.messages</code>。</li><li>保存当前正在进行的、鲜活的对话上下文。</li></ul></li><li><strong>中期记忆 (Mid-term)</strong>：<ul><li>存储于 <code>memory/history.jsonl</code>。</li><li>这是一个只追加（Append-only）的运行日志，用于记录被压缩后的前期对话流。</li></ul></li><li><strong>长期记忆 (Long-term)</strong>：<ul><li>持久化的 Markdown 知识文件，分为三个维度：<ul><li><code>SOUL.md</code>：定义 Bot 的人格、行为模式和沟通语气。</li><li><code>USER.md</code>：记录用户的身份、偏好和习惯。</li><li><code>memory/MEMORY.md</code>：记录项目上下文、长期事实和重要事件。</li></ul></li></ul></li></ol><p>这种分层设计使得系统在当前对话中保持轻量级和高响应速度，同时在长期使用中具备反思和知识积累的能力。值得一提的是，长期记忆文件由底层的 <code>GitStore</code> 自动进行版本控制，确保记忆的演变可追溯、可回滚。</p><hr><h2 id="核心组件与大模型-Prompt-深度解析：从流转到沉淀"><a href="#核心组件与大模型-Prompt-深度解析：从流转到沉淀" class="headerlink" title="核心组件与大模型 Prompt 深度解析：从流转到沉淀"></a>核心组件与大模型 Prompt 深度解析：从流转到沉淀</h2><p>整个记忆系统的核心代码位于 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/memory.py"><code>nanobot/agent/memory.py</code></a> 中，主要由三个类构成：<code>MemoryStore</code>（底层存储）、<code>Consolidator</code>（会话压缩）和 <code>Dream</code>（知识提炼）。大模型在其中扮演了关键的“记忆提炼器”角色，其行为由几个精心设计的 Prompt 控制。</p><h3 id="1-MemoryStore：纯文件-I-O-与数据结构"><a href="#1-MemoryStore：纯文件-I-O-与数据结构" class="headerlink" title="1. MemoryStore：纯文件 I/O 与数据结构"></a>1. MemoryStore：纯文件 I/O 与数据结构</h3><p><code>MemoryStore</code> 负责底层的文件读写、游标管理和 JSONL 格式化。</p><p><strong>关于 <code>history.jsonl</code> 的设计哲学</strong>：<br>在早期的版本中，历史记录存储在 <code>HISTORY.md</code> 中，这虽然适合人类阅读，但作为系统的操作基底（operational substrate）却过于脆弱。nanobot 将其迁移到了 <code>history.jsonl</code>，带来了以下优势：</p><ul><li>稳定的增量游标（cursor-based）。</li><li>更安全的机器解析和批量处理（easier batching）。</li><li>清晰的边界：将“原始历史”与“提炼后的知识”彻底分开。</li></ul><p>代码中通过 <code>append_history(entry)</code> 每次生成一个自增的 <code>cursor</code>，并写入带有时间戳的 JSON 记录：<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">append_history</span>(<span class="params">self, entry: <span class="built_in">str</span></span>) -&gt; <span class="built_in">int</span>:</span></span><br><span class="line">    cursor = self._next_cursor()</span><br><span class="line">    ts = datetime.now().strftime(<span class="string">&quot;%Y-%m-%d %H:%M&quot;</span>)</span><br><span class="line">    record = &#123;<span class="string">&quot;cursor&quot;</span>: cursor, <span class="string">&quot;timestamp&quot;</span>: ts, <span class="string">&quot;content&quot;</span>: strip_think(entry.rstrip()) <span class="keyword">or</span> entry.rstrip()&#125;</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(self.history_file, <span class="string">&quot;a&quot;</span>, encoding=<span class="string">&quot;utf-8&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">        f.write(json.dumps(record, ensure_ascii=<span class="literal">False</span>) + <span class="string">&quot;\n&quot;</span>)</span><br><span class="line">    <span class="comment"># 更新游标状态</span></span><br><span class="line">    self._cursor_file.write_text(<span class="built_in">str</span>(cursor), encoding=<span class="string">&quot;utf-8&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span> cursor</span><br></pre></td></tr></table></figure></p><h3 id="2-Consolidator：防止上下文爆炸的“压缩器”"><a href="#2-Consolidator：防止上下文爆炸的“压缩器”" class="headerlink" title="2. Consolidator：防止上下文爆炸的“压缩器”"></a>2. Consolidator：防止上下文爆炸的“压缩器”</h3><p>随着对话的进行，<code>session.messages</code> 会越来越长，最终对大模型的 Context Window 造成压力。<code>Consolidator</code> 是一个轻量级的、基于 Token 预算触发的合并器。</p><ul><li><strong>触发机制</strong>：<code>maybe_consolidate_by_tokens</code> 方法会实时估算当前会话的 Token 数。如果超过了安全预算（<code>Context Window - 最大生成 Token - 安全缓冲区</code>），就会触发合并逻辑。</li><li><strong>安全切分</strong>：它不会生硬地截断对话，而是通过 <code>pick_consolidation_boundary</code> 寻找一个合适的用户发言（User Turn）作为切分边界。</li><li><strong>大模型摘要 (Archive Prompt)</strong>：将切分出的旧对话通过大模型进行摘要提取，并调用 <code>archive()</code> 将摘要追加到 <code>history.jsonl</code> 中。这里使用的 Prompt (<a href="file:///Users/qixinbo/Projects/nanobot/nanobot/templates/agent/consolidator_archive.md"><code>consolidator_archive.md</code></a>) 非常强调<strong>实用性</strong>和<strong>高优排序</strong>：<figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Extract key facts from this conversation. Only output items matching these categories, skip everything else:</span><br><span class="line"><span class="bullet">-</span> User facts: personal info, preferences, stated opinions, habits</span><br><span class="line"><span class="bullet">-</span> Decisions: choices made, conclusions reached</span><br><span class="line"><span class="bullet">-</span> Solutions: working approaches discovered through trial and error, especially non-obvious methods that succeeded after failed attempts</span><br><span class="line"><span class="bullet">-</span> Events: plans, deadlines, notable occurrences</span><br><span class="line"><span class="bullet">-</span> Preferences: communication style, tool preferences</span><br><span class="line"></span><br><span class="line">Priority: user corrections and preferences &gt; solutions &gt; decisions &gt; events &gt; environment facts. The most valuable memory prevents the user from having to repeat themselves.</span><br><span class="line"></span><br><span class="line">Skip: code patterns derivable from source, git history, or anything already captured in existing memory.</span><br><span class="line"></span><br><span class="line">Output as concise bullet points, one fact per line. No preamble, no commentary.</span><br><span class="line">If nothing noteworthy happened, output: (nothing)</span><br></pre></td></tr></table></figure><strong>解析</strong>：<ul><li><strong>严格过滤</strong>：明确指出了需要保留的 5 种事实，其余全部丢弃。</li><li><strong>优先级定义</strong>：<code>用户的纠正与偏好</code> &gt; <code>解决方案</code> &gt; <code>决策</code> &gt; <code>事件</code> &gt; <code>环境事实</code>。核心理念是：“最有价值的记忆是防止用户重复自己的话”。</li><li><strong>降噪机制</strong>：明确要求跳过可以通过源码或 Git 历史推导的代码模式。</li></ul></li><li><strong>优雅降级</strong>：如果 LLM 调用失败，系统会触发 <code>raw_archive</code> 机制，直接将原始消息以 <code>[RAW]</code> 标签 Dump 进历史文件，保证数据不丢失。</li></ul><h3 id="3-Dream：从历史中提炼知识的“盗梦空间”"><a href="#3-Dream：从历史中提炼知识的“盗梦空间”" class="headerlink" title="3. Dream：从历史中提炼知识的“盗梦空间”"></a>3. Dream：从历史中提炼知识的“盗梦空间”</h3><p>如果说 <code>Consolidator</code> 是为了应对当下的生存压力（Context Window），那么 <code>Dream</code> 就是为了长远的知识沉淀。这是一个重型的、可通过定时任务或手动命令（<code>/dream</code>）触发的记忆处理器。</p><p><code>Dream</code> 类采用了非常工程化的<strong>两阶段（Two-phase）处理模式</strong>，这反映在它的两个不同的 Prompt 设计上：</p><h4 id="阶段一：分析提炼-Phase-1"><a href="#阶段一：分析提炼-Phase-1" class="headerlink" title="阶段一：分析提炼 (Phase 1)"></a>阶段一：分析提炼 (Phase 1)</h4><ul><li>读取自上次 <code>.dream_cursor</code> 以来的 <code>history.jsonl</code> 未处理条目。</li><li>将这段历史记录，连同当前的 <code>SOUL.md</code>、<code>USER.md</code>、<code>MEMORY.md</code> 内容一起发给大模型。</li><li><strong>分析 Prompt (<a href="file:///Users/qixinbo/Projects/nanobot/nanobot/templates/agent/dream_phase1.md"><code>dream_phase1.md</code></a>)</strong>：<figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Compare conversation history against current memory files.</span><br><span class="line">Output one line per finding:</span><br><span class="line">[FILE] atomic fact or change description</span><br><span class="line"></span><br><span class="line">Files: USER (identity, preferences, habits), SOUL (bot behavior, tone), MEMORY (knowledge, project context, tool patterns)</span><br><span class="line"></span><br><span class="line">Rules:</span><br><span class="line"><span class="bullet">-</span> Only new or conflicting information — skip duplicates and ephemera</span><br><span class="line"><span class="bullet">-</span> Prefer atomic facts: &quot;has a cat named Luna&quot; not &quot;discussed pet care&quot;</span><br><span class="line"><span class="bullet">-</span> Corrections: [USER] location is Tokyo, not Osaka</span><br><span class="line"><span class="bullet">-</span> Also capture confirmed approaches: if the user validated a non-obvious choice, note it</span><br><span class="line"></span><br><span class="line">If nothing needs updating: [SKIP] no new information</span><br></pre></td></tr></table></figure><strong>解析</strong>：<ul><li><strong>原子化原则</strong>：强制要求模型输出具体的事实（如 “has a cat named Luna”），而不是宽泛的总结（如 “discussed pet care”）。</li><li><strong>变更发现</strong>：不仅提取新信息，还要求识别与现有记忆冲突的信息并进行纠正。</li><li><strong>固定输出格式</strong>：要求以 <code>[FILE] 事实描述</code> 的格式输出，为阶段二的精准编辑提供清晰的指令。</li></ul></li></ul><h4 id="阶段二：增量编辑-Phase-2"><a href="#阶段二：增量编辑-Phase-2" class="headerlink" title="阶段二：增量编辑 (Phase 2)"></a>阶段二：增量编辑 (Phase 2)</h4><ul><li>将阶段一的分析结果交由一个专用的 <code>AgentRunner</code> 处理。</li><li>这个 Agent 被赋予了两个核心工具：<code>ReadFileTool</code> 和 <code>EditFileTool</code>。</li><li><strong>编辑 Prompt (<a href="file:///Users/qixinbo/Projects/nanobot/nanobot/templates/agent/dream_phase2.md"><code>dream_phase2.md</code></a>)</strong>：<figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Update memory files based on the analysis below.</span><br><span class="line"></span><br><span class="line"><span class="section">## Quality standards</span></span><br><span class="line"><span class="bullet">-</span> Every line must carry standalone value — no filler</span><br><span class="line"><span class="bullet">-</span> Concise bullet points under clear headers</span><br><span class="line"><span class="bullet">-</span> Remove outdated or contradicted information</span><br><span class="line"></span><br><span class="line"><span class="section">## Editing</span></span><br><span class="line"><span class="bullet">-</span> File contents provided below — edit directly, no read<span class="emphasis">_file needed</span></span><br><span class="line"><span class="emphasis">- Batch changes to the same file into one edit_</span>file call</span><br><span class="line"><span class="bullet">-</span> Surgical edits only — never rewrite entire files</span><br><span class="line"><span class="bullet">-</span> Do NOT overwrite correct entries — only add, update, or remove</span><br><span class="line"><span class="bullet">-</span> If nothing to update, stop without calling tools</span><br></pre></td></tr></table></figure><strong>解析</strong>：<ul><li><strong>外科手术式编辑 (Surgical edits only)</strong>：这是最核心的指导原则。它明确禁止 LLM 覆写整个文件，而是要求进行局部增删改。这极大地降低了破坏现有知识结构的风险。</li><li><strong>优化工具调用</strong>：提示词中告知 LLM 文件内容已经提供，不需要调用 <code>read_file</code>，并且要求将对同一个文件的修改合并到一次 <code>edit_file</code> 调用中，提升了处理效率。</li></ul></li></ul><h4 id="Git-自动版本控制"><a href="#Git-自动版本控制" class="headerlink" title="Git 自动版本控制"></a>Git 自动版本控制</h4><p>当 <code>Dream</code> 完成编辑后，不仅会推进 <code>.dream_cursor</code> 游标，还会通过 <code>GitStore.auto_commit()</code> 自动生成一次 Git 提交：<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> changelog <span class="keyword">and</span> self.store.git.is_initialized():</span><br><span class="line">    ts = batch[-<span class="number">1</span>][<span class="string">&quot;timestamp&quot;</span>]</span><br><span class="line">    sha = self.store.git.auto_commit(<span class="string">f&quot;dream: <span class="subst">&#123;ts&#125;</span>, <span class="subst">&#123;<span class="built_in">len</span>(changelog)&#125;</span> change(s)&quot;</span>)</span><br></pre></td></tr></table></figure><br>得益于此，用户可以通过 <code>/dream-log</code> 查看记忆的变更记录，或者通过 <code>/dream-restore &lt;sha&gt;</code> 安全地将记忆回滚到过去的某个状态。</p><hr><h2 id="总结与启示"><a href="#总结与启示" class="headerlink" title="总结与启示"></a>总结与启示</h2><p>nanobot 的记忆机制完美诠释了<strong>“结构与意义的分离”</strong>：</p><ul><li><code>history.jsonl</code> 负责<strong>结构</strong>（记录发生了什么）。</li><li><code>SOUL.md</code>, <code>USER.md</code>, <code>MEMORY.md</code> 负责<strong>意义</strong>（沉淀留下了什么）。</li></ul><p>通过 <code>Consolidator</code> 实现对话流的实时无损压缩，通过 <code>Dream</code> 机制实现异步的、增量的知识提炼，并结合 Git 工具链提供强大的版本控制与容错能力。这套机制避免了传统记忆管理中“上下文臃肿”和“信息被过度覆盖”的顽疾，为构建长期陪伴型 AI Agent 提供了一个极其优秀的工程参考范式。</p>]]></content>
    
    
    <summary type="html">深入解析 nanobot 的记忆机制：让 AI 拥有“活”的记忆
引言
在 AI Agent 的长期交互中，记忆管理一直是个痛点。nanobot 的记忆机制建立在一个非常优雅的理念之上：记忆应该是鲜活的，但不应该是混乱的（Memory should feel alive, but it should not feel chaotic）。

好的记忆不应该是一堆笔记的无序堆砌，而是一个安静的注意力系统。它能敏锐地注意到什么值得保留，优雅地放弃不再需要的内容，并将经历转化为平静、持久和有用的知识。本文将结合 nanobot 的实际代码（主要集中在 nanobot/agent/memory.py），</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着😺NanoBot学AI智能体设计和开发5：当我们问“使用子智能体做xxx”时nanobot做了啥</title>
    <link href="http://qixinbo.github.io/2026/04/10/nanobot-5/"/>
    <id>http://qixinbo.github.io/2026/04/10/nanobot-5/</id>
    <published>2026-04-10T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="深入源码：Nanobot-Agent-如何处理“使用子智能体”的交互指令"><a href="#深入源码：Nanobot-Agent-如何处理“使用子智能体”的交互指令" class="headerlink" title="深入源码：Nanobot Agent 如何处理“使用子智能体”的交互指令"></a>深入源码：Nanobot Agent 如何处理“使用子智能体”的交互指令</h1><p>在多智能体（Multi-Agent）架构中，主智能体如何理解用户意图并动态派发任务给子智能体，是一个非常核心的设计。本文将以 Nanobot Agent 为例，详细拆解当用户在终端输入交互指令：“<strong>请使用子智能体调研一下hermes agent</strong>” 时，系统在底层是如何一步步流转的。</p><h2 id="1-接收输入：构造-InboundMessage"><a href="#1-接收输入：构造-InboundMessage" class="headerlink" title="1. 接收输入：构造 InboundMessage"></a>1. 接收输入：构造 <code>InboundMessage</code></h2><p>一切的起点始于用户在交互界面（例如 CLI 终端）输入文本。Nanobot 使用统一的事件总线（Message Bus）来解耦各个渠道和核心 Agent 逻辑。</p><p>当用户敲下回车后，CLI 渠道会捕获这段文本，并将其封装为一个标准化的 <code>InboundMessage</code> 对象。这个数据类定义在 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/bus/events.py#L8-L26">events.py</a> 中：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@dataclass</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">InboundMessage</span>:</span></span><br><span class="line">    channel: <span class="built_in">str</span>  <span class="comment"># 例如 &quot;cli&quot;</span></span><br><span class="line">    sender_id: <span class="built_in">str</span>  <span class="comment"># 用户标识，例如 &quot;user&quot;</span></span><br><span class="line">    chat_id: <span class="built_in">str</span>  <span class="comment"># 会话标识，例如 &quot;direct&quot;</span></span><br><span class="line">    content: <span class="built_in">str</span>  <span class="comment"># 消息文本内容</span></span><br><span class="line">    <span class="comment"># ...</span></span><br></pre></td></tr></table></figure><p>在 CLI 交互模式下，具体的构造逻辑位于 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/cli/commands.py#L1057-L1063">commands.py</a> 中：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> bus.publish_inbound(InboundMessage(</span><br><span class="line">    channel=cli_channel,</span><br><span class="line">    sender_id=<span class="string">&quot;user&quot;</span>,</span><br><span class="line">    chat_id=cli_chat_id,</span><br><span class="line">    content=user_input,  <span class="comment"># 即 &quot;请使用子智能体调研一下hermes agent&quot;</span></span><br><span class="line">    metadata=&#123;<span class="string">&quot;_wants_stream&quot;</span>: <span class="literal">True</span>&#125;,</span><br><span class="line">))</span><br></pre></td></tr></table></figure><p>随后，该消息被推入消息总线，等待主 Agent 循环消费。</p><h2 id="2-消息路由：主循环与会话加锁"><a href="#2-消息路由：主循环与会话加锁" class="headerlink" title="2. 消息路由：主循环与会话加锁"></a>2. 消息路由：主循环与会话加锁</h2><p>主 Agent 在后台运行着一个监听循环，即 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/loop.py#L396-L427">loop.py</a> 中的 <code>run()</code> 方法。这个方法是整个机器人响应系统的心脏，它采用了异步和非阻塞的设计模式。我们一步步解析 <code>run()</code> 到底做了什么：</p><h3 id="run-方法的执行拆解"><a href="#run-方法的执行拆解" class="headerlink" title="run() 方法的执行拆解"></a><code>run()</code> 方法的执行拆解</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">run</span>(<span class="params">self</span>) -&gt; <span class="literal">None</span>:</span></span><br><span class="line">    self._running = <span class="literal">True</span></span><br><span class="line">    <span class="keyword">await</span> self._connect_mcp()  <span class="comment"># 1. 启动前初始化 MCP (Model Context Protocol) 插件连接</span></span><br><span class="line">    logger.info(<span class="string">&quot;Agent loop started&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> self._running:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="comment"># 2. 带超时的消息轮询</span></span><br><span class="line">            msg = <span class="keyword">await</span> asyncio.wait_for(self.bus.consume_inbound(), timeout=<span class="number">1.0</span>)</span><br><span class="line">        <span class="keyword">except</span> asyncio.TimeoutError:</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        <span class="comment"># ... 省略异常处理 ...</span></span><br><span class="line"></span><br><span class="line">        raw = msg.content.strip()</span><br><span class="line">        <span class="comment"># 3. 优先级命令拦截</span></span><br><span class="line">        <span class="keyword">if</span> self.commands.is_priority(raw):</span><br><span class="line">            ctx = CommandContext(msg=msg, session=<span class="literal">None</span>, key=msg.session_key, raw=raw, loop=self)</span><br><span class="line">            result = <span class="keyword">await</span> self.commands.dispatch_priority(ctx)</span><br><span class="line">            <span class="keyword">if</span> result:</span><br><span class="line">                <span class="keyword">await</span> self.bus.publish_outbound(result)</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 4. 普通消息的异步派发</span></span><br><span class="line">        task = asyncio.create_task(self._dispatch(msg))</span><br><span class="line">        self._active_tasks.setdefault(msg.session_key, []).append(task)</span><br><span class="line">        task.add_done_callback(...)</span><br></pre></td></tr></table></figure><ol><li><strong>MCP 插件初始化</strong>：在进入死循环之前，先通过 <code>_connect_mcp()</code> 建立与配置的外部插件（如本地开发工具链、数据库查询接口等）的连接。</li><li><strong>带超时的非阻塞拉取</strong>：通过 <code>asyncio.wait_for(..., timeout=1.0)</code> 从总线 <code>consume_inbound()</code> 获取消息。设置 1 秒超时是为了让循环能周期性“醒来”检查 <code>self._running</code> 的状态，从而支持优雅的平滑退出（Graceful Shutdown）。</li><li><strong>优先指令拦截（Priority Commands）</strong>：如果收到像 <code>/stop</code> 这样紧急干预指令，<code>self.commands.is_priority(raw)</code> 会返回 <code>True</code>。此时直接 <code>await self.commands.dispatch_priority(ctx)</code> 并在当前协程同步执行完毕，它能立即终止对应会话下的所有活跃任务和子智能体。这正是为什么即便机器人还在长篇大论地生成或执行耗时任务，你依然可以随时用 <code>/stop</code> 叫停它。</li><li><strong>异步任务派发（Task Dispatch）</strong>：对于绝大多数普通对话消息（包括我们的“请使用子智能体…”），<code>run()</code> 方法<strong>不会</strong>在原地等待它执行完。而是通过 <code>asyncio.create_task(self._dispatch(msg))</code> 创建一个后台任务，并将其注册到该会话的活跃任务字典 <code>_active_tasks</code> 中，以便后续可能被 <code>/stop</code> 取消。</li></ol><p>通过异步派发，普通消息进入到 <code>_dispatch(msg)</code> 中。在这里，系统会根据 <code>session_key</code> 获取一把异步锁：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">lock = self._session_locks.setdefault(msg.session_key, asyncio.Lock())</span><br><span class="line"><span class="keyword">async</span> <span class="keyword">with</span> lock, gate:</span><br><span class="line">    <span class="comment"># ...</span></span><br><span class="line">    response = <span class="keyword">await</span> self._process_message(msg, ...)</span><br></pre></td></tr></table></figure><p>这保证了<strong>同一个用户的同一个会话是串行处理的，而不同用户的会话可以并发处理</strong>。随后消息进入 <code>_process_message</code> 方法。</p><h2 id="3-意图解析与工具选择：LLM-Function-Calling"><a href="#3-意图解析与工具选择：LLM-Function-Calling" class="headerlink" title="3. 意图解析与工具选择：LLM Function Calling"></a>3. 意图解析与工具选择：LLM Function Calling</h2><p>在 <code>_process_message</code> 中，Nanobot <strong>并没有</strong>硬编码写死 <code>if &quot;子智能体&quot; in text</code> 的判断逻辑。相反，它构建了上下文后，直接将消息丢给了大语言模型（LLM）。</p><h3 id="3-1-主智能体的-Prompt-构造"><a href="#3-1-主智能体的-Prompt-构造" class="headerlink" title="3.1 主智能体的 Prompt 构造"></a>3.1 主智能体的 Prompt 构造</h3><p>在每次请求大模型前，Nanobot 都会通过 <code>ContextBuilder.build_messages()</code>（位于 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/context.py#L102-L132">context.py</a>）来组装发给 LLM 的上下文，其中包括：</p><ol><li><strong>System Prompt</strong>：由 <code>build_system_prompt()</code> 动态生成，包含机器人的人设（<code>identity.md</code>）、用户的自定义规则（<code>AGENTS.md</code>, <code>SOUL.md</code> 等）、以及当前已加载的 Skills 列表。</li><li><strong>Runtime Context</strong>：通过 <code>_build_runtime_context()</code> 注入当前系统时间、渠道来源等元数据。</li><li><strong>对话历史</strong>：从数据库加载该 Session 的上下文记录。</li><li><strong>当前输入</strong>：即用户输入的“请使用子智能体调研一下hermes agent”。</li></ol><h3 id="3-2-发现工具：SpawnTool-Schema"><a href="#3-2-发现工具：SpawnTool-Schema" class="headerlink" title="3.2 发现工具：SpawnTool Schema"></a>3.2 发现工具：<code>SpawnTool</code> Schema</h3><p>大模型之所以“知道”它可以使用子智能体，是因为主智能体在请求时携带了所有可用工具的 JSON Schema 定义。</p><p>主智能体在初始化时，将 <code>SpawnTool</code> 注册进了工具集（见 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/loop.py#L282-L284">loop.py</a>）。在与 LLM 交互时（<a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/runner.py#L310-L330">runner.py</a> 的 <code>_request_model</code> 方法），Nanobot 会通过 <code>self.tools.get_definitions()</code> 获取工具定义。</p><p><code>SpawnTool</code> 的描述位于 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/tools/spawn.py#L12-L47">spawn.py</a>。最终转换为 OpenAI 标准的 Function Schema 发送给大模型，其结构大致如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;function&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;function&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;spawn&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently...&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;parameters&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;properties&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;task&quot;</span>: &#123;<span class="attr">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>, <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;The task for the subagent to complete&quot;</span>&#125;,</span><br><span class="line">        <span class="attr">&quot;label&quot;</span>: &#123;<span class="attr">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>, <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Optional short label for the task (for display)&quot;</span>&#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">&quot;required&quot;</span>: [<span class="string">&quot;task&quot;</span>]</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由于用户的输入明确要求“请使用子智能体…”，LLM 读取到 <code>spawn</code> 工具的 <code>description</code> 匹配该意图，于是决定触发 Function Calling，输出类似 <code>&#123;&quot;name&quot;: &quot;spawn&quot;, &quot;arguments&quot;: &#123;&quot;task&quot;: &quot;调研一下hermes agent&quot;&#125;&#125;</code> 的指令。</p><h2 id="4-派发任务：SpawnTool-与-SubagentManager"><a href="#4-派发任务：SpawnTool-与-SubagentManager" class="headerlink" title="4. 派发任务：SpawnTool 与 SubagentManager"></a>4. 派发任务：<code>SpawnTool</code> 与 <code>SubagentManager</code></h2><p>当 <code>AgentRunner</code> 接收到 LLM 返回的 Tool Call 请求后，会执行对应的工具代码。对于 <code>spawn</code> 工具，其执行体 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/tools/spawn.py#L48-L56">spawn.py</a> 仅仅是一个简单的转发层：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">execute</span>(<span class="params">self, task: <span class="built_in">str</span>, label: <span class="built_in">str</span> | <span class="literal">None</span> = <span class="literal">None</span>, **kwargs: <span class="type">Any</span></span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> self._manager.spawn(</span><br><span class="line">        task=task,</span><br><span class="line">        label=label,</span><br><span class="line">        origin_channel=self._origin_channel,</span><br><span class="line">        origin_chat_id=self._origin_chat_id,</span><br><span class="line">        session_key=self._session_key,</span><br><span class="line">    )</span><br></pre></td></tr></table></figure><p><code>SubagentManager.spawn</code> 方法（<a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/subagent.py#L69-L99">subagent.py</a>）接管后，会做两件事：</p><ol><li><strong>启动后台任务</strong>：通过 <code>asyncio.create_task(self._run_subagent(...))</code> 将耗时的调研任务丢到后台执行。</li><li><strong>立即回复主模型</strong>：返回类似 <code>&quot;Subagent [...] started (id: ...). I&#39;ll notify you when it completes.&quot;</code> 的字符串。这使得主智能体能立刻给用户一个响应，告知任务已在后台开启，而不必让用户在此处干等。</li></ol><h2 id="5-子智能体执行：专用的运行沙盒"><a href="#5-子智能体执行：专用的运行沙盒" class="headerlink" title="5. 子智能体执行：专用的运行沙盒"></a>5. 子智能体执行：专用的运行沙盒</h2><p>后台的 <code>_run_subagent</code> 任务（<a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/subagent.py#L101-L179">subagent.py</a>）代表了子智能体的实际运行过程。它的设计有几个关键特点：</p><ul><li><strong>隔离的工具集</strong>：它创建了一个全新的 <code>ToolRegistry</code>，包含了文件读写、Grep/Glob 检索，甚至 Web 搜索工具。但<strong>刻意去掉了</strong>发消息给用户的工具和再次生成子智能体的 <code>spawn</code> 工具，以防无限递归。</li><li><p><strong>独立的 System Prompt</strong>：在调用 LLM 之前，系统通过 <code>self._build_subagent_prompt()</code> 方法为子智能体构造了专属的人设。在 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/templates/agent/subagent_system.md">subagent_system.md</a> 模板中，系统提示词明确规定：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># Subagent</span></span><br><span class="line"></span><br><span class="line">&#123;&#123; time<span class="emphasis">_ctx &#125;&#125;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">You are a subagent spawned by the main agent to complete a specific task.</span></span><br><span class="line"><span class="emphasis">Stay focused on the assigned task. Your final response will be reported back to the main agent.</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">## Workspace</span></span><br><span class="line"><span class="emphasis">&#123;&#123; workspace &#125;&#125;</span></span><br></pre></td></tr></table></figure><p>这段提示词极大地压缩了主智能体复杂的闲聊属性，使得子智能体仅仅作为一个纯粹的“打工人”，去执行传入的 <code>task</code> 参数内容（即 <code>&#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;调研一下hermes agent&quot;&#125;</code>）。</p></li><li><strong>有限的迭代</strong>：它使用独立的 <code>AgentRunner</code> 跑一个上限为 15 轮的小迭代，防止子智能体在遇到死胡同时无限制地消耗 Tokens。</li></ul><p>在这个环节，子智能体带着上述提示词，会在代码库里检索 <code>hermes agent</code>，收集到相关结论后生成内部的总结报告。</p><h2 id="6-结果回注：巧妙的-system-消息通道"><a href="#6-结果回注：巧妙的-system-消息通道" class="headerlink" title="6. 结果回注：巧妙的 system 消息通道"></a>6. 结果回注：巧妙的 <code>system</code> 消息通道</h2><p>子智能体执行完毕后，并不能直接调用 API 发消息给用户。为了将结果带回给主智能体，它调用了 <code>_announce_result</code> 方法（<a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/subagent.py#L180-L210">subagent.py</a>）。</p><p>这里使用了非常巧妙的事件驱动设计：<strong>将结果伪装成一条来自 <code>system</code> 渠道的 <code>InboundMessage</code> 重新发布到消息总线上</strong>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">msg = InboundMessage(</span><br><span class="line">    channel=<span class="string">&quot;system&quot;</span>,</span><br><span class="line">    sender_id=<span class="string">&quot;subagent&quot;</span>,</span><br><span class="line">    chat_id=<span class="string">f&quot;<span class="subst">&#123;origin[<span class="string">&#x27;channel&#x27;</span>]&#125;</span>:<span class="subst">&#123;origin[<span class="string">&#x27;chat_id&#x27;</span>]&#125;</span>&quot;</span>,</span><br><span class="line">    content=announce_content,</span><br><span class="line">)</span><br><span class="line"><span class="keyword">await</span> self.bus.publish_inbound(msg)</span><br></pre></td></tr></table></figure><p><code>announce_content</code> 的渲染模板 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/templates/agent/subagent_announce.md">subagent_announce.md</a> 中还包含了一条隐藏指令：<em>“Summarize this naturally for the user. Keep it brief… Do not mention technical details like ‘subagent’ or task IDs.”</em>。</p><h2 id="7-二次转述：主智能体的最终回应"><a href="#7-二次转述：主智能体的最终回应" class="headerlink" title="7. 二次转述：主智能体的最终回应"></a>7. 二次转述：主智能体的最终回应</h2><p>这条带有系统通知的 <code>InboundMessage</code> 再次进入了主循环的 <code>_process_message</code>。</p><p>在 <a href="file:///Users/qixinbo/Projects/nanobot/nanobot/agent/loop.py#L519-L546">loop.py</a> 的 <code>system</code> 消息专用分支中，系统会从 <code>chat_id</code> 中解析出原始的 CLI 会话。由于消息的发送者是 <code>subagent</code>，系统会将其标记为 <code>current_role = &quot;assistant&quot;</code>，代表这是助手自身完成的后台任务结果：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">current_role = <span class="string">&quot;assistant&quot;</span> <span class="keyword">if</span> msg.sender_id == <span class="string">&quot;subagent&quot;</span> <span class="keyword">else</span> <span class="string">&quot;user&quot;</span></span><br><span class="line">messages = self.context.build_messages(..., current_role=current_role)</span><br><span class="line">final_content, _, all_msgs = <span class="keyword">await</span> self._run_agent_loop(messages, ...)</span><br></pre></td></tr></table></figure><p>带着之前历史上下文和这份子智能体的详细报告，主智能体（LLM）再次被唤醒。根据模板中的隐藏指令，LLM 会将繁杂的技术报告转化为对用户友好的自然语言摘要，最终封装为 <code>OutboundMessage</code> 返回给 CLI 渠道展示在屏幕上。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Nanobot 对“使用子智能体”这一请求的处理，优雅地展示了现代 Agent 框架的解耦美学：</p><ol><li><strong>渠道层</strong>：负责标准化 <code>InboundMessage</code>。</li><li><strong>意图层</strong>：不靠正则匹配，全权交由 LLM Function Calling 决策。</li><li><strong>执行层</strong>：后台异步调度，主从沙盒隔离。</li><li><strong>闭环层</strong>：利用事件总线回注系统消息，触发主智能体二次总结。</li></ol><p>这种基于事件总线和工具抽象的机制，赋予了系统极高的扩展性。</p>]]></content>
    
    
    <summary type="html">深入源码：Nanobot Agent 如何处理“使用子智能体”的交互指令
在多智能体（Multi-Agent）架构中，主智能体如何理解用户意图并动态派发任务给子智能体，是一个非常核心的设计。本文将以 Nanobot Agent 为例，详细拆解当用户在终端输入交互指令：“请使用子智能体调研一下hermes agent” 时，系统在底层是如何一步步流转的。

1. 接收输入：构造 InboundMessage
一切的起点始于用户在交互界面（例如 CLI 终端）输入文本。Nanobot 使用统一的事件总线（Message Bus）来解耦各个渠道和核心 Agent 逻辑。

当用户敲下回车后，CLI </summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着😺NanoBot学AI智能体设计和开发4：Python SDK！</title>
    <link href="http://qixinbo.github.io/2026/04/08/nanobot-4/"/>
    <id>http://qixinbo.github.io/2026/04/08/nanobot-4/</id>
    <published>2026-04-08T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>导语</strong>：Nanobot 不仅仅是一个命令行聊天工具，它底层是一个具备“全网搜索、网页抓取、文件操作、Shell 执行”的强大智能体框架。通过 Python SDK，可以将这套强大的 Agent 能力嵌入到任何 Python 业务中。</p></blockquote><p>本文将从最基础的单次对话开始，逐步解锁多轮记忆、工作区接管、内置工具链驱动，最终构建一个高并发的 Web API。</p><hr><h2 id="一、-初级篇：环境搭建与日志清理"><a href="#一、-初级篇：环境搭建与日志清理" class="headerlink" title="一、 初级篇：环境搭建与日志清理"></a>一、 初级篇：环境搭建与日志清理</h2><p>在最基础的场景中，我们只需要让 Nanobot 跑起来并回答问题。为了避免框架内部冗长的 <code>INFO</code>/<code>DEBUG</code> 日志污染你的终端，我们可以通过 <code>loguru</code> 屏蔽底层日志。</p><p><strong>实战代码 1：干净的单次问答</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"><span class="keyword">from</span> nanobot <span class="keyword">import</span> Nanobot</span><br><span class="line"></span><br><span class="line"><span class="comment"># 关闭 nanobot 框架内部的日志输出，让终端保持清爽</span></span><br><span class="line">logger.disable(<span class="string">&quot;nanobot&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">main</span>():</span></span><br><span class="line">    bot = Nanobot.from_config()</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;正在向 Nanobot 提问...&quot;</span>)</span><br><span class="line">    result = <span class="keyword">await</span> bot.run(<span class="string">&quot;请用一句话介绍 Python。&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;🤖 回答: <span class="subst">&#123;result.content&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    asyncio.run(main())</span><br></pre></td></tr></table></figure><hr><h2 id="二、-中级篇-1：构建-CLI-交互式多轮对话"><a href="#二、-中级篇-1：构建-CLI-交互式多轮对话" class="headerlink" title="二、 中级篇 1：构建 CLI 交互式多轮对话"></a>二、 中级篇 1：构建 CLI 交互式多轮对话</h2><p>单个问题往往不够，我们需要一个可以持续对话的控制台程序。要让 Nanobot 记住上下文（即实现“多轮对话”），核心在于<strong>复用同一个 <code>session_key</code></strong>。</p><p><strong>实战代码 2：打造你自己的 ChatGPT 终端</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"><span class="keyword">from</span> nanobot <span class="keyword">import</span> Nanobot</span><br><span class="line"></span><br><span class="line">logger.disable(<span class="string">&quot;nanobot&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">main</span>():</span></span><br><span class="line">    bot = Nanobot.from_config()</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;========================================&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;🤖 Nanobot CLI 模式已启动！(输入 &#x27;exit&#x27; 退出)&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;========================================&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 设定一个固定的 session_key，这是保持多轮对话记忆的关键</span></span><br><span class="line">    SESSION_ID = <span class="string">&quot;my-cli-session&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            user_input = <span class="built_in">input</span>(<span class="string">&quot;\n🧑 你: &quot;</span>).strip()</span><br><span class="line">        <span class="keyword">except</span> (KeyboardInterrupt, EOFError):</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">            </span><br><span class="line">        <span class="keyword">if</span> user_input.lower() <span class="keyword">in</span> [<span class="string">&#x27;exit&#x27;</span>, <span class="string">&#x27;quit&#x27;</span>]:</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> user_input:</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">            </span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;🤖 思考中...&quot;</span>, end=<span class="string">&quot;\r&quot;</span>)</span><br><span class="line">        result = <span class="keyword">await</span> bot.run(user_input, session_key=SESSION_ID)</span><br><span class="line">        </span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot; &quot;</span> * <span class="number">20</span>, end=<span class="string">&quot;\r&quot;</span>) </span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;🤖 Nanobot: <span class="subst">&#123;result.content&#125;</span>&quot;</span>)</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> result.tools_used:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;   [🔧 辅助操作: 使用了 <span class="subst">&#123;<span class="string">&#x27;, &#x27;</span>.join(result.tools_used)&#125;</span> 工具]&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    asyncio.run(main())</span><br></pre></td></tr></table></figure><hr><h2 id="三、-中级篇-2：工作区接管与多用户隔离"><a href="#三、-中级篇-2：工作区接管与多用户隔离" class="headerlink" title="三、 中级篇 2：工作区接管与多用户隔离"></a>三、 中级篇 2：工作区接管与多用户隔离</h2><p>在企业级开发中（例如构建一个面向多用户的微信机器人），所有用户不能共享同一个对话历史，同时 Agent 可能需要针对特定目录下的代码文件进行操作。</p><p><strong>实战代码 3：多用户聊天模拟器</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"><span class="keyword">from</span> nanobot <span class="keyword">import</span> Nanobot</span><br><span class="line"></span><br><span class="line">logger.disable(<span class="string">&quot;nanobot&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">main</span>():</span></span><br><span class="line">    <span class="comment"># 强制指定 Agent 工作的根目录，文件读写等工具将被限制在此目录下</span></span><br><span class="line">    bot = Nanobot.from_config(workspace=<span class="string">&quot;/var/www/my_project&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;--- 👱‍♀️ Alice 的会话 ---&quot;</span>)</span><br><span class="line">    <span class="keyword">await</span> bot.run(<span class="string">&quot;你好，我是前端开发 Alice&quot;</span>, session_key=<span class="string">&quot;user-alice&quot;</span>)</span><br><span class="line">    res_alice = <span class="keyword">await</span> bot.run(<span class="string">&quot;你还记得我是做什么的吗？&quot;</span>, session_key=<span class="string">&quot;user-alice&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;Alice 收到回答: <span class="subst">&#123;res_alice.content&#125;</span>&quot;</span>) </span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n--- 👨‍🦱 Bob 的会话 ---&quot;</span>)</span><br><span class="line">    <span class="comment"># Bob 属于另一个 session_key，无法访问 Alice 的记忆</span></span><br><span class="line">    res_bob = <span class="keyword">await</span> bot.run(<span class="string">&quot;你还记得我是做什么的吗？&quot;</span>, session_key=<span class="string">&quot;user-bob&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;Bob 收到回答: <span class="subst">&#123;res_bob.content&#125;</span>&quot;</span>) </span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    asyncio.run(main())</span><br></pre></td></tr></table></figure><hr><h2 id="四、-高级篇-1：释放-Nanobot-的内置工具链"><a href="#四、-高级篇-1：释放-Nanobot-的内置工具链" class="headerlink" title="四、 高级篇 1：释放 Nanobot 的内置工具链"></a>四、 高级篇 1：释放 Nanobot 的内置工具链</h2><p>Nanobot 底层不仅是一个聊天模型，它还内置了强大的工具链：</p><ol><li><strong>网络工具</strong>：<code>web_search</code>（全网搜索）、<code>web_fetch</code>（网页提取为 Markdown）。</li><li><strong>系统工具</strong>：<code>exec</code>（受限 Shell 执行）、<code>read_file</code> / <code>write_file</code> / <code>edit_file</code> / <code>grep</code> / <code>glob</code> 等。</li></ol><p>你可以直接用自然语言向它下达复合指令，它会自动规划并调用这些工具。</p><p><strong>实战代码 4：自动网页抓取与文件分析助手</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"><span class="keyword">from</span> nanobot <span class="keyword">import</span> Nanobot</span><br><span class="line"></span><br><span class="line">logger.disable(<span class="string">&quot;nanobot&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">main</span>():</span></span><br><span class="line">    <span class="comment"># 设定一个测试工作区</span></span><br><span class="line">    bot = Nanobot.from_config(workspace=<span class="string">&quot;./research_workspace&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    task_prompt = <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    1. 请帮我搜索一下 &#x27;2024年诺贝尔物理学奖得主是谁&#x27;。</span></span><br><span class="line"><span class="string">    2. 找到确切结果后，将他们的名字和主要贡献总结成一段话。</span></span><br><span class="line"><span class="string">    3. 将这段话保存到当前工作区的 &#x27;nobel_2024.md&#x27; 文件中。</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;🤖 正在执行复杂的网络检索与文件写入任务，请稍候...&quot;</span>)</span><br><span class="line">    result = <span class="keyword">await</span> bot.run(task_prompt, session_key=<span class="string">&quot;research-task&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n✅ 任务完成！&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;最终汇报:\n<span class="subst">&#123;result.content&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;\n🛠️ 期间 Agent 自主调用的工具列表: <span class="subst">&#123;result.tools_used&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    asyncio.run(main())</span><br></pre></td></tr></table></figure><p><em>在这个例子中，Agent 会自动调用 <code>web_search</code> 查找新闻，然后调用 <code>write_file</code> 写入文件，你无需编写任何爬虫或文件 IO 代码！</em></p><hr><h2 id="五、-高级篇-2：利用-Hooks-实现流式输出与安全审计"><a href="#五、-高级篇-2：利用-Hooks-实现流式输出与安全审计" class="headerlink" title="五、 高级篇 2：利用 Hooks 实现流式输出与安全审计"></a>五、 高级篇 2：利用 Hooks 实现流式输出与安全审计</h2><p>SDK 最强大的特性是 <strong>Hooks（钩子）</strong>。它允许你在不修改 Nanobot 核心代码的情况下，监控、拦截 Agent 的行为。我们可以利用 <code>on_stream</code> 钩子，给前面的 CLI 对话加上<strong>流式打字机效果</strong>。</p><p><strong>实战代码 5：支持流式输出与审计的高级终端</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"><span class="keyword">from</span> nanobot <span class="keyword">import</span> Nanobot</span><br><span class="line"><span class="keyword">from</span> nanobot.agent <span class="keyword">import</span> AgentHook, AgentHookContext</span><br><span class="line"></span><br><span class="line">logger.disable(<span class="string">&quot;nanobot&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 1. 流式输出 Hook：捕获每个 token 并实时打印</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">StreamingHook</span>(<span class="params">AgentHook</span>):</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">on_stream</span>(<span class="params">self, ctx: AgentHookContext, delta: <span class="built_in">str</span></span>) -&gt; <span class="literal">None</span>:</span></span><br><span class="line">        sys.stdout.write(delta)</span><br><span class="line">        sys.stdout.flush()</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">on_stream_end</span>(<span class="params">self, ctx: AgentHookContext</span>) -&gt; <span class="literal">None</span>:</span></span><br><span class="line">        sys.stdout.write(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 审计 Hook：记录所有将要执行的工具操作</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AuditHook</span>(<span class="params">AgentHook</span>):</span></span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">before_execute_tools</span>(<span class="params">self, ctx: AgentHookContext</span>) -&gt; <span class="literal">None</span>:</span></span><br><span class="line">        <span class="keyword">for</span> tc <span class="keyword">in</span> ctx.tool_calls:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;\n⚠️ [安全审计] 拦截到 Agent 尝试调用工具: <span class="subst">&#123;tc.name&#125;</span> (<span class="subst">&#123;tc.arguments&#125;</span>)&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">main</span>():</span></span><br><span class="line">    bot = Nanobot.from_config()</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;🤖 高级流式终端已启动！请提问：&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 组合传入多个 Hook</span></span><br><span class="line">    hooks = [StreamingHook(), AuditHook()]</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        user_input = <span class="built_in">input</span>(<span class="string">&quot;\n🧑 你: &quot;</span>).strip()</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> user_input: <span class="keyword">continue</span></span><br><span class="line">            </span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;🤖 Nanobot: &quot;</span>, end=<span class="string">&quot;&quot;</span>)</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 运行并传入 hooks，Bot 的回答将通过 StreamingHook 实时打印</span></span><br><span class="line">        <span class="keyword">await</span> bot.run(user_input, session_key=<span class="string">&quot;stream-session&quot;</span>, hooks=hooks)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    asyncio.run(main())</span><br></pre></td></tr></table></figure><hr><h2 id="六、-终极实战：构建一个基于-FastAPI-的智能-Agent-API"><a href="#六、-终极实战：构建一个基于-FastAPI-的智能-Agent-API" class="headerlink" title="六、 终极实战：构建一个基于 FastAPI 的智能 Agent API"></a>六、 终极实战：构建一个基于 FastAPI 的智能 Agent API</h2><p>将上述能力综合起来，我们可以将其封装成一个标准的 Web 后端 API。供前端、小程序或客户端调用。</p><p><strong>实战代码 6：结合 FastAPI 构建高并发后端</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 依赖安装: pip install fastapi uvicorn pydantic</span></span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">from</span> fastapi <span class="keyword">import</span> FastAPI, Header</span><br><span class="line"><span class="keyword">from</span> pydantic <span class="keyword">import</span> BaseModel</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"><span class="keyword">from</span> nanobot <span class="keyword">import</span> Nanobot</span><br><span class="line"><span class="keyword">from</span> nanobot.agent <span class="keyword">import</span> AgentHook, AgentHookContext</span><br><span class="line"></span><br><span class="line">logger.disable(<span class="string">&quot;nanobot&quot;</span>)</span><br><span class="line"></span><br><span class="line">app = FastAPI()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 全局单例加载 Bot</span></span><br><span class="line">bot = Nanobot.from_config(workspace=<span class="string">&quot;./api_workspace&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 自定义性能监控 Hook</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TimingHook</span>(<span class="params">AgentHook</span>):</span></span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">before_iteration</span>(<span class="params">self, ctx: AgentHookContext</span>) -&gt; <span class="literal">None</span>:</span></span><br><span class="line">        ctx.metadata[<span class="string">&quot;_t0&quot;</span>] = time.time()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">after_iteration</span>(<span class="params">self, ctx: AgentHookContext, response</span>) -&gt; <span class="literal">None</span>:</span></span><br><span class="line">        elapsed = time.time() - ctx.metadata.get(<span class="string">&quot;_t0&quot;</span>, <span class="number">0</span>)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;[监控] LLM 思考耗时: <span class="subst">&#123;elapsed:<span class="number">.2</span>f&#125;</span>s&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ChatRequest</span>(<span class="params">BaseModel</span>):</span></span><br><span class="line">    message: <span class="built_in">str</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/api/v1/agent/chat&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">chat_endpoint</span>(<span class="params">request: ChatRequest, user_id: <span class="built_in">str</span> = Header(<span class="params">..., alias=<span class="string">&quot;X-User-Id&quot;</span></span>)</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    处理对话请求，使用 X-User-Id 请求头进行会话隔离</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    session_key = <span class="string">f&quot;web_session_<span class="subst">&#123;user_id&#125;</span>&quot;</span></span><br><span class="line">    </span><br><span class="line">    result = <span class="keyword">await</span> bot.run(</span><br><span class="line">        request.message,</span><br><span class="line">        session_key=session_key,</span><br><span class="line">        hooks=[TimingHook()]</span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>,</span><br><span class="line">        <span class="string">&quot;user_id&quot;</span>: user_id,</span><br><span class="line">        <span class="string">&quot;reply&quot;</span>: result.content,</span><br><span class="line">        <span class="string">&quot;tools_used&quot;</span>: result.tools_used</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动命令: uvicorn main:app --host 0.0.0.0 --port 8000</span></span><br></pre></td></tr></table></figure><h2 id="七、-终极硬核篇：通过底层消息系统发送图片附件-多模态"><a href="#七、-终极硬核篇：通过底层消息系统发送图片附件-多模态" class="headerlink" title="七、 终极硬核篇：通过底层消息系统发送图片附件 (多模态)"></a>七、 终极硬核篇：通过底层消息系统发送图片附件 (多模态)</h2><p>在 Nanobot 的设计中，高层的 <code>bot.run(message)</code> 是为了纯文本对话设计的便捷接口。如果你在做深度的 SDK 集成（例如将 Nanobot 接入微信、企业内部 IM），当用户直接发来一张图片时，你需要构造底层的 <code>InboundMessage</code> 并通过底层的消息处理方法交由 Agent 分析。</p><p>Agent 在处理带有 <code>media</code>（附件）的消息时，会自动读取该路径的图片，将其转换为大模型支持的视觉输入块（Base64），从而让 Agent 具备“看图说话”的能力。</p><p><strong>实战代码 7：模拟 IM 机器人接收并分析图片</strong><br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"><span class="keyword">from</span> nanobot <span class="keyword">import</span> Nanobot</span><br><span class="line"><span class="keyword">from</span> nanobot.bus.events <span class="keyword">import</span> InboundMessage</span><br><span class="line"></span><br><span class="line">logger.disable(<span class="string">&quot;nanobot&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">main</span>():</span></span><br><span class="line">    bot = Nanobot.from_config()</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 1. 构造一条底层的 InboundMessage</span></span><br><span class="line">    <span class="comment"># 假设这是从微信或 Slack 接收到的消息和下载到本地的图片</span></span><br><span class="line">    msg = InboundMessage(</span><br><span class="line">        channel=<span class="string">&quot;wechat&quot;</span>,          <span class="comment"># 来源渠道</span></span><br><span class="line">        sender_id=<span class="string">&quot;user_10086&quot;</span>,    <span class="comment"># 用户 ID</span></span><br><span class="line">        chat_id=<span class="string">&quot;chat_001&quot;</span>,        <span class="comment"># 会话 ID</span></span><br><span class="line">        content=<span class="string">&quot;请帮我分析一下这张架构图里有几个核心模块？&quot;</span>,</span><br><span class="line">        media=[<span class="string">&quot;/absolute/path/to/architecture_diagram.png&quot;</span>]  <span class="comment"># 图片的绝对路径</span></span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;📸 正在将图片和问题发送给 Nanobot...&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 2. 绕过 bot.run()，直接调用底层的 _process_message 处理多模态消息</span></span><br><span class="line">    <span class="comment"># 这样可以同步等待结果返回，而不必去处理复杂的异步总线队列 (bus)</span></span><br><span class="line">    session_key = <span class="string">f&quot;im_session_<span class="subst">&#123;msg.sender_id&#125;</span>&quot;</span></span><br><span class="line">    result = <span class="keyword">await</span> bot._loop._process_message(msg, session_key=session_key)</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n✅ 分析完成！&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;🤖 回答:\n<span class="subst">&#123;result.content&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    asyncio.run(main())</span><br></pre></td></tr></table></figure><br><em>注意：这里的 <code>media</code> 列表可以包含多个图片的绝对路径，Agent 会将它们一起发送给大模型进行多模态推理。</em></p>]]></content>
    
    
    <summary type="html">导语：Nanobot 不仅仅是一个命令行聊天工具，它底层是一个具备“全网搜索、网页抓取、文件操作、Shell 执行”的强大智能体框架。通过 Python SDK，可以将这套强大的 Agent 能力嵌入到任何 Python 业务中。

本文将从最基础的单次对话开始，逐步解锁多轮记忆、工作区接管、内置工具链驱动，最终构建一个高并发的 Web API。




一、 初级篇：环境搭建与日志清理
在最基础的场景中，我们只需要让 Nanobot 跑起来并回答问题。为了避免框架内部冗长的 INFO/DEBUG 日志污染你的终端，我们可以通过 loguru 屏蔽底层日志。

实战代码 1：干净的单次问答

</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>Vibe Coding了一个“🦞龙虾问数”项目</title>
    <link href="http://qixinbo.github.io/2026/03/24/dataclaw/"/>
    <id>http://qixinbo.github.io/2026/03/24/dataclaw/</id>
    <published>2026-03-24T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.897Z</updated>
    
    <content type="html"><![CDATA[<p>这几天Vibe Coding了一个🦞龙虾问数系统——DataClaw：<a href="https://github.com/qixinbo/DataClaw">https://github.com/qixinbo/DataClaw</a><br>欢迎大家 ⭐Star + 💻PR，Let’s code!<br>目标就是通过说人话来搞定复杂的数据分析。<br>主要特点就是：<br>🗣️ 大白话提问，自动转 SQL，还能自我纠错（底层基于Nanobot）<br>🛠️ 支持Skills，业务逻辑随意定制<br>📊 图表一键 Pin 到看板</p><h1 id="🦞-龙虾问数-DataClaw"><a href="#🦞-龙虾问数-DataClaw" class="headerlink" title="🦞 龙虾问数 (DataClaw)"></a>🦞 龙虾问数 (DataClaw)</h1><blockquote><p><strong>释放你的数据潜能，让分析像养龙虾一样简单爽快！</strong> 🌊📊<br>龙虾问数 (DataClaw) 是一个智能的、AI 驱动的数据分析平台。通过自然语言与你的数据对话，瞬间生成可视化图表，轻松搭建仪表盘——从此告别繁琐的 SQL 语句！</p></blockquote><hr><h2 id="✨-为什么选择龙虾问数？"><a href="#✨-为什么选择龙虾问数？" class="headerlink" title="✨ 为什么选择龙虾问数？"></a>✨ 为什么选择龙虾问数？</h2><p>受够了为了画个简单的柱状图而写半天复杂的 SQL 语句吗？龙虾问数就是你的私人数据科学家。借助强大的大语言模型 (LLM) 和智能 Agent 工作流，它能将你的自然语言提问精准转化为数据库查询，提取数据，并即时渲染出美观的可视化图表。</p><p>无论你是要查询庞大的 Supabase/PostgreSQL 数据库，还是随手丢进一个 CSV 文件，龙虾问数都能轻松拿捏！🚀</p><h2 id="🌟-核心特性"><a href="#🌟-核心特性" class="headerlink" title="🌟 核心特性"></a>🌟 核心特性</h2><ul><li><strong>🗣️ 自然语言转 SQL</strong>: 用大白话提问！它能理解你的数据表结构，生成准确的 SQL，甚至在报错时进行自我纠正 (Self-correction)。</li><li><strong>📈 即时数据可视化</strong>: 拒绝枯燥的生肉表格，根据数据特征自动生成交互式图表。</li><li><strong>🗂️ 动态多数据源</strong>: 无缝连接 PostgreSQL、Supabase，以及本地 CSV/Excel 文件上传解析。</li><li><strong>🧠 灵活的模型接入</strong>: 原生集成 LiteLLM，支持随插随用 OpenAI、DeepSeek、智谱、通义千问 (DashScope)、火山引擎或任何兼容的 LLM 提供商。</li><li><strong>🛠️ 强大的 Agent 技能拓展</strong>: 基于核心 <code>nanobot</code>框架（<code>OpenClaw</code>的精简版）构建。支持通过斜杠命令 (<code>/</code>) 快速调用自定义工具 (Skills)，完美贴合特定业务逻辑。</li><li><strong>📊 可定制仪表盘 (Dashboard)</strong>: 一键将对话中生成的图表固定到看板，拖拽布局，随时查看核心指标。</li></ul><hr><h2 id="🏗️-项目架构"><a href="#🏗️-项目架构" class="headerlink" title="🏗️ 项目架构"></a>🏗️ 项目架构</h2><p>DataClaw 的架构主要分为三只“大钳子”：</p><ol><li><strong><code>frontend/</code></strong> 🎨: 闪亮的外壳。基于 <strong>React 19</strong>、<strong>Vite</strong>、<strong>TailwindCSS</strong> 和 <strong>Zustand</strong> 构建。拥有类似微信/ChatGPT的对话界面、支持流式思考过程渲染以及交互式图表展示。</li><li><strong><code>backend/</code></strong> ⚙️: 强健的肌肉。一个 <strong>FastAPI</strong> 后端服务，负责管理项目、数据源连接、用户会话持久化以及作为 API 网关。</li><li><strong><code>nanobot/</code></strong> 🧠: 智慧的大脑。核心的 AI Agent 框架，负责处理意图路由、NL2SQL 转换、Schema 缓存管理以及与 LLM 的底层交互。</li></ol><hr><h2 id="🚀-快速开始"><a href="#🚀-快速开始" class="headerlink" title="🚀 快速开始"></a>🚀 快速开始</h2><p>准备好大显身手了吗？让我们把龙虾问数在你的本地跑起来！</p><h3 id="1-后端服务启动-🐍"><a href="#1-后端服务启动-🐍" class="headerlink" title="1. 后端服务启动 🐍"></a>1. 后端服务启动 🐍</h3><p>请确保你已安装 Python 3.10 或以上版本。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> backend</span><br><span class="line"><span class="comment"># 创建虚拟环境（可选但强烈建议）</span></span><br><span class="line">python -m venv .venv</span><br><span class="line"><span class="built_in">source</span> .venv/bin/activate</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装依赖</span></span><br><span class="line">pip install -r requirements.txt</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动 FastAPI 服务器</span></span><br><span class="line">uvicorn app.main:app --reload --port 8000</span><br></pre></td></tr></table></figure><p><em>提示：请确保</em> <em><code>nanobot</code></em> <em>核心库已根据项目工作区的要求正确链接或以可编辑模式 (editable mode) 安装。</em></p><h3 id="2-前端服务启动-⚛️"><a href="#2-前端服务启动-⚛️" class="headerlink" title="2. 前端服务启动 ⚛️"></a>2. 前端服务启动 ⚛️</h3><p>请确保你已安装 Node.js 18 或以上版本。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> frontend</span><br><span class="line"><span class="comment"># 安装依赖</span></span><br><span class="line">npm install</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动 Vite 开发服务器</span></span><br><span class="line">npm run dev</span><br></pre></td></tr></table></figure><h3 id="3-初始账号配置-👤"><a href="#3-初始账号配置-👤" class="headerlink" title="3. 初始账号配置 👤"></a>3. 初始账号配置 👤</h3><p>系统首次注册的用户将自动成为管理员。您可以在登录页面直接点击“注册”按钮创建您的管理员账号（例如：用户名 <code>admin</code>，密码 <code>admin</code>），随后即可登录并管理项目、数据源和用户。</p><hr><h2 id="🔌-数据源配置说明"><a href="#🔌-数据源配置说明" class="headerlink" title="🔌 数据源配置说明"></a>🔌 数据源配置说明</h2><p>DataClaw 支持连接多种类型的数据源，以满足不同场景的分析需求。你可以在界面的 <strong>Data Sources</strong> 菜单中点击 <strong>+</strong> 新建并配置它们。以下是常见数据源的详细接入指南：</p><details><summary><b>▶ PostgreSQL (pgsql)</b></summary>连接标准的关系型数据库。你既可以通过表单填充分散的参数，也可以直接粘贴完整的 Connection String。- **Host**: 数据库的主机地址。如果你是在本地电脑运行了数据库（如使用 pgAdmin），请填入 `127.0.0.1`（不要填 `localhost`，以避免 Unix Socket 解析错误）。- **Port**: 默认一般为 `5432`。- **Database**: 你要连接的具体数据库名称。- **Username / Password**: 数据库的认证凭据（默认用户通常是 `postgres`）。- **Connection String (可选)**: 也可以直接输入类似 `postgresql://postgres:你的密码@127.0.0.1:5432/你的数据库名` 的字符串，它将覆盖上述单独的输入框配置。</details><details><summary><b>▶ Supabase</b></summary>专门针对 Supabase 云端 PostgreSQL 数据库优化的连接方式，强制开启 SSL 且默认使用连接池以提高稳定性。- 推荐直接使用 **Connection String** 配置：  进入你的 Supabase 项目控制台 -> `Project Settings` -> `Database` -> `Connection string` -> 选择 `URI` 选项卡。  复制那串类似 `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?sslmode=require` 的链接并填入。- *注意*: Supabase 默认开启了 Transaction Pooler（端口 6543）。如果想要直连（Direct connection），请将端口改为 `5432`，并确保 URL 中包含 `sslmode=require`。</details><details><summary><b>▶ SQLite</b></summary>轻量级的本地文件型数据库，非常适合快速测试或分析单机应用数据。- **File Upload**: 你可以直接点击按钮，从本地上传 `.db`、`.sqlite` 或 `.sqlite3` 后缀的数据库文件。文件会被安全地保存在服务端的上传目录中供分析使用。- **File Path (进阶)**: 如果服务部署在服务器上，且 SQLite 文件已存在于服务器的某个绝对路径中，你也可以直接在输入框中填入该文件的绝对路径（如 `/data/my_app.db`）。</details><details><summary><b>▶ CSV</b></summary>最常见的数据交换格式，即插即用，无需复杂的数据库配置。- **File Upload**: 与 SQLite 类似，点击按钮选择本地的 `.csv` 文件上传即可。系统会在后台利用 DuckDB 或 Pandas 等引擎将其虚拟化为一个可供 SQL 查询的表。- 上传成功后，在对话界面中，你可以直接把这个 CSV 文件当作一张数据库表来“提问”！</details><hr><h2 id="🤝-参与贡献"><a href="#🤝-参与贡献" class="headerlink" title="🤝 参与贡献"></a>🤝 参与贡献</h2><p>有个好点子？发现了一个 Bug？非常欢迎你的加入！随时可以提交 Issue 或 Pull Request。让我们一起让数据分析变得更加有趣！</p><hr><h2 id="💖-特别鸣谢"><a href="#💖-特别鸣谢" class="headerlink" title="💖 特别鸣谢"></a>💖 特别鸣谢</h2><p>DataClaw 的开发深受以下优秀开源项目的启发，特此致谢：</p><ul><li><a href="https://github.com/Canner/WrenAI">WrenAI</a>: 强大的 Text-to-SQL 解决方案，其架构和思路给了我们很大的启发。</li><li><a href="https://github.com/apconw/Aix-DB">Aix-DB</a>: 在智能数据分析和交互式体验方面提供了极好的参考。</li></ul><p><br /></p>]]></content>
    
    
    <summary type="html">这几天Vibe Coding了一个🦞龙虾问数系统——DataClaw：https://github.com/qixinbo/DataClaw
欢迎大家 ⭐Star + 💻PR，Let’s code!
目标就是通过说人话来搞定复杂的数据分析。
主要特点就是：
🗣️ 大白话提问，自动转 SQL，还能自我纠错（底层基于Nanobot）
🛠️ 支持Skills，业务逻辑随意定制
📊 图表一键 Pin 到看板

🦞 龙虾问数 (DataClaw)
释放你的数据潜能，让分析像养龙虾一样简单爽快！ 🌊📊
龙虾问数 (DataClaw) 是一个智能的、AI 驱动的数据分析平台。通过自然语言与你</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着😺NanoBot学AI智能体设计和开发3：一夫当关！</title>
    <link href="http://qixinbo.github.io/2026/02/04/nanobot-3/"/>
    <id>http://qixinbo.github.io/2026/02/04/nanobot-3/</id>
    <published>2026-02-04T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是<code>typescript</code>语言），或者想自己魔改加个小功能，往往要翻半天文档。</p><p>直到我遇到了 <strong>nanobot</strong>。</p><p>它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。</p><p>nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、Discord、Slack，甚至是国内的飞书、钉钉、QQ 和微信（通过 Mochat）。我现在把它挂在飞书上，平时想查个资料、翻译段文本，或者只是单纯想找个“人”聊聊代码思路，随时掏出手机就能发消息，它会像一个真正的助理一样回复你。</p><p>那么就从nanobot源码开始学习AI个人助理吧。</p><p><a href="https://www.qixinbo.info/2026/02/01/nanobot-0/">0在这里</a><br><a href="https://www.qixinbo.info/2026/02/02/nanobot-1/">1在这里</a><br><a href="https://www.qixinbo.info/2026/02/03/nanobot-2/">2在这里</a></p><h1 id="一夫当关"><a href="#一夫当关" class="headerlink" title="一夫当关"></a>一夫当关</h1><p>前面是看了“怎样只聊一句”，现在看看“网关模式”做了啥。</p><h1 id="具体流程"><a href="#具体流程" class="headerlink" title="具体流程"></a>具体流程</h1><p><code>nanobot gateway</code> 是 Nanobot 的<strong>核心后台服务进程</strong>。简单来说，它就像一个不知疲倦的“接线员”兼“大脑”，负责连接外部世界（如 Telegram/微信）、管理内部任务（如定时提醒），并指挥 AI（Agent）进行思考和回复。</p><p>以下是 <code>nanobot gateway</code> 启动和运行的详细步骤解析：</p><h2 id="第一阶段：初始化-Initialization"><a href="#第一阶段：初始化-Initialization" class="headerlink" title="第一阶段：初始化 (Initialization)"></a>第一阶段：初始化 (Initialization)</h2><p>当你运行 <code>nanobot gateway</code> 命令时，系统会按顺序完成以下准备工作：</p><ol><li><strong>加载配置</strong> (<code>load_config</code>)：读取你的 <code>~/.nanobot/config.json</code> 配置文件，确定要启用哪些功能。</li><li><strong>创建消息总线</strong> (<code>MessageBus</code>)：这是一个内部的通信管道，用于在“接收消息的渠道”和“处理消息的 Agent”之间传递数据。</li><li><strong>准备大脑</strong> (<code>AgentLoop</code>)：初始化 AI 核心循环。它加载了 LLM 提供商（如 OpenAI/Claude）、工具集（搜索、文件操作等）以及记忆管理器。</li><li><strong>准备定时器</strong> (<code>CronService</code> &amp; <code>HeartbeatService</code>)：<ul><li><strong>Cron</strong>: 加载保存的定时任务（如“每天早上 8 点提醒我看新闻”）。</li><li><strong>Heartbeat</strong>: 设置心跳机制（默认每 30 分钟），用于自我保活或周期性自检。</li></ul></li><li><strong>准备渠道</strong> (<code>ChannelManager</code>)：根据配置初始化外部通信渠道（如 Telegram, WhatsApp, Slack 等）。</li></ol><p>这个地方与之前的“只聊一句”又有很大差别，引入了新概念。</p><h2 id="SessionManager"><a href="#SessionManager" class="headerlink" title="SessionManager"></a>SessionManager</h2><p><code>SessionManager</code> 类是 Nanobot 用来管理聊天会话（Conversation Sessions）的核心组件。它的主要职责是<strong>持久化存储</strong>和<strong>检索</strong>用户与机器人之间的聊天记录。</p><p>它通过管理 <code>Session</code> 对象来实现这一功能，每个 <code>Session</code> 代表一个独立的对话上下文（例如你和机器人在 Telegram 上的私聊，或者你在命令行里的即兴对话）。</p><h3 id="核心功能"><a href="#核心功能" class="headerlink" title="核心功能"></a>核心功能</h3><ol><li><p><strong>会话存储 (Storage)</strong></p><ul><li><strong>位置</strong>: 所有会话都保存在 <code>~/.nanobot/sessions/</code> 目录下。</li><li><strong>格式</strong>: 采用 <strong>JSONL (JSON Lines)</strong> 格式。<ul><li>文件名通常是 <code>channel_chatid.jsonl</code>（例如 <code>telegram_123456789.jsonl</code>）。</li><li>文件的第一行始终是<strong>元数据 (Metadata)</strong>，包含创建时间、更新时间等。</li><li>后续每一行代表一条<strong>消息 (Message)</strong>，包含角色（user/assistant）、内容、时间戳等。</li></ul></li><li><strong>优点</strong>: JSONL 格式允许追加写入（Append-only），性能好且易于阅读和修复。</li></ul></li><li><p><strong>内存缓存 (Caching)</strong></p><ul><li>为了减少磁盘 I/O，<code>SessionManager</code> 会在内存中维护一个 <code>_cache</code> 字典。</li><li>当你频繁与机器人对话时，它会直接从内存中读取会话对象，只有在保存时才写入磁盘。</li></ul></li></ol><h3 id="关键类与方法"><a href="#关键类与方法" class="headerlink" title="关键类与方法"></a>关键类与方法</h3><h4 id="1-Session-数据类"><a href="#1-Session-数据类" class="headerlink" title="1. Session (数据类)"></a>1. <code>Session</code> (数据类)</h4><p>代表单个会话的数据结构。</p><ul><li><strong>属性</strong>:<ul><li><code>key</code>: 唯一标识符，格式通常为 <code>channel:chat_id</code>（如 <code>telegram:12345</code>）。</li><li><code>messages</code>: 消息列表。</li><li><code>created_at</code> / <code>updated_at</code>: 时间戳。</li></ul></li><li><strong>方法</strong>:<ul><li><code>add_message(role, content)</code>: 添加一条新消息。</li><li><code>get_history(max_messages)</code>: 获取最近的 N 条消息，并格式化为 LLM（大模型）可接受的格式（只保留 <code>role</code> 和 <code>content</code>）。</li><li><code>clear()</code>: 清空当前会话的所有消息。</li></ul></li></ul><h4 id="2-SessionManager-管理器类"><a href="#2-SessionManager-管理器类" class="headerlink" title="2. SessionManager (管理器类)"></a>2. <code>SessionManager</code> (管理器类)</h4><p>负责对 <code>Session</code> 进行增删改查。</p><ul><li><strong><code>__init__(workspace)</code></strong>: 初始化，确保 <code>~/.nanobot/sessions</code> 目录存在。</li><li><strong><code>get_or_create(key)</code></strong>: <strong>最常用的方法</strong>。<ol><li>先查内存缓存。</li><li>缓存没有，就去磁盘加载。</li><li>磁盘也没有，就创建一个新的空白会话。</li></ol></li><li><strong><code>save(session)</code></strong>: 将会话状态写回磁盘。它会重写整个文件，确保元数据和消息列表都是最新的。</li><li><strong><code>delete(key)</code></strong>: 删除某个会话（从内存移除并删除对应的 <code>.jsonl</code> 文件）。</li><li><strong><code>list_sessions()</code></strong>: 扫描目录，读取所有 <code>.jsonl</code> 文件的第一行（元数据），返回所有会话的列表，按更新时间倒序排列。</li></ul><h3 id="代码结构图解"><a href="#代码结构图解" class="headerlink" title="代码结构图解"></a>代码结构图解</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">classDiagram</span><br><span class="line">    class SessionManager &#123;</span><br><span class="line">        - Path sessions_dir</span><br><span class="line">        - dict _cache</span><br><span class="line">        + get_or_create(key) Session</span><br><span class="line">        + save(session)</span><br><span class="line">        + delete(key)</span><br><span class="line">        + list_sessions()</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    class Session &#123;</span><br><span class="line">        + str key</span><br><span class="line">        + list messages</span><br><span class="line">        + datetime created_at</span><br><span class="line">        + add_message(role, content)</span><br><span class="line">        + get_history(max_messages)</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    SessionManager &quot;1&quot; --&gt; &quot;*&quot; Session : manages</span><br></pre></td></tr></table></figure><h3 id="实际工作流程示例"><a href="#实际工作流程示例" class="headerlink" title="实际工作流程示例"></a>实际工作流程示例</h3><p>当你发一条消息 “Hello” 给机器人时：</p><ol><li><strong>加载</strong>: <code>gateway</code> 调用 <code>manager.get_or_create(&quot;telegram:123&quot;)</code>。管理器发现内存里没有，于是去读 <code>~/.nanobot/sessions/telegram_123.jsonl</code>，加载之前的聊天记录。</li><li><strong>更新</strong>: Agent 处理完消息后，调用 <code>session.add_message(&quot;user&quot;, &quot;Hello&quot;)</code> 和 <code>session.add_message(&quot;assistant&quot;, &quot;Hi there!&quot;)</code>。</li><li><strong>保存</strong>: Agent 调用 <code>manager.save(session)</code>。管理器将更新后的消息列表写回 <code>jsonl</code> 文件。</li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p><code>SessionManager</code> 是 Nanobot 的<strong>记忆中枢</strong>。它简单、高效地利用文件系统来保存对话上下文，确保机器人即使重启也能“记得”你们之前的聊天内容。</p><h2 id="CronService"><a href="#CronService" class="headerlink" title="CronService"></a>CronService</h2><p><code>CronService</code> 是 Nanobot 的<strong>定时任务调度器</strong>。它类似于 Linux 的 cron 或 Python 的 Celery，但更轻量且专为 Agent 设计。它的主要职责是管理和执行那些需要在未来某个时间点或周期性执行的任务。</p><h3 id="核心功能-1"><a href="#核心功能-1" class="headerlink" title="核心功能"></a>核心功能</h3><ol><li><p><strong>任务调度 (Scheduling)</strong></p><ul><li>支持三种调度方式：<ul><li><strong>一次性 (<code>at</code>)</strong>: 在指定时间点执行一次（例如“明天早上 8 点”）。</li><li><strong>间隔循环 (<code>every</code>)</strong>: 每隔一段时间执行一次（例如“每 30 分钟”）。</li><li><strong>Cron 表达式 (<code>cron</code>)</strong>: 使用标准 crontab 语法（例如 <code>0 8 * * *</code> 每天早上 8 点）。</li></ul></li><li>使用 <code>_compute_next_run</code> 函数计算下一次执行时间。</li></ul></li><li><p><strong>任务持久化 (Persistence)</strong></p><ul><li><strong>文件存储</strong>: 所有任务数据保存在 JSON 文件中（通常是 <code>~/.nanobot/data/cron/jobs.json</code>）。</li><li><strong>结构</strong>: <code>CronStore</code> 包含一个任务列表，每个 <code>CronJob</code> 包含 ID、名称、调度规则、Payload（要做什么）以及运行状态。</li><li><strong>优点</strong>: 即使 Nanobot 重启，任务也不会丢失。重启后会重新计算下次运行时间。</li></ul></li><li><p><strong>任务执行 (Execution)</strong></p><ul><li><strong>Payload</strong>: 任务的核心是一个 <code>message</code>（例如 “check weather”）。</li><li><strong>执行</strong>: 当时间到了，<code>CronService</code> 不会自己执行业务逻辑，而是调用回调函数 <code>on_job</code>（通常绑定到 <code>commands.py</code> 里的 <code>agent.process_direct</code>），把这个 message 扔给 Agent 去处理。</li><li><strong>结果分发</strong>: 如果任务配置了 <code>deliver=True</code> 和目标 <code>to</code>（如 Telegram 用户 ID），执行结果会自动发送给该用户。</li></ul></li><li><p><strong>定时器机制 (Timer Loop)</strong></p><ul><li>它不使用“每秒轮询”这种低效方式。</li><li><strong>智能休眠</strong>: <code>_arm_timer</code> 方法会计算距离“最近一个待执行任务”还有多久（<code>delay_s</code>），然后 <code>asyncio.sleep(delay_s)</code>。这样在空闲时几乎不消耗 CPU。</li><li>当有新任务添加或任务执行完毕时，会重新计算并重置定时器。</li></ul></li></ol><h3 id="关键类与方法-1"><a href="#关键类与方法-1" class="headerlink" title="关键类与方法"></a>关键类与方法</h3><h4 id="CronService-1"><a href="#CronService-1" class="headerlink" title="CronService"></a><code>CronService</code></h4><ul><li><strong><code>__init__(store_path, on_job)</code></strong>: 初始化服务，指定存储路径和执行回调。</li><li><strong><code>start()</code> / <code>stop()</code></strong>: 启动/停止服务。启动时会加载任务并计算下次运行时间。</li><li><strong><code>add_job(...)</code></strong>: 添加新任务。会自动保存到磁盘并更新定时器。</li><li><strong><code>remove_job(job_id)</code></strong>: 删除任务。</li><li><strong><code>_execute_job(job)</code></strong>: 执行单个任务。<ol><li>调用 <code>self.on_job(job)</code> 触发 Agent。</li><li>更新任务状态（<code>last_status</code>, <code>last_run_at</code>）。</li><li>对于一次性任务，执行完后将其禁用或删除；对于循环任务，计算下次运行时间。</li><li>保存状态到磁盘。</li></ol></li><li><strong><code>_arm_timer()</code></strong>: 核心调度逻辑。找到所有任务中 <code>next_run_at_ms</code> 最小的那个时间点，设置一个 asyncio 延时任务。</li></ul><h3 id="任务数据结构-CronJob"><a href="#任务数据结构-CronJob" class="headerlink" title="任务数据结构 (CronJob)"></a>任务数据结构 (<code>CronJob</code>)</h3><p>一个典型的任务数据如下：<br><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;a1b2c3d4&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;每日早报&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;schedule&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;kind&quot;</span>: <span class="string">&quot;cron&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;expr&quot;</span>: <span class="string">&quot;0 8 * * *&quot;</span>  <span class="comment">// 每天 8:00</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">&quot;payload&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;message&quot;</span>: <span class="string">&quot;总结今天的科技新闻&quot;</span>,  <span class="comment">// Agent 收到的指令</span></span><br><span class="line">    <span class="attr">&quot;deliver&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;channel&quot;</span>: <span class="string">&quot;telegram&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;to&quot;</span>: <span class="string">&quot;123456789&quot;</span>  <span class="comment">// 结果发给谁</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">&quot;state&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;nextRunAtMs&quot;</span>: <span class="number">1700000000000</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="实际工作流程示例-1"><a href="#实际工作流程示例-1" class="headerlink" title="实际工作流程示例"></a>实际工作流程示例</h3><ol><li><strong>用户指令</strong>: 你对机器人说“每天早上 8 点提醒我喝水”。</li><li><strong>创建任务</strong>: Agent 调用 <code>CronService.add_job</code>，创建一个 cron 任务，schedule 为 <code>0 8 * * *</code>，payload message 为 “提醒用户喝水”。</li><li><strong>等待</strong>: <code>CronService</code> 计算出下次 8 点的时间戳，进入休眠。</li><li><strong>触发</strong>: 第二天早上 8 点，定时器醒来，调用 <code>_execute_job</code>。</li><li><strong>回调</strong>: <code>CronService</code> 调用 <code>gateway</code> 中的 <code>on_cron_job</code> 回调。</li><li><strong>Agent 执行</strong>: Agent 收到 “提醒用户喝水” 的指令，生成回复 “早上好，该喝水了！”。</li><li><strong>推送</strong>: 因为 <code>deliver=True</code>，回复会自动推送到你的 Telegram。</li></ol><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p><code>CronService</code> 是 Nanobot 的<strong>生物钟</strong>。它让 Agent 具备了时间观念，不再只是被动等待用户说话，而是能主动在特定时间执行任务。它设计得非常轻量且健壮，利用文件系统持久化和 asyncio 实现高效调度。</p><h2 id="HeartbeatService"><a href="#HeartbeatService" class="headerlink" title="HeartbeatService"></a>HeartbeatService</h2><p><code>HeartbeatService</code> 是 Nanobot 的<strong>心跳机制</strong>，也是一种“周期性自检”服务。它的作用是让 Agent 即使在没有用户发消息的时候，也能每隔一段时间“醒来”一次，检查是否有后台任务需要处理。</p><p>与 <code>CronService</code>（基于精确时间调度）不同，<code>HeartbeatService</code> 更加简单粗暴，它基于一个固定的文件 <code>HEARTBEAT.md</code> 来驱动。</p><h3 id="核心逻辑"><a href="#核心逻辑" class="headerlink" title="核心逻辑"></a>核心逻辑</h3><ol><li><p><strong>心跳周期 (Interval)</strong></p><ul><li>默认每 <strong>30 分钟</strong>（1800 秒）触发一次。</li><li>这是一个死循环 (<code>while self._running</code>)，每次执行完 <code>await asyncio.sleep(self.interval_s)</code> 后就会“跳动”一次。</li></ul></li><li><p><strong>驱动源 (<code>HEARTBEAT.md</code>)</strong></p><ul><li>服务会检查工作区（Workspace）下是否存在 <code>HEARTBEAT.md</code> 文件。</li><li><strong>如果文件不存在或为空</strong>：Agent 继续睡觉，什么都不做。</li><li><strong>如果文件有内容</strong>：Agent 被唤醒。</li></ul></li><li><p><strong>唤醒与执行</strong></p><ul><li>当心跳触发且 <code>HEARTBEAT.md</code> 有内容时，服务会调用 <code>on_heartbeat</code> 回调函数。</li><li>这个回调实际上是向 Agent 发送了一条<strong>特殊的 Prompt</strong>：<figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Read HEARTBEAT.md in your workspace (if it exists).</span><br><span class="line">Follow any instructions or tasks listed there.</span><br><span class="line">If nothing needs attention, reply with just: HEARTBEAT_OK</span><br></pre></td></tr></table></figure></li><li>Agent 收到这条指令后，会读取 <code>HEARTBEAT.md</code>，执行里面列出的任务（例如“检查是否有新的待办事项”、“整理今天的日志”等）。</li></ul></li><li><p><strong>反馈机制</strong></p><ul><li>如果 Agent 执行完任务，或者发现其实没什么要做的，它应该回复 <code>HEARTBEAT_OK</code>。</li><li><code>HeartbeatService</code> 收到这个回复后，会记录一条日志 “Heartbeat: OK”，表示本次心跳正常结束。</li></ul></li></ol><h3 id="关键方法解析"><a href="#关键方法解析" class="headerlink" title="关键方法解析"></a>关键方法解析</h3><ul><li><p><strong><code>_is_heartbeat_empty(content)</code></strong>:</p><ul><li>这是一个辅助函数，用来判断 <code>HEARTBEAT.md</code> 是否真的有“干货”。</li><li>它会忽略空行、标题（<code>#</code> 开头）、HTML 注释（<code>&lt;!-- --&gt;</code>）以及空的复选框（<code>- [ ]</code>）。</li><li>只有当文件里有真正的文本内容时，才会触发心跳。这避免了因为文件里只写了个标题而频繁唤醒 Agent 浪费 Token。</li></ul></li><li><p><strong><code>_run_loop()</code></strong>:</p><ul><li>后台常驻协程。</li><li>只要 <code>self._running</code> 为 True，就无限循环：<code>sleep</code> -&gt; <code>_tick</code>。</li></ul></li><li><p><strong><code>_tick()</code></strong>:</p><ul><li>单次心跳的逻辑。</li><li><ol><li>读取 <code>HEARTBEAT.md</code>。</li></ol></li><li><ol><li>检查是否为空 (<code>_is_heartbeat_empty</code>)。为空则直接返回。</li></ol></li><li><ol><li>调用 <code>self.on_heartbeat(HEARTBEAT_PROMPT)</code> 唤醒 Agent。</li></ol></li><li><ol><li>检查 Agent 的回复是否包含 <code>HEARTBEAT_OK</code>，记录日志。</li></ol></li></ul></li></ul><h3 id="设计意图"><a href="#设计意图" class="headerlink" title="设计意图"></a>设计意图</h3><p><code>HeartbeatService</code> 的设计初衷是实现<strong>异步的长运行任务</strong>或<strong>状态维护</strong>，而不需要用户显式触发。</p><ul><li><p><strong>场景 1：长任务队列</strong></p><ul><li>你可以往 <code>HEARTBEAT.md</code> 里写入：”处理 data/raw 目录下的所有 PDF 文件”。</li><li>即使你下线了，Agent 也会每半小时醒来一次，处理一部分文件，直到处理完把 <code>HEARTBEAT.md</code> 清空。</li></ul></li><li><p><strong>场景 2：自我反思/整理</strong></p><ul><li>可以在 <code>HEARTBEAT.md</code> 里写：”检查 MEMORY.md，如果太乱了就整理一下”。</li><li>Agent 会定期整理自己的记忆，保持“头脑清醒”。</li></ul></li></ul><h3 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h3><p><code>HeartbeatService</code> 是一个基于文件的<strong>被动触发器</strong>。它通过检测 <code>HEARTBEAT.md</code> 文件的状态来决定是否唤醒 Agent。这是一种非常灵活的机制，允许用户通过简单地修改文件来控制 Agent 的后台行为，而无需编写复杂的 Cron 表达式。</p><h2 id="ChannelManager"><a href="#ChannelManager" class="headerlink" title="ChannelManager"></a>ChannelManager</h2><p><code>ChannelManager</code> 是 Nanobot 的<strong>外交部长</strong>。它统一管理所有外部通信渠道（如 Telegram, WhatsApp, Slack 等），负责它们的生命周期（启动/停止）以及消息的分发。</p><h3 id="核心职责"><a href="#核心职责" class="headerlink" title="核心职责"></a>核心职责</h3><ol><li><p><strong>渠道初始化 (<code>_init_channels</code>)</strong></p><ul><li>在启动时，根据 <code>nanobot.toml</code> 配置文件，动态加载并实例化启用的渠道。</li><li>目前支持的渠道包括：Telegram, WhatsApp, Discord, 飞书, Mochat, 钉钉, Email, Slack, QQ 等。</li><li>每个渠道都继承自 <code>BaseChannel</code>，拥有统一的接口（<code>start</code>, <code>stop</code>, <code>send</code>）。</li></ul></li><li><p><strong>生命周期管理</strong></p><ul><li><strong><code>start_all()</code></strong>: 并发启动所有已启用的渠道。<ul><li>每个渠道的 <code>start()</code> 方法通常会启动一个轮询循环（Polling）或 Webhook 服务器来接收消息。</li><li>同时启动一个 <code>_dispatch_outbound</code> 任务，负责处理发出去的消息。</li></ul></li><li><strong><code>stop_all()</code></strong>: 优雅关闭所有渠道，释放资源（如关闭 HTTP 连接）。</li></ul></li><li><p><strong>消息路由 (Routing)</strong></p><ul><li><strong>输入路由（隐式）</strong>: 各个 Channel 实例内部会持有 <code>bus</code>（消息总线）的引用。当它们收到外部消息时，会直接调用 <code>bus.publish_inbound(...)</code> 将消息扔进总线，供 Agent 消费。<code>ChannelManager</code> 不直接干预这一步。</li><li><strong>输出路由（显式）</strong>:<ul><li><code>ChannelManager</code> 启动一个后台任务 <code>_dispatch_outbound</code>。</li><li>这个任务不断从总线的<strong>输出队列</strong> (<code>consume_outbound</code>) 中取出消息。</li><li>它检查消息的 <code>channel</code> 属性（例如 “telegram”）。</li><li>然后在自己的 <code>self.channels</code> 字典里找到对应的 Channel 实例，调用 <code>channel.send(msg)</code> 将消息发出去。</li></ul></li></ul></li></ol><h3 id="关键代码逻辑"><a href="#关键代码逻辑" class="headerlink" title="关键代码逻辑"></a>关键代码逻辑</h3><ul><li><strong><code>_dispatch_outbound</code></strong>:<br>这是核心的消息分发循环。<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">    <span class="comment"># 1. 从总线获取待发送消息</span></span><br><span class="line">    msg = <span class="keyword">await</span> bus.consume_outbound()</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 2. 查找目标渠道</span></span><br><span class="line">    channel = self.channels.get(msg.channel)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 3. 发送</span></span><br><span class="line">    <span class="keyword">if</span> channel:</span><br><span class="line">        <span class="keyword">await</span> channel.send(msg)</span><br></pre></td></tr></table></figure>这个设计实现了 Agent 与具体通信协议的<strong>解耦</strong>。Agent 只需要产生一个通用的 <code>OutboundMessage</code>，不需要知道对方是用 HTTP 请求还是 WebSocket 发送，也不需要知道对方的 API Key 是什么。所有这些脏活累活都由 <code>ChannelManager</code> 委托给具体的 Channel 实现类去处理。</li></ul><h3 id="总结-3"><a href="#总结-3" class="headerlink" title="总结"></a>总结</h3><p><code>ChannelManager</code> 是连接 Nanobot 内核（Agent）与外部世界（用户）的桥梁。它通过统一的接口屏蔽了不同聊天平台的差异，让 Agent 可以专注于处理逻辑，而不用关心消息是如何传输的。</p><h2 id="第二阶段：并发启动-Startup"><a href="#第二阶段：并发启动-Startup" class="headerlink" title="第二阶段：并发启动 (Startup)"></a>第二阶段：并发启动 (Startup)</h2><p>初始化完成后，Gateway 会启动以下四个主要服务：<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">try:</span><br><span class="line">    await cron.start()</span><br><span class="line">    await heartbeat.start()</span><br><span class="line">    await asyncio.gather(</span><br><span class="line">        agent.run(),</span><br><span class="line">        channels.start_all(),</span><br><span class="line">    )</span><br></pre></td></tr></table></figure></p><ol><li><strong>启动定时任务调度器</strong> (<code>cron.start()</code>)：开始计时，等待任务触发。</li><li><strong>启动心跳服务</strong> (<code>heartbeat.start()</code>)：开始倒计时。</li><li><strong>启动 Agent 主循环</strong> (<code>agent.run()</code>)：Agent 进入“待机状态”，时刻监听消息总线上的<strong>输入消息</strong>。</li><li><strong>启动渠道管理</strong> (<code>channels.start_all()</code>)：<ul><li>启动所有启用的渠道（例如开始轮询 Telegram 消息，或监听 Webhook）。</li><li>启动<strong>输出分发器</strong> (<code>_dispatch_outbound</code>)，时刻监听消息总线上的<strong>输出消息</strong>。</li></ul></li></ol><p>这段代码是 <code>nanobot gateway</code> 启动的核心逻辑，它展示了从<strong>初始化后台服务</strong>到<strong>进入主运行循环</strong>的过程。</p><h3 id="代码逻辑分步解析"><a href="#代码逻辑分步解析" class="headerlink" title="代码逻辑分步解析"></a>代码逻辑分步解析</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 启动定时任务服务 (非阻塞)</span></span><br><span class="line"><span class="keyword">await</span> cron.start()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 启动心跳服务 (非阻塞)</span></span><br><span class="line"><span class="keyword">await</span> heartbeat.start()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 并发启动 Agent 和 Channels (阻塞/长运行)</span></span><br><span class="line"><span class="keyword">await</span> asyncio.gather(</span><br><span class="line">    agent.run(),</span><br><span class="line">    channels.start_all(),</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="为什么要按这个顺序？"><a href="#为什么要按这个顺序？" class="headerlink" title="为什么要按这个顺序？"></a>为什么要按这个顺序？</h3><p>这个顺序的设计主要基于<strong>任务的性质</strong>（是非阻塞初始化，还是长运行死循环）：</p><h4 id="1-为什么-cron-和-heartbeat-先行？"><a href="#1-为什么-cron-和-heartbeat-先行？" class="headerlink" title="1. 为什么 cron 和 heartbeat 先行？"></a>1. 为什么 <code>cron</code> 和 <code>heartbeat</code> 先行？</h4><ul><li><strong>非阻塞初始化</strong>: <code>cron.start()</code> 和 <code>heartbeat.start()</code> 方法内部仅仅是设置状态标志（<code>_running = True</code>）并创建后台的 <code>asyncio.Task</code>（计时器）。它们执行非常快，瞬间就会返回，<strong>不会阻塞</strong>主程序的执行。</li><li><strong>就绪原则</strong>: 按照依赖关系，最好先让内部的调度系统（定时器、心跳）准备就绪。这样一旦 Agent 开始工作，所有的定时触发机制都已经处于激活状态。</li></ul><h4 id="2-为什么-agent-和-channels-要用-asyncio-gather？"><a href="#2-为什么-agent-和-channels-要用-asyncio-gather？" class="headerlink" title="2. 为什么 agent 和 channels 要用 asyncio.gather？"></a>2. 为什么 <code>agent</code> 和 <code>channels</code> 要用 <code>asyncio.gather</code>？</h4><ul><li><strong>阻塞运行</strong>:<ul><li><code>agent.run()</code> 是一个 <code>while True</code> 循环，它会一直卡在那里监听消息总线，直到程序退出。</li><li><code>channels.start_all()</code> 也是一个阻塞操作，它会等待所有启用的渠道（Telegram, WhatsApp 等）运行，而这些渠道通常也是通过 <code>while True</code> 轮询或长连接挂起的。</li></ul></li><li><strong>并发必要性</strong>:<ul><li>如果写成顺序执行（例如先 <code>await agent.run()</code>），代码就会永远停在这一行，Agent 虽然跑起来了，但 <code>channels.start_all()</code> 永远不会被执行，导致机器人无法收发消息。</li><li><code>asyncio.gather</code> 的作用就是让这两个“死循环”在同一个事件循环中<strong>同时跑</strong>。Agent 负责“思考”，Channels 负责“听和说”，两者并行不悖。</li></ul></li></ul><h3 id="总结图解"><a href="#总结图解" class="headerlink" title="总结图解"></a>总结图解</h3><p>可以将这个启动过程想象成一家餐厅开门：</p><ol><li><code>cron.start()</code>: 经理先<strong>打开闹钟</strong>，设置好提醒（比如“中午12点开启特价午餐”）。这一步只是拨个开关，马上就好。</li><li><code>heartbeat.start()</code>: 经理<strong>开启打卡机</strong>，每半小时检查一次员工状态。这一步也只是通个电，马上就好。</li><li><code>asyncio.gather(...)</code>: 餐厅<strong>正式对外营业</strong>：<ul><li><code>agent.run()</code>: <strong>厨师（Agent）</strong> 站到灶台前，开始死循环等待订单。</li><li><code>channels.start_all()</code>: <strong>服务员（Channels）</strong> 站到门口，开始死循环等待顾客。</li></ul></li></ol><p>厨师和服务员必须<strong>同时</strong>进入工作状态，餐厅才能正常运转。</p><h2 id="第三阶段：消息处理循环-The-Loop"><a href="#第三阶段：消息处理循环-The-Loop" class="headerlink" title="第三阶段：消息处理循环 (The Loop)"></a>第三阶段：消息处理循环 (The Loop)</h2><p>这是 Gateway 运行时的核心逻辑，分为“接收”和“发送”两条路径：</p><h3 id="1-输入路径：从-用户-到-Agent"><a href="#1-输入路径：从-用户-到-Agent" class="headerlink" title="1. 输入路径：从 用户 到 Agent"></a>1. 输入路径：从 用户 到 Agent</h3><p>当你在 Telegram 给机器人发一条消息时：</p><ol><li><strong>接收</strong>: <code>TelegramChannel</code> 收到消息。</li><li><strong>发布</strong>: 渠道将消息封装为 <code>InboundMessage</code>（包含内容、发送者ID、渠道名），扔进<strong>消息总线</strong>。</li><li><strong>获取</strong>: 正在待机的 <code>AgentLoop</code> 从总线中抓取到这条消息。</li><li><strong>思考与执行</strong>:<ul><li>Agent 读取历史聊天记录。</li><li>Agent 将历史记录 + 新消息发送给 LLM（大模型）。</li><li><strong>工具调用</strong>: 如果 LLM 决定使用工具（如“搜索天气”），Agent 会执行工具代码，并将结果再次喂给 LLM。</li><li><strong>生成回复</strong>: LLM 生成最终的文本回复。</li></ul></li><li><strong>存储</strong>: Agent 将对话记录保存到数据库（SessionManager）。</li><li><strong>输出</strong>: Agent 将回复封装为 <code>OutboundMessage</code>，扔回<strong>消息总线</strong>。</li></ol><h3 id="2-输出路径：从-Agent-到-用户"><a href="#2-输出路径：从-Agent-到-用户" class="headerlink" title="2. 输出路径：从 Agent 到 用户"></a>2. 输出路径：从 Agent 到 用户</h3><p>当 Agent 产生回复（或定时任务触发）时：</p><ol><li><strong>分发</strong>: <code>ChannelManager</code> 的输出分发器从总线中抓取到 <code>OutboundMessage</code>。</li><li><strong>路由</strong>: 分发器查看消息的标签（例如 <code>channel=&quot;telegram&quot;</code>）。</li><li><strong>发送</strong>: 找到对应的 <code>TelegramChannel</code> 实例，调用其发送接口（如调用 Telegram API），将消息推送到你的手机上。</li></ol><h2 id="总结图解-1"><a href="#总结图解-1" class="headerlink" title="总结图解"></a>总结图解</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">graph TD</span><br><span class="line">    User[用户] --发消息--&gt; Channel[通信渠道 (Telegram&#x2F;Slack)]</span><br><span class="line">    Channel --InboundMessage--&gt; Bus[消息总线]</span><br><span class="line">    Bus --InboundMessage--&gt; Agent[AgentLoop (大脑)]</span><br><span class="line">    </span><br><span class="line">    Agent --思考&#x2F;查资料--&gt; Tools[工具集]</span><br><span class="line">    Tools --结果--&gt; Agent</span><br><span class="line">    </span><br><span class="line">    Agent --OutboundMessage--&gt; Bus</span><br><span class="line">    Bus --OutboundMessage--&gt; Dispatcher[渠道分发器]</span><br><span class="line">    Dispatcher --找到对应渠道--&gt; Channel</span><br><span class="line">    Channel --回复--&gt; User</span><br><span class="line">    </span><br><span class="line">    Cron[定时任务] --触发--&gt; Agent</span><br></pre></td></tr></table></figure><p>简而言之，<code>nanobot gateway</code> 就是一个<strong>死循环</strong>，不断地搬运消息、触发 AI 思考、并把结果送回给用户。</p>]]></content>
    
    
    <summary type="html">前言
最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是typescript语言），或者想自己魔改加个小功能，往往要翻半天文档。

直到我遇到了 nanobot。

它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。

nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、D</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着😺NanoBot学AI智能体设计和开发2：一波流！</title>
    <link href="http://qixinbo.github.io/2026/02/03/nanobot-2/"/>
    <id>http://qixinbo.github.io/2026/02/03/nanobot-2/</id>
    <published>2026-02-03T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.905Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是<code>typescript</code>语言），或者想自己魔改加个小功能，往往要翻半天文档。</p><p>直到我遇到了 <strong>nanobot</strong>。</p><p>它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。</p><p>nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、Discord、Slack，甚至是国内的飞书、钉钉、QQ 和微信（通过 Mochat）。我现在把它挂在飞书上，平时想查个资料、翻译段文本，或者只是单纯想找个“人”聊聊代码思路，随时掏出手机就能发消息，它会像一个真正的助理一样回复你。</p><p>那么就从nanobot源码开始学习AI个人助理吧。</p><p><a href="https://www.qixinbo.info/2026/02/01/nanobot-0/">0在这里</a><br><a href="https://www.qixinbo.info/2026/02/02/nanobot-1/">1在这里</a></p><h1 id="只聊一句"><a href="#只聊一句" class="headerlink" title="只聊一句"></a>只聊一句</h1><p>前面运行的<code>nanobot gateway</code>实际上启动了一个 全功能的AI服务端。你可以把它想象成一个繁忙的指挥中心，它同时处理着多条战线的工作。它是nanobot最典型的使用场景，但是这种模式有点复杂，不利于我们研究源码。<br>这里将先从<code>nanobot agent -m</code>这种模式入手，研究下“只与agent聊一句天，它背后干了啥”。</p><h1 id="具体流程"><a href="#具体流程" class="headerlink" title="具体流程"></a>具体流程</h1><p><code>nanobot agent -m</code> 命令用于在 <strong>单次消息模式 (Single Message Mode)</strong> 下运行智能体。它允许你从命令行直接发送一条指令给智能体，智能体执行完毕并返回结果后程序即刻退出，而不会进入交互式聊天界面。</p><p>下面是该命令从输入到输出的详细执行流程：</p><h2 id="CLI-入口与参数解析"><a href="#CLI-入口与参数解析" class="headerlink" title="CLI 入口与参数解析"></a>CLI 入口与参数解析</h2><p><strong>文件</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/cli/commands.py#L410">nanobot/cli/commands.py</a></p><p>当你运行 <code>nanobot agent -m &quot;你的指令&quot;</code> 时：</p><ul><li><strong>Typer 解析</strong>: <code>agent</code> 函数被调用，<code>-m</code> 参数的值被赋给 <code>message</code> 变量。</li><li><strong>环境准备</strong>:<ul><li><code>load_config()</code> 加载项目配置。</li><li>初始化 <code>MessageBus</code> (消息总线) 和 <code>LLMProvider</code> (大模型提供商)。</li><li>初始化核心控制器 <strong><code>AgentLoop</code></strong>。</li></ul></li></ul><p>这里非常关键，一下引出了3个关键概念，下面一一详细研究下。</p><h2 id="消息总线"><a href="#消息总线" class="headerlink" title="消息总线"></a>消息总线</h2><p><code>MessageBus</code> 类是一个基于 <code>asyncio.Queue</code> 的异步消息总线，其核心目的是<strong>解耦</strong>消息的生产者（Chat Channels，如 CLI、Slack、Discord 等）和消费者（Agent Core 智能体核心）。</p><p>它实现了经典的 <strong>生产者-消费者模式</strong>，并通过双向队列分别处理“输入”和“输出”。</p><h3 id="1-核心结构"><a href="#1-核心结构" class="headerlink" title="1. 核心结构"></a>1. 核心结构</h3><p><a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L11">nanobot/bus/queue.py</a></p><p><code>MessageBus</code> 内部维护了两个主要的异步队列：</p><ul><li><code>self.inbound</code>: 用于存放<strong>发给智能体的消息</strong>（用户 -&gt; 智能体）。</li><li><code>self.outbound</code>: 用于存放<strong>智能体发出的回复</strong>（智能体 -&gt; 用户）。</li><li><code>self._outbound_subscribers</code>: 一个字典，用于管理不同渠道（Channel）的回调函数，确保消息能准确路由回来源渠道。</li></ul><h3 id="2-主要功能模块"><a href="#2-主要功能模块" class="headerlink" title="2. 主要功能模块"></a>2. 主要功能模块</h3><h4 id="A-输入流处理-Inbound-Flow"><a href="#A-输入流处理-Inbound-Flow" class="headerlink" title="A. 输入流处理 (Inbound Flow)"></a>A. 输入流处理 (Inbound Flow)</h4><p>当用户发送消息时（例如通过 CLI 输入或 Slack 发送）：</p><ul><li><strong><code>publish_inbound(msg)</code></strong> (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L25">L25</a>): 外部渠道调用此方法，将 <code>InboundMessage</code> 放入输入队列。</li><li><strong><code>consume_inbound()</code></strong> (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L29">L29</a>): 智能体核心循环 (<code>AgentLoop</code>) 调用此方法，阻塞等待并获取下一条待处理的消息。</li></ul><h4 id="B-输出流处理-Outbound-Flow"><a href="#B-输出流处理-Outbound-Flow" class="headerlink" title="B. 输出流处理 (Outbound Flow)"></a>B. 输出流处理 (Outbound Flow)</h4><p>当智能体处理完任务并生成回复时：</p><ul><li><strong><code>publish_outbound(msg)</code></strong> (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L33">L33</a>): 智能体调用此方法，将 <code>OutboundMessage</code> 放入输出队列。</li><li><strong><code>consume_outbound()</code></strong> (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L37">L37</a>): 手动消费输出消息的方法（通常用于简单的同步场景）。</li></ul><h4 id="C-订阅与分发机制-Subscription-amp-Dispatch"><a href="#C-订阅与分发机制-Subscription-amp-Dispatch" class="headerlink" title="C. 订阅与分发机制 (Subscription &amp; Dispatch)"></a>C. 订阅与分发机制 (Subscription &amp; Dispatch)</h4><p>为了支持多渠道并发（例如同时运行 CLI 和 Slack 机器人），<code>MessageBus</code> 提供了订阅机制：</p><ul><li><strong><code>subscribe_outbound(channel, callback)</code></strong> (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L41">L41</a>): 允许特定渠道注册回调函数。例如，Slack 适配器会订阅 <code>channel=&quot;slack&quot;</code>，并提供一个将消息发送到 Slack API 的回调函数。</li><li><strong><code>dispatch_outbound()</code></strong> (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L51">L51</a>): 这是一个后台任务循环。它不断从 <code>outbound</code> 队列中取出消息，根据消息的 <code>channel</code> 属性查找对应的订阅者，并执行回调函数。这确保了如果消息来自 Slack，回复也会发回 Slack，而不是发到 CLI。</li></ul><h3 id="3-数据流向图解"><a href="#3-数据流向图解" class="headerlink" title="3. 数据流向图解"></a>3. 数据流向图解</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">graph LR</span><br><span class="line">    User[用户 (CLI&#x2F;Slack)] --&gt;|publish_inbound| InQueue[(Inbound Queue)]</span><br><span class="line">    InQueue --&gt;|consume_inbound| Agent[智能体核心 (AgentLoop)]</span><br><span class="line">    Agent --&gt;|publish_outbound| OutQueue[(Outbound Queue)]</span><br><span class="line">    OutQueue --&gt;|dispatch_outbound| Dispatcher[分发器]</span><br><span class="line">    Dispatcher --&gt;|callback| User</span><br></pre></td></tr></table></figure><h3 id="4-代码细节解析"><a href="#4-代码细节解析" class="headerlink" title="4. 代码细节解析"></a>4. 代码细节解析</h3><ul><li><strong>非阻塞与并发</strong>: 使用 <code>asyncio.Queue</code> 保证了线程安全和协程并发能力。<code>consume_inbound</code> 和 <code>consume_outbound</code> 都是 <code>await</code> 操作，不会阻塞事件循环。</li><li><strong>错误处理</strong>: 在 <code>dispatch_outbound</code> 循环中 (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L58-67">L58-67</a>)，使用了 <code>try-except</code> 捕获回调执行中的异常，防止因为某个渠道的发送失败导致整个总线崩溃。同时使用了 <code>asyncio.wait_for</code> 来定期检查停止标志 <code>_running</code>。</li><li><strong>生命周期管理</strong>: 提供了 <code>stop()</code> 方法 (<a href="file:///Users/qixinbo/test/nanobot/nanobot/bus/queue.py#L69">L69</a>) 来优雅地停止分发循环。</li></ul><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p><code>MessageBus</code> 是 Nanobot 架构中的<strong>中枢神经</strong>。它不仅实现了异步解耦，还通过订阅/分发机制支持了多渠道（Multi-channel）的扩展能力，使得核心智能体逻辑不需要关心消息是从哪里来的，也不需要知道如何发送回具体的平台，只需要关注处理 <code>InboundMessage</code> 并产生 <code>OutboundMessage</code> 即可。</p><h2 id="LiteLLMProvider"><a href="#LiteLLMProvider" class="headerlink" title="LiteLLMProvider"></a>LiteLLMProvider</h2><p><code>LiteLLMProvider</code> 类是 Nanobot 与各种大语言模型 (LLM) 进行交互的核心适配器。它利用 <code>litellm</code> 库的强大兼容性，实现了对 OpenAI、Anthropic、OpenRouter、Google Gemini 等数十种模型提供商的统一支持，而无需为每个提供商编写单独的代码。</p><p>代码位置：<a href="file:///Users/qixinbo/test/nanobot/nanobot/providers/litellm_provider.py#L14">nanobot/providers/litellm_provider.py</a></p><h3 id="1-核心设计理念"><a href="#1-核心设计理念" class="headerlink" title="1. 核心设计理念"></a>1. 核心设计理念</h3><p>该类采用 <strong>“配置驱动 + 统一接口”</strong> 的设计模式：</p><ul><li><strong>统一接口</strong>: 继承自 <code>LLMProvider</code>，对外暴露标准的 <code>chat()</code> 方法。无论底层是 GPT-4 还是 Claude 3，调用方式都一样。</li><li><strong>配置驱动</strong>: 通过 <code>providers/registry.py</code> 中的注册表来管理不同模型的特性（如 API Key 环境变量名、模型前缀、参数覆盖等），避免了大量的 <code>if-elif</code> 硬编码。</li></ul><h3 id="2-主要功能解析"><a href="#2-主要功能解析" class="headerlink" title="2. 主要功能解析"></a>2. 主要功能解析</h3><h4 id="A-初始化与环境自动配置-init-amp-setup-env"><a href="#A-初始化与环境自动配置-init-amp-setup-env" class="headerlink" title="A. 初始化与环境自动配置 (__init__ &amp; _setup_env)"></a>A. 初始化与环境自动配置 (<code>__init__</code> &amp; <code>_setup_env</code>)</h4><p><strong>代码</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/providers/litellm_provider.py#L23-L71">L23-L71</a></p><p>当 <code>LiteLLMProvider</code> 实例化时：</p><ol><li><strong>检测网关</strong>: 调用 <code>find_gateway</code> 判断是否使用了 API 网关（如 OpenRouter 或自定义代理）。</li><li><strong>设置环境变量</strong>: <code>_setup_env</code> 方法根据检测到的提供商（如 Anthropic 或 OpenAI），自动将传入的 <code>api_key</code> 设置到对应的环境变量中（如 <code>ANTHROPIC_API_KEY</code>）。这使得 <code>litellm</code> 库能自动找到凭证。</li><li><strong>LiteLLM 调优</strong>: 禁用调试日志 (<code>suppress_debug_info</code>) 并开启自动丢弃不支持参数 (<code>drop_params</code>)，提高稳定性。</li></ol><h4 id="B-智能模型路由-resolve-model"><a href="#B-智能模型路由-resolve-model" class="headerlink" title="B. 智能模型路由 (_resolve_model)"></a>B. 智能模型路由 (<code>_resolve_model</code>)</h4><p><strong>代码</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/providers/litellm_provider.py#L73-L90">L73-L90</a></p><p>LiteLLM 要求模型名称带有前缀（如 <code>anthropic/claude-3</code>）。此方法自动处理这些细节：</p><ul><li><strong>网关模式</strong>: 如果配置了网关，它会根据网关规则添加或去除前缀。</li><li><strong>标准模式</strong>: 自动为已知模型添加前缀。例如，如果用户配置 <code>model: claude-3-opus</code>，它会自动转换为 <code>anthropic/claude-3-opus</code>，用户无需操心前缀问题。</li></ul><h4 id="C-模型参数微调-apply-model-overrides"><a href="#C-模型参数微调-apply-model-overrides" class="headerlink" title="C. 模型参数微调 (_apply_model_overrides)"></a>C. 模型参数微调 (<code>_apply_model_overrides</code>)</h4><p><strong>代码</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/providers/litellm_provider.py#L92-L100">L92-L100</a></p><p>不同的模型对参数有不同的偏好。例如，某些国产模型可能对 <code>temperature</code> 敏感。此方法允许通过注册表为特定模型注入特定的参数覆盖，确保最佳效果。</p><h4 id="D-对话请求与响应标准化-chat-amp-parse-response"><a href="#D-对话请求与响应标准化-chat-amp-parse-response" class="headerlink" title="D. 对话请求与响应标准化 (chat &amp; _parse_response)"></a>D. 对话请求与响应标准化 (<code>chat</code> &amp; <code>_parse_response</code>)</h4><p><strong>代码</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/providers/litellm_provider.py#L102-L199">L102-L199</a></p><p>这是真正干活的地方：</p><ol><li><strong>准备参数</strong>: 将消息历史、工具定义、Token 限制等封装成字典。</li><li><strong>调用 LiteLLM</strong>: 使用 <code>acompletion</code> 异步发送请求。</li><li><strong>结果解析</strong>:<ul><li><strong>工具调用</strong>: 将原始 JSON 解析为标准的 <code>ToolCallRequest</code> 对象，处理 JSON 解析错误。</li><li><strong>Token 统计</strong>: 提取 <code>usage</code> 信息。</li><li><strong>推理内容</strong>: 如果模型支持思维链（如 DeepSeek R1），提取 <code>reasoning_content</code>。</li><li><strong>统一返回</strong>: 打包成 <code>LLMResponse</code> 对象返回给 <code>AgentLoop</code>。</li></ul></li></ol><h3 id="3-为什么使用-LiteLLM？"><a href="#3-为什么使用-LiteLLM？" class="headerlink" title="3. 为什么使用 LiteLLM？"></a>3. 为什么使用 LiteLLM？</h3><ul><li><strong>多模型支持</strong>: 一套代码支持 100+ 种模型。</li><li><strong>标准化</strong>: 屏蔽了不同 API（如 OpenAI 格式 vs Anthropic 格式）的差异，输入输出完全统一。</li><li><strong>容错性</strong>: <code>drop_params=True</code> 自动处理参数不兼容问题（例如某些模型不支持 <code>tool_choice</code>）。</li></ul><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p><code>LiteLLMProvider</code> 是 Nanobot 的 <strong>“万能插头”</strong>。它让 Nanobot 能够轻松切换底层模型，无论是本地部署的 Ollama，还是云端的 GPT-4/Claude-3，只需更改配置，无需修改代码。</p><h2 id="AgentLoop"><a href="#AgentLoop" class="headerlink" title="AgentLoop"></a>AgentLoop</h2><p><code>AgentLoop</code> 类是 Nanobot 的<strong>大脑和心脏</strong>。它负责协调所有的核心组件：接收消息、管理记忆、调用大模型 (LLM)、执行工具，并最终生成回复。</p><p>代码位置：<a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L26">nanobot/agent/loop.py</a></p><p>以下是 <code>AgentLoop</code> 的详细解构：</p><h3 id="1-核心职责"><a href="#1-核心职责" class="headerlink" title="1. 核心职责"></a>1. 核心职责</h3><p><code>AgentLoop</code> 是一个自主的智能体运行时环境。它的主要工作流程是：</p><ol><li>监听消息总线 (<code>MessageBus</code>)。</li><li>构建上下文 (Context)，包括历史记录、长期记忆和可用工具。</li><li>进入<strong>思考-执行循环 (ReAct Loop)</strong>：<ul><li>询问 LLM 下一步做什么。</li><li>如果 LLM 决定调用工具，执行工具并将结果反馈给 LLM。</li><li>重复上述步骤，直到 LLM 给出最终回复。</li></ul></li><li>将结果发送回消息总线。</li></ol><h3 id="2-关键组件-初始化"><a href="#2-关键组件-初始化" class="headerlink" title="2. 关键组件 (初始化)"></a>2. 关键组件 (初始化)</h3><p>在 <code>__init__</code> (<a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L38">L38</a>) 中，它组装了以下关键模块：</p><ul><li><strong><code>MessageBus</code></strong>: 通信管道，用于收发消息。</li><li><strong><code>LLMProvider</code></strong>: 大模型接口 (如 OpenAI, Anthropic)，负责生成文本和工具调用。</li><li><strong><code>ContextBuilder</code></strong>: 上下文构建器，负责将杂乱的信息（历史、文件、记忆）组装成 LLM 能理解的 Prompt。</li><li><strong><code>ToolRegistry</code></strong>: 工具箱，管理所有可用工具 (读写文件、Web搜索、执行Shell等)。</li><li><strong><code>SessionManager</code></strong>: 会话管理器，负责保存和加载聊天记录。</li><li><strong><code>SubagentManager</code></strong>: 子智能体管理器，允许主智能体生成 (Spawn) 其他智能体来处理特定任务。</li></ul><h3 id="3-运行模式"><a href="#3-运行模式" class="headerlink" title="3. 运行模式"></a>3. 运行模式</h3><p><code>AgentLoop</code> 支持两种运行模式：</p><h4 id="A-守护进程模式-run"><a href="#A-守护进程模式-run" class="headerlink" title="A. 守护进程模式 (run)"></a>A. 守护进程模式 (<code>run</code>)</h4><p><strong>代码</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L113">L113</a><br>这是智能体的主循环。它无限运行 (<code>while self._running</code>)，不断从 <code>bus.consume_inbound()</code> 获取消息。这种模式适用于长期运行的服务（如连接到 Slack 或后台任务）。</p><h4 id="B-直接调用模式-process-direct"><a href="#B-直接调用模式-process-direct" class="headerlink" title="B. 直接调用模式 (process_direct)"></a>B. 直接调用模式 (<code>process_direct</code>)</h4><p><strong>代码</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L427">L427</a><br>用于 CLI 命令 (如 <code>nanobot agent -m &quot;...&quot;</code>) 或 Cron 定时任务。它不经过队列等待，直接处理传入的字符串并返回结果。</p><h3 id="4-核心思考逻辑-process-message"><a href="#4-核心思考逻辑-process-message" class="headerlink" title="4. 核心思考逻辑 (_process_message)"></a>4. 核心思考逻辑 (<code>_process_message</code>)</h3><p>这是最复杂也是最核心的部分 (<a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L147">L147</a>)，处理单条消息的完整流程如下：</p><h4 id="第一阶段：上下文准备"><a href="#第一阶段：上下文准备" class="headerlink" title="第一阶段：上下文准备"></a>第一阶段：上下文准备</h4><ol><li><strong>加载会话</strong>: 根据 Session ID 获取历史聊天记录。</li><li><strong>记忆压缩 (<code>_consolidate_memory</code>)</strong>: 如果历史记录太长（超过 <code>memory_window</code>，默认 50 条），会触发后台压缩任务，将旧对话总结并存入 <code>MEMORY.md</code>，保持上下文窗口精简 (<a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L366">L366</a>)。</li><li><strong>工具注入</strong>: 为 Message/Spawn 等工具注入当前频道信息，确保回复能发回正确的地方。</li><li><strong>构建 Prompt</strong>: <code>self.context.build_messages(...)</code> 将所有信息打包成 LLM 消息列表。</li></ol><h4 id="第二阶段：思考-执行循环-The-Loop"><a href="#第二阶段：思考-执行循环-The-Loop" class="headerlink" title="第二阶段：思考-执行循环 (The Loop)"></a>第二阶段：思考-执行循环 (The Loop)</h4><p>代码位于 <a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L200-L244">L200-L244</a>。这是一个 <code>while</code> 循环，最大迭代次数由 <code>max_iterations</code> 控制（防止无限循环）。</p><ul><li><p><strong>Step 1: 询问 LLM</strong><br>调用 <code>self.provider.chat(messages, tools=...)</code>。</p></li><li><p><strong>Step 2: 判断行动</strong></p><ul><li><strong>情况 A: LLM 想要调用工具 (<code>response.has_tool_calls</code>)</strong><ol><li>将 LLM 的“思考过程”和“工具调用请求”加入消息历史。</li><li><strong>执行工具</strong>: 遍历 <code>tool_calls</code>，调用 <code>self.tools.execute(...)</code> 执行实际代码（如读取文件、搜索网页）。</li><li><strong>反馈结果</strong>: 将工具的执行结果 (<code>tool_result</code>) 作为新消息追加到历史中。</li><li><strong>继续循环</strong>: 带着工具的结果，进入下一次迭代，让 LLM 决定下一步。</li></ol></li><li><strong>情况 B: LLM 返回最终回复</strong><ol><li>获取文本内容。</li><li><strong>跳出循环 (break)</strong>。</li></ol></li></ul></li></ul><h4 id="第三阶段：收尾"><a href="#第三阶段：收尾" class="headerlink" title="第三阶段：收尾"></a>第三阶段：收尾</h4><ol><li><strong>保存记录</strong>: 将用户的输入、LLM 的回复以及使用过的工具列表保存到 Session 文件中 (<a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L253">L253</a>)。</li><li><strong>发送回复</strong>: 封装 <code>OutboundMessage</code> 并通过 Bus 发送出去。</li></ol><h3 id="5-记忆系统-consolidate-memory"><a href="#5-记忆系统-consolidate-memory" class="headerlink" title="5. 记忆系统 (_consolidate_memory)"></a>5. 记忆系统 (<code>_consolidate_memory</code>)</h3><p><strong>代码</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L366">L366</a><br>这是一个非常关键的自我维护机制。当对话过长时，<code>AgentLoop</code> 不会简单地截断，而是：</p><ol><li>提取旧的消息。</li><li>调用 LLM (作为 <code>memory consolidation agent</code>)。</li><li>要求 LLM 生成两样东西：<ul><li><strong>History Entry</strong>: 一段摘要，存入 <code>HISTORY.md</code>。</li><li><strong>Memory Update</strong>: 更新 <code>MEMORY.md</code> 中的长期事实（如用户偏好、项目背景）。</li></ul></li><li>这样，即使原始对话被丢弃，关键信息也留在了长期记忆中。</li></ol><h3 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h3><p><code>AgentLoop</code> 实现了一个标准的 <strong>ReAct (Reasoning + Acting)</strong> 架构。它不仅仅是一个聊天机器人，更是一个能通过工具与环境交互、并通过记忆系统维持长期连贯性的智能体运行时。</p><h2 id="进入单次执行模式"><a href="#进入单次执行模式" class="headerlink" title="进入单次执行模式"></a>进入单次执行模式</h2><p><strong>文件</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/cli/commands.py#L453">nanobot/cli/commands.py</a></p><p>代码检查 <code>if message:</code> 是否存在：</p><ul><li><strong>交互模式 (False)</strong>: 如果没有 <code>-m</code>，代码会进入 <code>else</code> 分支启动交互式 Shell (<code>run_interactive</code>)。</li><li><strong>单次模式 (True)</strong>: 存在 <code>-m</code> 时，定义并运行 <code>run_once</code> 异步函数：<ul><li>启动 <code>_thinking_ctx()</code> 上下文管理器，在终端显示 “nanobot is thinking…” 加载动画（除非开启了 <code>--logs</code>）。</li><li>调用核心处理函数 <code>agent_loop.process_direct(message, session_id)</code>。</li></ul></li></ul><h2 id="核心处理循环-AgentLoop"><a href="#核心处理循环-AgentLoop" class="headerlink" title="核心处理循环 (AgentLoop)"></a>核心处理循环 (AgentLoop)</h2><p><strong>文件</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L427">nanobot/agent/loop.py</a></p><p><code>process_direct</code> 是从 CLI 到智能体逻辑的桥梁，它将你的字符串指令封装为 <code>InboundMessage</code>，然后调用内部的 <code>_process_message</code> 方法。这是智能体真正”思考”的地方：</p><h3 id="A-上下文构建"><a href="#A-上下文构建" class="headerlink" title="A. 上下文构建"></a>A. 上下文构建</h3><ul><li><strong>会话管理</strong>: 根据 <code>session_id</code> 获取或创建会话记录 (Session)，加载历史对话。</li><li><strong>记忆整合</strong>: 如果历史消息超过 <code>memory_window</code> (默认 50 条)，触发 <code>_consolidate_memory</code> 将旧消息压缩为长期记忆 (<a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L366">L366</a>)。</li><li><strong>工具准备</strong>: 为 Message/Spawn/Cron 等工具注入当前上下文信息。</li><li><strong>构建 Prompt</strong>: <code>ContextBuilder</code> 将系统提示词、历史记录、当前指令、长期记忆组合成发送给 LLM 的最终消息列表。</li></ul><h3 id="B-思考与执行循环-Loop"><a href="#B-思考与执行循环-Loop" class="headerlink" title="B. 思考与执行循环 (Loop)"></a>B. 思考与执行循环 (Loop)</h3><p><strong>文件</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/agent/loop.py#L200">nanobot/agent/loop.py</a></p><p>智能体进入一个 <code>while</code> 循环 (最多 <code>max_iterations</code> 次，默认 20 次)：</p><ol><li><strong>调用 LLM</strong>: 将消息列表和工具定义发送给模型 (<code>self.provider.chat</code>)。</li><li><strong>处理响应</strong>:<ul><li><strong>情况 1：模型决定调用工具</strong> (Has Tool Calls)<ul><li>解析工具调用请求 (如 <code>read_file</code>, <code>web_search</code> 等)。</li><li>执行工具逻辑 (<code>self.tools.execute</code>)。</li><li>将工具执行结果追加到消息列表中。</li><li><strong>继续循环</strong>，让模型根据工具结果进行下一步思考。</li></ul></li><li><strong>情况 2：模型返回文本</strong> (No Tool Calls)<ul><li>获取最终回复内容。</li><li><strong>跳出循环</strong>。</li></ul></li></ul></li></ol><h2 id="结果返回与展示"><a href="#结果返回与展示" class="headerlink" title="结果返回与展示"></a>结果返回与展示</h2><p><strong>文件</strong>: <a href="file:///Users/qixinbo/test/nanobot/nanobot/cli/commands.py#L458">nanobot/cli/commands.py</a></p><p>一旦 <code>process_direct</code> 返回最终文本：</p><ul><li><strong>保存会话</strong>: 你的指令和智能体的回复被保存到 Session 中，供下次调用使用。</li><li><strong>打印输出</strong>: <code>_print_agent_response</code> 函数将结果渲染为 Markdown 格式打印到终端。</li><li><strong>程序退出</strong>: <code>asyncio.run(run_once())</code> 结束，Python 进程退出。</li></ul><h3 id="总结-3"><a href="#总结-3" class="headerlink" title="总结"></a>总结</h3><p><code>nanobot agent -m</code> 本质上是 <strong>“启动环境 -&gt; 执行一次思考循环 -&gt; 打印结果 -&gt; 退出”</strong> 的快捷通道，非常适合用于脚本集成或快速的单次问答任务。</p>]]></content>
    
    
    <summary type="html">前言
最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是typescript语言），或者想自己魔改加个小功能，往往要翻半天文档。

直到我遇到了 nanobot。

它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。

nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、D</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着😺NanoBot学AI智能体设计和开发1：一键装机！</title>
    <link href="http://qixinbo.github.io/2026/02/02/nanobot-1/"/>
    <id>http://qixinbo.github.io/2026/02/02/nanobot-1/</id>
    <published>2026-02-02T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.905Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是<code>typescript</code>语言），或者想自己魔改加个小功能，往往要翻半天文档。</p><p>直到我遇到了 <strong>nanobot</strong>。</p><p>它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。</p><p>nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、Discord、Slack，甚至是国内的飞书、钉钉、QQ 和微信（通过 Mochat）。我现在把它挂在飞书上，平时想查个资料、翻译段文本，或者只是单纯想找个“人”聊聊代码思路，随时掏出手机就能发消息，它会像一个真正的助理一样回复你。</p><p>那么就从nanobot源码开始学习AI个人助理吧。</p><p><a href="https://www.qixinbo.info/2026/02/01/nanobot-0/">0在这里</a></p><h1 id="一键装机"><a href="#一键装机" class="headerlink" title="一键装机"></a>一键装机</h1><p>在运行nanobot之前，需要先初始化一下：<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nanobot onboard</span><br></pre></td></tr></table></figure><br>当在终端输入 <code>nanobot onboard</code> 后，系统会执行 <a href="file:///Users/qixinbo/test/nanobot/nanobot/cli/commands.py">nanobot/cli/commands.py</a> 中的 <code>onboard</code> 函数。这个函数的主要作用是<strong>初始化 nanobot 的运行环境</strong>，具体做了以下四件事：</p><h2 id="检查并创建配置文件"><a href="#检查并创建配置文件" class="headerlink" title="检查并创建配置文件"></a>检查并创建配置文件</h2><ul><li><strong>位置</strong>：<code>~/.nanobot/config.json</code></li><li><strong>动作</strong>：<ul><li>首先检查该文件是否存在。如果存在，会询问你是否覆盖。</li><li>如果不存在（或确认覆盖），它会创建一个默认的 <code>Config</code> 对象并保存。</li><li>这个文件是你后续配置 API Key（如 OpenAI、Anthropic）的地方。</li></ul></li></ul><h2 id="创建工作区目录"><a href="#创建工作区目录" class="headerlink" title="创建工作区目录"></a>创建工作区目录</h2><ul><li><strong>位置</strong>：默认为 <code>~/.nanobot/workspace</code>（具体路径由 <code>get_workspace_path()</code> 决定）。</li><li><strong>动作</strong>：确保这个目录存在，如果不存在则创建它。这个目录是 nanobot 的“大脑”和“手脚”，它在这里读写文件、存储记忆。</li></ul><h2 id="生成模板文件-Bootstrap"><a href="#生成模板文件-Bootstrap" class="headerlink" title="生成模板文件 (Bootstrap)"></a>生成模板文件 (Bootstrap)</h2><p>它会在你的工作区里自动生成一些基础的 Markdown 文件，用来定义 nanobot 的行为和记忆：</p><ul><li><strong>AGENTS.md</strong>:  <ul><li><strong>作用</strong>：Agent 的“员工手册”。定义了它作为一个 AI 助手的基本准则（比如要友善、准确、行动前要解释）。</li><li>提示词是：<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;&quot;</span><span class="comment"># Agent Instructions</span></span><br><span class="line"></span><br><span class="line">You are a helpful AI assistant. Be concise, accurate, and friendly.</span><br><span class="line"></span><br><span class="line"><span class="comment">## Guidelines</span></span><br><span class="line"></span><br><span class="line">- Always explain what you<span class="string">&#x27;re doing before taking actions</span></span><br><span class="line"><span class="string">- Ask for clarification when the request is ambiguous</span></span><br><span class="line"><span class="string">- Use tools to help accomplish tasks</span></span><br><span class="line"><span class="string">- Remember important information in memory/MEMORY.md; past events are logged in memory/HISTORY.md</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure></li></ul></li><li><strong>SOUL.md</strong>:  <ul><li><strong>作用</strong>：Agent 的“人设”。定义了它的性格（乐于助人、好奇）、价值观（隐私优先、透明）。</li><li>提示词是：<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;&quot;</span><span class="string">&quot;# Soul</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">I am nanobot, a lightweight AI assistant.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">## Personality</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- Helpful and friendly</span></span><br><span class="line"><span class="string">- Concise and to the point</span></span><br><span class="line"><span class="string">- Curious and eager to learn</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">## Values</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- Accuracy over speed</span></span><br><span class="line"><span class="string">- User privacy and safety</span></span><br><span class="line"><span class="string">- Transparency in actions</span></span><br><span class="line"><span class="string">&quot;</span><span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure></li></ul></li><li><strong>USER.md</strong>:  <ul><li><strong>作用</strong>：Agent 对你的认知。这里记录了你的偏好（语言、时区、沟通风格）。</li><li>提示词是：<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;&quot;</span><span class="string">&quot;# User</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Information about the user goes here.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">## Preferences</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- Communication style: (casual/formal)</span></span><br><span class="line"><span class="string">- Timezone: (your timezone)</span></span><br><span class="line"><span class="string">- Language: (your preferred language)</span></span><br><span class="line"><span class="string">&quot;</span><span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure></li></ul></li><li><strong>memory/MEMORY.md</strong>:  <ul><li><strong>作用</strong>：长期记忆。用来存储那些需要跨会话记住的重要信息。</li><li>提示词是：<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;&quot;</span><span class="string">&quot;# Long-term Memory</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">This file stores important information that should persist across sessions.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">## User Information</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">(Important facts about the user)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">## Preferences</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">(User preferences learned over time)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">## Important Notes</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">(Things to remember)</span></span><br><span class="line"><span class="string">&quot;</span><span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure></li></ul></li><li><strong>memory/HISTORY.md</strong>:  <ul><li><strong>作用</strong>：历史记录。用来记录过去的事件（初始化为空）。</li><li>提示词默认为空。</li></ul></li><li><strong>skills/</strong>:<ul><li><strong>作用</strong>：技能目录。预留给未来存放自定义技能的地方。</li></ul></li></ul><h2 id="打印后续指引"><a href="#打印后续指引" class="headerlink" title="打印后续指引"></a>打印后续指引</h2><p>最后，它会在终端输出一个“下一步”指南，告诉你：</p><ol><li>去 <code>~/.nanobot/config.json</code> 填入你的 API Key。</li><li>如何开始第一次对话（<code>nanobot agent -m &quot;Hello!&quot;</code>）。<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">✓ Created config at /root/.nanobot/config.json</span><br><span class="line">✓ Created workspace at /root/.nanobot/workspace</span><br><span class="line">  Created AGENTS.md</span><br><span class="line">  Created SOUL.md</span><br><span class="line">  Created USER.md</span><br><span class="line">  Created memory/MEMORY.md</span><br><span class="line"></span><br><span class="line">🐈 nanobot is ready!</span><br><span class="line"></span><br><span class="line">Next steps:</span><br><span class="line">  1. Add your API key to ~/.nanobot/config.json</span><br><span class="line">     Get one at: https://openrouter.ai/keys</span><br><span class="line">  2. Chat: nanobot agent -m <span class="string">&quot;Hello!&quot;</span></span><br><span class="line"></span><br><span class="line">Want Telegram/WhatsApp? See: https://github.com/HKUDS/nanobot<span class="comment">#-chat-apps</span></span><br></pre></td></tr></table></figure></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><code>nanobot onboard</code> 就是一个<strong>一键装机</strong>命令。它帮你铺好了路，建好了房（目录结构），写好了家规（模板文件），你只需要拿着钥匙（API Key）住进去（配置一下）就可以开始了。</p><h1 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h1><p>完成 <code>onboard</code> 之后，你的环境已经准备好了，接下来只需要把“钥匙”交给 nanobot。你需要编辑配置文件 <code>~/.nanobot/config.json</code>。</p><h2 id="配置模型供应商"><a href="#配置模型供应商" class="headerlink" title="配置模型供应商"></a>配置模型供应商</h2><p>需要至少配置一个 AI 提供商，就两个参数：<code>apiBase</code>和<code>apiKey</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;providers&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;openrouter&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;apiKey&quot;</span>: <span class="string">&quot;sk-or-v1-xxxxxxxx&quot;</span> </span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">&quot;agents&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;defaults&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-opus-4-5&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em>注意：<code>agents.defaults.model</code> 决定了 nanobot 默认用哪个模型。你可以随时通过命令行参数 <code>-m</code> 覆盖它。</em></p><p>配置保存后，在终端运行以下命令来测试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 检查状态，看看 Key 是否被识别</span></span><br><span class="line">nanobot status</span><br><span class="line"></span><br><span class="line"><span class="comment"># 试着聊一句</span></span><br><span class="line">nanobot agent -m <span class="string">&quot;你好，介绍一下你自己&quot;</span></span><br></pre></td></tr></table></figure><p>如果它回复了你，恭喜！你的 nanobot 已经正式上岗了。🎉</p><h2 id="配置飞书频道"><a href="#配置飞书频道" class="headerlink" title="配置飞书频道"></a>配置飞书频道</h2><p>上面的运行方式只能“聊一句”，nanobot真正的交互方式是通过各种通讯工具与它进行对话，这里使用飞书作为案例搞一下。</p><p>配置飞书（Feishu）需要两个步骤：先在飞书开放平台创建一个机器人应用，然后把凭证填入 nanobot 的配置文件。</p><p>nanobot 使用飞书的<strong>WebSocket 长连接模式</strong>，这意味着<strong>你不需要公网 IP，也不需要内网穿透</strong>，直接在本地电脑运行即可。</p><h3 id="第一步：创建飞书机器人"><a href="#第一步：创建飞书机器人" class="headerlink" title="第一步：创建飞书机器人"></a>第一步：创建飞书机器人</h3><ol><li>登录 <a href="https://open.feishu.cn/app">飞书开放平台</a>。</li><li>点击 <strong>创建企业自建应用</strong>，填写名称（比如 “Nanobot”）和描述。</li><li><strong>添加机器人能力</strong>：<ul><li>进入左侧菜单 <strong>添加应用能力</strong> -&gt; <strong>机器人</strong>，点击添加。</li></ul></li><li><strong>配置权限</strong>：<ul><li>进入 <strong>权限管理</strong>，搜索并添加以下权限：<ul><li><code>im:message</code> (获取与发送单聊、群组消息)</li><li><code>im:message.group_at_msg</code> (获取群组中 @机器人的消息)</li><li><code>im:message.p2p_msg</code> (获取用户发给机器人的单聊消息)</li></ul></li><li><em>注意：添加权限后，需要发布版本才能生效。</em></li></ul></li><li><strong>配置事件订阅</strong>（注意！这一步先不要做，等到把ID和secret配置给nanob，然后启动gateway后再设置）：<ul><li>进入 <strong>事件订阅</strong>。</li><li><strong>订阅方式</strong>：选择 <strong>长连接模式</strong>（这一步很关键！）。</li><li><strong>添加事件</strong>：搜索并添加 <code>im.message.receive_v1</code> (接收消息)。</li></ul></li><li><strong>获取凭证</strong>：<ul><li>进入 <strong>凭证与基础信息</strong>。</li><li>复制 <strong>App ID</strong> 和 <strong>App Secret</strong>。</li></ul></li><li><strong>发布应用</strong>：<ul><li>进入 <strong>版本管理与发布</strong>，创建并发布一个版本（如果不发布，机器人无法被外部搜到）。</li></ul></li></ol><h3 id="第二步：配置-nanobot"><a href="#第二步：配置-nanobot" class="headerlink" title="第二步：配置 nanobot"></a>第二步：配置 nanobot</h3><p>打开配置文件 <code>~/.nanobot/config.json</code>，在 <code>channels</code> 下添加 <code>feishu</code> 配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;channels&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;feishu&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;enabled&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">&quot;appId&quot;</span>: <span class="string">&quot;cli_xxxxxxxxxxxx&quot;</span>, </span><br><span class="line">      <span class="attr">&quot;appSecret&quot;</span>: <span class="string">&quot;xxxxxxxxxxxxxxxxxxxxxxxx&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;encryptKey&quot;</span>: <span class="string">&quot;&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;verificationToken&quot;</span>: <span class="string">&quot;&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;allowFrom&quot;</span>: []</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><code>appId</code>: 填入你刚才复制的 App ID。</li><li><code>appSecret</code>: 填入你刚才复制的 App Secret。</li><li><code>encryptKey</code> 和 <code>verificationToken</code>: 在长连接模式下通常留空即可。</li><li><code>allowFrom</code>: 如果你想限制只有特定用户能跟机器人聊天，可以在这里填入用户的 <code>open_id</code>。留空 <code>[]</code> 表示允许所有人。</li></ul><h3 id="第三步：启动"><a href="#第三步：启动" class="headerlink" title="第三步：启动"></a>第三步：启动</h3><p>配置完成后，启动网关模式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nanobot gateway</span><br></pre></td></tr></table></figure><p>现在，打开飞书 App，搜索你的机器人名字，给它发一条消息，它应该就会回复你了！🎉</p><p>如果它回复了你，恭喜！你的 nanobot 已经正式上岗了。🎉</p><h1 id="后台运行"><a href="#后台运行" class="headerlink" title="后台运行"></a>后台运行</h1><p><code>nanobot gateway</code> 命令本身是一个前台进程，它没有内置 <code>--daemon</code> 参数来自动转入后台。</p><p>如果你想让它在后台长期运行，有几种标准的方法：</p><h2 id="使用-nohup-最简单"><a href="#使用-nohup-最简单" class="headerlink" title="使用 nohup (最简单)"></a>使用 nohup (最简单)</h2><p>这是最传统的后台运行方式，即使你退出了 SSH 终端，它也会继续运行。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动并把日志输出到 nanobot.log</span></span><br><span class="line">nohup nanobot gateway &gt; nanobot.log 2&gt;&amp;1 &amp;</span><br></pre></td></tr></table></figure><ul><li><strong>查看日志</strong>：<code>tail -f nanobot.log</code></li><li><strong>停止</strong>：你需要先找到进程 ID，然后 kill 掉。<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ps aux | grep nanobot</span><br><span class="line"><span class="built_in">kill</span> &lt;PID&gt;</span><br></pre></td></tr></table></figure></li></ul><h2 id="使用-Docker-生产环境推荐"><a href="#使用-Docker-生产环境推荐" class="headerlink" title="使用 Docker (生产环境推荐)"></a>使用 Docker (生产环境推荐)</h2><p>如果你想要更稳定的部署，用 Docker 跑是最省心的，它自带重启策略。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name nanobot \</span><br><span class="line">  --restart always \</span><br><span class="line">  -v ~/.nanobot:/root/.nanobot \</span><br><span class="line">  -p 18790:18790 \</span><br><span class="line">  nanobot gateway</span><br></pre></td></tr></table></figure><ul><li><code>-d</code>: 后台运行 (Detach)</li><li><code>--restart always</code>: 如果挂了或者重启电脑，自动重新启动</li></ul><h2 id="使用-systemd-Linux-服务器推荐"><a href="#使用-systemd-Linux-服务器推荐" class="headerlink" title="使用 systemd (Linux 服务器推荐)"></a>使用 systemd (Linux 服务器推荐)</h2><p>如果你是在 Linux 服务器上部署，可以把它注册成系统服务。</p><p>创建一个文件 <code>/etc/systemd/system/nanobot.service</code>：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Nanobot AI Gateway</span><br><span class="line"><span class="attr">After</span>=network.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">User</span>=your_username</span><br><span class="line"><span class="attr">ExecStart</span>=/path/to/your/python/bin/nanobot gateway</span><br><span class="line"><span class="attr">Restart</span>=always</span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">5</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure><p>然后启用：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl <span class="built_in">enable</span> nanobot</span><br><span class="line">sudo systemctl start nanobot</span><br></pre></td></tr></table></figure></p><p>这一章先到这吧，未完待续。</p>]]></content>
    
    
    <summary type="html">前言
最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是typescript语言），或者想自己魔改加个小功能，往往要翻半天文档。

直到我遇到了 nanobot。

它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。

nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、D</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着😺NanoBot学AI智能体设计和开发0：纳米机器人启动！</title>
    <link href="http://qixinbo.github.io/2026/02/01/nanobot-0/"/>
    <id>http://qixinbo.github.io/2026/02/01/nanobot-0/</id>
    <published>2026-02-01T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.905Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是<code>typescript</code>语言），或者想自己魔改加个小功能，往往要翻半天文档。</p><p>直到我遇到了 <strong>nanobot</strong>。</p><p>它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。</p><p>nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、Discord、Slack，甚至是国内的飞书、钉钉、QQ 和微信（通过 Mochat）。我现在把它挂在飞书上，平时想查个资料、翻译段文本，或者只是单纯想找个“人”聊聊代码思路，随时掏出手机就能发消息，它会像一个真正的助理一样回复你。</p><p>那么就从nanobot源码开始学习AI个人助理吧。</p><p>这次的源码学习我想换个方式，就是由原来的“先考虑顶层架构，然后再看细节实现”，变为“直接深入细节，按图索骥，窥一斑见全貌”这种方式，因为我发现NanoBot的架构跟之前的OpenCode的架构类似，都是由“智能体循环+消息总线+记忆系统+工具/Skill系统”等核心系统构成，如果按原先方式进行梳理的话，跟OpenCode之前的解析是重复了。</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><p>使用UV：<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uv tool install nanobot-ai</span><br></pre></td></tr></table></figure><br>或者使用pip：<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install nanobot-ai</span><br></pre></td></tr></table></figure><br>因为我们主要是研究源码，所以需要把源码下载下来，项目地址在这里：<a href="https://github.com/HKUDS/nanobot">GitHub - HKUDS/nanobot</a></p><h1 id="纳米机器人启动"><a href="#纳米机器人启动" class="headerlink" title="纳米机器人启动"></a>纳米机器人启动</h1><p>安装好nanobot后，如果我们输入：<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nanobot</span><br></pre></td></tr></table></figure><br>会出现：<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">root@iZ0jlfb93iky3umeol0f22Z:~<span class="comment"># nanobot</span></span><br><span class="line">                                                                                                                                      </span><br><span class="line"> Usage: nanobot [OPTIONS] COMMAND [ARGS]...                                                                                           </span><br><span class="line">                                                                                                                                      </span><br><span class="line"> 🐈 nanobot - Personal AI Assistant                                                                                                   </span><br><span class="line">                                                                                                                                      </span><br><span class="line">╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮</span><br><span class="line">│ --version             -v                                                                                                           │</span><br><span class="line">│ --install-completion            Install completion <span class="keyword">for</span> the current shell.                                                          │</span><br><span class="line">│ --show-completion               Show completion <span class="keyword">for</span> the current shell, to copy it or customize the installation.                   │</span><br><span class="line">│ --<span class="built_in">help</span>                          Show this message and <span class="built_in">exit</span>.                                                                        │</span><br><span class="line">╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span><br><span class="line">╭─ Commands ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮</span><br><span class="line">│ onboard   Initialize nanobot configuration and workspace.                                                                          │</span><br><span class="line">│ gateway   Start the nanobot gateway.                                                                                               │</span><br><span class="line">│ agent     Interact with the agent directly.                                                                                        │</span><br><span class="line">│ status    Show nanobot status.                                                                                                     │</span><br><span class="line">│ channels  Manage channels                                                                                                          │</span><br><span class="line">│ cron      Manage scheduled tasks                                                                                                   │</span><br><span class="line">╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</span><br></pre></td></tr></table></figure></p><h1 id="入口探究"><a href="#入口探究" class="headerlink" title="入口探究"></a>入口探究</h1><p>当你输入 <code>nanobot</code> 命令时，后台的执行流程其实是一个标准的 Python 命令行工具启动过程。我们可以把它分为 <strong>三个阶段</strong> 来理解：</p><h2 id="寻找入口-Shell-gt-Python"><a href="#寻找入口-Shell-gt-Python" class="headerlink" title="寻找入口 (Shell -&gt; Python)"></a>寻找入口 (Shell -&gt; Python)</h2><p>当在终端敲下 <code>nanobot</code> 并回车时：</p><ol><li><strong>Shell 查找</strong>：Shell 会在你的系统路径（PATH）中寻找名为 <code>nanobot</code> 的可执行文件。</li><li><strong>Entry Point</strong>：这个可执行文件实际上是由 Python 的包管理器（如 pip/uv）在安装时自动生成的。它会指向 <code>pyproject.toml</code> 中定义的位置：<figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[project.scripts]</span></span><br><span class="line"><span class="attr">nanobot</span> = <span class="string">&quot;nanobot.cli.commands:app&quot;</span></span><br></pre></td></tr></table></figure>这意味着：<em>“请去加载 <code>nanobot.cli.commands</code> 模块，并运行里面的 <code>app</code> 对象。”</em></li></ol><h2 id="加载与解析-Typer-框架"><a href="#加载与解析-Typer-框架" class="headerlink" title="加载与解析 (Typer 框架)"></a>加载与解析 (Typer 框架)</h2><p>Python 解释器启动后，会加载 <a href="file:///Users/qixinbo/test/nanobot/nanobot/cli/commands.py">nanobot/cli/commands.py</a> 文件。这里面的核心主角是 <code>app</code> 对象：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># nanobot/cli/commands.py</span></span><br><span class="line"></span><br><span class="line">app = typer.Typer(</span><br><span class="line">    name=<span class="string">&quot;nanobot&quot;</span>,</span><br><span class="line">    <span class="built_in">help</span>=<span class="string">f&quot;<span class="subst">&#123;__logo__&#125;</span> nanobot - Personal AI Assistant&quot;</span>,</span><br><span class="line">    no_args_is_help=<span class="literal">True</span>,  <span class="comment"># 重点：如果没有子命令，就显示帮助</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><p><strong>Typer</strong> 是一个基于 Python 类型提示的现代 CLI 库。此时，<code>app()</code> 被调用，它会做两件事：</p><ol><li><strong>解析参数</strong>：检查你有没有输入子命令（比如 <code>agent</code>, <code>gateway</code>, <code>onboard</code>）。</li><li><strong>分发任务</strong>：<ul><li>如果你只输入了 <code>nanobot</code>（没带参数），因为设置了 <code>no_args_is_help=True</code>，它会直接打印帮助菜单，列出所有可用命令。</li><li>如果你输入了 <code>nanobot agent</code>，它就会去执行被 <code>@app.command()</code> 装饰的 <code>agent</code> 函数。</li></ul></li></ol><h2 id="执行具体功能-Function-Execution"><a href="#执行具体功能-Function-Execution" class="headerlink" title="执行具体功能 (Function Execution)"></a>执行具体功能 (Function Execution)</h2><p>根据你输入的子命令，Typer 会路由到对应的 Python 函数执行。例如：</p><ul><li><p><strong><code>nanobot agent</code></strong> -&gt; 执行 <code>def agent(...)</code>:</p><ul><li>初始化 <code>AgentLoop</code>（核心逻辑循环）。</li><li>连接 LLM 提供商（OpenRouter/OpenAI 等）。</li><li>进入一个 <code>while True</code> 循环，等待你的输入，发送给 AI，处理工具调用，然后返回结果。</li></ul></li><li><p><strong><code>nanobot gateway</code></strong> -&gt; 执行 <code>def gateway(...)</code>:</p><ul><li>启动一个长期运行的服务进程。</li><li>初始化 <code>MessageBus</code>（消息总线）。</li><li>启动各个渠道（Telegram, Discord 等）的监听器。</li><li>挂载 <code>CronService</code>（定时任务）和 <code>HeartbeatService</code>（心跳保活）。</li></ul></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>简单来说，<code>nanobot</code> 命令就是一个 <strong>路由器</strong>。它负责解析你的意图，然后把任务分发给后台具体的 Python 函数去执行。</p><p>这一节就到这里吧，后面继续。</p>]]></content>
    
    
    <summary type="html">前言
最近在看OpenClaw这个当今最火的AI个人助理，想通过OpenClaw来研究下这种个人智能助理的设计和开发原理。但是发现OpenClaw太“重”了，动不动就是几十万行代码，层层封装，想从源码层面理解它的运行逻辑（它还是typescript语言），或者想自己魔改加个小功能，往往要翻半天文档。

直到我遇到了 nanobot。

它给我的第一感觉就是“干净”。核心代码只有 4000 行左右（大概只有OpenClaw 的 1%），但麻雀虽小，五脏俱全。它去掉了很多复杂的抽象，保留了 Agent 最核心的能力。

nanobot 内置了非常丰富的渠道支持。你可以把它接入 Telegram、D</summary>
    
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/categories/Agent/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>在阿里云上部署当今最强AI个人助理OpenClaw</title>
    <link href="http://qixinbo.github.io/2026/01/31/aliyun-openclaw/"/>
    <id>http://qixinbo.github.io/2026/01/31/aliyun-openclaw/</id>
    <published>2026-01-31T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.893Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近OpenClaw（曾用名ClawdBot、MoltBot）大火，本文基于 <strong>OpenClaw 官方文档</strong>，结合 <strong>阿里云 ECS / 轻量应用服务器</strong> 的实际使用场景，给出一份 <strong>从零开始、可落地</strong> 的 OpenClaw 安装与部署指南，适用于测试与生产环境。</p><h1 id="部署前准备"><a href="#部署前准备" class="headerlink" title="部署前准备"></a>部署前准备</h1><h2 id="云服务器选择"><a href="#云服务器选择" class="headerlink" title="云服务器选择"></a>云服务器选择</h2><p>推荐使用 <strong>阿里云 ECS</strong> 或 <strong>轻量应用服务器（SAS）</strong>。</p><p><strong>推荐配置：</strong></p><ul><li>操作系统：Ubuntu 22.04 LTS（或 20.04）</li><li>CPU：≥ 2 vCPU  </li><li>内存：≥ 4 GB  </li><li>磁盘：≥ 40 GB SSD  </li><li>网络：分配公网 IP</li></ul><blockquote><p>OpenClaw 官方推荐运行在标准 Linux VPS 环境中，支持容器化部署。</p></blockquote><h2 id="安全组与网络"><a href="#安全组与网络" class="headerlink" title="安全组与网络"></a>安全组与网络</h2><p>在阿里云控制台 <strong>安全组规则</strong> 中，至少放行以下端口：</p><div class="table-container"><table><thead><tr><th>用途</th><th>端口</th><th>协议</th></tr></thead><tbody><tr><td>SSH 登录</td><td>22</td><td>TCP</td></tr><tr><td>OpenClaw Gateway</td><td>18789</td><td>TCP</td></tr></tbody></table></div><h1 id="环境安装"><a href="#环境安装" class="headerlink" title="环境安装"></a>环境安装</h1><h2 id="安装-Node-js（非-Docker-模式）"><a href="#安装-Node-js（非-Docker-模式）" class="headerlink" title="安装 Node.js（非 Docker 模式）"></a>安装 Node.js（非 Docker 模式）</h2><p>OpenClaw 需要 <strong>Node.js ≥ 22</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -</span><br><span class="line">sudo apt install -y nodejs</span><br><span class="line">node -v</span><br><span class="line">npm -v</span><br></pre></td></tr></table></figure><h2 id="配置大模型"><a href="#配置大模型" class="headerlink" title="配置大模型"></a>配置大模型</h2><p>这里使用的<code>kimi 2.5</code>模型。<br>具体订阅和获取key的方法不再详述。</p><h1 id="安装-OpenClaw"><a href="#安装-OpenClaw" class="headerlink" title="安装 OpenClaw"></a>安装 OpenClaw</h1><h2 id="官方一键安装脚本（推荐，如果不行用下面的npm）"><a href="#官方一键安装脚本（推荐，如果不行用下面的npm）" class="headerlink" title="官方一键安装脚本（推荐，如果不行用下面的npm）"></a>官方一键安装脚本（推荐，如果不行用下面的npm）</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://openclaw.bot/install.sh | bash</span><br></pre></td></tr></table></figure><p>安装完成后，确认命令是否可用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openclaw --version</span><br></pre></td></tr></table></figure><p>如命令不存在，请确认 <code>npm global bin</code> 已加入 <code>$PATH</code>。</p><h2 id="使用npm"><a href="#使用npm" class="headerlink" title="使用npm"></a>使用npm</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i -g openclaw</span><br></pre></td></tr></table></figure><h1 id="初始化与配置"><a href="#初始化与配置" class="headerlink" title="初始化与配置"></a>初始化与配置</h1><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openclaw onboard --install-daemon</span><br></pre></td></tr></table></figure><p>初始化过程中将配置：</p><ul><li>OpenClaw 服务端口（默认 <code>18789</code>）</li><li>Agent 运行模式</li><li>LLM 模型提供方与 API Key</li><li>是否安装 systemd 后台服务</li></ul><h1 id="接入飞书"><a href="#接入飞书" class="headerlink" title="接入飞书"></a>接入飞书</h1><p>这一部分直接参考向阳乔木的图文教程，见<a href="https://mp.weixin.qq.com/s?src=11&amp;timestamp=1769869801&amp;ver=6514&amp;signature=uk1ZIp93XoacLXw23EuJxGfxHzvnV8WG3*3NHojQC-wtoG8WqC3xFX*4nmMjBCpAXbGO6McmGFkbStmXh5RJWnkX2ZT9C3u97u0HdOEInTuUTpF4WeI71-fQ0YbcY0**&amp;new=1">这里</a></p><h2 id="安装飞书插件"><a href="#安装飞书插件" class="headerlink" title="安装飞书插件"></a>安装飞书插件</h2><p>在openclaw的终端里直接输入：<br><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openclaw plugins install @m1heng-clawd/feishu</span><br></pre></td></tr></table></figure></p><h2 id="配置飞书应用"><a href="#配置飞书应用" class="headerlink" title="配置飞书应用"></a>配置飞书应用</h2><p>详见上面的图文教程。</p><h1 id="常见命令"><a href="#常见命令" class="headerlink" title="常见命令"></a>常见命令</h1><h2 id="启动Openclaw的TUI"><a href="#启动Openclaw的TUI" class="headerlink" title="启动Openclaw的TUI"></a>启动Openclaw的TUI</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openclaw tui</span><br></pre></td></tr></table></figure><h2 id="重启网关"><a href="#重启网关" class="headerlink" title="重启网关"></a>重启网关</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openclaw gateway restart</span><br></pre></td></tr></table></figure><h2 id="开启新对话"><a href="#开启新对话" class="headerlink" title="开启新对话"></a>开启新对话</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/new</span><br></pre></td></tr></table></figure><h2 id="不会就问"><a href="#不会就问" class="headerlink" title="不会就问"></a>不会就问</h2><p>既然已经连上Openclaw了，不会的都可以问它。</p>]]></content>
    
    
    <summary type="html">前言
最近OpenClaw（曾用名ClawdBot、MoltBot）大火，本文基于 OpenClaw 官方文档，结合 阿里云 ECS / 轻量应用服务器 的实际使用场景，给出一份 从零开始、可落地 的 OpenClaw 安装与部署指南，适用于测试与生产环境。

部署前准备
云服务器选择
推荐使用 阿里云 ECS 或 轻量应用服务器（SAS）。

推荐配置：

 * 操作系统：Ubuntu 22.04 LTS（或 20.04）
 * CPU：≥ 2 vCPU 
 * 内存：≥ 4 GB 
 * 磁盘：≥ 40 GB SSD 
 * 网络：分配公网 IP

OpenClaw 官方推荐运行在标准 L</summary>
    
    
    
    <category term="Vibe coding" scheme="http://qixinbo.github.io/categories/Vibe-coding/"/>
    
    
    <category term="Aliyun" scheme="http://qixinbo.github.io/tags/Aliyun/"/>
    
  </entry>
  
  <entry>
    <title>跟着OpenCode学智能体设计和开发6：服务器API</title>
    <link href="http://qixinbo.github.io/2026/01/21/opencode-6/"/>
    <id>http://qixinbo.github.io/2026/01/21/opencode-6/</id>
    <published>2026-01-21T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="REST-API-端点：OpenAPI-规范与使用"><a href="#REST-API-端点：OpenAPI-规范与使用" class="headerlink" title="REST API 端点：OpenAPI 规范与使用"></a>REST API 端点：OpenAPI 规范与使用</h1><p>OpenCode 服务器提供了一个基于 OpenAPI 3.1.1 规范构建的全面 REST API，支持与 AI 编码 Agent、会话管理和项目操作的程序化交互。该架构支持同步 HTTP 请求以及通过 Server-Sent Events 和 WebSockets 进行实时通信，为多样化的集成场景提供了灵活性。</p><p>来源：openapi.json, server.ts</p><h2 id="API-架构概览"><a href="#API-架构概览" class="headerlink" title="API 架构概览"></a>API 架构概览</h2><p>OpenCode API 遵循模块化设计，组织为不同的资源域，每个域服务于 AI 辅助开发工作流中的特定功能。服务器实现使用 Hono 框架和 hono-openapi 中间件，从路由定义和 Zod schemas 自动生成 OpenAPI 规范，确保类型安全和文档一致性。</p><p>API 服务器实现了基于目录的上下文中间件，从查询参数或 x-opencode-directory header 中提取工作目录，从而在单个服务器实例中支持多项目。请求通过特定于实例的处理程序路由，这些处理程序按需延迟初始化项目资源。</p><p>来源：server.ts, client.ts</p><h2 id="OpenAPI-规范"><a href="#OpenAPI-规范" class="headerlink" title="OpenAPI 规范"></a>OpenAPI 规范</h2><p>OpenAPI 规范维护在 packages/sdk/openapi.json 中，并从路由定义自动生成。该规范遵循 OpenAPI 3.1.1 标准，包含全面的元数据、请求/响应 schemas 和每个端点的代码示例。</p><p>规范元数据：</p><div class="table-container"><table><thead><tr><th>属性</th><th>值</th><th>描述</th></tr></thead><tbody><tr><td>OpenAPI Version</td><td>3.1.1</td><td>改进了 JSON Schema 支持的最新 OpenAPI 规范</td></tr><tr><td>Title</td><td>opencode</td><td>API 标识符</td></tr><tr><td>Description</td><td>opencode api</td><td>简要描述</td></tr><tr><td>Version</td><td>1.0.0</td><td>用于兼容性跟踪的 API 版本</td></tr></tbody></table></div><p>该规范在 /doc 端点公开，用于交互式探索和客户端代码生成。这使得开发者可以使用标准 OpenAPI 工具（如 OpenAPI Generator 或 Swagger Codegen）自动生成多种语言的客户端 SDK。</p><p>来源：openapi.json, server.ts</p><h2 id="全局端点"><a href="#全局端点" class="headerlink" title="全局端点"></a>全局端点</h2><p>全局端点管理与特定项目或会话无关的服务器级操作。这些端点提供健康监控、生命周期管理和系统级事件订阅。</p><h3 id="健康检查"><a href="#健康检查" class="headerlink" title="健康检查"></a>健康检查</h3><p>GET /global/health</p><p>返回服务器健康状态和版本信息，使负载均衡器和监控系统能够验证服务器可用性。</p><p>响应 Schema：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  healthy: true,  // 如果服务器运行则始终为 true</span><br><span class="line">  version: string  // 来自 Installation.VERSION 的服务器版本</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>示例请求：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createOpencodeClient &#125; <span class="keyword">from</span> <span class="string">&quot;@opencode-ai/sdk&quot;</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> client = createOpencodeClient()</span><br><span class="line"><span class="keyword">const</span> health = <span class="keyword">await</span> client.global.health()</span><br><span class="line"><span class="built_in">console</span>.log(health.healthy, health.version)</span><br></pre></td></tr></table></figure><p>来源：server.ts, openapi.json</p><h3 id="全局事件流"><a href="#全局事件流" class="headerlink" title="全局事件流"></a>全局事件流</h3><p>GET /global/event</p><p>建立 Server-Sent Events (SSE) 连接，以接收来自 OpenCode 系统的实时更新。该端点流式传输事件，包括服务器连接、心跳和系统级通知。</p><p>响应： text/event-stream，包含遵循 GlobalEvent schema 模式的 JSON 载荷。</p><p>流事件：</p><ul><li>server.connected - 初始连接确认</li><li>server.heartbeat - 30 秒间隔心跳（防止 WKWebView 超时）</li><li>来自全局事件总线的自定义事件类型</li></ul><p>连接每 30 秒维持一次心跳，以防止连接超时，这对于基于 WebView 的客户端（如 iOS Safari）尤为重要。</p><p>来源：server.ts, openapi.json</p><h3 id="全局销毁"><a href="#全局销毁" class="headerlink" title="全局销毁"></a>全局销毁</h3><p>POST /global/dispose</p><p>干净地关闭所有 OpenCode 实例，释放包括文件监视器、LSP 服务器和 Agent 进程在内的资源。此操作不可逆，应谨慎使用。</p><p>响应： boolean，表示销毁是否成功。</p><p>该端点在全局总线上触发 global.disposed 事件，并调用 Instance.disposeAll() 来终止所有活动的项目实例。</p><p>来源：server.ts, openapi.json</p><h2 id="项目端点"><a href="#项目端点" class="headerlink" title="项目端点"></a>项目端点</h2><p>项目端点管理 OpenCode 项目，包括列出可用项目、检索当前项目上下文和更新项目元数据。</p><h3 id="列出项目"><a href="#列出项目" class="headerlink" title="列出项目"></a>列出项目</h3><p>GET /project</p><p>检索已使用 OpenCode 打开的所有项目列表，按最近访问时间排序。</p><p>参数：</p><ul><li>directory (query, optional): 用于上下文的工作目录路径</li></ul><p>响应： 包含项目元数据的 Project 对象数组。</p><h3 id="获取当前项目"><a href="#获取当前项目" class="headerlink" title="获取当前项目"></a>获取当前项目</h3><p>GET /project/current</p><p>返回 OpenCode 当前正在处理的活跃项目。此端点对于需要显示活跃项目上下文的 UI 组件很有用。</p><p>参数：</p><ul><li>directory (query, optional): 用于上下文的工作目录路径</li></ul><p>响应： 单个 Project 对象。</p><h3 id="更新项目"><a href="#更新项目" class="headerlink" title="更新项目"></a>更新项目</h3><p>PATCH /project/{projectID}</p><p>更新项目属性，例如名称、图标和颜色。这主要用于 UI 自定义和项目标识。</p><p>参数：</p><ul><li>directory (query, optional): 工作目录路径</li><li>projectID (path, required): 项目标识符</li></ul><p>请求体：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  name?: string,</span><br><span class="line">  icon?: &#123;</span><br><span class="line">    url?: string,</span><br><span class="line">    color?: string</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>响应： 包含修改属性的更新后的 Project 对象。</p><p>来源：project.ts, openapi.json</p><h2 id="会话端点"><a href="#会话端点" class="headerlink" title="会话端点"></a>会话端点</h2><p>会话端点提供对 OpenCode 对话的全面管理，包括创建、查询、分支和删除。会话代表与消息历史记录、工具调用和状态相关联的单独 AI 交互。</p><h3 id="列出会话"><a href="#列出会话" class="headerlink" title="列出会话"></a>列出会话</h3><p>GET /session</p><p>检索所有会话，并具有可选的过滤功能。会话默认按最近更新时间排序。</p><p>参数：</p><ul><li>directory (query, optional): 工作目录路径</li><li>start (query, optional): 过滤在此时间戳之后（含）更新的会话（自纪元以来的毫秒数）</li><li>search (query, optional): 按标题过滤会话（不区分大小写）</li><li>limit (query, optional): 要返回的会话最大数量</li></ul><p>响应： Session 对象数组。</p><h3 id="获取会话状态"><a href="#获取会话状态" class="headerlink" title="获取会话状态"></a>获取会话状态</h3><p>GET /session/status</p><p>检索所有会话的当前状态，包括活跃、空闲和已完成状态。这对于监控活跃 AI 操作和显示进度指示器很有用。</p><p>参数：</p><ul><li>directory (query, optional): 工作目录路径</li></ul><p>响应： 将会话 ID 映射到 SessionStatus 对象的对象。</p><h3 id="获取会话"><a href="#获取会话" class="headerlink" title="获取会话"></a>获取会话</h3><p>GET /session/{sessionID}</p><p>检索有关特定会话的详细信息，包括元数据、消息历史记录和状态。</p><p>参数：</p><ul><li>directory (query, optional): 工作目录路径</li><li>sessionID (path, required): 会话标识符，匹配模式 ^ses.*</li></ul><p>响应： 完整的 Session 对象。</p><h3 id="创建会话"><a href="#创建会话" class="headerlink" title="创建会话"></a>创建会话</h3><p>POST /session</p><p>创建一个新的 OpenCode 会话，用于与 AI 助手交互。会话可以使用父引用进行初始化，以便进行分支工作流和自定义权限配置。</p><p>参数：</p><ul><li>directory (query, optional): 工作目录路径</li></ul><p>请求体：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  parentID?: string,  // 用于分支的可选父会话 ID</span><br><span class="line">  title?: string,     // 会话标题</span><br><span class="line">  permission?: PermissionRuleset  // 自定义权限配置</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>响应： 新创建的 Session 对象。</p><h3 id="更新会话"><a href="#更新会话" class="headerlink" title="更新会话"></a>更新会话</h3><p>PATCH /session/{sessionID}</p><p>更新会话属性，例如标题或归档时间戳。这通常用于会话管理和清理操作。</p><p>参数：</p><ul><li>sessionID (path, required): 会话标识符</li></ul><p>请求体：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  title?: string,</span><br><span class="line">  time?: &#123;</span><br><span class="line">    archived?: number  // 归档时间戳</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>响应： 更新后的 Session 对象。</p><h3 id="删除会话"><a href="#删除会话" class="headerlink" title="删除会话"></a>删除会话</h3><p>DELETE /session/{sessionID}</p><p>永久删除会话及所有关联数据，包括消息、工具调用历史记录和缓存状态。此操作无法撤销。</p><p>参数：</p><ul><li>directory (query, optional): 工作目录路径</li><li>sessionID (path, required): 会话标识符</li></ul><p>响应： boolean，表示删除是否成功。</p><h3 id="会话操作"><a href="#会话操作" class="headerlink" title="会话操作"></a>会话操作</h3><p>几个额外的会话操作支持高级工作流：</p><ul><li>获取子会话 (GET /session/{sessionID}/children)：检索从指定父会话分支出来的所有会话</li><li>获取会话待办事项 (GET /session/{sessionID}/todo)：检索包含会话操作项的待办事项列表</li><li>初始化会话 (POST /session/{sessionID}/init)：分析应用程序并创建包含项目特定配置的 AGENTS.md</li><li>分支会话 (POST /session/{sessionID}/fork)：从现有会话的特定消息点创建新会话</li><li>还原会话 (POST /session/{sessionID}/revert)：将会话状态还原到以前的某个消息点</li></ul><p>来源：server.ts, openapi.json</p><h2 id="配置端点"><a href="#配置端点" class="headerlink" title="配置端点"></a>配置端点</h2><p>配置端点管理 OpenCode 的运行时设置和首选项，允许在不重新启动服务器的情况下动态调整行为。</p><h3 id="获取配置"><a href="#获取配置" class="headerlink" title="获取配置"></a>获取配置</h3><p>GET /config</p><p>检索当前的 OpenCode 配置设置，包括提供程序设置、模型首选项和 UI 选项。</p><p>响应： 包含所有配置参数的 Config.Info 对象。</p><h3 id="更新配置"><a href="#更新配置" class="headerlink" title="更新配置"></a>更新配置</h3><p>PATCH /config</p><p>更新 OpenCode 配置设置。更改将立即应用并持久化到存储中。</p><p>请求体： 包含更新值的 Config.Info 对象。</p><p>响应： 更新后的 Config.Info 对象。</p><p>配置更新会影响服务器实例中的所有操作，包括 Agent 行为、提供程序身份验证和工具权限。</p><p>来源：server.ts</p><h2 id="PTY-伪终端-端点"><a href="#PTY-伪终端-端点" class="headerlink" title="PTY (伪终端) 端点"></a>PTY (伪终端) 端点</h2><p>PTY 端点管理终端会话，用于在 OpenCode 的 Agent 工作流中执行 shell 命令和进程。这些功能提供实时终端交互能力。</p><h3 id="列出-PTY-会话"><a href="#列出-PTY-会话" class="headerlink" title="列出 PTY 会话"></a>列出 PTY 会话</h3><p>GET /pty</p><p>检索所有活动的伪终端会话，包括其当前状态和进程信息。</p><p>响应： Pty.Info 对象数组。</p><h3 id="创建-PTY-会话"><a href="#创建-PTY-会话" class="headerlink" title="创建 PTY 会话"></a>创建 PTY 会话</h3><p>POST /pty</p><p>创建一个新的伪终端会话，用于运行 shell 命令。PTY 会话是具有自己的工作目录和环境的隔离进程。</p><p>请求体： 指定会话参数的 Pty.CreateInput。</p><p>响应： 新创建会话的 Pty.Info 对象。</p><h3 id="获取-PTY-会话"><a href="#获取-PTY-会话" class="headerlink" title="获取 PTY 会话"></a>获取 PTY 会话</h3><p>GET /pty/{ptyID}</p><p>检索有关特定 PTY 会话的详细信息。</p><p>参数：</p><ul><li>ptyID (path, required): 会话标识符</li></ul><p>响应： Pty.Info 对象。</p><h3 id="更新-PTY-会话"><a href="#更新-PTY-会话" class="headerlink" title="更新 PTY 会话"></a>更新 PTY 会话</h3><p>PUT /pty/{ptyID}</p><p>更新现有 PTY 会话的属性，例如窗口大小或工作目录。</p><p>参数：</p><ul><li>ptyID (path, required): 会话标识符</li></ul><p>请求体： 包含更新属性的 Pty.UpdateInput。</p><p>响应： 更新后的 Pty.Info 对象。</p><h3 id="移除-PTY-会话"><a href="#移除-PTY-会话" class="headerlink" title="移除 PTY 会话"></a>移除 PTY 会话</h3><p>DELETE /pty/{ptyID}</p><p>终止并移除 PTY 会话，向子进程发送适当的信号。</p><p>参数：</p><ul><li>ptyID (path, required): 会话标识符</li></ul><p>响应： boolean，表示移除是否成功。</p><h3 id="连接到-PTY-会话"><a href="#连接到-PTY-会话" class="headerlink" title="连接到 PTY 会话"></a>连接到 PTY 会话</h3><p>GET /pty/{ptyID}/connect</p><p>建立 WebSocket 连接，以便与 PTY 会话进行实时交互。这支持流式终端输入/输出，用于交互式命令执行。</p><p>参数：</p><ul><li>ptyID (path, required): 会话标识符</li></ul><p>协议： 支持双向消息流式传输的 WebSocket。</p><p>来源：server.ts</p><h2 id="工具注册端点"><a href="#工具注册端点" class="headerlink" title="工具注册端点"></a>工具注册端点</h2><p>工具端点提供对可用工具注册表的访问，Agent 可以使用这些工具与代码库交互，包括文件操作、搜索、LSP 集成等。</p><h2 id="列出工具-ID"><a href="#列出工具-ID" class="headerlink" title="列出工具 ID"></a>列出工具 ID</h2><p>GET /experimental/tool/ids</p><p>检索所有可用工具标识符的列表，包括内置工具和来自插件的动态注册工具。</p><p>响应： 工具 ID 字符串数组。</p><h3 id="列出工具"><a href="#列出工具" class="headerlink" title="列出工具"></a>列出工具</h3><p>GET /experimental/tool</p><p>检索特定提供程序和模型组合的可用工具的详细信息。工具包括用于 AI Agent 集成的 JSON schema 参数。</p><p>参数：</p><ul><li>provider (query, required): AI 提供程序名称（例如 “anthropic”、”openai”）</li><li>model (query, required): 用于参数格式的模型标识符</li></ul><p>响应： ToolListItem 对象数组：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  id: string,          // 工具标识符</span><br><span class="line">  description: string, // 供 AI 使用的工具描述</span><br><span class="line">  parameters: object   // 参数的 JSON Schema</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>工具 schemas 是从 Zod 定义动态生成的，并使用 zodToJsonSchema 转换为 JSON Schema 格式，确保与各种 AI 提供程序 SDK 兼容。</p><p>来源：server.ts</p><h2 id="实例和路径端点"><a href="#实例和路径端点" class="headerlink" title="实例和路径端点"></a>实例和路径端点</h2><p>实例管理端点控制单个 OpenCode 项目实例，提供生命周期管理和路径信息。</p><h3 id="销毁实例"><a href="#销毁实例" class="headerlink" title="销毁实例"></a>销毁实例</h3><p>POST /instance/dispose</p><p>清理并处置当前的 OpenCode 实例，释放活跃项目的资源。与全局销毁不同，这仅影响当前项目上下文。</p><p>响应： boolean，表示销毁是否成功。</p><h3 id="获取路径"><a href="#获取路径" class="headerlink" title="获取路径"></a>获取路径</h3><p>GET /path</p><p>检索当前 OpenCode 实例的路径信息，包括工作目录和配置路径。</p><p>响应： Path 对象：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  home: string,      // 用户主目录</span><br><span class="line">  state: string,     // OpenCode 状态目录</span><br><span class="line">  config: string,    // 配置文件路径</span><br><span class="line">  worktree: string,  // Git worktree 路径（如果适用）</span><br><span class="line">  directory: string  // 当前工作目录</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：server.ts</p><h2 id="VCS-和-Worktree-端点"><a href="#VCS-和-Worktree-端点" class="headerlink" title="VCS 和 Worktree 端点"></a>VCS 和 Worktree 端点</h2><p>版本控制系统端点提供 Git 集成，用于管理分支、worktrees 和存储库状态。</p><h3 id="获取-VCS-信息"><a href="#获取-VCS-信息" class="headerlink" title="获取 VCS 信息"></a>获取 VCS 信息</h3><p>GET /vcs</p><p>检索当前项目的版本控制系统信息，包括当前 git 分支和存储库状态。</p><p>响应： 包含分支信息的 Vcs.Info 对象。</p><h3 id="创建-Worktree"><a href="#创建-Worktree" class="headerlink" title="创建 Worktree"></a>创建 Worktree</h3><p>POST /experimental/worktree</p><p>为当前项目创建一个新的 git worktree，实现隔离的开发工作流和沙盒实验。</p><p>请求体： 包含 worktree 配置的 Worktree.create.schema。</p><p>响应： Worktree.Info 对象。</p><h3 id="列出-Worktree"><a href="#列出-Worktree" class="headerlink" title="列出 Worktree"></a>列出 Worktree</h3><p>GET /experimental/worktree</p><p>列出当前项目的所有沙盒 worktrees，提供对隔离开发环境的可见性。</p><p>响应： worktree 目录路径数组。</p><p>来源：server.ts</p><h2 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h2><p>API 使用 NamedError 类和一致的响应格式实现了标准化的错误处理系统。所有错误响应都包含结构化数据，以帮助调试和客户端错误恢复。</p><p>错误响应 Schema：</p><div class="table-container"><table><thead><tr><th>状态码</th><th>Schema</th><th>描述</th></tr></thead><tbody><tr><td>400</td><td>BadRequestError</td><td>验证错误、无效参数或格式错误的请求</td></tr><tr><td>404</td><td>NotFoundError</td><td>未找到资源（会话、项目、PTY 会话）</td></tr><tr><td>500</td><td>UnknownError</td><td>带有堆栈跟踪的意外服务器错误</td></tr></tbody></table></div><p>BadRequestError 结构：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  data: any,                              // 导致错误的请求数据</span><br><span class="line">  errors: Array&lt;Record&lt;string, any&gt;&gt;,    // 特定的验证错误</span><br><span class="line">  success: false                          // 错误始终为 false</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>错误中间件拦截异常并将其映射到适当的 HTTP 响应，同时通过 Zod schema 验证维护类型安全，从而提供详细的错误信息。</p><p>来源：error.ts</p><h2 id="TypeScript-SDK-用法"><a href="#TypeScript-SDK-用法" class="headerlink" title="TypeScript SDK 用法"></a>TypeScript SDK 用法</h2><p>OpenCode SDK 提供了一个从 OpenAPI 规范生成的类型安全客户端库，自动处理序列化、错误处理和身份验证。</p><p>SDK 在配置时会自动将 directory 参数注入请求头，通过 createOpencodeClient 函数简化了多项目 API 的使用。</p><h3 id="客户端初始化"><a href="#客户端初始化" class="headerlink" title="客户端初始化"></a>客户端初始化</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createOpencodeClient, OpencodeClient &#125; <span class="keyword">from</span> <span class="string">&quot;@opencode-ai/sdk&quot;</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">// 基本客户端初始化</span></span><br><span class="line"><span class="keyword">const</span> client = createOpencodeClient(&#123;</span><br><span class="line">  baseUrl: <span class="string">&quot;http://localhost:4096&quot;</span>,</span><br><span class="line">  directory: <span class="string">&quot;/path/to/project&quot;</span>  <span class="comment">// 可选目录上下文</span></span><br><span class="line">&#125;)</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 自定义 fetch 配置（禁用长时间运行操作的超时）</span></span><br><span class="line"><span class="keyword">const</span> customClient = createOpencodeClient(&#123;</span><br><span class="line">  fetch: <span class="function">(<span class="params">req</span>) =&gt;</span> &#123;</span><br><span class="line">    req.timeout = <span class="literal">false</span></span><br><span class="line">    <span class="keyword">return</span> fetch(req)</span><br><span class="line">  &#125;,</span><br><span class="line">  headers: &#123;</span><br><span class="line">    <span class="string">&quot;x-opencode-directory&quot;</span>: <span class="string">&quot;/custom/path&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="常见-SDK-模式"><a href="#常见-SDK-模式" class="headerlink" title="常见 SDK 模式"></a>常见 SDK 模式</h3><p>会话管理：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用过滤器列出会话</span></span><br><span class="line"><span class="keyword">const</span> sessions = <span class="keyword">await</span> client.session.list(&#123;</span><br><span class="line">  search: <span class="string">&quot;refactor&quot;</span>,</span><br><span class="line">  limit: <span class="number">10</span></span><br><span class="line">&#125;)</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 创建新会话</span></span><br><span class="line"><span class="keyword">const</span> session = <span class="keyword">await</span> client.session.create(&#123;</span><br><span class="line">  title: <span class="string">&quot;New Development Session&quot;</span>,</span><br><span class="line">  permission: customPermissions</span><br><span class="line">&#125;)</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 分支现有会话</span></span><br><span class="line"><span class="keyword">const</span> forked = <span class="keyword">await</span> client.session.fork(&#123;</span><br><span class="line">  sessionID: <span class="string">&quot;ses_abc123&quot;</span>,</span><br><span class="line">  messageID: <span class="string">&quot;msg_def456&quot;</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>项目操作：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 列出所有项目</span></span><br><span class="line"><span class="keyword">const</span> projects = <span class="keyword">await</span> client.project.list()</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 获取当前项目</span></span><br><span class="line"><span class="keyword">const</span> current = <span class="keyword">await</span> client.project.current()</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 更新项目元数据</span></span><br><span class="line"><span class="keyword">await</span> client.project.update(&#123;</span><br><span class="line">  projectID: <span class="string">&quot;proj_123&quot;</span>,</span><br><span class="line">  body: &#123;</span><br><span class="line">    name: <span class="string">&quot;Updated Project Name&quot;</span>,</span><br><span class="line">    icon: &#123; <span class="attr">color</span>: <span class="string">&quot;#FF5733&quot;</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>实时事件流式传输：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 订阅全局事件</span></span><br><span class="line"><span class="keyword">const</span> eventStream = <span class="keyword">await</span> client.global.event(&#123;</span><br><span class="line">  onMessage: <span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&quot;Global event:&quot;</span>, event.payload.type)</span><br><span class="line">  &#125;,</span><br><span class="line">  onError: <span class="function">(<span class="params">error</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.error(<span class="string">&quot;Stream error:&quot;</span>, error)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>来源：client.ts, openapi.json</p><h2 id="CORS-和安全性"><a href="#CORS-和安全性" class="headerlink" title="CORS 和安全性"></a>CORS 和安全性</h2><p>API 实现了支持本地开发和生产部署的全面 CORS 策略。源验证遵循特定模式，以确保安全的跨源访问。</p><p>允许的源：</p><ul><li><a href="http://localhost:*">http://localhost:*</a> - 本地开发服务器</li><li><a href="http://127.0.0.1:*">http://127.0.0.1:*</a> - Localhost IPv4 地址</li><li>tauri://localhost 和 <a href="http://tauri.localhost">http://tauri.localhost</a> - Tauri 桌面应用程序</li><li><a href="https://*.opencode.ai">https://*.opencode.ai</a> - 生产域（仅限 HTTPS）</li><li>在服务器启动时配置的自定义白名单条目</li></ul><p>生产部署必须使用 HTTPS 并进行适当的源验证。CORS 中间件强制执行源匹配，以防止来自恶意站点的未经授权的跨源请求。</p><p>请求头：</p><ul><li>x-opencode-directory：指定工作目录上下文，覆盖查询参数</li><li>标准 HTTP 头（Content-Type、Authorization）被传递</li></ul><p>来源：server.ts</p><h1 id="WebSocket-通信：实时更新与事件"><a href="#WebSocket-通信：实时更新与事件" class="headerlink" title="WebSocket 通信：实时更新与事件"></a>WebSocket 通信：实时更新与事件</h1><p>OpenCode 利用 WebSocket 和服务器发送事件（SSE）在客户端与服务器之间提供实时双向通信。这种架构实现了实时终端会话、事件驱动更新以及全系统的即时通知。该实现结合了 Hono 的 WebSocket 能力和 SSE 流式传输，同时支持传统的 WebSocket 连接和基于 HTTP 的事件流。</p><p>来源：packages/opencode/src/server/server.ts, packages/opencode/src/server/server.ts</p><h2 id="WebSocket-架构概览"><a href="#WebSocket-架构概览" class="headerlink" title="WebSocket 架构概览"></a>WebSocket 架构概览</h2><p>OpenCode 中的实时通信层包含两个互补机制：用于低延迟双向数据传输（主要用于 PTY 会话）的 WebSocket 连接，以及用于系统事件单向流式传输的 服务器发送事件（SSE）。这种混合方法针对不同的用例进行了优化——WebSocket 为交互式会话提供真正的双向通信，而 SSE 提供高效且防火墙友好的事件广播。</p><p>WebSocket 系统通过服务器设置中的 Hono upgradeWebSocket 中间件和 websocket 配置进行初始化。PTY 系统与 WebSocket 订阅者保持活跃会话，向连接的客户端广播终端输出并接受用户输入。</p><p>来源：packages/opencode/src/server/server.ts, packages/opencode/src/server/server.ts</p><h2 id="PTY-WebSocket-连接"><a href="#PTY-WebSocket-连接" class="headerlink" title="PTY WebSocket 连接"></a>PTY WebSocket 连接</h2><p>主要的 WebSocket 实现实现了与伪终端（PTY）会话的实时交互。每个 PTY 会话可以有多个 WebSocket 订阅者，允许多个客户端同时观察同一个终端会话。这种架构支持交互式终端使用和协作查看。</p><p>WebSocket 生命周期：</p><ol><li>建立连接：客户端连接到 /pty/:ptyID/connect 端点</li><li>会话验证：服务器验证 PTY 会话是否存在</li><li>处理器分配：返回连接生命周期回调</li><li>数据流传输：终端输出广播给所有订阅者</li><li>输入处理：客户端消息转发到 PTY 进程</li><li>清理：自动移除断开连接的订阅者</li></ol><p>PTY 连接处理器管理完整的 WebSocket 生命周期：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">upgradeWebSocket(<span class="function">(<span class="params">c</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> id = c.req.param(<span class="string">&quot;ptyID&quot;</span>)</span><br><span class="line">  <span class="keyword">let</span> handler: ReturnType&lt;<span class="keyword">typeof</span> Pty.connect&gt;</span><br><span class="line">  <span class="keyword">if</span> (!Pty.get(id)) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">&quot;Session not found&quot;</span>)</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="function"><span class="title">onOpen</span>(<span class="params">_event, ws</span>)</span> &#123;</span><br><span class="line">      handler = Pty.connect(id, ws)</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="function"><span class="title">onMessage</span>(<span class="params">event</span>)</span> &#123;</span><br><span class="line">      handler?.onMessage(<span class="built_in">String</span>(event.data))</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="function"><span class="title">onClose</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">      handler?.onClose()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/server/server.ts, packages/opencode/src/pty/index.ts</p><h2 id="PTY-会话状态管理"><a href="#PTY-会话状态管理" class="headerlink" title="PTY 会话状态管理"></a>PTY 会话状态管理</h2><p>每个活跃的 PTY 会话都维护订阅者集和输出缓冲区。系统实现了具有可配置限制（默认 2MB）的缓冲区管理策略，以处理客户端临时断开连接的场景。当客户端连接时，它们会从断开连接点接收缓冲输出，确保终端状态的连续性。</p><p>PTY 接口定义了会话元数据：</p><div class="table-container"><table><thead><tr><th>字段</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>id</td><td>string</td><td>唯一的 PTY 会话标识符</td></tr><tr><td>title</td><td>string</td><td>会话的显示标题</td></tr><tr><td>command</td><td>string</td><td>正在执行的 Shell 命令</td></tr><tr><td>args</td><td>string[]</td><td>命令参数</td></tr><tr><td>cwd</td><td>string</td><td>当前工作目录</td></tr><tr><td>status</td><td>“running” “exited”</td><td>-</td></tr><tr><td>pid</td><td>number</td><td>PTY 进程的进程 ID</td></tr></tbody></table></div><p>来源：packages/opencode/src/pty/index.ts, packages/opencode/src/pty/index.ts</p><h2 id="多订阅者支持"><a href="#多订阅者支持" class="headerlink" title="多订阅者支持"></a>多订阅者支持</h2><p>PTY 系统支持到单个终端会话的多个并发 WebSocket 连接。这实现了屏幕共享和协作调试等用例。系统高效地管理订阅者集：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> ActiveSession &#123;</span><br><span class="line">  info: Info</span><br><span class="line">  process: IPty</span><br><span class="line">  buffer: <span class="built_in">string</span></span><br><span class="line">  subscribers: <span class="built_in">Set</span>&lt;WSContext&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当终端数据从 PTY 进程到达时，它会分发给所有活跃的订阅者。连接已关闭的订阅者会自动从集合中移除。如果没有订阅者连接，数据会在缓冲区中累积，直到达到配置的限制，然后循环最旧的数据。</p><p>来源：packages/opencode/src/pty/index.ts, packages/opencode/src/pty/index.ts</p><h2 id="服务器发送事件-SSE"><a href="#服务器发送事件-SSE" class="headerlink" title="服务器发送事件 (SSE)"></a>服务器发送事件 (SSE)</h2><p>对于事件广播，OpenCode 通过 Hono 的 streamSSE 功能使用 SSE。这为单向通信提供了一种比 WebSocket 更轻量的替代方案，特别适合系统范围的通知和状态更新。</p><h3 id="全局事件流-1"><a href="#全局事件流-1" class="headerlink" title="全局事件流"></a>全局事件流</h3><p>/global/event 端点提供对 全局总线 的访问，该总线跨所有 OpenCode 实例广播事件。此流非常适合监控系统范围的更改和接收跨实例通知。</p><p>主要特性：</p><ul><li>初始连接事件 (server.connected) 确认成功订阅</li><li>心跳机制每 30 秒发送一次保活信号，以防止 WKWebView 超时</li><li>客户端断开连接时自动清理</li><li>与目录无关的事件传播</li></ul><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">streamSSE(c, <span class="keyword">async</span> (stream) =&gt; &#123;</span><br><span class="line">  stream.writeSSE(&#123;</span><br><span class="line">    data: <span class="built_in">JSON</span>.stringify(&#123;</span><br><span class="line">      payload: &#123;</span><br><span class="line">        <span class="keyword">type</span>: <span class="string">&quot;server.connected&quot;</span>,</span><br><span class="line">        properties: &#123;&#125;,</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;),</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">handler</span>(<span class="params">event: <span class="built_in">any</span></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">await</span> stream.writeSSE(&#123;</span><br><span class="line">      data: <span class="built_in">JSON</span>.stringify(event),</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">  GlobalBus.on(<span class="string">&quot;event&quot;</span>, handler)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> heartbeat = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    stream.writeSSE(&#123;</span><br><span class="line">      data: <span class="built_in">JSON</span>.stringify(&#123;</span><br><span class="line">        payload: &#123;</span><br><span class="line">          <span class="keyword">type</span>: <span class="string">&quot;server.heartbeat&quot;</span>,</span><br><span class="line">          properties: &#123;&#125;,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;),</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;, <span class="number">30000</span>)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">await</span> <span class="keyword">new</span> <span class="built_in">Promise</span>&lt;<span class="built_in">void</span>&gt;(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line">    stream.onAbort(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="built_in">clearInterval</span>(heartbeat)</span><br><span class="line">      GlobalBus.off(<span class="string">&quot;event&quot;</span>, handler)</span><br><span class="line">      resolve()</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/server/server.ts, packages/opencode/src/bus/global.ts</p><h3 id="实例特定事件流"><a href="#实例特定事件流" class="headerlink" title="实例特定事件流"></a>实例特定事件流</h3><p>/event 端点订阅 实例总线，该总线广播特定于当前项目目录和 OpenCode 实例的事件。此流提供作用于工作上下文的事件，例如会话更改、权限请求和 LSP 服务器状态更新。</p><p>实例事件流包含一个 处置时关闭 机制——当实例被处置时，流会自动关闭以防止传递陈旧事件。</p><p>来源：packages/opencode/src/server/server.ts</p><h2 id="事件系统架构"><a href="#事件系统架构" class="headerlink" title="事件系统架构"></a>事件系统架构</h2><p>OpenCode 实现了一个基于 EventEmitter 构建的 类型化事件系统，支持结构化、经过架构验证的事件定义。该系统确保通过 WebSocket 和 SSE 通道流动的所有事件类型的类型安全和文档记录。</p><h3 id="事件定义模式"><a href="#事件定义模式" class="headerlink" title="事件定义模式"></a>事件定义模式</h3><p>事件使用带有 Zod 架构的 BusEvent.define 助手定义：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> Event = &#123;</span><br><span class="line">  Connected: BusEvent.define(<span class="string">&quot;server.connected&quot;</span>, z.object(&#123;&#125;)),</span><br><span class="line">  Disposed: BusEvent.define(<span class="string">&quot;global.disposed&quot;</span>, z.object(&#123;&#125;)),</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>事件注册表维护所有已定义的事件类型，支持运行时验证和架构生成。BusEvent.payloads() 函数生成涵盖所有已注册事件的可辨识联合架构，该架构用于 OpenAPI 规范。</p><p>来源：packages/opencode/src/server/server.ts, packages/opencode/src/bus/bus-event.ts, packages/opencode/src/bus/bus-event.ts</p><h3 id="PTY-事件广播"><a href="#PTY-事件广播" class="headerlink" title="PTY 事件广播"></a>PTY 事件广播</h3><p>PTY 系统为生命周期更改发出事件：</p><ul><li>pty.created - 创建新终端会话时发布</li><li>pty.updated - 会话属性（标题、大小）更改时发布</li><li>pty.exited - 终端进程退出时发布</li><li>pty.deleted - 删除会话时发布</li></ul><p>这些事件通过事件总线流动，并可供 SSE 订阅者使用，从而实现对应用程序中终端会话状态更改的实时监控。</p><p>来源：packages/opencode/src/pty/index.ts, packages/opencode/src/pty/index.ts</p><h2 id="客户端集成"><a href="#客户端集成" class="headerlink" title="客户端集成"></a>客户端集成</h2><p>客户端通过 React/Solid 应用程序架构中专门的上下文提供者和钩子与 WebSocket 和 SSE 流集成。</p><h3 id="终端-WebSocket-客户端"><a href="#终端-WebSocket-客户端" class="headerlink" title="终端 WebSocket 客户端"></a>终端 WebSocket 客户端</h3><p>终端组件管理与 PTY 会话的 WebSocket 连接：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> Terminal = <span class="function">(<span class="params">props: TerminalProps</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> sdk = useSDK()</span><br><span class="line">  <span class="keyword">const</span> theme = useTheme()</span><br><span class="line">  <span class="keyword">let</span> ws: WebSocket | <span class="literal">undefined</span></span><br><span class="line">  <span class="keyword">let</span> term: Term | <span class="literal">undefined</span></span><br><span class="line">  <span class="comment">// ... connection management</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>终端处理 WebSocket 生命周期事件、主题适配以及使用 Ghostty Web 组件的终端仿真器集成。</p><p>来源：packages/app/src/components/terminal.tsx, packages/app/src/components/terminal.tsx</p><h3 id="事件订阅上下文"><a href="#事件订阅上下文" class="headerlink" title="事件订阅上下文"></a>事件订阅上下文</h3><p>同步上下文管理用于实时状态同步的 SSE 订阅：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> &#123; <span class="attr">use</span>: useSync, <span class="attr">provider</span>: SyncProvider &#125; = createSimpleContext(&#123;</span><br><span class="line">  name: <span class="string">&quot;Sync&quot;</span>,</span><br><span class="line">  init: <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> globalSync = useGlobalSync()</span><br><span class="line">    <span class="keyword">const</span> sdk = useSDK()</span><br><span class="line">    <span class="keyword">const</span> [store, setStore] = globalSync.child(sdk.directory)</span><br><span class="line">    <span class="comment">// ... SSE subscription logic</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这使得组件能够订阅实例特定的事件，并自动响应服务器事件更新其状态。</p><p>来源：packages/app/src/context/sync.tsx</p><h2 id="连接管理和可靠性"><a href="#连接管理和可靠性" class="headerlink" title="连接管理和可靠性"></a>连接管理和可靠性</h2><h3 id="心跳机制"><a href="#心跳机制" class="headerlink" title="心跳机制"></a>心跳机制</h3><p>两个 SSE 流都实现了 30 秒心跳 以防止连接超时，这对于具有严格超时策略（默认 60 秒）的基于 WebKit 的浏览器尤为重要。这确保长期连接在不活动期间保持活跃。</p><h3 id="错误处理和重新连接"><a href="#错误处理和重新连接" class="headerlink" title="错误处理和重新连接"></a>错误处理和重新连接</h3><p>WebSocket 连接包括通过可选的 onConnectError 回调进行的错误处理。PTY 系统会自动从订阅者集中移除失败的连接，防止死连接累积。SSE 流在客户端断开连接时使用 stream.onAbort 回调进行清理。</p><h3 id="缓冲区管理"><a href="#缓冲区管理" class="headerlink" title="缓冲区管理"></a>缓冲区管理</h3><p>当没有订阅者连接时，PTY 系统为终端输出实现了 循环缓冲区：</p><ul><li>默认缓冲区限制：2MB</li><li>块大小：64KB（用于高效传输）</li><li>行为：达到限制时丢弃最旧的数据</li></ul><p>当客户端连接时，缓冲区分块发送，以避免使 WebSocket 连接不堪重负。</p><p>缓冲区管理策略确保在临时断开后重新连接的客户端可以恢复终端状态，同时防止因孤立会话导致的内存无限增长。</p><p>来源：packages/opencode/src/pty/index.ts, packages/opencode/src/pty/index.ts</p><h2 id="API-端点参考"><a href="#API-端点参考" class="headerlink" title="API 端点参考"></a>API 端点参考</h2><h3 id="WebSocket-端点"><a href="#WebSocket-端点" class="headerlink" title="WebSocket 端点"></a>WebSocket 端点</h3><div class="table-container"><table><thead><tr><th>端点</th><th>方法</th><th>目的</th><th>WebSocket 处理器</th></tr></thead><tbody><tr><td>/pty/:ptyID/connect</td><td>GET</td><td>连接到 PTY 会话</td><td>upgradeWebSocket</td></tr></tbody></table></div><h3 id="SSE-端点"><a href="#SSE-端点" class="headerlink" title="SSE 端点"></a>SSE 端点</h3><div class="table-container"><table><thead><tr><th>端点</th><th>方法</th><th>目的</th><th>事件源</th></tr></thead><tbody><tr><td>/global/event</td><td>GET</td><td>订阅全局事件</td><td>GlobalBus</td></tr><tr><td>/event</td><td>GET</td><td>订阅实例事件</td><td>Instance Bus</td></tr></tbody></table></div><h3 id="PTY-管理端点-REST"><a href="#PTY-管理端点-REST" class="headerlink" title="PTY 管理端点 (REST)"></a>PTY 管理端点 (REST)</h3><div class="table-container"><table><thead><tr><th>端点</th><th>方法</th><th>目的</th></tr></thead><tbody><tr><td>/pty</td><td>GET</td><td>列出所有 PTY 会话</td></tr><tr><td>/pty</td><td>POST</td><td>创建新的 PTY 会话</td></tr><tr><td>/pty/:ptyID</td><td>GET</td><td>获取 PTY 会话信息</td></tr><tr><td>/pty/:ptyID</td><td>PUT</td><td>更新 PTY 会话</td></tr><tr><td>/pty/:ptyID</td><td>DELETE</td><td>删除 PTY 会话</td></tr></tbody></table></div><p>来源：packages/opencode/src/server/server.ts, packages/opencode/src/server/server.ts, packages/opencode/src/server/server.ts</p><h1 id="身份验证与安全"><a href="#身份验证与安全" class="headerlink" title="身份验证与安全"></a>身份验证与安全</h1><p>OpenCode 平台实施了一个全面的多层安全架构，旨在保护用户账户、API 端点和实时协作会话。本文档探讨了认证机制、授权流程以及安全措施，这些措施保障了平台在 Web 控制台、CLI 客户端和 GitHub Action 集成方面的安全。</p><h2 id="认证架构概览"><a href="#认证架构概览" class="headerlink" title="认证架构概览"></a>认证架构概览</h2><p>OpenCode 采用基于 OpenAuth.js 框架的联邦认证模型，在支持多个身份提供商的同时保持统一的安全边界。系统区分不同的参与者类型——公共用户、认证账户、工作区用户和系统进程——每种类型都有独特的权限级别和访问模式。</p><p>认证架构遵循清晰的关注点分离原则：身份提供商处理凭证验证，OpenAuth.js 服务器管理会话创建和令牌签发，Actor 系统则在所有应用层强制执行授权规则。</p><p>来源：auth.ts，api.ts，actor.ts</p><h3 id="身份提供商集成"><a href="#身份提供商集成" class="headerlink" title="身份提供商集成"></a>身份提供商集成</h3><p>OpenCode 支持与 GitHub 和 Google 的 OAuth 2.0 集成，允许用户使用其现有的受信任身份进行认证。系统验证提供商响应，提取经过验证的电子邮件地址，并跨提供商创建统一的账户记录。</p><h4 id="GitHub-认证流程"><a href="#GitHub-认证流程" class="headerlink" title="GitHub 认证流程"></a>GitHub 认证流程</h4><p>GitHub 认证利用 OAuth 2.0 授权码流程，请求访问用户资料数据和经过验证的电子邮件地址。在创建账户之前，系统会验证用户是否拥有主要且经过验证的电子邮件。</p><p>认证处理程序在 GitHub 认证期间执行关键的安全验证：</p><ul><li>电子邮件验证：仅接受具有经过验证的主要电子邮件的账户</li><li>账户关联：通过提供商 ID 或电子邮件地址识别现有账户</li><li>工作区预配：为新账户创建默认工作区</li><li>主题令牌生成：JWT 令牌包含账户 ID 和电子邮件声明</li></ul><p>来源：auth.ts</p><h4 id="Google-认证流程"><a href="#Google-认证流程" class="headerlink" title="Google 认证流程"></a>Google 认证流程</h4><p>Google 认证遵循 OpenID Connect (OIDC)，从 ID 令牌中提取主题标识符（sub）和电子邮件。系统通过 email_verified 声明要求验证电子邮件。</p><div class="table-container"><table><thead><tr><th>Provider</th><th>Scopes Required</th><th>Token Type</th><th>Subject Source</th></tr></thead><tbody><tr><td>GitHub</td><td>read:user, user:email</td><td>OAuth Access Token</td><td>GitHub User ID</td></tr><tr><td>Google</td><td>openid, email</td><td>OIDC ID Token</td><td>Google sub claim</td></tr></tbody></table></div><p>两个提供商最终都创建与提供商特定的主题和电子邮件地址关联的账户记录，从而实现跨提供商账户发现。</p><p>来源：auth.ts</p><h3 id="账户管理和数据模型"><a href="#账户管理和数据模型" class="headerlink" title="账户管理和数据模型"></a>账户管理和数据模型</h3><p>认证系统维护多表数据库架构，支持账户联邦、用户到工作区的关系以及 API 密钥管理。每个表都通过软删除模式强制执行参照完整性。</p><h4 id="账户架构结构"><a href="#账户架构结构" class="headerlink" title="账户架构结构"></a>账户架构结构</h4><p>认证数据模型跨越三个主要表，具有清晰的关注点分离：</p><p>架构设计支持多种安全功能：</p><ul><li>账户联邦：多个 auth 记录可以引用同一个 account，从而实现从不同提供商登录</li><li>软删除：所有表都包含 time_deleted 以用于数据恢复和审计跟踪</li><li>工作区隔离：用户被限制在具有基于角色权限的特定工作区内</li><li>API 密钥范围：密钥绑定到用户和工作区，防止跨工作区访问</li></ul><p>来源：auth.sql.ts，user.sql.ts，key.sql.ts</p><h3 id="会话管理和令牌处理"><a href="#会话管理和令牌处理" class="headerlink" title="会话管理和令牌处理"></a>会话管理和令牌处理</h3><p>OpenCode 实施了双令牌策略：用于浏览器会话的短期 JWT 令牌和用于 CLI 和 GitHub Action 集成的长期 API 密钥。会话状态在服务器端维护，包含参与者信息的客户端 JWT 声明。</p><h4 id="JWT-令牌结构"><a href="#JWT-令牌结构" class="headerlink" title="JWT 令牌结构"></a>JWT 令牌结构</h4><p>OpenAuth.js 服务器生成包含主题声明的 JWT 令牌，这些声明编码了已认证参与者的类型和属性：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;iss&quot;</span>: <span class="string">&quot;https://auth.opencode.dev&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;aud&quot;</span>: <span class="string">&quot;app&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;sub&quot;</span>: <span class="string">&quot;account|account123|user@example.com&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;exp&quot;</span>: <span class="number">1735689600</span>,</span><br><span class="line">  <span class="attr">&quot;iat&quot;</span>: <span class="number">1735686000</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主题字段以管道分隔的格式编码参与者类型和属性，从而无需在每个请求上查询数据库即可实现轻量级授权。</p><h4 id="会话生命周期"><a href="#会话生命周期" class="headerlink" title="会话生命周期"></a>会话生命周期</h4><p>浏览器会话遵循具有安全令牌交换的 OAuth 2.0 授权码流程：</p><p>会话中间件验证 JWT 签名并为每个请求提取参与者上下文，将其注入请求上下文以进行下游授权检查。</p><p>来源：callback.ts，auth.ts</p><h3 id="API-密钥认证"><a href="#API-密钥认证" class="headerlink" title="API 密钥认证"></a>API 密钥认证</h3><p>API 密钥为 CLI 客户端、GitHub Actions 和桌面应用程序提供长期认证。系统生成加密安全的随机密钥，这些密钥的范围限定在工作区内的特定用户。</p><h4 id="密钥生成和存储"><a href="#密钥生成和存储" class="headerlink" title="密钥生成和存储"></a>密钥生成和存储</h4><p>API 密钥是使用加密安全的随机数生成器生成的：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Generate secret key: sk- + 64 random characters (upper, lower, numbers)</span></span><br><span class="line"><span class="keyword">const</span> chars = <span class="string">&quot;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&quot;</span></span><br><span class="line"><span class="keyword">let</span> secretKey = <span class="string">&quot;sk-&quot;</span></span><br><span class="line"><span class="keyword">const</span> array = <span class="keyword">new</span> <span class="built_in">Uint32Array</span>(<span class="number">64</span>)</span><br><span class="line">crypto.getRandomValues(array)</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, l = array.length; i &lt; l; i++) &#123;</span><br><span class="line">  secretKey += chars[array[i] % chars.length]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这将生成格式为 sk-[64 个随机字符] 的密钥，提供 256 位的熵（log2(62^64) ≈ 381 bits）。</p><h4 id="API-密钥使用流程"><a href="#API-密钥使用流程" class="headerlink" title="API 密钥使用流程"></a>API 密钥使用流程</h4><p>CLI 客户端通过在 Authorization 标头中包含 API 密钥进行认证：</p><p>系统跟踪密钥使用时间戳以进行审计，并使安全团队能够识别未使用或受损的密钥。密钥的范围限定在工作区内的个人用户，防止权限提升。</p><p>API 密钥仅在创建时显示一次。用户必须安全存储它们，因为现有密钥没有检索机制。这遵循了凭证管理的安全最佳实践。</p><p>来源：key.ts</p><h3 id="基于-Actor-的授权"><a href="#基于-Actor-的授权" class="headerlink" title="基于 Actor 的授权"></a>基于 Actor 的授权</h3><p>OpenCode 实施了一个 Actor 系统，该系统在整个应用程序中提供统一的授权上下文。Actor 代表具有特定类型和属性的安全主体，能够实现细粒度的访问控制决策。</p><h4 id="Actor-类型和权限"><a href="#Actor-类型和权限" class="headerlink" title="Actor 类型和权限"></a>Actor 类型和权限</h4><p>系统定义了四种具有不同权限级别的 Actor 类型：</p><div class="table-container"><table><thead><tr><th>Actor Type</th><th>Properties</th><th>Use Case</th><th>Typical Privileges</th></tr></thead><tbody><tr><td>public</td><td>None</td><td>Unauthenticated users</td><td>Read-only public resources</td></tr><tr><td>account</td><td>accountID, email</td><td>Authenticated users (no workspace context)</td><td>Workspace listing, workspace creation</td></tr><tr><td>user</td><td>userID, workspaceID, accountID, role</td><td>Authenticated users within a workspace</td><td>Workspace-specific resources, role-based permissions</td></tr><tr><td>system</td><td>workspaceID</td><td>Background processes</td><td>All operations within workspace</td></tr></tbody></table></div><h4 id="基于角色的访问控制-RBAC"><a href="#基于角色的访问控制-RBAC" class="headerlink" title="基于角色的访问控制 (RBAC)"></a>基于角色的访问控制 (RBAC)</h4><p>工作区实施具有管理员和成员角色的两层 RBAC 模型：</p><p>Actor.assertAdmin() 函数强制执行仅限管理员执行的操作，当非管理员尝试执行特权操作时抛出描述性错误。</p><h4 id="Actor-上下文注入"><a href="#Actor-上下文注入" class="headerlink" title="Actor 上下文注入"></a>Actor 上下文注入</h4><p>Actor 系统使用异步上下文传播，使授权决策在整个请求生命周期中可用：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">provide</span>&lt;<span class="title">R</span>, <span class="title">T</span> <span class="title">extends</span> <span class="title">Info</span>[&quot;<span class="title">type</span>&quot;]&gt;(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params">  <span class="keyword">type</span>: T,</span></span></span><br><span class="line"><span class="function"><span class="params">  properties: Extract&lt;Info, &#123; <span class="keyword">type</span>: T &#125;&gt;[<span class="string">&quot;properties&quot;</span>],</span></span></span><br><span class="line"><span class="function"><span class="params">  cb: () =&gt; R,</span></span></span><br><span class="line"><span class="function"><span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> ctx.provide(</span><br><span class="line">    &#123; <span class="keyword">type</span>, properties &#125; <span class="keyword">as</span> <span class="built_in">any</span>,</span><br><span class="line">    () =&gt; &#123;</span><br><span class="line">      <span class="keyword">return</span> Log.provide(&#123; ...properties &#125;, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;provided&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span> cb()</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;,</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种模式使得授权检查无需通过函数调用显式传递参与者上下文。</p><p>Actor 上下文会自动记录在每个操作中，从而创建将操作与特定用户和工作区相关联的完整审计跟踪。这对于安全调查和合规要求非常有价值。</p><p>来源：actor.ts，workspace.ts</p><h3 id="GitHub-Actions-集成"><a href="#GitHub-Actions-集成" class="headerlink" title="GitHub Actions 集成"></a>GitHub Actions 集成</h3><p>OpenCode 通过 OpenID Connect (OIDC) 令牌交换提供安全的 GitHub Actions 集成，消除了在 CI/CD 管道中对长期密钥的需求。</p><h4 id="OIDC-令牌验证"><a href="#OIDC-令牌验证" class="headerlink" title="OIDC 令牌验证"></a>OIDC 令牌验证</h4><p>系统根据 GitHub 的 JWKS 端点验证 GitHub Actions OIDC 令牌：</p><p>令牌验证确保：</p><ul><li>颁发者有效性：令牌必须源自 GitHub 的 OIDC 提供商</li><li>受众限制：令牌必须用于 opencode-github-action</li><li>存储库绑定：主题声明嵌入存储库信息，防止跨存储库令牌使用</li></ul><h4 id="PAT-令牌支持"><a href="#PAT-令牌支持" class="headerlink" title="PAT 令牌支持"></a>PAT 令牌支持</h4><p>为了进行本地测试，系统支持 GitHub 个人访问令牌 (PAT) 认证：</p><ul><li>验证 PAT 权限（admin、push 或 maintain）</li><li>查找存储库的 GitHub App 安装</li><li>交换为安装范围的令牌</li></ul><p>此回退机制使得可以使用与生产 CI/CD 工作流相同的安全模型进行本地开发。</p><p>来源：api.ts</p><h3 id="共享会话安全"><a href="#共享会话安全" class="headerlink" title="共享会话安全"></a>共享会话安全</h3><p>OpenCode 通过使用 Cloudflare Durable Objects 实施的基于密钥的访问控制系统，实现了安全的会话共享。</p><h4 id="共享链接生命周期"><a href="#共享链接生命周期" class="headerlink" title="共享链接生命周期"></a>共享链接生命周期</h4><p>共享会话使用随机生成的 UUID 作为访问密钥：</p><p>系统在多个级别强制执行访问控制：</p><ul><li>密钥验证：所有修改请求都需要正确的密钥</li><li>密钥前缀验证：数据密钥必须匹配预期的会话 ID 模式</li><li>管理员覆盖：平台管理员可以在没有用户密钥的情况下删除共享</li></ul><h4 id="会话隔离"><a href="#会话隔离" class="headerlink" title="会话隔离"></a>会话隔离</h4><p>Durable Objects 通过为每个共享维护单独的存储上下文来确保会话隔离：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="title">publish</span>(<span class="params">key: <span class="built_in">string</span>, content: <span class="built_in">any</span></span>)</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> sessionID = <span class="keyword">await</span> <span class="built_in">this</span>.getSessionID()</span><br><span class="line">  <span class="keyword">if</span> (</span><br><span class="line">    !key.startsWith(<span class="string">`session/info/<span class="subst">$&#123;sessionID&#125;</span>`</span>) &amp;&amp;</span><br><span class="line">    !key.startsWith(<span class="string">`session/message/<span class="subst">$&#123;sessionID&#125;</span>/`</span>) &amp;&amp;</span><br><span class="line">    !key.startsWith(<span class="string">`session/part/<span class="subst">$&#123;sessionID&#125;</span>/`</span>)</span><br><span class="line">  )</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Response(<span class="string">&quot;Error: Invalid key&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">400</span> &#125;)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">await</span> <span class="built_in">this</span>.ctx.storage.put(key, content)</span><br><span class="line">  <span class="keyword">const</span> clients = <span class="built_in">this</span>.ctx.getWebSockets()</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> client <span class="keyword">of</span> clients) &#123;</span><br><span class="line">    client.send(<span class="built_in">JSON</span>.stringify(&#123; key, content &#125;))</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>即使密钥泄露，这也能防止跨会话数据泄露。</p><p>来源：api.ts</p><h3 id="密钥管理"><a href="#密钥管理" class="headerlink" title="密钥管理"></a>密钥管理</h3><p>平台使用 SST 的 Secret 抽象来管理敏感凭证，确保密钥永远不会以明文形式提交到源代码或部署到基础设施中。</p><h4 id="密钥配置"><a href="#密钥配置" class="headerlink" title="密钥配置"></a>密钥配置</h4><p>密钥在基础架构代码中定义，并在运行时注入到 worker 环境中：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> GITHUB_APP_ID = <span class="keyword">new</span> sst.Secret(<span class="string">&quot;GITHUB_APP_ID&quot;</span>)</span><br><span class="line"><span class="keyword">const</span> GITHUB_APP_PRIVATE_KEY = <span class="keyword">new</span> sst.Secret(<span class="string">&quot;GITHUB_APP_PRIVATE_KEY&quot;</span>)</span><br><span class="line"><span class="keyword">const</span> ADMIN_SECRET = <span class="keyword">new</span> sst.Secret(<span class="string">&quot;ADMIN_SECRET&quot;</span>)</span><br></pre></td></tr></table></figure><p>这些密钥在请求处理期间被访问：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> auth = createAppAuth(&#123;</span><br><span class="line">  appId: Resource.GITHUB_APP_ID.value,</span><br><span class="line">  privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>密钥管理策略确保：</p><ul><li>特定环境的密钥：开发和生产使用不同的密钥</li><li>审计跟踪：密钥访问通过 SST 平台记录</li><li>零信任：密钥在构建产物或配置文件中永远不可见</li></ul><p>来源：app.ts，api.ts</p><h3 id="安全最佳实践和建议"><a href="#安全最佳实践和建议" class="headerlink" title="安全最佳实践和建议"></a>安全最佳实践和建议</h3><h4 id="对于应用程序开发者"><a href="#对于应用程序开发者" class="headerlink" title="对于应用程序开发者"></a>对于应用程序开发者</h4><ul><li>验证 Actor 上下文：在执行敏感操作之前，始终使用 Actor.assert() 来验证预期的 Actor 类型</li><li>使用 RBAC 助手：利用 Actor.assertAdmin() 执行仅限管理员执行的操作，而不是手动检查角色</li><li>检查工作区成员资格：在授予访问权限之前，验证用户是否属于目标工作区</li><li>记录安全事件：Actor 系统会自动记录上下文——确保敏感操作记录了适当的详细信息</li></ul><h4 id="对于-CLI-和集成开发者"><a href="#对于-CLI-和集成开发者" class="headerlink" title="对于 CLI 和集成开发者"></a>对于 CLI 和集成开发者</h4><ul><li>安全存储 API 密钥：切勿在配置文件或源代码中硬编码 API 密钥</li><li>使用环境变量：通过环境变量或密钥管理器传递 API 密钥</li><li>定期轮换密钥：为生产部署实施密钥轮换策略</li><li>处理令牌过期：在长时间运行的 CI/CD 作业中 OIDC 令牌过期时，实施优雅的回退</li></ul><h4 id="对于工作区管理员"><a href="#对于工作区管理员" class="headerlink" title="对于工作区管理员"></a>对于工作区管理员</h4><ul><li>清理未使用的密钥：定期审查并删除未使用的 API 密钥以减少攻击面</li><li>管理用户访问：当用户离开组织时，将其从工作区中删除</li><li>监控使用情况：跟踪每月使用情况和限制以防止成本超支</li><li>审查日志：监控审计日志中是否存在异常活动模式</li></ul><h4 id="对于平台用户"><a href="#对于平台用户" class="headerlink" title="对于平台用户"></a>对于平台用户</h4><ul><li>使用经过验证的电子邮件：确保您的电子邮件地址已在 OAuth 提供商处验证</li><li>保护您的会话：从共享设备和浏览器注销</li><li>报告可疑活动：如果您注意到未经授权的访问，请联系支持人员</li><li>使用唯一密码：尽管 OpenCode 使用 OAuth，但请确保您的 OAuth 提供商账户拥有强密码且唯一</li></ul><h3 id="安全架构总结"><a href="#安全架构总结" class="headerlink" title="安全架构总结"></a>安全架构总结</h3><div class="table-container"><table><thead><tr><th>Layer</th><th>Component</th><th>Security Mechanism</th><th>Primary Threat Addressed</th></tr></thead><tbody><tr><td>Identity</td><td>OAuth/OIDC Providers</td><td>Cryptographic verification</td><td>Account compromise</td></tr><tr><td>Authentication</td><td>OpenAuth.js</td><td>JWT token signing</td><td>Token forgery</td></tr><tr><td>Authorization</td><td>Actor System</td><td>Context-aware access control</td><td>Privilege escalation</td></tr><tr><td>API Security</td><td>API Keys</td><td>Cryptographic randomness</td><td>Credential theft</td></tr><tr><td>Session Management</td><td>Durable Objects</td><td>Per-session isolation</td><td>Cross-session data leakage</td></tr><tr><td>CI/CD Integration</td><td>OIDC Token Exchange</td><td>Short-lived tokens</td><td>Secret leakage in CI/CD</td></tr></tbody></table></div><p>这种多层深度防御方法确保一个层的安全故障由相邻层的保护措施来缓解，从而创建适合企业工作负载的强大安全态势。</p>]]></content>
    
    
    <summary type="html">REST API 端点：OpenAPI 规范与使用
OpenCode 服务器提供了一个基于 OpenAPI 3.1.1 规范构建的全面 REST API，支持与 AI 编码 Agent、会话管理和项目操作的程序化交互。该架构支持同步 HTTP 请求以及通过 Server-Sent Events 和 WebSockets 进行实时通信，为多样化的集成场景提供了灵活性。

来源：openapi.json, server.ts

API 架构概览
OpenCode API 遵循模块化设计，组织为不同的资源域，每个域服务于 AI 辅助开发工作流中的特定功能。服务器实现使用 Hono 框架和 hono-</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着OpenCode学智能体设计和开发5：LSP集成与代码智能</title>
    <link href="http://qixinbo.github.io/2026/01/20/opencode-5/"/>
    <id>http://qixinbo.github.io/2026/01/20/opencode-5/</id>
    <published>2026-01-20T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="LSP-集成：服务器管理与客户端通信"><a href="#LSP-集成：服务器管理与客户端通信" class="headerlink" title="LSP 集成：服务器管理与客户端通信"></a>LSP 集成：服务器管理与客户端通信</h1><p>OpenCode 的 LSP 集成是一套<strong>高性能、可扩展的智能代码辅助系统</strong>，基于客户端-服务器架构实现，支持 35+ 编程语言的诊断、导航、符号查询等功能，核心特点是<strong>延迟生成、资源优化、多服务器协调</strong>，同时提供灵活的配置和扩展能力。</p><h2 id="一、LSP-架构核心：三层组件与延迟生成策略"><a href="#一、LSP-架构核心：三层组件与延迟生成策略" class="headerlink" title="一、LSP 架构核心：三层组件与延迟生成策略"></a>一、LSP 架构核心：三层组件与延迟生成策略</h2><p>OpenCode LSP 采用<strong>关注点分离</strong>的三层架构，确保系统高效、可维护，同时通过延迟生成优化资源占用。</p><h3 id="1-三层核心组件"><a href="#1-三层核心组件" class="headerlink" title="1. 三层核心组件"></a>1. 三层核心组件</h3><div class="table-container"><table><thead><tr><th>组件</th><th>职责</th><th>核心功能</th></tr></thead><tbody><tr><td><strong>服务器管理</strong></td><td>语言服务器的生命周期控制</td><td>服务器注册、项目根目录检测、进程生成（二进制/包管理器/自动下载）</td></tr><tr><td><strong>客户端通信</strong></td><td>处理 LSP 协议交互</td><td>JSON-RPC 连接建立、文档生命周期管理、诊断接收、请求响应处理</td></tr><tr><td><strong>编排层</strong></td><td>多服务器/客户端的协调控制</td><td>状态管理、延迟客户端获取、错误处理、优雅关闭</td></tr></tbody></table></div><h3 id="2-延迟生成策略（核心优化）"><a href="#2-延迟生成策略（核心优化）" class="headerlink" title="2. 延迟生成策略（核心优化）"></a>2. 延迟生成策略（核心优化）</h3><p>系统<strong>不会预启动所有语言服务器</strong>，而是遵循「按需启动」原则：</p><ul><li>只有当用户访问<strong>与服务器扩展名匹配</strong>的文件时，才会触发对应语言服务器的生成。</li><li>优势：在大型多语言项目中，避免启动未使用的服务器进程，大幅降低内存和 CPU 占用。</li></ul><h2 id="二、服务器管理：注册、根目录检测与进程生成"><a href="#二、服务器管理：注册、根目录检测与进程生成" class="headerlink" title="二、服务器管理：注册、根目录检测与进程生成"></a>二、服务器管理：注册、根目录检测与进程生成</h2><p>服务器管理是 LSP 系统的基础，负责定义语言服务器的行为、定位项目根目录、启动服务器进程。</p><h3 id="1-服务器注册：统一-Info-接口"><a href="#1-服务器注册：统一-Info-接口" class="headerlink" title="1. 服务器注册：统一 Info 接口"></a>1. 服务器注册：统一 <code>Info</code> 接口</h3><p>所有语言服务器都通过 <code>LSPServer.Info</code> 接口注册，包含 4 个核心属性：</p><div class="table-container"><table><thead><tr><th>属性</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td><code>id</code></td><td><code>string</code></td><td>服务器唯一标识（如 <code>typescript</code>、<code>rust</code>）</td></tr><tr><td><code>extensions</code></td><td><code>string[]</code></td><td>处理的文件扩展名（如 <code>[&quot;.ts&quot;, &quot;.tsx&quot;]</code>）</td></tr><tr><td><code>root</code></td><td><code>RootFunction</code></td><td>异步函数，用于检测当前文件的项目根目录</td></tr><tr><td><code>spawn</code></td><td>`(root: string) =&gt; Promise&lt;Handle</td><td>undefined&gt;`</td><td>生成服务器进程的函数</td></tr></tbody></table></div><p><strong>注册来源</strong>：内置服务器定义 + 用户配置文件中的自定义服务器，初始化时合并到注册表。</p><h3 id="2-项目根目录检测：NearestRoot-辅助函数"><a href="#2-项目根目录检测：NearestRoot-辅助函数" class="headerlink" title="2. 项目根目录检测：NearestRoot 辅助函数"></a>2. 项目根目录检测：<code>NearestRoot</code> 辅助函数</h3><p>根目录检测是 LSP 的关键能力（语言服务器需要基于项目根目录加载配置），核心实现是 <code>NearestRoot</code> 函数，采用<strong>向上搜索模式</strong>：</p><ol><li>从当前文件目录开始，向上遍历至实例根目录。</li><li>匹配 <code>includePatterns</code> 中的项目标记文件（如 <code>package-lock.json</code>、<code>Cargo.toml</code>）。</li><li>跳过 <code>excludePatterns</code> 中的文件（如 Deno 项目的 <code>deno.json</code>）。</li><li>未找到则返回实例根目录。</li></ol><p><strong>示例（TypeScript 服务器）</strong>：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> Typescript: Info = &#123;</span><br><span class="line">  id: <span class="string">&quot;typescript&quot;</span>,</span><br><span class="line">  root: NearestRoot(</span><br><span class="line">    [<span class="string">&quot;package-lock.json&quot;</span>, <span class="string">&quot;bun.lockb&quot;</span>], <span class="comment">// 匹配包管理器锁文件</span></span><br><span class="line">    [<span class="string">&quot;deno.json&quot;</span>, <span class="string">&quot;deno.jsonc&quot;</span>] <span class="comment">// 排除 Deno 项目</span></span><br><span class="line">  ),</span><br><span class="line">  extensions: [<span class="string">&quot;.ts&quot;</span>, <span class="string">&quot;.tsx&quot;</span>, <span class="string">&quot;.js&quot;</span>],</span><br><span class="line">  <span class="keyword">async</span> <span class="function"><span class="title">spawn</span>(<span class="params">root</span>)</span> &#123; <span class="comment">/* 生成逻辑 */</span> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-服务器生成：三种启动策略"><a href="#3-服务器生成：三种启动策略" class="headerlink" title="3. 服务器生成：三种启动策略"></a>3. 服务器生成：三种启动策略</h3><p>系统支持三种灵活的服务器启动方式，适配不同类型的语言服务器：</p><div class="table-container"><table><thead><tr><th>策略</th><th>适用场景</th><th>示例</th></tr></thead><tbody><tr><td><strong>二进制发现</strong></td><td>已全局安装的服务器</td><td>Rust Analyzer：通过 <code>Bun.which(&quot;rust-analyzer&quot;)</code> 查找可执行文件</td></tr><tr><td><strong>包管理器集成</strong></td><td>项目内局部安装的服务器</td><td>如 ESLint 服务器，通过 <code>npx eslint --lsp</code> 启动</td></tr><tr><td><strong>自动下载</strong></td><td>无全局安装的服务器</td><td>Clangd：从 GitHub Releases 下载预编译二进制，解压并创建符号链接</td></tr></tbody></table></div><p><strong>受限环境适配</strong>：设置 <code>OPENCODE_DISABLE_LSP_DOWNLOAD</code> 标志可禁用自动下载，支持手动预安装。</p><h3 id="4-支持的语言服务器生态"><a href="#4-支持的语言服务器生态" class="headerlink" title="4. 支持的语言服务器生态"></a>4. 支持的语言服务器生态</h3><p>覆盖 35+ 编程语言，涵盖主流开发场景：</p><div class="table-container"><table><thead><tr><th>类别</th><th>代表服务器</th><th>核心功能</th></tr></thead><tbody><tr><td>JavaScript/TypeScript</td><td>TypeScript、ESLint、Biome</td><td>类型检查、代码格式化、Linting</td></tr><tr><td>系统语言</td><td>Rust Analyzer、Gopls、Clangd</td><td>高性能代码分析、重构支持</td></tr><tr><td>Python</td><td>Pyright、Ty</td><td>类型推断、静态检查</td></tr><tr><td>Web 框架</td><td>Vue、Svelte、Astro</td><td>组件语法分析、智能提示</td></tr><tr><td>DevOps 工具</td><td>Terraform、Prisma、Docker</td><td>配置文件校验、语法提示</td></tr></tbody></table></div><h2 id="三、客户端通信：JSON-RPC-协议交互与文档管理"><a href="#三、客户端通信：JSON-RPC-协议交互与文档管理" class="headerlink" title="三、客户端通信：JSON-RPC 协议交互与文档管理"></a>三、客户端通信：JSON-RPC 协议交互与文档管理</h2><p>客户端是 LSP 协议的执行者，负责与服务器建立连接、管理文档状态、处理诊断和请求，基于 <code>vscode-jsonrpc</code> 实现。</p><h3 id="1-连接建立：基于-Stdio-的-JSON-RPC-握手"><a href="#1-连接建立：基于-Stdio-的-JSON-RPC-握手" class="headerlink" title="1. 连接建立：基于 Stdio 的 JSON-RPC 握手"></a>1. 连接建立：基于 Stdio 的 JSON-RPC 握手</h3><p>客户端通过 <code>LSPClient.create()</code> 创建，核心步骤：</p><ol><li><strong>建立连接</strong>：通过服务器进程的 <code>stdin/stdout</code> 创建 JSON-RPC 消息流。</li><li><strong>发送初始化请求</strong>：包含工作区根目录、进程 ID、客户端能力（如诊断支持、文件监听），设置 <strong>45 秒超时</strong>防止无响应。</li><li><strong>发送初始化通知</strong>：初始化成功后，发送 <code>initialized</code> 通知，标志连接就绪。</li></ol><h3 id="2-文档生命周期管理：版本控制与增量更新"><a href="#2-文档生命周期管理：版本控制与增量更新" class="headerlink" title="2. 文档生命周期管理：版本控制与增量更新"></a>2. 文档生命周期管理：版本控制与增量更新</h3><p>客户端通过版本号跟踪文档状态，确保服务器获取最新内容，操作分为两种场景：</p><div class="table-container"><table><thead><tr><th>场景</th><th>操作步骤</th></tr></thead><tbody><tr><td><strong>新文件打开</strong></td><td>1. 发送 <code>workspace/didChangeWatchedFiles</code>（类型：Created）<br>2. 发送 <code>textDocument/didOpen</code>（携带文件内容、语言 ID）<br>3. 清除该文件的历史诊断</td></tr><tr><td><strong>现有文件修改</strong></td><td>1. 发送 <code>workspace/didChangeWatchedFiles</code>（类型：Changed）<br>2. 发送 <code>textDocument/didChange</code>（携带增量内容、递增版本号）<br>3. 更新本地版本计数器</td></tr></tbody></table></div><p><strong>语言 ID 映射</strong>：通过 <code>LANGUAGE_EXTENSIONS</code> 字典将扩展名转为 LSP 标准语言 ID（如 <code>.ts</code> → <code>typescript</code>）。</p><h3 id="3-诊断处理：接收与事件发布"><a href="#3-诊断处理：接收与事件发布" class="headerlink" title="3. 诊断处理：接收与事件发布"></a>3. 诊断处理：接收与事件发布</h3><p>诊断是 LSP 的核心功能（如语法错误、代码警告），处理流程：</p><ol><li>服务器通过 <code>textDocument/publishDiagnostics</code> 通知发送诊断结果。</li><li>客户端将诊断结果存储在内存 <code>Map</code>（键：文件路径，值：诊断数组）。</li><li>发布诊断事件到 <code>Bus</code> 系统，供其他组件（如编辑器、AI Agent）消费。</li><li><strong>特殊优化</strong>：抑制 TypeScript 服务器的初始诊断（避免早期无配置时的冗余错误）。</li></ol><h3 id="4-请求-响应处理：支持-LSP-标准请求"><a href="#4-请求-响应处理：支持-LSP-标准请求" class="headerlink" title="4. 请求/响应处理：支持 LSP 标准请求"></a>4. 请求/响应处理：支持 LSP 标准请求</h3><p>客户端内置处理程序，响应服务器的各类请求，确保协议合规：</p><div class="table-container"><table><thead><tr><th>请求类型</th><th>用途</th></tr></thead><tbody><tr><td><code>workspace/configuration</code></td><td>返回服务器初始化配置选项</td></tr><tr><td><code>client/registerCapability</code></td><td>确认动态能力注册（如文件监听）</td></tr><tr><td><code>window/workDoneProgress/create</code></td><td>支持服务器显示进度条（如索引构建）</td></tr></tbody></table></div><h2 id="四、LSP-编排层：多服务器协调与状态管理"><a href="#四、LSP-编排层：多服务器协调与状态管理" class="headerlink" title="四、LSP 编排层：多服务器协调与状态管理"></a>四、LSP 编排层：多服务器协调与状态管理</h2><p>编排层是 LSP 系统的「大脑」，通过集中式状态管理，协调多个服务器和客户端的生命周期，确保稳定性和资源效率。</p><h3 id="1-核心状态集合"><a href="#1-核心状态集合" class="headerlink" title="1. 核心状态集合"></a>1. 核心状态集合</h3><p>初始化时创建 4 个核心集合，维护系统状态：</p><div class="table-container"><table><thead><tr><th>集合</th><th>类型</th><th>用途</th></tr></thead><tbody><tr><td><code>servers</code></td><td><code>Record&lt;string, LSPServer.Info&gt;</code></td><td>所有注册的服务器定义</td></tr><tr><td><code>clients</code></td><td><code>LSPClient.Info[]</code></td><td>活跃的客户端连接</td></tr><tr><td><code>broken</code></td><td><code>Set&lt;string&gt;</code></td><td>启动失败的服务器标识符（防止重复尝试）</td></tr><tr><td><code>spawning</code></td><td><code>Map&lt;string, Promise&lt;...&gt;&gt;</code></td><td>进行中的服务器生成任务（去重）</td></tr></tbody></table></div><h3 id="2-延迟客户端获取：按需加载优化"><a href="#2-延迟客户端获取：按需加载优化" class="headerlink" title="2. 延迟客户端获取：按需加载优化"></a>2. 延迟客户端获取：按需加载优化</h3><p><code>getClients()</code> 函数实现延迟加载逻辑，核心步骤：</p><ol><li>根据文件扩展名过滤匹配的服务器。</li><li>检查是否已有活跃客户端（避免重复创建）。</li><li>检查是否有进行中的生成任务（去重，防止并发生成）。</li><li>无则启动新的生成任务，并加入 <code>spawning</code> 映射。</li><li>启动失败则加入 <code>broken</code> 集合，同一会话不再重试。</li></ol><h3 id="3-优雅关闭与状态报告"><a href="#3-优雅关闭与状态报告" class="headerlink" title="3. 优雅关闭与状态报告"></a>3. 优雅关闭与状态报告</h3><ul><li><strong>优雅关闭</strong>：实例销毁时，自动终止所有活跃客户端的连接，释放进程资源。</li><li><strong>状态报告</strong>：通过 <code>status()</code> 函数返回所有连接服务器的状态（ID、根目录、连接状态），支持监控和调试。</li></ul><h2 id="五、工具集成：向-AI-Agent-暴露代码智能功能"><a href="#五、工具集成：向-AI-Agent-暴露代码智能功能" class="headerlink" title="五、工具集成：向 AI Agent 暴露代码智能功能"></a>五、工具集成：向 AI Agent 暴露代码智能功能</h2><p>LSP 系统通过 <code>lsp</code> 工具，为 OpenCode 的 AI Agent 提供统一的代码分析接口，支持 9 种核心操作：</p><div class="table-container"><table><thead><tr><th>操作</th><th>描述</th><th>典型用例</th></tr></thead><tbody><tr><td><code>goToDefinition</code></td><td>查找符号的定义位置</td><td>了解函数/类的实现来源</td></tr><tr><td><code>findReferences</code></td><td>查找符号的所有引用</td><td>重构前的影响范围分析</td></tr><tr><td><code>hover</code></td><td>获取符号的文档和类型信息</td><td>快速查看函数参数、返回值</td></tr><tr><td><code>documentSymbol</code></td><td>获取当前文件的符号结构</td><td>生成代码大纲、导航跳转</td></tr><tr><td><code>workspaceSymbol</code></td><td>跨项目搜索符号</td><td>查找全局函数、类的位置</td></tr></tbody></table></div><p><strong>核心适配</strong>：自动将编辑器的<strong>1 基坐标</strong>转换为 LSP 标准的<strong>0 基坐标</strong>，匹配人类使用习惯。</p><h2 id="六、配置与性能优化：灵活定制与资源高效"><a href="#六、配置与性能优化：灵活定制与资源高效" class="headerlink" title="六、配置与性能优化：灵活定制与资源高效"></a>六、配置与性能优化：灵活定制与资源高效</h2><h3 id="1-丰富的配置选项"><a href="#1-丰富的配置选项" class="headerlink" title="1. 丰富的配置选项"></a>1. 丰富的配置选项</h3><p>用户可通过配置文件自定义 LSP 行为，满足个性化需求：</p><div class="table-container"><table><thead><tr><th>配置场景</th><th>示例</th></tr></thead><tbody><tr><td><strong>全局禁用 LSP</strong></td><td><code>&quot;lsp&quot;: false</code></td></tr><tr><td><strong>禁用特定服务器</strong></td><td><code>&quot;lsp&quot;: &#123; &quot;typescript&quot;: &#123; &quot;disabled&quot;: true &#125; &#125;</code></td></tr><tr><td><strong>配置自定义服务器</strong></td><td><pre>"lsp": {<br>  "my-custom-lsp": {<br>    "command": ["my-lsp", "--stdio"],<br>    "extensions": [".custom"],<br>    "initialization": { "opt": "val" }<br>  }<br>}</pre></td></tr><tr><td><strong>启用实验性服务器</strong></td><td>设置 <code>OPENCODE_EXPERIMENTAL_LSP_TY</code> 标志，自动禁用冲突的 Pyright</td></tr></tbody></table></div><h3 id="2-关键性能优化策略"><a href="#2-关键性能优化策略" class="headerlink" title="2. 关键性能优化策略"></a>2. 关键性能优化策略</h3><p>LSP 系统内置多项优化，平衡功能与资源占用：</p><div class="table-container"><table><thead><tr><th>优化手段</th><th>效果</th></tr></thead><tbody><tr><td><strong>延迟生成</strong></td><td>仅启动当前使用的语言服务器，减少内存占用</td></tr><tr><td><strong>连接重用</strong></td><td>持久化客户端-服务器连接，增量发送文件更新，避免重复进程启动</td></tr><tr><td><strong>诊断防抖</strong></td><td>150ms 防抖延迟，合并服务器连续发送的诊断更新，减少事件处理次数</td></tr><tr><td><strong>生成去重</strong></td><td>通过 <code>spawning</code> 映射避免同一服务器/根目录的并发生成</td></tr><tr><td><strong>失败抑制</strong></td><td>启动失败的服务器标记为 <code>broken</code>，同一会话不再重试，防止级联故障</td></tr></tbody></table></div><h2 id="七、测试与稳定性保障"><a href="#七、测试与稳定性保障" class="headerlink" title="七、测试与稳定性保障"></a>七、测试与稳定性保障</h2><p>系统通过<strong>虚假 LSP 服务器</strong>实现全面测试，无需依赖外部语言服务器：</p><ol><li>虚假服务器实现最小化 LSP 握手逻辑（如响应 <code>initialize</code> 请求）。</li><li>测试覆盖工作区配置、能力注册、诊断发送等核心场景。</li><li>验证协议合规性和边界情况（如初始化超时、服务器崩溃）。</li></ol><h1 id="支持的语言服务器与配置"><a href="#支持的语言服务器与配置" class="headerlink" title="支持的语言服务器与配置"></a>支持的语言服务器与配置</h1><p>OpenCode 的 LSP（Language Server Protocol）集成是一套<strong>开箱即用的高级代码智能系统</strong>，支持 35+ 种编程语言，提供语法高亮、错误诊断、代码补全、符号导航等核心功能，采用延迟初始化模型优化资源占用，同时支持灵活配置和自定义扩展。</p><h2 id="一、核心架构与关键特性"><a href="#一、核心架构与关键特性" class="headerlink" title="一、核心架构与关键特性"></a>一、核心架构与关键特性</h2><h3 id="1-延迟初始化模型（核心优化）"><a href="#1-延迟初始化模型（核心优化）" class="headerlink" title="1. 延迟初始化模型（核心优化）"></a>1. 延迟初始化模型（核心优化）</h3><p>LSP 系统不预启动任何语言服务器，<strong>仅在用户访问对应文件类型时才按需启动</strong>，既减少了内存和 CPU 占用，又能保证代码智能功能的快速响应。</p><h3 id="2-三大核心组件"><a href="#2-三大核心组件" class="headerlink" title="2. 三大核心组件"></a>2. 三大核心组件</h3><p>系统通过三个组件协同工作，实现完整的 LSP 协议支持：</p><ol><li><strong>服务器定义</strong>：包含每种语言服务器的启动逻辑、配置参数、根目录检测规则。</li><li><strong>LSP 客户端</strong>：基于 JSON-RPC 协议与语言服务器通信，处理消息收发、诊断接收、请求响应。</li><li><strong>语言扩展</strong>：维护文件扩展名与语言服务器的映射关系，确保正确匹配对应服务器。</li></ol><h3 id="3-多服务器并存支持"><a href="#3-多服务器并存支持" class="headerlink" title="3. 多服务器并存支持"></a>3. 多服务器并存支持</h3><p>同一文件可由多个互补的语言服务器处理（例如 TypeScript 文件同时启动 <code>typescript</code> 语言服务器和 <code>eslint</code> 校验服务器），提供更全面的代码智能能力。</p><h2 id="二、支持的语言服务器生态（35-种）"><a href="#二、支持的语言服务器生态（35-种）" class="headerlink" title="二、支持的语言服务器生态（35+ 种）"></a>二、支持的语言服务器生态（35+ 种）</h2><p>OpenCode 内置覆盖主流开发场景的语言服务器，按生态分类整理如下，核心信息包含自动安装支持和关键说明：</p><h3 id="1-JavaScript-TypeScript-生态（前端核心）"><a href="#1-JavaScript-TypeScript-生态（前端核心）" class="headerlink" title="1. JavaScript/TypeScript 生态（前端核心）"></a>1. JavaScript/TypeScript 生态（前端核心）</h3><div class="table-container"><table><thead><tr><th>Server ID</th><th>支持扩展名</th><th>自动安装</th><th>关键说明</th></tr></thead><tbody><tr><td><code>typescript</code></td><td>.ts、.tsx、.js、.jsx 等</td><td>是</td><td>基于 Bun 运行 <code>typescript-language-server</code>，从项目解析 <code>tsserver.js</code></td></tr><tr><td><code>vue</code></td><td>.vue</td><td>是</td><td>PATH 中不存在时，通过 npm 下载 <code>@vue/language-server</code></td></tr><tr><td><code>eslint</code></td><td>.ts、.vue 等</td><td>是</td><td>从 GitHub Releases 获取并构建 VS Code ESLint 服务器</td></tr><tr><td><code>biome</code></td><td>.ts、.json、.vue 等</td><td>部分</td><td>优先使用本地/全局二进制文件，回退到 Bun 安装</td></tr></tbody></table></div><h3 id="2-其他主流生态摘要"><a href="#2-其他主流生态摘要" class="headerlink" title="2. 其他主流生态摘要"></a>2. 其他主流生态摘要</h3><div class="table-container"><table><thead><tr><th>生态分类</th><th>代表 Server ID</th><th>自动安装</th><th>关键依赖</th></tr></thead><tbody><tr><td>Python</td><td><code>pyright</code>、<code>ty</code>（实验性）</td><td><code>pyright</code> 是</td><td><code>ty</code> 需要 <code>OPENCODE_EXPERIMENTAL_LSP_TY</code> 标志，与 <code>pyright</code> 冲突</td></tr><tr><td>系统语言</td><td><code>rust</code>、<code>gopls</code>、<code>clangd</code></td><td><code>gopls</code>、<code>clangd</code> 是</td><td><code>rust</code> 需手动安装 <code>rust-analyzer</code>，<code>clangd</code> 从 GitHub 下载预编译二进制</td></tr><tr><td>.NET/JVM</td><td><code>csharp</code>、<code>jdtls</code></td><td>是</td><td><code>jdtls</code> 需要 Java 21+，<code>csharp</code> 依赖 .NET SDK</td></tr><tr><td>Web 框架</td><td><code>svelte</code>、<code>astro</code>、<code>yaml-ls</code></td><td>是</td><td><code>astro</code> 依赖 TypeScript 环境</td></tr></tbody></table></div><h3 id="3-专用语言（需手动安装居多）"><a href="#3-专用语言（需手动安装居多）" class="headerlink" title="3. 专用语言（需手动安装居多）"></a>3. 专用语言（需手动安装居多）</h3><p><code>ruby-lsp</code>、<code>elixir-ls</code>、<code>prisma</code> 等，其中仅 <code>ruby-lsp</code>、<code>elixir-ls</code> 支持自动安装，其余需手动配置二进制文件或对应 CLI 工具。</p><h2 id="三、完整配置指南（opencode-json）"><a href="#三、完整配置指南（opencode-json）" class="headerlink" title="三、完整配置指南（opencode.json）"></a>三、完整配置指南（opencode.json）</h2><p>LSP 配置通过 <code>opencode.json</code>/<code>opencode.jsonc</code> 管理，支持全局控制、单个服务器自定义、自定义服务器添加。</p><h3 id="1-全局启用-禁用"><a href="#1-全局启用-禁用" class="headerlink" title="1. 全局启用/禁用"></a>1. 全局启用/禁用</h3><p>完全禁用所有 LSP 功能，阻止任何语言服务器启动：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;lsp&quot;</span>: <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-单个服务器配置（启用-禁用-自定义）"><a href="#2-单个服务器配置（启用-禁用-自定义）" class="headerlink" title="2. 单个服务器配置（启用/禁用/自定义）"></a>2. 单个服务器配置（启用/禁用/自定义）</h3><p>可针对内置服务器进行启用状态切换、命令覆盖、环境变量配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;lsp&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;typescript&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;disabled&quot;</span>: <span class="literal">false</span> <span class="comment">// 启用 TypeScript 服务器（默认值）</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;eslint&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;disabled&quot;</span>: <span class="literal">true</span> <span class="comment">// 禁用 ESLint 服务器</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;pyright&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;command&quot;</span>: [<span class="string">&quot;custom-pyright&quot;</span>, <span class="string">&quot;--stdio&quot;</span>], <span class="comment">// 自定义启动命令</span></span><br><span class="line">      <span class="attr">&quot;env&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;PYTHON_PATH&quot;</span>: <span class="string">&quot;/usr/bin/python3&quot;</span> <span class="comment">// 传递环境变量</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">&quot;initialization&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;pythonPath&quot;</span>: <span class="string">&quot;/custom/path/to/python&quot;</span> <span class="comment">// LSP 初始化参数</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-自定义语言服务器（添加新支持）"><a href="#3-自定义语言服务器（添加新支持）" class="headerlink" title="3. 自定义语言服务器（添加新支持）"></a>3. 自定义语言服务器（添加新支持）</h3><p>定义全新的自定义服务器，需指定 <code>command</code>、<code>extensions</code> 核心字段：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;lsp&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;my-custom-server&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;command&quot;</span>: [<span class="string">&quot;/path/to/custom-server&quot;</span>, <span class="string">&quot;--stdio&quot;</span>], <span class="comment">// 启动命令与参数</span></span><br><span class="line">      <span class="attr">&quot;extensions&quot;</span>: [<span class="string">&quot;.custom&quot;</span>], <span class="comment">// 关联的文件扩展名</span></span><br><span class="line">      <span class="attr">&quot;env&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;CUSTOM_SETTING&quot;</span>: <span class="string">&quot;value&quot;</span> <span class="comment">// 自定义环境变量</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">&quot;initialization&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;configuration&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;enabled&quot;</span>: <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>注意：自定义服务器 ID 若与内置 ID 重复，将完全替换内置配置；如需互补，建议使用不同 ID。</p></blockquote><h2 id="四、关键高级特性"><a href="#四、关键高级特性" class="headerlink" title="四、关键高级特性"></a>四、关键高级特性</h2><h3 id="1-根目录检测"><a href="#1-根目录检测" class="headerlink" title="1. 根目录检测"></a>1. 根目录检测</h3><p>语言服务器通过「向上搜索模式」确定项目工作区边界，影响文件监视、导入解析等功能，常见检测模式：</p><ol><li>基于锁文件：<code>package-lock.json</code>、<code>Cargo.lock</code> 等。</li><li>基于配置文件：<code>deno.json</code>、<code>go.mod</code>、<code>pom.xml</code> 等。</li><li>基于 VCS：回退到 Git 仓库根目录。</li><li>支持 Monorepo：识别 Rust 工作区、Gradle 多项目等结构。</li></ol><p><strong>示例（TypeScript 服务器）</strong>：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">root: NearestRoot(</span><br><span class="line">  [<span class="string">&quot;package-lock.json&quot;</span>, <span class="string">&quot;bun.lockb&quot;</span>], <span class="comment">// 匹配的锁文件</span></span><br><span class="line">  [<span class="string">&quot;deno.json&quot;</span>, <span class="string">&quot;deno.jsonc&quot;</span>] <span class="comment">// 排除的配置文件（Deno 项目）</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="2-自动下载管理"><a href="#2-自动下载管理" class="headerlink" title="2. 自动下载管理"></a>2. 自动下载管理</h3><p>多数主流服务器支持自动安装，行为由 <code>OPENCODE_DISABLE_LSP_DOWNLOAD</code> 环境变量控制：</p><ul><li>启用自动下载（默认）：无需额外配置，缺失服务器时自动从对应源下载。</li><li>禁用自动下载（受限环境）：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> OPENCODE_DISABLE_LSP_DOWNLOAD=1</span><br></pre></td></tr></table></figure></li><li>缓存目录：自动下载的二进制文件缓存在 <code>~/.opencode/bin</code>（Unix 系统）。</li></ul><h3 id="3-实验性功能"><a href="#3-实验性功能" class="headerlink" title="3. 实验性功能"></a>3. 实验性功能</h3><p>部分服务器标记为实验性，需设置对应环境标志才能启用：</p><ul><li>代表功能：Ty LSP（Python 替代 Pyright）</li><li>启用命令：<code>export OPENCODE_EXPERIMENTAL_LSP_TY=1</code></li><li>行为：启用 Ty 同时自动禁用冲突的 Pyright 服务器。</li></ul><h3 id="4-防抖诊断系统"><a href="#4-防抖诊断系统" class="headerlink" title="4. 防抖诊断系统"></a>4. 防抖诊断系统</h3><p>为避免频繁处理诊断更新，客户端实现 150ms 防抖逻辑，处理流程：</p><ol><li>文件打开/修改时，发送 <code>didOpen</code>/<code>didChange</code> 通知。</li><li>服务器返回 <code>publishDiagnostics</code> 诊断信息。</li><li>客户端防抖 150ms，合并连续诊断更新（如语法检查→语义分析）。</li><li>发布诊断事件供下游组件消费，严重性映射：1=ERROR、2=WARN、3=INFO、4=HINT。</li></ol><h2 id="五、核心-LSP-操作（供-AI-Agent-与工具调用）"><a href="#五、核心-LSP-操作（供-AI-Agent-与工具调用）" class="headerlink" title="五、核心 LSP 操作（供 AI Agent 与工具调用）"></a>五、核心 LSP 操作（供 AI Agent 与工具调用）</h2><p>LSP 系统通过 <code>lsp</code> 工具暴露 9 种核心操作，支持代码分析与导航：</p><div class="table-container"><table><thead><tr><th>操作</th><th>描述</th><th>核心参数</th></tr></thead><tbody><tr><td><code>goToDefinition</code></td><td>跳转到符号定义位置</td><td>file、line、character</td></tr><tr><td><code>findReferences</code></td><td>查找符号的所有引用</td><td>file、line、character</td></tr><tr><td><code>hover</code></td><td>显示符号的悬停文档与类型信息</td><td>file、line、character</td></tr><tr><td><code>documentSymbol</code></td><td>列出当前文件的所有符号（大纲）</td><td>uri</td></tr><tr><td><code>workspaceSymbol</code></td><td>跨项目搜索全局符号</td><td>query</td></tr><tr><td><code>goToImplementation</code></td><td>跳转到接口/抽象方法的实现</td><td>file、line、character</td></tr></tbody></table></div><p>这些操作自动对 AI Agent 开放，无需额外配置即可用于代码分析与重构。</p><h2 id="六、故障排除与性能注意事项"><a href="#六、故障排除与性能注意事项" class="headerlink" title="六、故障排除与性能注意事项"></a>六、故障排除与性能注意事项</h2><h3 id="1-常见问题排查"><a href="#1-常见问题排查" class="headerlink" title="1. 常见问题排查"></a>1. 常见问题排查</h3><h4 id="（1）服务器无法初始化"><a href="#（1）服务器无法初始化" class="headerlink" title="（1）服务器无法初始化"></a>（1）服务器无法初始化</h4><ol><li>查看控制台输出，获取服务器启动日志。</li><li>验证依赖环境（如 <code>jdtls</code> 需 Java 21+、<code>gopls</code> 需 Go SDK）。</li><li>确认未设置 <code>OPENCODE_DISABLE_LSP_DOWNLOAD</code>（如需自动安装）。</li></ol><h4 id="（2）根目录检测异常"><a href="#（2）根目录检测异常" class="headerlink" title="（2）根目录检测异常"></a>（2）根目录检测异常</h4><ol><li>验证项目根目录是否存在预期的配置/锁文件（如 <code>package-lock.json</code>）。</li><li>全局服务器（如 <code>bash</code>）会回退到 <code>Instance.directory</code> 作为工作区。</li></ol><h4 id="（3）OAuth-回调-诊断失败"><a href="#（3）OAuth-回调-诊断失败" class="headerlink" title="（3）OAuth 回调/诊断失败"></a>（3）OAuth 回调/诊断失败</h4><ol><li>检查服务器配置是否正确，扩展是否匹配。</li><li>损坏的服务器会被缓存，重启 OpenCode 可重试初始化。</li></ol><h3 id="2-性能优化建议"><a href="#2-性能优化建议" class="headerlink" title="2. 性能优化建议"></a>2. 性能优化建议</h3><ol><li>禁用未使用的服务器，减少内存与 CPU 占用，提升启动速度。</li><li>手动安装大型服务器（如 <code>clangd</code>），避免自动下载的网络延迟。</li><li>对于 Monorepo，确保根目录检测正确，避免多服务器重复启动。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>OpenCode LSP 集成的核心优势是<strong>延迟初始化、开箱即用、灵活扩展</strong>，支持 35+ 语言的全量代码智能功能。</li><li>配置核心是 <code>opencode.json</code> 的 <code>lsp</code> 字段，支持全局、单个服务器、自定义服务器三层配置。</li><li>高级特性（根目录检测、自动下载、防抖诊断）保障了系统的稳定性与高效性。</li><li>实验性功能需通过环境标志启用，故障排查优先查看控制台日志与依赖环境。</li></ol><h1 id="符号导航与文档符号"><a href="#符号导航与文档符号" class="headerlink" title="符号导航与文档符号"></a>符号导航与文档符号</h1><p>符号导航和文档符号功能通过语言服务器协议（LSP）集成提供了强大的代码智能能力，使 Agent 能够理解代码结构、定位定义并高效导航代码库。</p><h2 id="架构概览"><a href="#架构概览" class="headerlink" title="架构概览"></a>架构概览</h2><p>符号导航系统通过多层架构运行，在抽象 LSP 服务器复杂性的同时，为符号查询提供统一接口。该架构支持 30 多种语言服务器，每种服务器都具备专门的符号提取功能。</p><h2 id="文档符号"><a href="#文档符号" class="headerlink" title="文档符号"></a>文档符号</h2><p>文档符号提供特定文件内所有符号的分层视图，包括类、函数、方法、接口、变量、常量、结构和枚举。系统利用 LSP textDocument/documentSymbol 请求从语言服务器提取结构信息。</p><h3 id="符号类型分类"><a href="#符号类型分类" class="headerlink" title="符号类型分类"></a>符号类型分类</h3><p>系统实现了 LSP SymbolKind 规范，包含 26 个不同类别，但结果会过滤为与导航最相关的类型：</p><div class="table-container"><table><thead><tr><th>Symbol Kind</th><th>Value</th><th>Description</th></tr></thead><tbody><tr><td>Class</td><td>5</td><td>类定义和声明</td></tr><tr><td>Function</td><td>12</td><td>函数定义和声明</td></tr><tr><td>Method</td><td>6</td><td>类/对象内的方法定义</td></tr><tr><td>Interface</td><td>11</td><td>接口定义</td></tr><tr><td>Variable</td><td>13</td><td>变量声明</td></tr><tr><td>Constant</td><td>14</td><td>常量声明</td></tr><tr><td>Struct</td><td>23</td><td>结构定义 (Rust, Go)</td></tr><tr><td>Enum</td><td>10</td><td>枚举定义</td></tr></tbody></table></div><p>系统过滤工作区符号查询，仅包含这八种主要符号类型，以减少噪音并提高相关性 [来源: packages/opencode/src/lsp/index.ts#L319-L358]。</p><h3 id="DocumentSymbol-Schema"><a href="#DocumentSymbol-Schema" class="headerlink" title="DocumentSymbol Schema"></a>DocumentSymbol Schema</h3><p>文档符号包含详细的元数据，用于精确导航和代码理解：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  name: string,           // 符号名称</span><br><span class="line">  detail?: string,        // 附加详细信息 (例如：签名)</span><br><span class="line">  kind: number,           // SymbolKind 枚举值</span><br><span class="line">  range: &#123;                // 符号的完整范围</span><br><span class="line">    start: &#123; line: number, character: number &#125;,</span><br><span class="line">    end: &#123; line: number, character: number &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  selectionRange: &#123;        // 标识符高亮范围</span><br><span class="line">    start: &#123; line: number, character: number &#125;,</span><br><span class="line">    end: &#123; line: number, character: number &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>range 和 selectionRange 的区别实现了精确的符号定位——前者覆盖整个符号声明，后者隔离标识符以便准确的光标定位 [来源: packages/opencode/src/lsp/index.ts#L51-L62]。</p><p>文档符号通过递归的 children 属性支持分层嵌套，使 Agent 能够理解类结构、嵌套函数和模块组织，而无需额外查询。</p><h3 id="通过-LSP-Tool-使用"><a href="#通过-LSP-Tool-使用" class="headerlink" title="通过 LSP Tool 使用"></a>通过 LSP Tool 使用</h3><p>Agent 通过 lsp 工具与文档符号交互，该工具需要指定文件路径和位置上下文：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;operation&quot;</span>: <span class="string">&quot;documentSymbol&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;filePath&quot;</span>: <span class="string">&quot;src/components/Button.tsx&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;line&quot;</span>: <span class="number">1</span>,</span><br><span class="line">  <span class="attr">&quot;character&quot;</span>: <span class="number">1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>虽然 line 和 character 参数是验证所必需的，但 documentSymbol 查询对整个文件进行操作，而不管指定的位置如何。这种设计在所有 LSP 操作中保持 API 一致性 [来源: packages/opencode/src/tool/lsp.ts#L10-L30]。</p><h2 id="工作区符号"><a href="#工作区符号" class="headerlink" title="工作区符号"></a>工作区符号</h2><p>工作区符号通过项目所有索引文件的模糊搜索实现跨文件符号发现。此功能对于理解大型代码库和在不知道确切文件位置的情况下定位符号至关重要。</p><h3 id="查询处理"><a href="#查询处理" class="headerlink" title="查询处理"></a>查询处理</h3><p>workspaceSymbol 函数接受查询字符串并对所有已注册的语言服务器执行模式匹配：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">workspaceSymbol</span>(<span class="params">query: <span class="built_in">string</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> runAll(<span class="function">(<span class="params">client</span>) =&gt;</span></span><br><span class="line">    client.connection</span><br><span class="line">      .sendRequest(<span class="string">&quot;workspace/symbol&quot;</span>, &#123; query &#125;)</span><br><span class="line">      .then(<span class="function">(<span class="params">result: <span class="built_in">any</span></span>) =&gt;</span> result.filter(<span class="function">(<span class="params">x: LSP.<span class="built_in">Symbol</span></span>) =&gt;</span> kinds.includes(x.kind)))</span><br><span class="line">      .then(<span class="function">(<span class="params">result: <span class="built_in">any</span></span>) =&gt;</span> result.slice(<span class="number">0</span>, <span class="number">10</span>))</span><br><span class="line">      .catch(<span class="function">() =&gt;</span> []),</span><br><span class="line">  ).then(<span class="function">(<span class="params">result</span>) =&gt;</span> result.flat() <span class="keyword">as</span> LSP.Symbol[])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来自多个服务器的结果被聚合、过滤为相关的符号类型，并限制为每个服务器 10 个结果，以管理响应大小 [来源: packages/opencode/src/lsp/index.ts#L359-L369]。</p><h3 id="符号位置-Schema"><a href="#符号位置-Schema" class="headerlink" title="符号位置 Schema"></a>符号位置 Schema</h3><p>工作区符号包含支持直接导航的位置元数据：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  name: string,           // 符号名称</span><br><span class="line">  kind: number,           // SymbolKind 枚举值</span><br><span class="line">  location: &#123;</span><br><span class="line">    uri: string,          // 文件 URI</span><br><span class="line">    range: &#123;              // 文件中的符号位置</span><br><span class="line">      start: &#123; line: number, character: number &#125;,</span><br><span class="line">      end: &#123; line: number, character: number &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>URI 使用 file:// 协议，需要转换为文件系统路径才能进行文件操作 [来源: packages/opencode/src/lsp/index.ts#L37-L49]。</p><h3 id="使用示例"><a href="#使用示例" class="headerlink" title="使用示例"></a>使用示例</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;operation&quot;</span>: <span class="string">&quot;workspaceSymbol&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;filePath&quot;</span>: <span class="string">&quot;src/index.ts&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;line&quot;</span>: <span class="number">1</span>,</span><br><span class="line">  <span class="attr">&quot;character&quot;</span>: <span class="number">1</span>,</span><br><span class="line">  <span class="attr">&quot;query&quot;</span>: <span class="string">&quot;Button&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这会在工作区的所有 TypeScript 和 JavaScript 文件中搜索匹配 “Button” 的符号，并返回来自多个文件的定义 [来源: packages/opencode/src/tool/lsp.ts#L10-L30]。</p><h3 id="服务器特定能力"><a href="#服务器特定能力" class="headerlink" title="服务器特定能力"></a>服务器特定能力</h3><p>不同的语言服务器提供不同级别的符号支持。下表概述了主要服务器的文档符号能力：</p><div class="table-container"><table><thead><tr><th>Language Server</th><th>Document Symbols</th><th>Workspace Symbols</th><th>Hierarchical Support</th></tr></thead><tbody><tr><td>Typescript</td><td>✓</td><td>✓</td><td>✓ (嵌套类、方法)</td></tr><tr><td>Pyright</td><td>✓</td><td>✓</td><td>✓ (类、嵌套函数)</td></tr><tr><td>rust-analyzer</td><td>✓</td><td>✓</td><td>✓ (模块、impl)</td></tr><tr><td>gopls</td><td>✓</td><td>✓</td><td>✓ (结构、接口)</td></tr><tr><td>clangd</td><td>✓</td><td>✓</td><td>✓ (命名空间、类)</td></tr><tr><td>lua-language-server</td><td>✓</td><td>✓</td><td>✓ (表、模块)</td></tr><tr><td>dart</td><td>✓</td><td>✓</td><td>✓ (类、mixin)</td></tr></tbody></table></div><p>某些语言服务器（例如 lua-language-server、bash-language-server）如果 PATH 中不存在，会在首次使用时自动下载和安装，而其他服务器（例如 rust-analyzer、gopls）需要手动安装。</p><h2 id="与-Agent-工作流的集成"><a href="#与-Agent-工作流的集成" class="headerlink" title="与 Agent 工作流的集成"></a>与 Agent 工作流的集成</h2><p>符号导航与其他 LSP 操作无缝集成，提供全面的代码智能：</p><p>这使得 Agent 能够首先探索文档结构，然后导航到特定定义、查找引用或分析调用层次结构——所有这些都通过一致的 API 完成 [来源: packages/opencode/src/tool/lsp.txt#L1-L20]。</p><h2 id="错误处理和回退"><a href="#错误处理和回退" class="headerlink" title="错误处理和回退"></a>错误处理和回退</h2><p>系统为 LSP 通信失败实现了强大的错误处理：</p><ol><li>服务器不可用：如果未为文件类型配置 LSP 服务器，工具将返回错误：”No LSP server available for this file type” [来源: packages/opencode/src/tool/lsp.ts#L45-L47]。</li><li>通信失败：捕获并记录单个服务器故障，返回空数组而不是传播错误。这允许工作区查询即使某些服务器无响应也能成功 [来源: packages/opencode/src/lsp/index.ts#L365-L367]。</li><li>初始化超时：服务器初始化具有 45 秒超时，并具有用于跟踪的特定错误类型 [来源: packages/opencode/src/lsp/client.ts#L82-L125]。</li></ol><p>诊断信息通过 150ms 的防抖处理，以防止当 LSP 服务器快速连续发送语法和语义诊断信息时出现重复通知。</p><h2 id="性能考虑"><a href="#性能考虑" class="headerlink" title="性能考虑"></a>性能考虑</h2><p>几项优化确保了高效的符号导航：</p><ol><li>延迟服务器生成：LSP 服务器在首次访问特定文件类型和工作区根目录时按需生成 [来源: packages/opencode/src/lsp/index.ts#L177-L262]。</li><li>客户端缓存：活动客户端在同一工作区根目录内的多个请求中被重用，避免重复的服务器初始化 [来源: packages/opencode/src/lsp/index.ts#L231-L234]。</li><li>结果限制：工作区符号查询限制为每个服务器 10 个结果，以防止常见查询的响应过大 [来源: packages/opencode/src/lsp/index.ts#L366]。</li><li>并行查询：对多个服务器的请求并行执行，使用 Promise.all 聚合结果 [来源: packages/opencode/src/lsp/index.ts#L457-L461]。</li></ol><h2 id="支持的语言服务器"><a href="#支持的语言服务器" class="headerlink" title="支持的语言服务器"></a>支持的语言服务器</h2><p>系统内置支持 30 多种语言服务器，具有自动配置和初始化功能：</p><ul><li>TypeScript/JavaScript: typescript, deno</li><li>Python: pyright, ty (实验性)</li><li>Rust: rust-analyzer</li><li>Go: gopls</li><li>C/C++: clangd</li><li>Java: jdtls</li><li>Kotlin: kotlin-ls</li><li>C#/F#: csharp, fsharp</li><li>Web: vue, svelte, astro, eslint, oxlint, biome</li><li>Ruby: ruby-lsp</li><li>Lua: lua-ls</li><li>PHP: intelephense</li><li>Terraform: terraform</li><li>Docker: dockerfile</li><li>以及更多…</li></ul><p>每个服务器都包含专门的根目录检测逻辑（例如，为 Rust 查找 Cargo.toml，为 Node.js 项目查找 package.json），以确保正确的工作区上下文 [来源: packages/opencode/src/lsp/server.ts#L53-L2032]。</p><h1 id="代码格式化集成"><a href="#代码格式化集成" class="headerlink" title="代码格式化集成"></a>代码格式化集成</h1><p>OpenCode 提供自动代码格式化集成功能，该功能在文件保存时运行，支持独立格式化工具和基于 LSP 的格式化能力。此系统可确保你的项目代码保持一致性，而无需手动干预。</p><h2 id="架构概述"><a href="#架构概述" class="headerlink" title="架构概述"></a>架构概述</h2><p>格式化系统采用事件驱动架构运行，响应文件编辑事件，并根据文件扩展名和项目配置自动调用相应的格式化工具。</p><p>该系统利用项目状态管理来维护格式化工具配置，并支持多种格式化工具来源，包括独立 CLI 工具和 LSP 服务器。</p><p>来源：format/index.ts</p><h2 id="内置格式化工具"><a href="#内置格式化工具" class="headerlink" title="内置格式化工具"></a>内置格式化工具</h2><p>OpenCode 内置了对众多流行代码格式化工具的支持，这些工具可在文件保存时自动调用。</p><h3 id="JavaScript-TypeScript-格式化工具"><a href="#JavaScript-TypeScript-格式化工具" class="headerlink" title="JavaScript/TypeScript 格式化工具"></a>JavaScript/TypeScript 格式化工具</h3><p><strong>Prettier</strong> - 最受欢迎的 JavaScript/TypeScript 格式化工具，支持多种文件类型，包括 HTML、CSS、JSON 和 Markdown。当 package.json 中包含 prettier 作为依赖项或 devDependency 时，会自动启用。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">extensions: [&quot;.js&quot;, &quot;.jsx&quot;, &quot;.mjs&quot;, &quot;.cjs&quot;, &quot;.ts&quot;, &quot;.tsx&quot;, &quot;.mts&quot;, &quot;.cts&quot;, </span><br><span class="line">             &quot;.html&quot;, &quot;.htm&quot;, &quot;.css&quot;, &quot;.scss&quot;, &quot;.sass&quot;, &quot;.less&quot;, &quot;.vue&quot;, &quot;.svelte&quot;,</span><br><span class="line">             &quot;.json&quot;, &quot;.jsonc&quot;, &quot;.yaml&quot;, &quot;.yml&quot;, &quot;.toml&quot;, &quot;.xml&quot;, &quot;.md&quot;, &quot;.mdx&quot;, </span><br><span class="line">             &quot;.graphql&quot;, &quot;.gql&quot;]</span><br></pre></td></tr></table></figure><p>来源：format/formatter.ts</p><p><strong>Biome</strong> - 一款快速的 JavaScript/TypeScript 格式化工具和 Linter。当在项目层级中检测到 biome.json 或 biome.jsonc 配置文件时，会自动启用。</p><p>来源：format/formatter.ts</p><p><strong>oxfmt</strong> - 一款实验性格式化工具，需要启用 OPENCODE_EXPERIMENTAL_OXFMT 标志。当 package.json 中包含 oxfmt 作为依赖项时激活。</p><p>来源：format/formatter.ts</p><h3 id="特定语言格式化工具"><a href="#特定语言格式化工具" class="headerlink" title="特定语言格式化工具"></a>特定语言格式化工具</h3><div class="table-container"><table><thead><tr><th>Formatter</th><th>Language(s)</th><th>Configuration Detection</th></tr></thead><tbody><tr><td>gofmt</td><td>Go</td><td>Binary availability check</td></tr><tr><td>mix</td><td>Elixir</td><td>Binary availability check</td></tr><tr><td>zig</td><td>Zig</td><td>Binary availability check</td></tr><tr><td>clang-format</td><td>C/C++</td><td>.clang-format file presence</td></tr><tr><td>ktlint</td><td>Kotlin</td><td>Binary availability check</td></tr><tr><td>ruff</td><td>Python</td><td>Config files (pyproject.toml, ruff.toml, .ruff.toml) or dependency detection</td></tr><tr><td>rustfmt</td><td>Rust</td><td>Binary availability check</td></tr><tr><td>rubocop</td><td>Ruby</td><td>Binary availability check</td></tr><tr><td>standardrb</td><td>Ruby</td><td>Binary availability check</td></tr><tr><td>htmlbeautifier</td><td>HTML/Ruby</td><td>Binary availability check</td></tr><tr><td>dart</td><td>Dart</td><td>Binary availability check</td></tr><tr><td>ocamlformat</td><td>OCaml</td><td>Binary availability check</td></tr><tr><td>terraform</td><td>HCL</td><td>Binary availability check</td></tr><tr><td>latexindent</td><td>LaTeX</td><td>Binary availability check</td></tr><tr><td>gleam</td><td>Gleam</td><td>Binary availability check</td></tr><tr><td>shfmt</td><td>Shell scripts</td><td>Binary availability check</td></tr><tr><td>nixfmt</td><td>Nix</td><td>Binary availability check</td></tr></tbody></table></div><p>来源：format/formatter.ts</p><h2 id="基于-LSP-的格式化"><a href="#基于-LSP-的格式化" class="headerlink" title="基于 LSP 的格式化"></a>基于 LSP 的格式化</h2><p>除了独立的格式化工具外，OpenCode 还可以利用语言服务器协议 (LSP) 服务器来实现格式化功能。许多现代 LSP 服务器将格式化作为核心功能提供，而 OpenCode 的 LSP 集成支持这一功能。</p><h3 id="支持-LSP-格式化的服务器"><a href="#支持-LSP-格式化的服务器" class="headerlink" title="支持 LSP 格式化的服务器"></a>支持 LSP 格式化的服务器</h3><p>以下 LSP 服务器提供格式化功能并受 OpenCode 支持：</p><ul><li>TypeScript - typescript-language-server</li><li>Biome - 内置支持格式化的 LSP</li><li>Deno - 原生 Deno LSP，支持格式化</li><li>Gopls - Go 语言服务器，集成 gofmt</li><li>Rust Analyzer - Rust 语言服务器，集成 rustfmt</li><li>Pyright - Python 语言服务器</li><li>Clangd - C/C++ 语言服务器，集成 clang-format</li><li>Zls - Zig 语言服务器</li><li>以及更多</li></ul><p>来源：lsp/server.ts</p><p>当相应的 LSP 服务器对某个文件处于活动状态时，会自动使用基于 LSP 的格式化工具。这提供了语言感知的格式化，能够遵守你项目的特定配置和约定，通常优于独立的格式化工具。</p><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>格式化工具可以通过项目级别或全局级别的 opencode.json 或 opencode.jsonc 配置文件进行配置。</p><h3 id="禁用所有格式化工具"><a href="#禁用所有格式化工具" class="headerlink" title="禁用所有格式化工具"></a>禁用所有格式化工具</h3><p>要禁用所有自动格式化功能：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;formatter&quot;</span>: <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：config/config.ts</p><h3 id="禁用特定格式化工具"><a href="#禁用特定格式化工具" class="headerlink" title="禁用特定格式化工具"></a>禁用特定格式化工具</h3><p>要在保持其他格式化工具启用的同时禁用单个格式化工具：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;formatter&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;prettier&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;disabled&quot;</span>: <span class="literal">true</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;ruff&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;disabled&quot;</span>: <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="自定义格式化工具配置"><a href="#自定义格式化工具配置" class="headerlink" title="自定义格式化工具配置"></a>自定义格式化工具配置</h3><p>你可以通过覆盖命令、扩展名或环境变量来自定义格式化工具：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;formatter&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;prettier&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;command&quot;</span>: [<span class="string">&quot;bun&quot;</span>, <span class="string">&quot;x&quot;</span>, <span class="string">&quot;prettier&quot;</span>, <span class="string">&quot;--write&quot;</span>, <span class="string">&quot;$FILE&quot;</span>, <span class="string">&quot;--config&quot;</span>, <span class="string">&quot;.prettierrc.custom&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;environment&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;BUN_BE_BUN&quot;</span>: <span class="string">&quot;1&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;PRETTIER_CONFIG&quot;</span>: <span class="string">&quot;.prettierrc.custom&quot;</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">&quot;extensions&quot;</span>: [<span class="string">&quot;.js&quot;</span>, <span class="string">&quot;.jsx&quot;</span>, <span class="string">&quot;.ts&quot;</span>, <span class="string">&quot;.tsx&quot;</span>]</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="添加自定义格式化工具"><a href="#添加自定义格式化工具" class="headerlink" title="添加自定义格式化工具"></a>添加自定义格式化工具</h3><p>定义全新的格式化工具：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;formatter&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;my-custom-formatter&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;command&quot;</span>: [<span class="string">&quot;my-formatter&quot;</span>, <span class="string">&quot;--format&quot;</span>, <span class="string">&quot;$FILE&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;extensions&quot;</span>: [<span class="string">&quot;.custom&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;disabled&quot;</span>: <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：config/config.ts</p><h2 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h2><h3 id="格式化工具检测与激活"><a href="#格式化工具检测与激活" class="headerlink" title="格式化工具检测与激活"></a>格式化工具检测与激活</h3><p>格式化工具系统遵循复杂的检测流程：</p><ol><li>配置加载 - 系统按优先顺序从多个来源加载格式化工具配置：远程配置 → 全局配置 → 项目配置 → 内联配置</li><li>状态初始化 - 格式化工具状态通过合并可用格式化工具与自定义配置进行初始化</li><li>扩展名匹配 - 编辑文件时，提取其扩展名并与已注册的格式化工具进行匹配</li><li>启用检查 - 调用每个匹配到的格式化工具的 enabled() 函数，以验证其是否应处于活动状态</li><li>命令执行 - 如果已启用，则执行格式化工具的命令，并将 $FILE 占位符替换为实际文件路径</li></ol><p>来源：format/index.ts</p><h3 id="事件驱动的格式化"><a href="#事件驱动的格式化" class="headerlink" title="事件驱动的格式化"></a>事件驱动的格式化</h3><p>格式化系统通过全局事件总线订阅文件编辑事件：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">Bus.subscribe(File.Event.Edited, <span class="keyword">async</span> (payload) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> file = payload.properties.file</span><br><span class="line">  <span class="keyword">const</span> ext = path.extname(file)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> item <span class="keyword">of</span> <span class="keyword">await</span> getFormatter(ext)) &#123;</span><br><span class="line">    <span class="comment">// Execute formatter</span></span><br><span class="line">    <span class="keyword">const</span> proc = Bun.spawn(&#123;</span><br><span class="line">      cmd: item.command.map(<span class="function">(<span class="params">x</span>) =&gt;</span> x.replace(<span class="string">&quot;$FILE&quot;</span>, file)),</span><br><span class="line">      cwd: Instance.directory,</span><br><span class="line">      env: &#123; ...process.env, ...item.environment &#125;,</span><br><span class="line">      stdout: <span class="string">&quot;ignore&quot;</span>,</span><br><span class="line">      stderr: <span class="string">&quot;ignore&quot;</span>,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="comment">// Handle exit codes</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>来源：format/index.ts</p><h3 id="格式化工具状态-API"><a href="#格式化工具状态-API" class="headerlink" title="格式化工具状态 API"></a>格式化工具状态 API</h3><p>你可以通过 API 查询所有已配置格式化工具的状态：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> status = <span class="keyword">await</span> Format.status()</span><br><span class="line"><span class="comment">// Returns array of:</span></span><br><span class="line"><span class="comment">// &#123;</span></span><br><span class="line"><span class="comment">//   name: string</span></span><br><span class="line"><span class="comment">//   extensions: string[]</span></span><br><span class="line"><span class="comment">//   enabled: boolean</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br></pre></td></tr></table></figure><p>这有助于了解项目中哪些格式化工具是可用且处于活动状态的。</p><p>来源：format/index.ts</p><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="项目级配置"><a href="#项目级配置" class="headerlink" title="项目级配置"></a>项目级配置</h3><p>在项目级别配置格式化工具以确保团队范围内的一致性。将 opencode.json 放置在项目根目录，并包含格式化工具设置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;formatter&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;prettier&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;disabled&quot;</span>: <span class="literal">false</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;ruff&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;disabled&quot;</span>: <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="格式化工具优先级"><a href="#格式化工具优先级" class="headerlink" title="格式化工具优先级"></a>格式化工具优先级</h3><p>当多个格式化工具支持同一文件扩展名时，所有已启用的格式化工具都将执行。在配置功能重叠的工具时需考虑到这一点：</p><ol><li>Prettier 和 Biome 都支持 JavaScript/TypeScript 文件</li><li>为每种文件类型选择一个主格式化工具以避免冲突</li><li>使用 disabled 字段防止不需要的格式化工具运行</li></ol><h3 id="环境变量"><a href="#环境变量" class="headerlink" title="环境变量"></a>环境变量</h3><p>格式化工具可以利用环境变量进行配置。系统会自动包含 process.env 并合并特定格式化工具的环境变量：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">env: &#123;</span><br><span class="line">  ...process.env,</span><br><span class="line">  ...item.environment</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：format/index.ts</p><h3 id="性能考量"><a href="#性能考量" class="headerlink" title="性能考量"></a>性能考量</h3><p>格式化工具异步执行，且忽略 stdout 和 stderr 输出以防止干扰 UI。退出代码会被记录以便调试。该系统设计为非阻塞的，即使格式化工具失败也允许继续操作。</p><p>来源：format/index.ts</p><h2 id="故障排除"><a href="#故障排除" class="headerlink" title="故障排除"></a>故障排除</h2><h3 id="格式化工具未运行"><a href="#格式化工具未运行" class="headerlink" title="格式化工具未运行"></a>格式化工具未运行</h3><ol><li>检查格式化工具二进制文件是否在你的 PATH 中可用</li><li>验证配置文件是否被检测到（例如 .prettierrc、biome.json）</li><li>确保格式化工具未在 opencode.json 中被禁用</li><li>查看日志以查找执行错误</li></ol><h3 id="格式化工具冲突"><a href="#格式化工具冲突" class="headerlink" title="格式化工具冲突"></a>格式化工具冲突</h3><p>如果多个格式化工具修改同一文件，最后完成执行的将决定最终状态。使用 disabled 配置来控制针对特定文件扩展名运行哪些格式化工具。</p><h3 id="缺少依赖项"><a href="#缺少依赖项" class="headerlink" title="缺少依赖项"></a>缺少依赖项</h3><p>某些格式化工具需要安装依赖项。例如：</p><ol><li>Prettier 需要列在 package.json 依赖项中</li><li>Ruff 需要 ruff.toml 或包含在 requirements.txt 中</li></ol><p>enabled() 函数会在尝试运行格式化工具之前自动执行这些检查。</p><p>来源：format/formatter.ts</p>]]></content>
    
    
    <summary type="html">LSP 集成：服务器管理与客户端通信
OpenCode 的 LSP 集成是一套高性能、可扩展的智能代码辅助系统，基于客户端-服务器架构实现，支持 35+ 编程语言的诊断、导航、符号查询等功能，核心特点是延迟生成、资源优化、多服务器协调，同时提供灵活的配置和扩展能力。

一、LSP 架构核心：三层组件与延迟生成策略
OpenCode LSP 采用关注点分离的三层架构，确保系统高效、可维护，同时通过延迟生成优化资源占用。

1. 三层核心组件
组件职责核心功能服务器管理语言服务器的生命周期控制服务器注册、项目根目录检测、进程生成（二进制/包管理器/自动下载）客户端通信处理 LSP 协议交互JSON</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>【转载】AI Agent 核心策略：如何判断 Agent 应该停止</title>
    <link href="http://qixinbo.github.io/2026/01/19/agent-terminate/"/>
    <id>http://qixinbo.github.io/2026/01/19/agent-terminate/</id>
    <published>2026-01-19T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.889Z</updated>
    
    <content type="html"><![CDATA[<p>前言：转载潘锦大神的一篇博客，原文在<a href="https://www.phppan.com/2025/10/ai-agent-stop/">这里</a></p><h1 id="AI-Agent-核心策略：如何判断-Agent-应该停止"><a href="#AI-Agent-核心策略：如何判断-Agent-应该停止" class="headerlink" title="AI Agent 核心策略：如何判断 Agent 应该停止"></a>AI Agent 核心策略：如何判断 Agent 应该停止</h1><p>简单来讲，AI Agent 实现的的大逻辑就是一个大的循环 + 获取上下文 + 不停的 LLM 调用 + 工具的调用。</p><p>那么一个关键问题就出现了：这个循环什么时候应该停止？如果处理不当，Agent 可能会陷入无限循环，浪费计算资源，或者过早停止而无法完成任务。本文将深入探讨 AI Agent 停止策略的核心设计思路。</p><h2 id="常用停止策略"><a href="#常用停止策略" class="headerlink" title="常用停止策略"></a>常用停止策略</h2><p>AI Agent 停止策略无外乎以下几种情况：</p><ol><li>硬性限制</li></ol><p>最简单粗暴的方法：</p><ul><li>最大步数限制（比如最多循环 30 次）</li><li>执行时间限制（比如最多跑 5 分钟）</li><li>API 调用次数限制（比如最多调 100 次）</li><li>API 调用 Token 数限制</li></ul><p>这种方法简单有效，但用户体验很差。经常出现任务做到一半被强制停止的情况。</p><ol><li>任务完成检测</li></ol><p>让 LLM 判断任务是否完成：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 每次循环后问 LLM</span></span><br><span class="line">response = llm.ask(<span class="string">&quot;任务是否已经完成？&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> response == <span class="string">&quot;是&quot;</span>:</span><br><span class="line">    stop()</span><br></pre></td></tr></table></figure><ol><li>显式停止信号</li></ol><p>给 Agent 一个专门的”停止”工具：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">tools = [</span><br><span class="line">    <span class="string">&quot;search&quot;</span>,</span><br><span class="line">    <span class="string">&quot;calculate&quot;</span>, </span><br><span class="line">    <span class="string">&quot;terminate&quot;</span>  <span class="comment"># 专门用来停止</span></span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>当 Agent 调用 terminate 工具时就停止。这个方法不错，但需要在 prompt 里教会 Agent 什么时候该调用它。</p><ol><li>循环检测</li></ol><p>检测 Agent 是否在做重复的事：</p><ul><li>连续多次调用同一个工具</li><li>动作序列出现循环模式（A→B→A→B…）</li><li>输出内容高度相似</li></ul><ol><li>错误累积</li></ol><p>连续失败多次就放弃：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> consecutive_errors &gt; <span class="number">3</span>:</span><br><span class="line">    stop(<span class="string">&quot;连续失败太多次&quot;</span>)</span><br></pre></td></tr></table></figure><ol><li>用户中断</li></ol><p>让用户能随时喊停。</p><p>下面我们以 OpenManus 和 Gemini CLI 的源码来看一下他们是怎么做的。</p><h2 id="OpenManus-的停止逻辑"><a href="#OpenManus-的停止逻辑" class="headerlink" title="OpenManus 的停止逻辑"></a>OpenManus 的停止逻辑</h2><p>OpenManus 的停止机制设计得比较完整，它用了一个多层防护的思路。</p><h3 id="核心：terminate-工具"><a href="#核心：terminate-工具" class="headerlink" title="核心：terminate 工具"></a>核心：terminate 工具</h3><p>OpenManus 给每个 Agent 都配了一个 terminate 工具：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Terminate</span>(<span class="params">BaseTool</span>):</span></span><br><span class="line">    name: <span class="built_in">str</span> = <span class="string">&quot;terminate&quot;</span></span><br><span class="line">    description = <span class="string">&quot;&quot;&quot;当请求已满足或无法继续时终止交互。</span></span><br><span class="line"><span class="string">    完成所有任务后，调用此工具结束工作。&quot;&quot;&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">execute</span>(<span class="params">self, status: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&quot;交互已完成，状态：<span class="subst">&#123;status&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p>以上为示例，原始代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> app.tool.base <span class="keyword">import</span> BaseTool</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">_TERMINATE_DESCRIPTION = <span class="string">&quot;&quot;&quot;Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.</span></span><br><span class="line"><span class="string">When you have finished all the tasks, call this tool to end the work.&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Terminate</span>(<span class="params">BaseTool</span>):</span></span><br><span class="line">    name: <span class="built_in">str</span> = <span class="string">&quot;terminate&quot;</span></span><br><span class="line">    description: <span class="built_in">str</span> = _TERMINATE_DESCRIPTION</span><br><span class="line">    parameters: <span class="built_in">dict</span> = &#123;</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">        <span class="string">&quot;properties&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;status&quot;</span>: &#123;</span><br><span class="line">                <span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>,</span><br><span class="line">                <span class="string">&quot;description&quot;</span>: <span class="string">&quot;The finish status of the interaction.&quot;</span>,</span><br><span class="line">                <span class="string">&quot;enum&quot;</span>: [<span class="string">&quot;success&quot;</span>, <span class="string">&quot;failure&quot;</span>],</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;required&quot;</span>: [<span class="string">&quot;status&quot;</span>],</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">execute</span>(<span class="params">self, status: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;Finish the current execution&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&quot;The interaction has been completed with status: <span class="subst">&#123;status&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p>OpenManus 使用的是方案 3，把「何时停止」的决策权交给了 LLM。prompt 里会明确告诉 Agent：任务完成了就调用 terminate。</p><h3 id="状态机管理"><a href="#状态机管理" class="headerlink" title="状态机管理"></a>状态机管理</h3><p>OpenManus 用状态机来管理 Agent 的生命周期：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AgentState</span>(<span class="params">Enum</span>):</span></span><br><span class="line">    IDLE = <span class="string">&quot;idle&quot;</span></span><br><span class="line">    RUNNING = <span class="string">&quot;running&quot;</span>  </span><br><span class="line">    FINISHED = <span class="string">&quot;finished&quot;</span></span><br><span class="line">    ERROR = <span class="string">&quot;error&quot;</span></span><br></pre></td></tr></table></figure><p>当检测到特殊工具（如 terminate）被调用时，会触发状态转换：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">_handle_special_tool</span>(<span class="params">self, name: <span class="built_in">str</span>, result: <span class="type">Any</span></span>):</span></span><br><span class="line">    <span class="keyword">if</span> name.lower() == <span class="string">&quot;terminate&quot;</span>:</span><br><span class="line">        self.state = AgentState.FINISHED</span><br><span class="line">        logger.info(<span class="string">&quot; 任务完成！&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="步数限制"><a href="#步数限制" class="headerlink" title="步数限制"></a>步数限制</h3><p>不同类型的 Agent 有不同的步数上限：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ToolCallAgent: 30 步</span></span><br><span class="line"><span class="comment"># SWEAgent: 20 步</span></span><br><span class="line"><span class="comment"># PlanningFlow: 可配置</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> self.current_step &lt; self.max_steps <span class="keyword">and</span> self.state != AgentState.FINISHED:</span><br><span class="line">    self.current_step += <span class="number">1</span></span><br><span class="line">    <span class="keyword">await</span> self.step()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> self.current_step &gt;= self.max_steps:</span><br><span class="line">    results.append(<span class="string">f&quot;达到最大步数限制 (<span class="subst">&#123;self.max_steps&#125;</span>)&quot;</span>)</span><br></pre></td></tr></table></figure><p>这是一个保底机制，防止 Agent 无限运行。</p><h3 id="卡死检测"><a href="#卡死检测" class="headerlink" title="卡死检测"></a>卡死检测</h3><p>OpenManus 还会检测 Agent 是否卡住了：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">is_stuck</span>(<span class="params">self</span>) -&gt; <span class="built_in">bool</span>:</span></span><br><span class="line">    <span class="comment"># 检查是否有重复的 assistant 消息</span></span><br><span class="line">    <span class="comment"># 如果最近的回复都一样，说明卡住了</span></span><br><span class="line">    recent_messages = self.get_recent_assistant_messages()</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(<span class="built_in">set</span>(recent_messages)) == <span class="number">1</span>:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure><h3 id="Planning-Agent-的结束逻辑"><a href="#Planning-Agent-的结束逻辑" class="headerlink" title="Planning Agent 的结束逻辑"></a>Planning Agent 的结束逻辑</h3><ol><li>计划完成的判断机制<br>PlanningFlow 的结束判断并不是简单检查所有步骤是否完成：</li></ol><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在主执行循环中</span></span><br><span class="line"><span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">    <span class="comment"># 获取当前需要执行的步骤</span></span><br><span class="line">    self.current_step_index, step_info = <span class="keyword">await</span> self._get_current_step_info()</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 如果没有更多活跃步骤，则结束计划</span></span><br><span class="line">    <span class="keyword">if</span> self.current_step_index <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">        result += <span class="keyword">await</span> self._finalize_plan()</span><br><span class="line">        <span class="keyword">break</span></span><br></pre></td></tr></table></figure><ol><li>步骤状态检查逻辑<br><code>_get_current_step_info()</code> 方法负责判断是否还有未完成的步骤：</li></ol><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查找第一个非完成状态的步骤</span></span><br><span class="line"><span class="keyword">for</span> i, step <span class="keyword">in</span> <span class="built_in">enumerate</span>(steps):</span><br><span class="line">    <span class="keyword">if</span> i &gt;= <span class="built_in">len</span>(step_statuses):</span><br><span class="line">        status = PlanStepStatus.NOT_STARTED.value</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        status = step_statuses[i]</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 如果步骤状态为活跃状态（未开始或进行中），返回该步骤</span></span><br><span class="line">    <span class="keyword">if</span> status <span class="keyword">in</span> PlanStepStatus.get_active_statuses():</span><br><span class="line">        <span class="keyword">return</span> i, step_info</span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果没找到活跃步骤，返回 None</span></span><br><span class="line"><span class="keyword">return</span> <span class="literal">None</span>, <span class="literal">None</span></span><br></pre></td></tr></table></figure><p>其中 <code>get_active_statuses()</code> 返回 <code>[&quot;not_started&quot;, &quot;in_progress&quot;]</code>，意味着只有当所有步骤都是 <code>&quot;completed&quot;</code> 或 <code>&quot;blocked&quot;</code> 状态时，计划才会结束。</p><ol><li>计划结束处理<br>当没有更多活跃步骤时，会调用 <code>_finalize_plan()</code> 方法：</li></ol><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">_finalize_plan</span>(<span class="params">self</span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;使用 LLM 生成计划完成总结&quot;&quot;&quot;</span></span><br><span class="line">    plan_text = <span class="keyword">await</span> self._get_plan_text()</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 使用 LLM 生成总结</span></span><br><span class="line">    system_message = Message.system_message(</span><br><span class="line">        <span class="string">&quot;You are a planning assistant. Your task is to summarize the completed plan.&quot;</span></span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    user_message = Message.user_message(</span><br><span class="line">        <span class="string">f&quot;The plan has been completed. Here is the final plan status:\n\n<span class="subst">&#123;plan_text&#125;</span>\n\nPlease provide a summary of what was accomplished and any final thoughts.&quot;</span></span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    response = <span class="keyword">await</span> self.llm.ask(messages=[user_message], system_msgs=[system_message])</span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&quot;Plan completed:\n\n<span class="subst">&#123;response&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h2 id="Gemini-CLI-的停止逻辑"><a href="#Gemini-CLI-的停止逻辑" class="headerlink" title="Gemini CLI 的停止逻辑"></a>Gemini CLI 的停止逻辑</h2><p>Gemini CLI 的设计思路完全不同，它用了一个更优雅但也更复杂的方案。</p><h3 id="subagent-的停止逻辑"><a href="#subagent-的停止逻辑" class="headerlink" title="subagent 的停止逻辑"></a>subagent 的停止逻辑</h3><ol><li>达到最大轮次（MAX_TURNS）</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="built_in">this</span>.runConfig.max_turns &amp;&amp; turnCounter &gt;= <span class="built_in">this</span>.runConfig.max_turns) &#123;</span><br><span class="line">    <span class="built_in">this</span>.output.terminate_reason = SubagentTerminateMode.MAX_TURNS;</span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是最简单的保护机制，防止无限循环。</p><ol><li>执行超时（TIMEOUT）</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> durationMin = (<span class="built_in">Date</span>.now() - startTime) / (<span class="number">1000</span> * <span class="number">60</span>);</span><br><span class="line"><span class="keyword">if</span> (durationMin &gt;= <span class="built_in">this</span>.runConfig.max_time_minutes) &#123;</span><br><span class="line">    <span class="built_in">this</span>.output.terminate_reason = SubagentTerminateMode.TIMEOUT;</span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意这里检查了两次超时：</p><ul><li>在调用 LLM 之前检查一次</li><li>在调用 LLM 之后又检查一次</li></ul><p>这是因为 LLM 调用可能很耗时，要确保不会超时太多。</p><ol><li>用户中断（通过 AbortSignal）</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (abortController.signal.aborted) <span class="keyword">return</span>;</span><br></pre></td></tr></table></figure><p>这个检查出现在 stream 处理循环里，确保能及时响应用户的取消操作。</p><ol><li>错误异常（ERROR）</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="built_in">console</span>.error(<span class="string">&#x27;Error during subagent execution:&#x27;</span>, error);</span><br><span class="line">    <span class="built_in">this</span>.output.terminate_reason = SubagentTerminateMode.ERROR;</span><br><span class="line">    <span class="keyword">throw</span> error;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>任何未捕获的异常都会导致停止。</p><ol><li>目标完成（GOAL）<br>目标完成的判断分两种情况：</li></ol><p><strong>情况A：没有预定输出要求</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!<span class="built_in">this</span>.outputConfig || <span class="built_in">Object</span>.keys(<span class="built_in">this</span>.outputConfig.outputs).length === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 没有要求特定输出，LLM 不调用工具就认为完成了</span></span><br><span class="line">    <span class="keyword">if</span> (functionCalls.length === <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="built_in">this</span>.output.terminate_reason = SubagentTerminateMode.GOAL;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>情况B：有预定输出要求</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 检查是否所有要求的变量都已输出</span></span><br><span class="line"><span class="keyword">const</span> remainingVars = <span class="built_in">Object</span>.keys(<span class="built_in">this</span>.outputConfig.outputs).filter(</span><br><span class="line">    (key) =&gt; !(key <span class="keyword">in</span> <span class="built_in">this</span>.output.emitted_vars)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (remainingVars.length === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="built_in">this</span>.output.terminate_reason = SubagentTerminateMode.GOAL;</span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="声明式输出系统的实现"><a href="#声明式输出系统的实现" class="headerlink" title="声明式输出系统的实现"></a>声明式输出系统的实现</h3><p>声明式输出系统的核心是 <code>outputConfig</code>：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 预先声明需要什么输出</span></span><br><span class="line"><span class="built_in">this</span>.outputConfig = &#123;</span><br><span class="line">    outputs: &#123;</span><br><span class="line">        <span class="string">&quot;summary&quot;</span>: <span class="string">&quot;string&quot;</span>,</span><br><span class="line">        <span class="string">&quot;recommendations&quot;</span>: <span class="string">&quot;array&quot;</span>, </span><br><span class="line">        <span class="string">&quot;risk_score&quot;</span>: <span class="string">&quot;number&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Agent 通过 self.emitvalue 工具来产生输出</span></span><br><span class="line"><span class="comment">// 每次调用会把值存到 this.output.emitted_vars 里</span></span><br><span class="line"><span class="built_in">this</span>.output.emitted_vars = &#123;</span><br><span class="line">    <span class="string">&quot;summary&quot;</span>: <span class="string">&quot;这是总结...&quot;</span>,</span><br><span class="line">    <span class="string">&quot;recommendations&quot;</span>: [<span class="string">&quot;建议1&quot;</span>, <span class="string">&quot;建议2&quot;</span>]</span><br><span class="line">    <span class="comment">// risk_score 还没输出</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>系统会不断检查 <code>emitted_vars</code> 是否包含了所有 <code>outputs</code> 中声明的变量。只有全部输出了才认为目标完成。</p><h3 id="Nudge-机制"><a href="#Nudge-机制" class="headerlink" title="Nudge 机制"></a>Nudge 机制</h3><p>Nudge（轻推）机制代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (functionCalls.length === <span class="number">0</span>) &#123;  <span class="comment">// LLM 停止调用工具了</span></span><br><span class="line">    <span class="comment">// 检查是否还有变量没输出</span></span><br><span class="line">    <span class="keyword">const</span> remainingVars = <span class="built_in">Object</span>.keys(<span class="built_in">this</span>.outputConfig.outputs).filter(</span><br><span class="line">        (key) =&gt; !(key <span class="keyword">in</span> <span class="built_in">this</span>.output.emitted_vars)</span><br><span class="line">    );</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (remainingVars.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 还有变量没输出，&quot;推&quot;它一下</span></span><br><span class="line">        <span class="keyword">const</span> nudgeMessage = <span class="string">`You have stopped calling tools but have not emitted </span></span><br><span class="line"><span class="string">        the following required variables: <span class="subst">$&#123;remainingVars.join(<span class="string">&#x27;, &#x27;</span>)&#125;</span>. </span></span><br><span class="line"><span class="string">        Please use the &#x27;self.emitvalue&#x27; tool to emit them now, </span></span><br><span class="line"><span class="string">        or continue working if necessary.`</span>;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 把提醒作为新的用户消息发给 LLM</span></span><br><span class="line">        currentMessages = [&#123;</span><br><span class="line">            role: <span class="string">&#x27;user&#x27;</span>,</span><br><span class="line">            parts: [&#123; <span class="attr">text</span>: nudgeMessage &#125;]</span><br><span class="line">        &#125;];</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 继续循环，不退出</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="完整的-subagent-执行流程"><a href="#完整的-subagent-执行流程" class="headerlink" title="完整的 subagent 执行流程"></a>完整的 subagent 执行流程</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">开始</span><br><span class="line">  ↓</span><br><span class="line">while (true) &#123;</span><br><span class="line">  检查是否超时&#x2F;超轮次 → 是 → 退出</span><br><span class="line">    ↓ 否</span><br><span class="line">  调用 LLM</span><br><span class="line">    ↓</span><br><span class="line">  LLM 返回工具调用？</span><br><span class="line">    ├─ 是 → 执行工具 → 检查目标是否完成</span><br><span class="line">    │         ├─ 是 → 退出</span><br><span class="line">    │         └─ 否 → 继续循环</span><br><span class="line">    │</span><br><span class="line">    └─ 否（LLM 停止调用工具）</span><br><span class="line">         ↓</span><br><span class="line">       有预定输出要求吗？</span><br><span class="line">         ├─ 没有 → 退出（认为完成）</span><br><span class="line">         └─ 有 → 检查是否都输出了</span><br><span class="line">                   ├─ 是 → 退出</span><br><span class="line">                   └─ 否 → Nudge 提醒 → 继续循环</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="三层循环检测机制"><a href="#三层循环检测机制" class="headerlink" title="三层循环检测机制"></a>三层循环检测机制</h3><h4 id="第一层：工具调用重复检测"><a href="#第一层：工具调用重复检测" class="headerlink" title="第一层：工具调用重复检测"></a>第一层：工具调用重复检测</h4><p>这是最简单直接的检测，针对 Agent 反复调用相同工具的情况。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">private checkToolCallLoop(toolCall: &#123; <span class="attr">name</span>: string; args: object &#125;): boolean &#123;</span><br><span class="line">    <span class="comment">// 把工具名和参数一起哈希，生成唯一标识</span></span><br><span class="line">    <span class="keyword">const</span> key = <span class="built_in">this</span>.getToolCallKey(toolCall);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.lastToolCallKey === key) &#123;</span><br><span class="line">        <span class="comment">// 和上次调用完全一样，计数+1</span></span><br><span class="line">        <span class="built_in">this</span>.toolCallRepetitionCount++;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 不一样，重置计数</span></span><br><span class="line">        <span class="built_in">this</span>.lastToolCallKey = key;</span><br><span class="line">        <span class="built_in">this</span>.toolCallRepetitionCount = <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 连续5次调用相同工具+相同参数 = 循环</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.toolCallRepetitionCount &gt;= TOOL_CALL_LOOP_THRESHOLD) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>触发条件：连续 5 次调用完全相同的工具（包括参数）。</p><p>这种检测很严格——必须是连续的、完全相同的调用。如果中间插入了其他工具调用，计数就会重置。</p><h4 id="第二层：内容重复检测（”咒语”检测）"><a href="#第二层：内容重复检测（”咒语”检测）" class="headerlink" title="第二层：内容重复检测（”咒语”检测）"></a>第二层：内容重复检测（”咒语”检测）</h4><p>这是最复杂的部分，用来检测 LLM 输出重复内容的情况，就像在念咒语一样。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">private checkContentLoop(content: string): boolean &#123;</span><br><span class="line">    <span class="comment">// 1. 先检查是否在特殊内容块中（代码块、表格、列表等）</span></span><br><span class="line">    <span class="keyword">const</span> numFences = (content.match(<span class="regexp">/```/g</span>) ?? []).length;</span><br><span class="line">    <span class="keyword">const</span> hasTable = <span class="regexp">/(^|\n)\s*(\|.*\||[|+-]&#123;3,&#125;)/</span>.test(content);</span><br><span class="line">    <span class="comment">// ... 检查各种格式</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 在代码块中不检测循环（代码本来就可能有重复）</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.inCodeBlock) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 把新内容加入历史</span></span><br><span class="line">    <span class="built_in">this</span>.streamContentHistory += content;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 保持历史在 1000 字符以内</span></span><br><span class="line">    <span class="built_in">this</span>.truncateAndUpdate();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4. 分析内容块是否重复</span></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">this</span>.analyzeContentChunksForLoop();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>核心算法是滑动窗口 + 哈希检测：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">private analyzeContentChunksForLoop(): boolean &#123;</span><br><span class="line">    <span class="keyword">while</span> (<span class="built_in">this</span>.hasMoreChunksToProcess()) &#123;</span><br><span class="line">        <span class="comment">// 提取 50 字符的块</span></span><br><span class="line">        <span class="keyword">const</span> currentChunk = <span class="built_in">this</span>.streamContentHistory.substring(</span><br><span class="line">            <span class="built_in">this</span>.lastContentIndex,</span><br><span class="line">            <span class="built_in">this</span>.lastContentIndex + CONTENT_CHUNK_SIZE  <span class="comment">// 50</span></span><br><span class="line">        );</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 计算哈希</span></span><br><span class="line">        <span class="keyword">const</span> chunkHash = createHash(<span class="string">&#x27;sha256&#x27;</span>).update(currentChunk).digest(<span class="string">&#x27;hex&#x27;</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 检查这个块是否重复出现</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.isLoopDetectedForChunk(currentChunk, chunkHash)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 滑动窗口向前移动 1 个字符</span></span><br><span class="line">        <span class="built_in">this</span>.lastContentIndex++;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>判断循环的条件：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">private isLoopDetectedForChunk(chunk: string, <span class="attr">hash</span>: string): boolean &#123;</span><br><span class="line">    <span class="keyword">const</span> existingIndices = <span class="built_in">this</span>.contentStats.get(hash);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (!existingIndices) &#123;</span><br><span class="line">        <span class="comment">// 第一次见到这个块，记录位置</span></span><br><span class="line">        <span class="built_in">this</span>.contentStats.set(hash, [<span class="built_in">this</span>.lastContentIndex]);</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 验证内容确实相同（防止哈希碰撞）</span></span><br><span class="line">    <span class="keyword">if</span> (!<span class="built_in">this</span>.isActualContentMatch(chunk, existingIndices[<span class="number">0</span>])) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    existingIndices.push(<span class="built_in">this</span>.lastContentIndex);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 需要出现至少 10 次</span></span><br><span class="line">    <span class="keyword">if</span> (existingIndices.length &lt; CONTENT_LOOP_THRESHOLD) &#123;  <span class="comment">// 10</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 关键：这 10 次必须距离很近（平均距离 ≤ 75 字符）</span></span><br><span class="line">    <span class="keyword">const</span> recentIndices = existingIndices.slice(-CONTENT_LOOP_THRESHOLD);</span><br><span class="line">    <span class="keyword">const</span> totalDistance = recentIndices[recentIndices.length - <span class="number">1</span>] - recentIndices[<span class="number">0</span>];</span><br><span class="line">    <span class="keyword">const</span> averageDistance = totalDistance / (CONTENT_LOOP_THRESHOLD - <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">const</span> maxAllowedDistance = CONTENT_CHUNK_SIZE * <span class="number">1.5</span>;  <span class="comment">// 75</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> averageDistance &lt;= maxAllowedDistance;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>触发条件：同一个 50 字符的内容块，在很短的距离内重复出现 10 次。</p><h4 id="第三层：LLM-智能检测"><a href="#第三层：LLM-智能检测" class="headerlink" title="第三层：LLM 智能检测"></a>第三层：LLM 智能检测</h4><p>这是最高级的检测，用 AI 来判断 AI 是否陷入循环。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">private <span class="keyword">async</span> <span class="function"><span class="title">checkForLoopWithLLM</span>(<span class="params">signal: AbortSignal</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 取最近 20 轮对话</span></span><br><span class="line">    <span class="keyword">const</span> recentHistory = <span class="built_in">this</span>.config</span><br><span class="line">        .getGeminiClient()</span><br><span class="line">        .getHistory()</span><br><span class="line">        .slice(-LLM_LOOP_CHECK_HISTORY_COUNT);  <span class="comment">// 20</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 清理历史（去掉悬空的函数调用等）</span></span><br><span class="line">    <span class="keyword">const</span> trimmedHistory = <span class="built_in">this</span>.trimRecentHistory(recentHistory);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 让 Gemini Flash 模型分析</span></span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="built_in">this</span>.config.getBaseLlmClient().generateJson(&#123;</span><br><span class="line">        contents: [...trimmedHistory, &#123; <span class="attr">role</span>: <span class="string">&#x27;user&#x27;</span>, <span class="attr">parts</span>: [&#123; <span class="attr">text</span>: taskPrompt &#125;] &#125;],</span><br><span class="line">        schema: &#123;</span><br><span class="line">            type: <span class="string">&#x27;object&#x27;</span>,</span><br><span class="line">            properties: &#123;</span><br><span class="line">                reasoning: &#123; <span class="attr">type</span>: <span class="string">&#x27;string&#x27;</span> &#125;,</span><br><span class="line">                confidence: &#123; <span class="attr">type</span>: <span class="string">&#x27;number&#x27;</span> &#125;  <span class="comment">// 0-1 之间</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        model: DEFAULT_GEMINI_FLASH_MODEL,</span><br><span class="line">        systemInstruction: LOOP_DETECTION_SYSTEM_PROMPT</span><br><span class="line">    &#125;);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (result[<span class="string">&#x27;confidence&#x27;</span>] &gt; <span class="number">0.9</span>) &#123;</span><br><span class="line">        <span class="comment">// 高置信度认为是循环</span></span><br><span class="line">        <span class="built_in">console</span>.warn(result[<span class="string">&#x27;reasoning&#x27;</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>触发时机：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="title">turnStarted</span>(<span class="params">signal: AbortSignal</span>)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.turnsInCurrentPrompt++;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (</span><br><span class="line">        <span class="built_in">this</span>.turnsInCurrentPrompt &gt;= LLM_CHECK_AFTER_TURNS &amp;&amp;  <span class="comment">// 至少 30 轮</span></span><br><span class="line">        <span class="built_in">this</span>.turnsInCurrentPrompt - <span class="built_in">this</span>.lastCheckTurn &gt;= <span class="built_in">this</span>.llmCheckInterval</span><br><span class="line">    ) &#123;</span><br><span class="line">        <span class="built_in">this</span>.lastCheckTurn = <span class="built_in">this</span>.turnsInCurrentPrompt;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">await</span> <span class="built_in">this</span>.checkForLoopWithLLM(signal);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>必须执行超过 30 轮才开始检查（避免误判）</li><li>不是每轮都检查，有间隔（默认 3 轮）</li><li>间隔会根据置信度动态调整（5-15 轮）</li></ul><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 动态调整检查频率</span></span><br><span class="line"><span class="built_in">this</span>.llmCheckInterval = <span class="built_in">Math</span>.round(</span><br><span class="line">    MIN_LLM_CHECK_INTERVAL +  <span class="comment">// 5</span></span><br><span class="line">    (MAX_LLM_CHECK_INTERVAL - MIN_LLM_CHECK_INTERVAL) * (<span class="number">1</span> - result[<span class="string">&#x27;confidence&#x27;</span>])</span><br><span class="line">    <span class="comment">// 置信度越高，检查越频繁</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure><h4 id="三种循环类型"><a href="#三种循环类型" class="headerlink" title="三种循环类型"></a>三种循环类型</h4><p>系统定义了三种循环类型：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">enum LoopType &#123;</span><br><span class="line">    CONSECUTIVE_IDENTICAL_TOOL_CALLS,  <span class="comment">// 连续相同工具调用</span></span><br><span class="line">    CHANTING_IDENTICAL_SENTENCES,      <span class="comment">// 重复输出相同内容</span></span><br><span class="line">    LLM_DETECTED_LOOP                  <span class="comment">// LLM 检测到的逻辑循环</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每种都有不同的检测方法和触发条件。</p><p>这比较适合处理长对话场景，既能有效检测循环，又不会因为过于敏感而误判正常的迭代操作。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>AI Agent 的停止策略是一个容易被忽视但极其重要的技术问题。从原理上看，Agent 就是一个大循环，不断调用 LLM 和工具来完成任务，但如果没有合理的停止机制，就会出现无限循环浪费资源，或者过早停止无法完成任务的问题。常见的停止方案包括硬性限制（步数、时间、API调用次数）、任务完成检测、显式停止信号、循环检测、错误累积和用户中断等，实际应用中需要组合使用多种策略。</p><p>OpenManus 采用了相对简单直接的设计：给每个 Agent 配备 terminate 工具，让 LLM 自己决定何时停止，同时用状态机管理生命周期，配合步数限制作为保底，并确保无论如何停止都会正确清理资源。</p><p>而 Gemini CLI 的设计更加精巧，核心是声明式输出系统——预先定义需要什么输出，只有全部输出才算完成，如果 Agent 停止调用工具但还有变量未输出，系统会通过 Nudge 机制温和提醒；在循环检测上，Gemini 实现了三层防护：工具调用重复检测（连续5次相同调用）、内容重复检测（滑动窗口+哈希算法检测”咒语”现象）、以及用 LLM 分析对话历史判断是否陷入逻辑循环。</p><p>实践中的关键是不要依赖单一停止机制，要组合使用多种策略形成多层防护，给 LLM 明确的停止指引，为不同类型的停止原因提供清晰的用户反馈，并确保资源能够可靠清理。停止策略的本质是在”让 Agent 完成任务”和”防止失控”之间找到平衡点。</p>]]></content>
    
    
    <summary type="html">前言：转载潘锦大神的一篇博客，原文在这里

AI Agent 核心策略：如何判断 Agent 应该停止
简单来讲，AI Agent 实现的的大逻辑就是一个大的循环 + 获取上下文 + 不停的 LLM 调用 + 工具的调用。

那么一个关键问题就出现了：这个循环什么时候应该停止？如果处理不当，Agent 可能会陷入无限循环，浪费计算资源，或者过早停止而无法完成任务。本文将深入探讨 AI Agent 停止策略的核心设计思路。

常用停止策略
AI Agent 停止策略无外乎以下几种情况：

 1. 硬性限制

最简单粗暴的方法：

 * 最大步数限制（比如最多循环 30 次）
 * 执行时间限制（</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着OpenCode学智能体设计和开发4：大模型提供商集成</title>
    <link href="http://qixinbo.github.io/2026/01/19/opencode-4/"/>
    <id>http://qixinbo.github.io/2026/01/19/opencode-4/</id>
    <published>2026-01-19T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="提供商架构：多提供商支持模型"><a href="#提供商架构：多提供商支持模型" class="headerlink" title="提供商架构：多提供商支持模型"></a>提供商架构：多提供商支持模型</h1><p>OpenCode 的提供商架构通过<strong>统一可扩展接口</strong>实现了多 AI 提供商的无缝集成，既抽象了不同提供商的底层复杂性，又保留了自定义配置和提供商专属优化的灵活性，核心支撑了超过 19 个内置提供商的快速接入与自定义提供商的扩展能力。</p><h2 id="一、架构核心概述"><a href="#一、架构核心概述" class="headerlink" title="一、架构核心概述"></a>一、架构核心概述</h2><p>提供商系统采用<strong>分层架构设计</strong>，将「配置管理、身份验证、模型发现、运行时执行」四大核心能力整合到内聚框架中，具备两大关键特性：</p><ol><li>内置丰富支持：直接捆绑 SDK 集成 19+ 主流 AI 提供商，消除常见用例的依赖管理成本</li><li>无限扩展能力：通过插件系统支持自定义提供商加载，无需修改核心代码即可扩展新能力</li><li>核心目标：让用户以一致的方式使用不同 AI 提供商的能力，同时最大化利用各提供商的专属优势</li></ol><p>来源：provider.ts, models.ts</p><h2 id="二、提供商状态管理与配置优先级"><a href="#二、提供商状态管理与配置优先级" class="headerlink" title="二、提供商状态管理与配置优先级"></a>二、提供商状态管理与配置优先级</h2><h3 id="1-集中式状态管理"><a href="#1-集中式状态管理" class="headerlink" title="1. 集中式状态管理"></a>1. 集中式状态管理</h3><p>提供商状态通过 <code>Provider.state()</code> 函数统一协调初始化，核心完成「提供商发现、配置解析、功能验证」三大流程，确保所有提供商的状态一致性。</p><h3 id="2-严格的配置优先级链"><a href="#2-严格的配置优先级链" class="headerlink" title="2. 严格的配置优先级链"></a>2. 严格的配置优先级链</h3><p>初始化过程中，配置会按照<strong>从低到高</strong>的优先级进行多源合并，既支持组织级基准配置，又允许开发者在项目/命令行级别灵活覆盖，优先级链如下（从低到高）：</p><ol><li>Remote/well-known config：组织默认设置，优先级最低</li><li>Global user config：全局用户配置，适用于所有项目</li><li>Custom config path：通过 <code>OPENCODE_CONFIG</code> 标志指定的自定义配置文件</li><li>Project config：项目目录中的 <code>opencode.json</code> / <code>opencode.jsonc</code>，项目级专属配置</li><li>Inline config content：通过 <code>OPENCODE_CONFIG_CONTENT</code> 标志传入的内联配置，优先级最高</li></ol><p>这种分层配置模式，兼顾了组织标准化与项目个性化的需求。</p><p>来源：provider.ts#L600-L700</p><h2 id="三、内置提供商支持与自定义加载器"><a href="#三、内置提供商支持与自定义加载器" class="headerlink" title="三、内置提供商支持与自定义加载器"></a>三、内置提供商支持与自定义加载器</h2><h3 id="1-19-内置提供商核心清单"><a href="#1-19-内置提供商核心清单" class="headerlink" title="1. 19+ 内置提供商核心清单"></a>1. 19+ 内置提供商核心清单</h3><p>OpenCode 直接捆绑主流 SDK，预集成了 19+ 提供商，每个提供商都具备专属优化功能，核心清单如下（关键提供商节选）：</p><div class="table-container"><table><thead><tr><th>提供商</th><th>依赖 SDK 包</th><th>核心特殊功能</th></tr></thead><tbody><tr><td>Anthropic</td><td><code>@ai-sdk/anthropic</code></td><td>Beta 头部配置、缓存控制</td></tr><tr><td>OpenAI</td><td><code>@ai-sdk/openai</code></td><td>补全 URL 支持、Codex 兼容</td></tr><tr><td>Google Generative AI</td><td><code>@ai-sdk/google</code></td><td>项目/区域配置、多模态支持</td></tr><tr><td>Amazon Bedrock</td><td><code>@ai-sdk/amazon-bedrock</code></td><td>区域感知模型前缀、凭证链解析</td></tr><tr><td>Azure</td><td><code>@ai-sdk/azure</code></td><td>认知服务集成、企业级部署支持</td></tr><tr><td>GitHub Copilot</td><td>自定义 OpenAI 兼容</td><td>OAuth 流程、企业版/个人版分离</td></tr></tbody></table></div><p>来源：provider.ts#L16-L40</p><h3 id="2-自定义提供商加载器（CUSTOM-LOADERS）"><a href="#2-自定义提供商加载器（CUSTOM-LOADERS）" class="headerlink" title="2. 自定义提供商加载器（CUSTOM_LOADERS）"></a>2. 自定义提供商加载器（CUSTOM_LOADERS）</h3><p><code>CUSTOM_LOADERS</code> 对象专门处理超越标准 SDK 的提供商专属初始化逻辑，核心负责「身份验证、凭据解析、模型加载行为定制」，针对复杂提供商提供精细化支持，以下是典型场景：</p><h4 id="（1）Amazon-Bedrock-区域处理"><a href="#（1）Amazon-Bedrock-区域处理" class="headerlink" title="（1）Amazon Bedrock 区域处理"></a>（1）Amazon Bedrock 区域处理</h4><p>实现了<strong>区域感知的模型前缀自动添加</strong>，支持跨区域推理，区域配置解析遵循三层优先级：</p><ol><li>Provider config options（提供商配置，最高优先级）</li><li><code>AWS_REGION</code> 环境变量</li><li>默认 <code>us-east-1</code>（后备方案）</li></ol><p>自动前缀示例：<code>claude-sonnet-4-5</code> 在不同区域会被转换为 <code>us.claude-sonnet-4-5</code>（美区）、<code>eu.claude-sonnet-4-5</code>（欧区）、<code>jp.claude-sonnet-4-5</code>（东京区）。</p><p>来源：provider.ts#L200-L300</p><h4 id="（2）Google-Vertex-集成"><a href="#（2）Google-Vertex-集成" class="headerlink" title="（2）Google Vertex 集成"></a>（2）Google Vertex 集成</h4><p>依赖环境变量完成项目与位置配置，缺少则自动禁用（<code>autoload: false</code>）：</p><ul><li>项目标识：<code>GOOGLE_CLOUD_PROJECT</code> / <code>GCP_PROJECT</code> / <code>GCLOUD_PROJECT</code></li><li>位置配置：<code>GOOGLE_CLOUD_LOCATION</code> / <code>VERTEX_LOCATION</code></li></ul><p>来源：provider.ts#L350-L380</p><h4 id="（3）GitHub-Copilot-Enterprise-变体"><a href="#（3）GitHub-Copilot-Enterprise-变体" class="headerlink" title="（3）GitHub Copilot Enterprise 变体"></a>（3）GitHub Copilot Enterprise 变体</h4><p>自动创建 <code>github-copilot-enterprise</code> 提供商变体，继承标准 <code>github-copilot</code> 的模型，使用独立身份验证凭据，支持同时使用个人版与企业版账户。</p><p>来源：provider.ts#L620-L640</p><h2 id="四、身份验证架构：类型安全与多流程支持"><a href="#四、身份验证架构：类型安全与多流程支持" class="headerlink" title="四、身份验证架构：类型安全与多流程支持"></a>四、身份验证架构：类型安全与多流程支持</h2><p>身份验证系统采用<strong>可区分联合架构</strong>，支持三种身份验证类型，实现跨提供商的类型安全凭据管理，兼顾灵活性与安全性。</p><h3 id="1-三大核心身份验证类型"><a href="#1-三大核心身份验证类型" class="headerlink" title="1. 三大核心身份验证类型"></a>1. 三大核心身份验证类型</h3><div class="table-container"><table><thead><tr><th>验证类型</th><th>适用场景</th><th>核心特性</th></tr></thead><tbody><tr><td>OAuth</td><td>企业级提供商、需要令牌刷新的场景</td><td>1. 存储访问令牌、刷新令牌、过期时间戳<br>2. 支持可选账户 ID 和企业 URL<br>3. 通过 <code>ProviderAuth.authorize()</code> / <code>ProviderAuth.callback()</code> 管理生命周期</td></tr><tr><td>API Key</td><td>大多数主流提供商（Anthropic/OpenAI 等）</td><td>1. 简单键值对存储，配置便捷<br>2. 支持通过环境变量或配置文件加载<br>3. 无需复杂令牌刷新流程</td></tr><tr><td>Well-known</td><td>组织级托管配置场景</td><td>1. 从提供商 <code>.well-known/opencode</code> 端点获取配置<br>2. 支持组织默认设置，简化团队部署<br>3. 无需手动配置单个用户凭据</td></tr></tbody></table></div><p>来源：auth.ts#L1-L50、auth.ts#L8-L30</p><h3 id="2-插件扩展的身份验证流程"><a href="#2-插件扩展的身份验证流程" class="headerlink" title="2. 插件扩展的身份验证流程"></a>2. 插件扩展的身份验证流程</h3><p>插件系统通过 <code>auth</code> 钩子扩展身份验证能力，允许自定义 OAuth 流程、凭据加载器，无需修改核心代码即可适配特殊提供商的身份验证需求。</p><p>来源：auth.ts#L1-L148</p><h2 id="五、模型发现与元数据管理"><a href="#五、模型发现与元数据管理" class="headerlink" title="五、模型发现与元数据管理"></a>五、模型发现与元数据管理</h2><p>OpenCode 与 <code>models.dev</code> API 深度集成，实现可用模型的自动发现与全面元数据管理，为智能模型选择、成本跟踪提供支撑。</p><h3 id="1-模型元数据结构"><a href="#1-模型元数据结构" class="headerlink" title="1. 模型元数据结构"></a>1. 模型元数据结构</h3><p>每个模型包含详细的元数据，覆盖身份、能力、定价、限制等全维度信息：</p><ul><li>Identity：id、名称、模型家族、发布日期</li><li>Capabilities：附件支持、推理能力、温度调节、工具调用、交错响应</li><li>Pricing：输入/输出成本、缓存读/写成本、扩展上下文定价</li><li>Limits：上下文窗口大小、输出 Token 限制</li><li>Modalities：文本、音频、图像、视频、PDF 的输入/输出支持</li><li>Status：Alpha/Beta/Deprecated 状态标志</li><li>Provider Configuration：API URL、请求头、模型变体</li></ul><p>来源：models.ts#L21-L50</p><h3 id="2-缓存策略"><a href="#2-缓存策略" class="headerlink" title="2. 缓存策略"></a>2. 缓存策略</h3><p><code>models.dev</code> 的响应结果缓存于本地 <code>$OPENCODE_CACHE_DIR/models.json</code>，兼顾启动速度与数据新鲜度：</p><ol><li>缓存刷新周期：每小时自动刷新一次，获取最新模型定义</li><li>禁用方式：可通过 <code>OPENCODE_DISABLE_MODELS_FETCH</code> 标志禁用缓存</li><li>核心优势：快速启动的同时，确保不遗漏模型更新、定价调整等关键信息</li></ol><p>来源：models.ts#L85-L108</p><h2 id="六、提供商配置：结构与过滤机制"><a href="#六、提供商配置：结构与过滤机制" class="headerlink" title="六、提供商配置：结构与过滤机制"></a>六、提供商配置：结构与过滤机制</h2><h3 id="1-核心配置结构"><a href="#1-核心配置结构" class="headerlink" title="1. 核心配置结构"></a>1. 核心配置结构</h3><p>提供商配置支持精细化定制，涵盖名称、环境变量、请求选项、模型黑白名单等，典型配置示例：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;anthropic&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;Anthropic (Custom)&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;env&quot;</span>: [<span class="string">&quot;ANTHROPIC_API_KEY&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;baseURL&quot;</span>: <span class="string">&quot;https://api.anthropic.com&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;timeout&quot;</span>: <span class="number">60000</span>,</span><br><span class="line">        <span class="attr">&quot;headers&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;anthropic-beta&quot;</span>: <span class="string">&quot;custom-header&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">&quot;api&quot;</span>: <span class="string">&quot;https://custom-proxy.com&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;whitelist&quot;</span>: [<span class="string">&quot;claude-sonnet-4-5&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;blacklist&quot;</span>: [<span class="string">&quot;deprecated-model&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;models&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;claude-sonnet-4-5&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;cost&quot;</span>: &#123;</span><br><span class="line">            <span class="attr">&quot;input&quot;</span>: <span class="number">0.003</span>,</span><br><span class="line">            <span class="attr">&quot;output&quot;</span>: <span class="number">0.015</span></span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="attr">&quot;limit&quot;</span>: &#123;</span><br><span class="line">            <span class="attr">&quot;context&quot;</span>: <span class="number">200000</span>,</span><br><span class="line">            <span class="attr">&quot;output&quot;</span>: <span class="number">8192</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">&quot;enabled_providers&quot;</span>: [<span class="string">&quot;anthropic&quot;</span>, <span class="string">&quot;openai&quot;</span>],</span><br><span class="line">  <span class="attr">&quot;disabled_providers&quot;</span>: [<span class="string">&quot;deprecated-provider&quot;</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：config.ts#L1-L100</p><h3 id="2-三大提供商过滤机制"><a href="#2-三大提供商过滤机制" class="headerlink" title="2. 三大提供商过滤机制"></a>2. 三大提供商过滤机制</h3><p>支持灵活的提供商与模型过滤，满足不同场景的权限控制与功能限制需求：</p><ol><li><code>enabled_providers</code>：白名单模式，仅加载配置中的提供商</li><li><code>disabled_providers</code>：黑名单模式，排除指定提供商</li><li>Per-provider whitelist/blacklist：提供商内部模型过滤，仅启用/禁用特定模型</li></ol><p>来源：provider.ts#L720-L850</p><h2 id="七、运行时提供商管理"><a href="#七、运行时提供商管理" class="headerlink" title="七、运行时提供商管理"></a>七、运行时提供商管理</h2><p>运行时层负责 SDK 实例化、模型加载、请求路由，通过缓存层优化重复访问，确保高效运行。</p><h3 id="1-SDK-实例化与缓存（getSDK-）"><a href="#1-SDK-实例化与缓存（getSDK-）" class="headerlink" title="1. SDK 实例化与缓存（getSDK()）"></a>1. SDK 实例化与缓存（getSDK()）</h3><p><code>getSDK()</code> 函数统一管理提供商 SDK 生命周期，核心流程如下：</p><ol><li>缓存检查：根据「npm 包名称 + 选项哈希」检查 SDK 缓存是否存在实例</li><li>凭据获取：从会话状态中提取该提供商的身份验证凭据</li><li>选项合并：合并全局配置、项目配置、模型专属的请求头与选项</li><li>实例化：加载捆绑提供商 SDK 或自动安装自定义提供商 SDK</li><li>自定义配置：配置带超时处理的自定义 fetch 方法</li><li>缓存存储：将实例存入缓存，供后续相同配置请求复用</li></ol><p>核心优势：相同配置复用 SDK 实例，不同配置触发新实例，兼顾效率与灵活性。</p><p>来源：provider.ts#L900-L980、provider.ts#L930-L940</p><h3 id="2-自定义模型加载"><a href="#2-自定义模型加载" class="headerlink" title="2. 自定义模型加载"></a>2. 自定义模型加载</h3><p>针对不同提供商的模型特性，实现专属加载逻辑，确保功能兼容：</p><ul><li>OpenAI：补全 API 使用 <code>sdk.responses()</code>，聊天 API 使用 <code>sdk.chat()</code></li><li>GitHub Copilot：Codex 模型路由到 <code>sdk.responses()</code>，聊天模型路由到 <code>sdk.chat()</code></li><li>Amazon Bedrock：加载前自动添加区域模型前缀</li></ul><p>来源：provider.ts#L90-L140</p><h2 id="八、跨提供商转换层：规范化与优化"><a href="#八、跨提供商转换层：规范化与优化" class="headerlink" title="八、跨提供商转换层：规范化与优化"></a>八、跨提供商转换层：规范化与优化</h2><p>转换层的核心作用是<strong>抹平提供商差异，规范化消息与参数</strong>，同时保留提供商专属优化，确保一致的使用体验。</p><h3 id="1-消息规范化"><a href="#1-消息规范化" class="headerlink" title="1. 消息规范化"></a>1. 消息规范化</h3><p>处理不同提供商的消息结构差异，避免调用失败：</p><ul><li>空内容过滤：Anthropic 拒绝空字符串消息，自动过滤</li><li>工具调用 ID 清理：Mistral 要求 9 位字母数字 ID，自动格式化</li><li>推理内容提取：分离部分提供商的推理文本与响应文本</li><li>消息序列修复：修正工具消息后的消息类型冲突（如部分提供商不支持工具消息后直接跟用户消息）</li></ul><p>来源：transform.ts#L10-L100</p><h3 id="2-提供商专属缓存策略"><a href="#2-提供商专属缓存策略" class="headerlink" title="2. 提供商专属缓存策略"></a>2. 提供商专属缓存策略</h3><p>自动为系统提示词和最近消息应用缓存，不同提供商使用专属缓存控制请求头：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> providerOptions = &#123;</span><br><span class="line">  anthropic: &#123; <span class="attr">cacheControl</span>: &#123; <span class="attr">type</span>: <span class="string">&quot;ephemeral&quot;</span> &#125; &#125;,</span><br><span class="line">  openrouter: &#123; <span class="attr">cacheControl</span>: &#123; <span class="attr">type</span>: <span class="string">&quot;ephemeral&quot;</span> &#125; &#125;,</span><br><span class="line">  bedrock: &#123; <span class="attr">cachePoint</span>: &#123; <span class="attr">type</span>: <span class="string">&quot;ephemeral&quot;</span> &#125; &#125;,</span><br><span class="line">  openaiCompatible: &#123; <span class="attr">cache_control</span>: &#123; <span class="attr">type</span>: <span class="string">&quot;ephemeral&quot;</span> &#125; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：transform.ts#L100-L130</p><h2 id="九、插件扩展：自定义提供商接入"><a href="#九、插件扩展：自定义提供商接入" class="headerlink" title="九、插件扩展：自定义提供商接入"></a>九、插件扩展：自定义提供商接入</h2><p>插件系统允许无需修改核心代码即可集成自定义提供商，核心扩展点包括「身份验证钩子、转换钩子、模型加载器」。</p><h3 id="核心插件身份验证钩子示例"><a href="#核心插件身份验证钩子示例" class="headerlink" title="核心插件身份验证钩子示例"></a>核心插件身份验证钩子示例</h3><p>插件通过导出 <code>auth</code> 对象定义自定义提供商的身份验证流程：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  name: <span class="string">&quot;custom-provider&quot;</span>,</span><br><span class="line">  auth: &#123;</span><br><span class="line">    provider: <span class="string">&quot;custom-provider&quot;</span>,</span><br><span class="line">    methods: [</span><br><span class="line">      &#123; <span class="attr">type</span>: <span class="string">&quot;oauth&quot;</span>, <span class="attr">label</span>: <span class="string">&quot;OAuth 2.0&quot;</span> &#125;,</span><br><span class="line">      &#123; <span class="attr">type</span>: <span class="string">&quot;api&quot;</span>, <span class="attr">label</span>: <span class="string">&quot;API Key&quot;</span> &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">authorize</span>(<span class="params"></span>)</span> &#123; <span class="comment">/* 自定义 OAuth 授权流程 */</span> &#125;,</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">callback</span>(<span class="params">code</span>)</span> &#123; <span class="comment">/* 令牌交换逻辑 */</span> &#125;,</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">loader</span>(<span class="params">getAuth, provider</span>)</span> &#123; <span class="comment">/* 凭据加载逻辑 */</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：provider/auth.ts#L20-L80、plugin/index.ts#L1-L118</p><h2 id="十、模型选择逻辑与错误处理"><a href="#十、模型选择逻辑与错误处理" class="headerlink" title="十、模型选择逻辑与错误处理"></a>十、模型选择逻辑与错误处理</h2><h3 id="1-智能模型选择"><a href="#1-智能模型选择" class="headerlink" title="1. 智能模型选择"></a>1. 智能模型选择</h3><p>系统基于配置优先级与模型功能实现自动模型选择，兼顾性能与成本：</p><ul><li>默认模型优先级：倾向强大通用模型，排序如 <code>gpt-5</code> &gt; <code>claude-sonnet-4</code> &gt; <code>big-pickle</code> &gt; <code>gemini-3-pro</code></li><li>小模型选择：工具调用、快速分析等场景，优先轻量级模型（<code>claude-haiku-4-5</code>、<code>gemini-3-flash</code>、<code>gpt-5-nano</code>），兼顾速度与成本</li></ul><p>来源：provider.ts#L1050-L1136</p><h3 id="2-全面错误处理"><a href="#2-全面错误处理" class="headerlink" title="2. 全面错误处理"></a>2. 全面错误处理</h3><p>定义专属错误类型，方便问题排查与自动回退：</p><ul><li><code>ModelNotFoundError</code>：请求模型不存在，附带模糊匹配建议</li><li><code>InitError</code>：SDK 初始化失败</li><li>OAuth 相关错误：<code>OAuthMissing</code>、<code>OauthCodeMissing</code>、<code>OauthCallbackFailed</code></li></ul><p>来源：provider.ts#L1100-L1136</p><h2 id="十一、使用示例：配置并使用-Anthropic-提供商"><a href="#十一、使用示例：配置并使用-Anthropic-提供商" class="headerlink" title="十一、使用示例：配置并使用 Anthropic 提供商"></a>十一、使用示例：配置并使用 Anthropic 提供商</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 登录 Anthropic 进行身份验证</span></span><br><span class="line">opencode auth login anthropic</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 编写项目级提供商配置</span></span><br><span class="line">cat &gt; opencode.json &lt;&lt; <span class="string">EOF</span></span><br><span class="line"><span class="string">&#123;</span></span><br><span class="line"><span class="string">  &quot;model&quot;: &quot;anthropic/claude-sonnet-4-5&quot;,</span></span><br><span class="line"><span class="string">  &quot;provider&quot;: &#123;</span></span><br><span class="line"><span class="string">    &quot;anthropic&quot;: &#123;</span></span><br><span class="line"><span class="string">      &quot;options&quot;: &#123;</span></span><br><span class="line"><span class="string">        &quot;timeout&quot;: 120000,</span></span><br><span class="line"><span class="string">        &quot;headers&quot;: &#123;</span></span><br><span class="line"><span class="string">          &quot;anthropic-beta&quot;: &quot;max-tokens-3-5-sonnet-2024-07-15&quot;</span></span><br><span class="line"><span class="string">        &#125;</span></span><br><span class="line"><span class="string">      &#125;</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br><span class="line"><span class="string">EOF</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 使用配置的提供商执行任务</span></span><br><span class="line">opencode <span class="string">&quot;Explain this codebase&quot;</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>OpenCode 提供商架构的核心是<strong>「统一接口 + 分层扩展」</strong>，既抹平了提供商差异，又保留了专属优化空间。</li><li>配置优先级链、SDK 缓存、消息规范化是保障系统高效运行与一致体验的关键。</li><li>内置 19+ 提供商 + 插件扩展体系，满足从个人开发到企业级部署的多样化需求。</li><li>与 <code>models.dev</code> 集成的模型发现与元数据管理，为智能模型选择与成本控制提供了支撑。</li></ol><h1 id="支持的-AI-提供商：Anthropic、OpenAI、Google-等"><a href="#支持的-AI-提供商：Anthropic、OpenAI、Google-等" class="headerlink" title="支持的 AI 提供商：Anthropic、OpenAI、Google 等"></a>支持的 AI 提供商：Anthropic、OpenAI、Google 等</h1><p>OpenCode 的多提供商架构以<strong>统一标准化接口</strong>为核心，实现了与 18+ 主流 AI 提供商的无缝集成，既动态管理配置、身份验证与模型选择，又提供丰富的定制选项满足高级用例，同时抹平了不同提供商的 API 差异，为用户提供一致且高效的使用体验。</p><h2 id="一、提供商架构核心概述"><a href="#一、提供商架构核心概述" class="headerlink" title="一、提供商架构核心概述"></a>一、提供商架构核心概述</h2><h3 id="1-分层初始化模型"><a href="#1-分层初始化模型" class="headerlink" title="1. 分层初始化模型"></a>1. 分层初始化模型</h3><p>提供商系统基于<strong>多源配置合并</strong>的分层初始化模型运行，配置来源按优先级从低到高依次为：</p><ol><li>远程众所周知配置（.well-known/opencode）</li><li>全局用户设置</li><li>环境变量</li><li>项目特定配置（opencode.json/opencode.jsonc）</li></ol><p>这种分层设计既支持组织级集中化配置分发，又允许开发者在项目层面灵活覆盖，兼顾标准化与个性化需求。</p><h3 id="2-核心运行机制"><a href="#2-核心运行机制" class="headerlink" title="2. 核心运行机制"></a>2. 核心运行机制</h3><p>每个提供商均通过 AI SDK 生态系统实例化，同时针对各提供商的独特性应用专属定制，核心解决三大问题：</p><ul><li>独特 API 行为差异（如消息格式、接口类型）</li><li>多样化身份验证模式（如 API Key、OAuth、AWS 凭证链）</li><li>特定模型的功能要求（如区域前缀、Beta 功能头）</li></ul><p>来源：provider/provider.ts</p><h2 id="二、主流提供商支持与专属配置"><a href="#二、主流提供商支持与专属配置" class="headerlink" title="二、主流提供商支持与专属配置"></a>二、主流提供商支持与专属配置</h2><p>OpenCode 内置 18+ 提供商，每个提供商均有对应的 SDK 依赖、身份验证方式和专属优化，以下是核心提供商的关键配置细节：</p><h3 id="1-核心提供商清单（关键节选）"><a href="#1-核心提供商清单（关键节选）" class="headerlink" title="1. 核心提供商清单（关键节选）"></a>1. 核心提供商清单（关键节选）</h3><div class="table-container"><table><thead><tr><th>提供商</th><th>SDK 包</th><th>身份验证方式</th><th>核心特殊功能</th></tr></thead><tbody><tr><td>Anthropic</td><td><code>@ai-sdk/anthropic</code></td><td>API Key</td><td>Beta 功能头注入、交错推理支持</td></tr><tr><td>OpenAI</td><td><code>@ai-sdk/openai</code></td><td>API Key</td><td>动态切换 Responses/Chat API</td></tr><tr><td>Google Vertex</td><td><code>@ai-sdk/google-vertex</code></td><td>OAuth/服务账号</td><td>云项目集成、位置感知模型解析</td></tr><tr><td>Amazon Bedrock</td><td><code>@ai-sdk/amazon-bedrock</code></td><td>AWS 凭证链</td><td>跨区域推理、自动模型前缀添加</td></tr><tr><td>GitHub Copilot</td><td>自定义 OpenAI 兼容</td><td>OAuth</td><td>企业 URL 支持、代码/对话模型分流</td></tr></tbody></table></div><h3 id="2-重点提供商专属配置"><a href="#2-重点提供商专属配置" class="headerlink" title="2. 重点提供商专属配置"></a>2. 重点提供商专属配置</h3><h4 id="（1）Anthropic：Beta-功能头自动注入"><a href="#（1）Anthropic：Beta-功能头自动注入" class="headerlink" title="（1）Anthropic：Beta 功能头自动注入"></a>（1）Anthropic：Beta 功能头自动注入</h4><p>需要 <code>ANTHROPIC_API_KEY</code> 环境变量或配置中的 API Key，系统自动注入高级功能 Beta 头，无需手动配置：</p><ul><li><code>claude-code-20250219</code>：增强代码理解能力</li><li><code>interleaved-thinking-2025-05-14</code>：支持 CoT（思维链）推理可视化</li><li><code>fine-grained-tool-streaming-2025-05-14</code>：实现精确的工具调用流控制</li></ul><p>来源：provider/provider.ts</p><h4 id="（2）OpenAI：双-API-动态路由"><a href="#（2）OpenAI：双-API-动态路由" class="headerlink" title="（2）OpenAI：双 API 动态路由"></a>（2）OpenAI：双 API 动态路由</h4><p>根据模型需求自动选择对应的 API 模式，无需手动指定：</p><ul><li>Responses API（<code>sdk.responses(modelID)</code>）：适用于结构化输出、函数调用场景（如 Codex 模型）</li><li>Chat API（<code>sdk.chat(modelID)</code>）：适用于标准对话场景（如 GPT-4、GPT-3.5-turbo）</li><li>额外支持：标准 OpenAI 端点与 Azure OpenAI 部署的配置兼容</li></ul><p>来源：provider/provider.ts</p><h4 id="（3）Amazon-Bedrock：复杂凭证链与跨区域推理"><a href="#（3）Amazon-Bedrock：复杂凭证链与跨区域推理" class="headerlink" title="（3）Amazon Bedrock：复杂凭证链与跨区域推理"></a>（3）Amazon Bedrock：复杂凭证链与跨区域推理</h4><p>这是配置最复杂的提供商，支持多层凭证优先级与跨区域模型适配：</p><ul><li><strong>身份验证优先级</strong>（从高到低）：<ol><li>身份验证存储中显式提供的 API Key</li><li>环境变量中的 AWS 凭证（<code>AWS_ACCESS_KEY_ID</code>、<code>AWS_BEARER_TOKEN_BEDROCK</code>）</li><li>支持配置文件的 AWS 凭证提供商链</li></ol></li><li><p><strong>核心配置示例</strong>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;amazon-bedrock&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;region&quot;</span>: <span class="string">&quot;us-east-1&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;profile&quot;</span>: <span class="string">&quot;default&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;baseURL&quot;</span>: <span class="string">&quot;custom-endpoint&quot;</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><strong>跨区域推理</strong>：自动为模型添加区域前缀，适配不同区域的模型可用性：<ul><li>美区：<code>us.</code> 前缀（如 <code>us.claude-sonnet-4-5</code>）</li><li>欧区：<code>eu.</code> 前缀（如 <code>eu.claude-sonnet-4-5</code>）</li><li>亚太区：<code>apac.</code> / <code>jp.</code> 前缀（如 <code>jp.claude-sonnet-4-5</code>）</li></ul></li></ul><p>来源：provider/provider.ts</p><h4 id="（4）GitHub-Copilot-amp-Enterprise：OAuth-认证与企业配置"><a href="#（4）GitHub-Copilot-amp-Enterprise：OAuth-认证与企业配置" class="headerlink" title="（4）GitHub Copilot &amp; Enterprise：OAuth 认证与企业配置"></a>（4）GitHub Copilot &amp; Enterprise：OAuth 认证与企业配置</h4><ul><li>身份验证：依赖插件系统实现 OAuth 流程，支持个人版与企业版双变体</li><li>API 分流：代码模型（Codex）使用 Responses API，对话模型（GPT-5-mini）使用 Chat API</li><li><p>企业版额外配置（需指定企业 URL）：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;github-copilot-enterprise&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;enterpriseUrl&quot;</span>: <span class="string">&quot;https://github.yourcompany.com&quot;</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><p>来源：provider/provider.ts</p><h2 id="三、三大身份验证方法：灵活适配不同场景"><a href="#三、三大身份验证方法：灵活适配不同场景" class="headerlink" title="三、三大身份验证方法：灵活适配不同场景"></a>三、三大身份验证方法：灵活适配不同场景</h2><p>提供商系统通过 <code>Auth</code> 模块统一管理三种身份验证类型，实现跨提供商的类型安全凭据管理，覆盖从简单个人使用到复杂企业部署的所有场景。</p><h3 id="1-API-Key-身份验证：简单高效（适用于大多数提供商）"><a href="#1-API-Key-身份验证：简单高效（适用于大多数提供商）" class="headerlink" title="1. API Key 身份验证：简单高效（适用于大多数提供商）"></a>1. API Key 身份验证：简单高效（适用于大多数提供商）</h3><p>直接存储与加载 API Key，适用于支持简单令牌验证的提供商（Anthropic、OpenAI 等），核心结构：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;api&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;key&quot;</span>: <span class="string">&quot;sk-xxxxxxxxxxxxxxxxxxxxxxxx&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>配置方式：环境变量、项目配置文件、全局 auth.json 均可配置</li><li>核心优势：配置简单，无需复杂流程，快速上手</li></ul><p>来源：auth/index.ts</p><h3 id="2-OAuth-身份验证：复杂企业场景（适用于-Copilot、Google-Vertex-等）"><a href="#2-OAuth-身份验证：复杂企业场景（适用于-Copilot、Google-Vertex-等）" class="headerlink" title="2. OAuth 身份验证：复杂企业场景（适用于 Copilot、Google Vertex 等）"></a>2. OAuth 身份验证：复杂企业场景（适用于 Copilot、Google Vertex 等）</h3><p>由插件驱动的完整 OAuth 流程，支持令牌刷新、账户关联，适用于复杂身份验证需求的提供商，核心结构：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;oauth&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;access&quot;</span>: <span class="string">&quot;ya29.a0AfH6xxxxxxxxxxxxxxxx&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;refresh&quot;</span>: <span class="string">&quot;1//0gxxxxxxxxxxxxxxxxxxxx&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;expires&quot;</span>: <span class="number">1735689600</span>,</span><br><span class="line">  <span class="attr">&quot;accountId&quot;</span>: <span class="string">&quot;user@example.com&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>生命周期管理：通过 <code>ProviderAuth</code> 模块协调授权 URL、回调处理、自动令牌刷新</li><li>核心优势：安全可靠，支持企业级账户管理，无需手动存储长期有效密钥</li></ul><p>来源：auth/index.ts, provider/auth.ts</p><h3 id="3-Well-Known-身份验证：组织级集中配置（适用于企业部署）"><a href="#3-Well-Known-身份验证：组织级集中配置（适用于企业部署）" class="headerlink" title="3. Well-Known 身份验证：组织级集中配置（适用于企业部署）"></a>3. Well-Known 身份验证：组织级集中配置（适用于企业部署）</h3><p>遵循 <code>.well-known/opencode</code> 规范，从远程企业服务器获取集中化配置，核心结构：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;wellknown&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;key&quot;</span>: <span class="string">&quot;https://enterprise.internal&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;token&quot;</span>: <span class="string">&quot;org-token-xxxxxxxx&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>核心价值：实现跨组织的提供商配置统一分发，简化团队部署与权限管理</li><li>适用场景：大型企业、团队协作，需要标准化所有成员的提供商配置</li></ul><p>来源：auth/index.ts</p><h2 id="四、模型元数据与发现：动态缓存与全面信息"><a href="#四、模型元数据与发现：动态缓存与全面信息" class="headerlink" title="四、模型元数据与发现：动态缓存与全面信息"></a>四、模型元数据与发现：动态缓存与全面信息</h2><h3 id="1-模型元数据来源与缓存策略"><a href="#1-模型元数据来源与缓存策略" class="headerlink" title="1. 模型元数据来源与缓存策略"></a>1. 模型元数据来源与缓存策略</h3><ul><li><strong>数据来源</strong>：从 <code>models.dev</code> API 动态获取最新模型信息</li><li><strong>缓存机制</strong>：获取后缓存到本地 <code>~/.cache/opencode/models.json</code>，每 60 分钟自动刷新</li><li><strong>手动操作</strong>：可通过 <code>opencode models --refresh</code> 手动刷新缓存，或直接删除缓存文件</li><li><strong>禁用缓存</strong>：无需最新数据时，可通过配置禁用远程获取，仅使用本地缓存</li></ul><h3 id="2-全面的模型元数据结构"><a href="#2-全面的模型元数据结构" class="headerlink" title="2. 全面的模型元数据结构"></a>2. 全面的模型元数据结构</h3><p>每个模型的元数据覆盖<strong>功能、成本、限制、状态</strong>四大维度，为智能模型选择与成本跟踪提供支撑：</p><h4 id="（1）模型功能（capabilities）"><a href="#（1）模型功能（capabilities）" class="headerlink" title="（1）模型功能（capabilities）"></a>（1）模型功能（capabilities）</h4><p>标记模型支持的核心能力与多模态输入输出：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;capabilities&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;temperature&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;reasoning&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;attachment&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;toolcall&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;input&quot;</span>: &#123; <span class="attr">&quot;text&quot;</span>: <span class="literal">true</span>, <span class="attr">&quot;image&quot;</span>: <span class="literal">false</span> &#125;,</span><br><span class="line">    <span class="attr">&quot;output&quot;</span>: &#123; <span class="attr">&quot;text&quot;</span>: <span class="literal">true</span>, <span class="attr">&quot;audio&quot;</span>: <span class="literal">false</span> &#125;,</span><br><span class="line">    <span class="attr">&quot;interleaved&quot;</span>: &#123; <span class="attr">&quot;field&quot;</span>: <span class="string">&quot;reasoning_content&quot;</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="（2）成本结构（cost）"><a href="#（2）成本结构（cost）" class="headerlink" title="（2）成本结构（cost）"></a>（2）成本结构（cost）</h4><p>详细定义每 100 万 Token 的输入/输出成本，包含缓存与扩展上下文定价：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;cost&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;input&quot;</span>: <span class="number">0.003</span>,</span><br><span class="line">    <span class="attr">&quot;output&quot;</span>: <span class="number">0.015</span>,</span><br><span class="line">    <span class="attr">&quot;cache&quot;</span>: &#123; <span class="attr">&quot;read&quot;</span>: <span class="number">0.001</span>, <span class="attr">&quot;write&quot;</span>: <span class="number">0.002</span> &#125;,</span><br><span class="line">    <span class="attr">&quot;experimentalOver200K&quot;</span>: &#123; <span class="attr">&quot;input&quot;</span>: <span class="number">0.006</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">0.03</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="（3）限制与状态（limit-amp-status）"><a href="#（3）限制与状态（limit-amp-status）" class="headerlink" title="（3）限制与状态（limit &amp; status）"></a>（3）限制与状态（limit &amp; status）</h4><p>定义模型的上下文窗口、输出限制与生命周期状态：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;limit&quot;</span>: &#123; <span class="attr">&quot;context&quot;</span>: <span class="number">200000</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">8192</span> &#125;,</span><br><span class="line">  <span class="attr">&quot;status&quot;</span>: <span class="string">&quot;active&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;release_date&quot;</span>: <span class="string">&quot;2024-05-14T00:00:00Z&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：provider/models.ts</p><h2 id="五、提供商配置定制：覆盖与扩展"><a href="#五、提供商配置定制：覆盖与扩展" class="headerlink" title="五、提供商配置定制：覆盖与扩展"></a>五、提供商配置定制：覆盖与扩展</h2><p>OpenCode 支持通过 <code>opencode.json</code> 对提供商进行深度定制，满足各种高级用例，核心定制方式包括三种：</p><h3 id="1-基本提供商覆盖：修改核心配置"><a href="#1-基本提供商覆盖：修改核心配置" class="headerlink" title="1. 基本提供商覆盖：修改核心配置"></a>1. 基本提供商覆盖：修改核心配置</h3><p>自定义提供商名称、API 端点、超时时间等基础属性：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;anthropic&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;Anthropic Custom&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;apiKey&quot;</span>: <span class="string">&quot;&#123;env:ANTHROPIC_KEY&#125;&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;baseURL&quot;</span>: <span class="string">&quot;https://custom.anthropic.com&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;timeout&quot;</span>: <span class="number">600000</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-模型黑白名单：过滤可用模型"><a href="#2-模型黑白名单：过滤可用模型" class="headerlink" title="2. 模型黑白名单：过滤可用模型"></a>2. 模型黑白名单：过滤可用模型</h3><p>仅启用指定模型（白名单）或排除特定模型（黑名单）：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;openai&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;whitelist&quot;</span>: [<span class="string">&quot;gpt-4&quot;</span>, <span class="string">&quot;gpt-3.5-turbo&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;blacklist&quot;</span>: [<span class="string">&quot;gpt-4-32k&quot;</span>]</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-自定义提供商集成：添加非内置提供商"><a href="#3-自定义提供商集成：添加非内置提供商" class="headerlink" title="3. 自定义提供商集成：添加非内置提供商"></a>3. 自定义提供商集成：添加非内置提供商</h3><p>通过 OpenAI 兼容接口，集成自定义提供商（如私有部署的 Llama 3）：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;custom-llama&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;api&quot;</span>: <span class="string">&quot;https://api.custom-llama.com/v1&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm&quot;</span>: <span class="string">&quot;@ai-sdk/openai-compatible&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;env&quot;</span>: [<span class="string">&quot;CUSTOM_LLAMA_KEY&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;models&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;llama-3-70b&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;meta-llama/Meta-Llama-3-70B&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;Llama 3 70B&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;cost&quot;</span>: &#123; <span class="attr">&quot;input&quot;</span>: <span class="number">0.7</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">0.7</span> &#125;,</span><br><span class="line">          <span class="attr">&quot;limit&quot;</span>: &#123; <span class="attr">&quot;context&quot;</span>: <span class="number">8192</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">4096</span> &#125;,</span><br><span class="line">          <span class="attr">&quot;capabilities&quot;</span>: &#123; <span class="attr">&quot;toolcall&quot;</span>: <span class="literal">true</span>, <span class="attr">&quot;temperature&quot;</span>: <span class="literal">true</span> &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：config/config.ts</p><h2 id="六、提供商发现与过滤：自动识别与精准控制"><a href="#六、提供商发现与过滤：自动识别与精准控制" class="headerlink" title="六、提供商发现与过滤：自动识别与精准控制"></a>六、提供商发现与过滤：自动识别与精准控制</h2><h3 id="1-提供商发现来源（优先级顺序）"><a href="#1-提供商发现来源（优先级顺序）" class="headerlink" title="1. 提供商发现来源（优先级顺序）"></a>1. 提供商发现来源（优先级顺序）</h3><p>系统自动从多渠道发现可用提供商，无需手动注册：</p><ol><li>环境变量（扫描已知 API Key 模式）</li><li>API Key 存储（从 auth.json 加载已配置凭据）</li><li>插件身份验证（从插件获取 OAuth 令牌）</li><li>配置文件（opencode.json 中的显式定义）</li><li>远程数据库（models.dev API 中的提供商清单）</li></ol><h3 id="2-多层过滤机制"><a href="#2-多层过滤机制" class="headerlink" title="2. 多层过滤机制"></a>2. 多层过滤机制</h3><p>确保仅加载所需的提供商与模型，避免冗余：</p><ol><li><strong>提供商级别</strong>：通过 <code>disabled_providers</code> / <code>enabled_providers</code> 配置数组，全局禁用/启用提供商</li><li><strong>模型级别</strong>：通过每个提供商的 <code>whitelist</code> / <code>blacklist</code> 数组，过滤内部模型</li><li><strong>状态过滤</strong>：Alpha 模型需要 <code>OPENCODE_ENABLE_EXPERIMENTAL_MODELS</code> 标志才能启用</li><li><strong>自动过滤</strong>：已弃用模型自动从可用列表中排除，无需手动配置</li></ol><h3 id="3-CLI-工具：查看可用模型"><a href="#3-CLI-工具：查看可用模型" class="headerlink" title="3. CLI 工具：查看可用模型"></a>3. CLI 工具：查看可用模型</h3><p>通过 <code>opencode models</code> 命令可快速查看提供商与模型信息，支持多种参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 列出所有可用模型</span></span><br><span class="line">opencode models</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出来自 Anthropic 的模型</span></span><br><span class="line">opencode models anthropic</span><br><span class="line"></span><br><span class="line"><span class="comment"># 显示完整元数据（详细输出）</span></span><br><span class="line">opencode models --verbose</span><br><span class="line"></span><br><span class="line"><span class="comment"># 刷新模型缓存</span></span><br><span class="line">opencode models --refresh</span><br></pre></td></tr></table></figure><p>来源：cli/cmd/models.ts</p><h2 id="七、自定义提供商高级集成"><a href="#七、自定义提供商高级集成" class="headerlink" title="七、自定义提供商高级集成"></a>七、自定义提供商高级集成</h2><p>对于需要特殊处理的提供商（非 OpenAI 兼容），可通过<strong>插件开发</strong>实现深度集成，核心步骤：</p><ol><li>实现 <code>auth.loader</code> 接口，自定义身份验证流程</li><li>注册请求转换钩子，处理提供商专属的消息格式与参数</li><li>配置模型加载逻辑，适配提供商的 API 接口</li><li>注意事项：确保 API 支持流式响应与格式正确的工具输出，不支持则设置 <code>includeUsage: false</code></li></ol><p>来源：provider/provider.ts</p><h2 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h2><p>OpenCode 多提供商架构的核心优势在于<strong>「统一接口与个性化定制的平衡」</strong>：</p><ol><li>以分层配置、标准化接口抹平了不同提供商的差异，降低使用成本</li><li>以专属定制、插件扩展保留了各提供商的独特优势，满足高级需求</li><li>以动态发现、缓存机制、多层过滤确保了系统的高效性与灵活性</li><li>覆盖从个人开发到企业部署的全场景，支持 18+ 内置提供商与无限自定义扩展</li></ol><p>这种架构设计，既让普通用户能够快速上手不同 AI 提供商，又让高级用户能够实现精细化的定制与优化。</p><h1 id="模型选择与配置"><a href="#模型选择与配置" class="headerlink" title="模型选择与配置"></a>模型选择与配置</h1><p>在 OpenCode 中配置和选择 AI 模型的核心是<strong>多层级配置系统</strong>，它通过合并多来源设置、灵活过滤规则和智能选择逻辑，让你能精准匹配不同任务的模型需求，同时兼顾成本、性能与功能的平衡。</p><h2 id="一、核心配置架构：多层级设置合并"><a href="#一、核心配置架构：多层级设置合并" class="headerlink" title="一、核心配置架构：多层级设置合并"></a>一、核心配置架构：多层级设置合并</h2><p>OpenCode 的模型配置遵循<strong>优先级递增</strong>的层级结构，系统会自动合并不同来源的配置，最终生成一份统一的可用模型列表。</p><ol><li><strong>配置合并逻辑</strong>：低优先级配置会被高优先级配置覆盖，数组类型字段（如插件、指令）会进行拼接而非替换。</li><li><p><strong>配置来源优先级</strong>（从低到高）：</p><p> | 配置位置 | 适用范围 | 优先级 |<br> |—————|—————|————|<br> | Remote <code>.well-known/opencode</code> | 组织默认值 | 最低 |<br> | <code>~/.opencode/config.json</code> | 全局用户配置 | 低 |<br> | <code>~/.opencode/opencode.json</code> | 全局用户配置 | 中 |<br> | <code>.opencode/config.json</code> | 项目配置 | 高 |<br> | <code>.opencode/opencode.json</code> | 项目配置 | 较高 |<br> | <code>OPENCODE_CONFIG</code> 标志指定路径 | 自定义路径 | 最高 |<br> | <code>OPENCODE_CONFIG_CONTENT</code> 标志内联内容 | 内联配置 | 最高 |</p></li></ol><p>这种设计既支持团队级的标准化配置，又允许开发者在项目中灵活定制。<br>来源：provider.ts, config.ts</p><h2 id="二、基础模型配置：默认与小模型"><a href="#二、基础模型配置：默认与小模型" class="headerlink" title="二、基础模型配置：默认与小模型"></a>二、基础模型配置：默认与小模型</h2><h3 id="1-全局默认模型"><a href="#1-全局默认模型" class="headerlink" title="1. 全局默认模型"></a>1. 全局默认模型</h3><p>通过 <code>model</code> 字段设置所有交互的默认模型，适用于未指定 Agent 专属模型的场景。<br><strong>配置格式</strong>：模型 ID 遵循 <code>providerID/modelID</code> 规范，系统通过 <code>parseModel()</code> 函数验证格式。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-sonnet-4-20250514&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-小模型配置（轻量级任务专用）"><a href="#2-小模型配置（轻量级任务专用）" class="headerlink" title="2. 小模型配置（轻量级任务专用）"></a>2. 小模型配置（轻量级任务专用）</h3><p>小模型用于处理<strong>会话标题生成、摘要生成</strong>等低复杂度任务，可独立配置以优化成本和延迟。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;small_model&quot;</span>: <span class="string">&quot;anthropic/claude-haiku-4-20250514&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>自动选择规则</strong>：若未配置 <code>small_model</code>，系统会从优先级列表自动选取，优先顺序包括 <code>claude-haiku-4-5</code>、<code>gemini-3-flash</code>、<code>gpt-5-nano</code> 等轻量模型。</li><li><strong>核心优势</strong>：小模型的成本和延迟仅为大型模型的一小部分，完全能满足实用工具类任务的需求。</li></ul><p>来源：provider.ts</p><h2 id="三、提供商配置：身份验证与自定义集成"><a href="#三、提供商配置：身份验证与自定义集成" class="headerlink" title="三、提供商配置：身份验证与自定义集成"></a>三、提供商配置：身份验证与自定义集成</h2><h3 id="1-内置提供商配置"><a href="#1-内置提供商配置" class="headerlink" title="1. 内置提供商配置"></a>1. 内置提供商配置</h3><p>针对每个内置提供商，可配置身份验证、端点、超时等专属选项，支持 <code>models.dev</code> 生态系统的所有提供商类型。<br><strong>示例配置</strong>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;anthropic&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;apiKey&quot;</span>: <span class="string">&quot;your-api-key&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;timeout&quot;</span>: <span class="number">300000</span>,</span><br><span class="line">        <span class="attr">&quot;headers&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;anthropic-version&quot;</span>: <span class="string">&quot;2023-06-01&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;amazon-bedrock&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;region&quot;</span>: <span class="string">&quot;us-west-2&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;profile&quot;</span>: <span class="string">&quot;production&quot;</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-提供商身份验证机制"><a href="#2-提供商身份验证机制" class="headerlink" title="2. 提供商身份验证机制"></a>2. 提供商身份验证机制</h3><p>系统支持多种身份验证方式，并按顺序自动发现凭据，无需手动指定：</p><div class="table-container"><table><thead><tr><th>验证方式</th><th>凭据来源</th><th>示例</th></tr></thead><tbody><tr><td>环境变量</td><td><code>provider.env</code> 数组定义的变量</td><td><code>ANTHROPIC_API_KEY</code></td></tr><tr><td>配置文件</td><td><code>provider.options.apiKey</code> 字段</td><td>直接填写密钥字符串</td></tr><tr><td>加密存储</td><td><code>Auth.get()</code> 方法</td><td>安全存储的凭据</td></tr><tr><td>插件加载器</td><td>自定义认证流程</td><td>特定提供商的 OAuth 令牌</td></tr></tbody></table></div><p>系统会优先扫描 <code>provider.env</code> 数组中定义的环境变量，找到第一个有效值后停止扫描。<br>来源：provider.ts</p><h3 id="3-自定义提供商集成"><a href="#3-自定义提供商集成" class="headerlink" title="3. 自定义提供商集成"></a>3. 自定义提供商集成</h3><p>对于未内置的提供商，可通过指定 NPM 包和模型元数据，实现无代码集成。<br><strong>配置示例</strong>（以自定义模型为例）：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;custom-provider&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;My Custom Provider&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;api&quot;</span>: <span class="string">&quot;https://api.custom.com/v1&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm&quot;</span>: <span class="string">&quot;@company/custom-ai-sdk&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;env&quot;</span>: [<span class="string">&quot;CUSTOM_API_KEY&quot;</span>],</span><br><span class="line">      <span class="attr">&quot;models&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;custom-model-v1&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;custom-model-v1&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;Custom Model V1&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;cost&quot;</span>: &#123; <span class="attr">&quot;input&quot;</span>: <span class="number">0.001</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">0.002</span> &#125;,</span><br><span class="line">          <span class="attr">&quot;limit&quot;</span>: &#123; <span class="attr">&quot;context&quot;</span>: <span class="number">128000</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">4096</span> &#125;,</span><br><span class="line">          <span class="attr">&quot;capabilities&quot;</span>: &#123;</span><br><span class="line">            <span class="attr">&quot;toolcall&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">            <span class="attr">&quot;temperature&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">            <span class="attr">&quot;input&quot;</span>: &#123; <span class="attr">&quot;text&quot;</span>: <span class="literal">true</span> &#125;,</span><br><span class="line">            <span class="attr">&quot;output&quot;</span>: &#123; <span class="attr">&quot;text&quot;</span>: <span class="literal">true</span> &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>核心机制</strong>：系统会动态安装指定的 NPM 包，并通过 <code>create*</code> 函数模式自动创建提供商实例。</li></ul><p>来源：provider.ts</p><h2 id="四、模型过滤与选择：精准控制可用范围"><a href="#四、模型过滤与选择：精准控制可用范围" class="headerlink" title="四、模型过滤与选择：精准控制可用范围"></a>四、模型过滤与选择：精准控制可用范围</h2><h3 id="1-模型黑白名单（提供商内过滤）"><a href="#1-模型黑白名单（提供商内过滤）" class="headerlink" title="1. 模型黑白名单（提供商内过滤）"></a>1. 模型黑白名单（提供商内过滤）</h3><p>通过 <code>whitelist</code>/<code>blacklist</code> 控制单个提供商下的可用模型，实现精细化管理。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;anthropic&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;whitelist&quot;</span>: [<span class="string">&quot;claude-sonnet-4-20250514&quot;</span>, <span class="string">&quot;claude-haiku-4-20250514&quot;</span>]</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;openai&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;blacklist&quot;</span>: [<span class="string">&quot;gpt-3.5-turbo&quot;</span>, <span class="string">&quot;gpt-4-turbo-preview&quot;</span>]</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>白名单模式</strong>：仅允许列表内的模型被使用，适合生产环境严格控风险。</li><li><strong>黑名单模式</strong>：排除列表内的模型，适合临时禁用某些模型。</li><li><strong>生效时机</strong>：过滤规则在提供商初始化时应用，被排除的模型不会进入 Agent 的可用模型池。</li></ul><p>来源：provider.ts</p><h3 id="2-提供商全局启用-禁用"><a href="#2-提供商全局启用-禁用" class="headerlink" title="2. 提供商全局启用/禁用"></a>2. 提供商全局启用/禁用</h3><p>通过 <code>enabled_providers</code>/<code>disabled_providers</code> 配置，控制整个提供商的加载状态。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;enabled_providers&quot;</span>: [<span class="string">&quot;anthropic&quot;</span>, <span class="string">&quot;openai&quot;</span>],</span><br><span class="line">  <span class="attr">&quot;disabled_providers&quot;</span>: [<span class="string">&quot;google-vertex&quot;</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>白名单逻辑</strong>：设置 <code>enabled_providers</code> 后，仅列表内的提供商被加载。</li><li><strong>黑名单逻辑</strong>：设置 <code>disabled_providers</code> 后，列表内的提供商被排除。</li></ul><h3 id="3-基于状态的过滤（实验性-已弃用模型）"><a href="#3-基于状态的过滤（实验性-已弃用模型）" class="headerlink" title="3. 基于状态的过滤（实验性/已弃用模型）"></a>3. 基于状态的过滤（实验性/已弃用模型）</h3><p>模型会根据 <code>models.dev</code> 数据库中的状态自动过滤，默认禁用高风险模型：</p><ul><li><strong>默认排除</strong>：Alpha 测试版、已弃用的模型。</li><li><strong>启用方式</strong>：设置 <code>Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS</code> 标志，即可解锁实验性模型。</li></ul><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;anthropic&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;models&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;claude-experimental&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;status&quot;</span>: <span class="string">&quot;alpha&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：provider.ts</p><h2 id="五、模型变体：同一模型的多套参数配置"><a href="#五、模型变体：同一模型的多套参数配置" class="headerlink" title="五、模型变体：同一模型的多套参数配置"></a>五、模型变体：同一模型的多套参数配置</h2><p>模型变体允许你为同一个底层模型配置多套不同的参数，适配不同任务场景，由 <code>ProviderTransform.variants()</code> 系统处理。<br><strong>配置示例</strong>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;anthropic&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;models&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;claude-sonnet-4-20250514&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;variants&quot;</span>: &#123;</span><br><span class="line">            <span class="attr">&quot;fast&quot;</span>: &#123;</span><br><span class="line">              <span class="attr">&quot;maxTokens&quot;</span>: <span class="number">4096</span>,</span><br><span class="line">              <span class="attr">&quot;temperature&quot;</span>: <span class="number">0.5</span></span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="attr">&quot;detailed&quot;</span>: &#123;</span><br><span class="line">              <span class="attr">&quot;maxTokens&quot;</span>: <span class="number">8192</span>,</span><br><span class="line">              <span class="attr">&quot;temperature&quot;</span>: <span class="number">0.3</span></span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>典型用途</strong>：<ul><li><code>fast</code> 变体：低温度、小输出长度，适合快速迭代的代码生成。</li><li><code>detailed</code> 变体：更低温度、大输出长度，适合深度分析和文档撰写。</li></ul></li></ul><p>来源：provider.ts</p><h2 id="六、Agent-特定模型配置：任务专属优化"><a href="#六、Agent-特定模型配置：任务专属优化" class="headerlink" title="六、Agent 特定模型配置：任务专属优化"></a>六、Agent 特定模型配置：任务专属优化</h2><p>Agent 级别的模型配置<strong>优先级高于全局配置</strong>，可针对不同功能的 Agent 单独指定模型，匹配其任务特性。<br><strong>配置示例</strong>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;agent&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;build&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-sonnet-4-20250514&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Code generation and implementation&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;plan&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-opus-4-20250514&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;High-level planning and architecture&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;explore&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-haiku-4-20250514&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;Fast codebase exploration&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>合并规则</strong>：Agent 配置会与全局默认设置合并，只需覆盖 <code>model</code> 字段，其他 Agent 行为保持不变。</li></ul><h3 id="模型选择解析顺序（优先级从高到低）"><a href="#模型选择解析顺序（优先级从高到低）" class="headerlink" title="模型选择解析顺序（优先级从高到低）"></a>模型选择解析顺序（优先级从高到低）</h3><p>当 Agent 发起请求时，系统会按以下顺序确定最终使用的模型：</p><ol><li>Agent 特定配置的模型</li><li>全局默认模型（<code>model</code> 字段）</li><li>提供商的默认模型</li><li>系统预定义的优先级模型列表</li></ol><p>系统通过 <code>defaultModel()</code> 函数实现上述逻辑，确保模型选择的合理性。<br>来源：provider.ts, agent.ts</p><h2 id="七、模型元数据：功能、成本与限制"><a href="#七、模型元数据：功能、成本与限制" class="headerlink" title="七、模型元数据：功能、成本与限制"></a>七、模型元数据：功能、成本与限制</h2><p>每个模型的元数据都存储在 <code>models.dev</code> 数据库中，包含功能、成本、限制等关键信息，是智能模型选择的依据。</p><h3 id="核心元数据字段说明"><a href="#核心元数据字段说明" class="headerlink" title="核心元数据字段说明"></a>核心元数据字段说明</h3><div class="table-container"><table><thead><tr><th>字段分类</th><th>具体字段</th><th>描述</th><th>示例</th></tr></thead><tbody><tr><td>功能（capabilities）</td><td><code>toolcall</code></td><td>是否支持函数调用</td><td><code>true</code></td></tr><tr><td></td><td><code>temperature</code></td><td>是否支持温度参数调节</td><td><code>true</code></td></tr><tr><td></td><td><code>reasoning</code></td><td>是否具备思维链推理能力</td><td><code>false</code></td></tr><tr><td></td><td><code>modalities.input</code></td><td>支持的输入类型</td><td><code>[&quot;text&quot;, &quot;image&quot;]</code></td></tr><tr><td>成本（cost）</td><td><code>input</code></td><td>每 100 万输入 Token 的成本</td><td><code>0.003</code></td></tr><tr><td></td><td><code>output</code></td><td>每 100 万输出 Token 的成本</td><td><code>0.015</code></td></tr><tr><td>限制（limit）</td><td><code>context</code></td><td>最大上下文窗口 Token 数</td><td><code>200000</code></td></tr><tr><td></td><td><code>output</code></td><td>最大输出 Token 数</td><td><code>8192</code></td></tr><tr><td>状态（status）</td><td><code>status</code></td><td>模型生命周期状态</td><td><code>alpha</code>/<code>active</code>/<code>deprecated</code></td></tr></tbody></table></div><p>来源：provider.ts</p><h2 id="八、动态模型数据库：缓存与刷新"><a href="#八、动态模型数据库：缓存与刷新" class="headerlink" title="八、动态模型数据库：缓存与刷新"></a>八、动态模型数据库：缓存与刷新</h2><p>OpenCode 维护一个动态的模型数据库，确保模型信息的时效性和访问速度：</p><ol><li><strong>数据来源</strong>：从 <code>https://models.dev/api.json</code> 接口获取。</li><li><strong>缓存机制</strong>：<ul><li>缓存位置：<code>~/.cache/opencode/models.json</code></li><li>刷新间隔：每 60 分钟自动刷新一次</li></ul></li><li><strong>禁用远程获取</strong>：设置 <code>OPENCODE_DISABLE_MODELS_FETCH</code> 标志，可仅使用本地缓存。</li></ol><p>系统通过 <code>ModelsDev.get()</code> 函数管理缓存的读取与更新。<br>来源：models.ts</p><h2 id="九、成本优化与最佳实践"><a href="#九、成本优化与最佳实践" class="headerlink" title="九、成本优化与最佳实践"></a>九、成本优化与最佳实践</h2><h3 id="1-成本优化配置技巧"><a href="#1-成本优化配置技巧" class="headerlink" title="1. 成本优化配置技巧"></a>1. 成本优化配置技巧</h3><ul><li><p><strong>区分任务配置模型</strong>：轻量任务用小模型，核心任务用大模型，示例如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-sonnet-4-20250514&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;small_model&quot;</span>: <span class="string">&quot;anthropic/claude-haiku-4-20250514&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;agent&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;title&quot;</span>: &#123; <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-haiku-4-20250514&quot;</span> &#125;,</span><br><span class="line">    <span class="attr">&quot;build&quot;</span>: &#123; <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-sonnet-4-20250514&quot;</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><strong>利用模型变体</strong>：为同一模型配置不同参数，避免为不同任务启用多个模型。</li></ul><h3 id="2-通用最佳实践"><a href="#2-通用最佳实践" class="headerlink" title="2. 通用最佳实践"></a>2. 通用最佳实践</h3><ol><li><strong>从全局默认开始</strong>：先配置一个通用的全局默认模型，再针对特殊 Agent 进行覆盖。</li><li><strong>生产环境用白名单</strong>：通过 <code>whitelist</code> 限制可用模型，防止误用昂贵或实验性模型。</li><li><strong>按功能选模型</strong>：优先匹配任务所需功能（如工具调用、多模态），而非盲目选择“最先进”的模型。</li><li><strong>配置提供商冗余</strong>：同时启用多个功能类似的提供商，确保服务可用性，支持故障切换。</li><li><strong>监控 Token 成本</strong>：利用系统的成本跟踪功能，优化模型选择以匹配预算。</li></ol><h1 id="自定义提供商集成与身份验证"><a href="#自定义提供商集成与身份验证" class="headerlink" title="自定义提供商集成与身份验证"></a>自定义提供商集成与身份验证</h1><p>扩展 OpenCode 提供商系统的核心是<strong>利用分层架构、自定义加载器和插件化认证</strong>，既支持简单的自定义提供商配置，又能满足复杂 OAuth 流程等高级用例，同时遵循系统的配置优先级和状态管理逻辑。</p><h2 id="一、提供商架构核心与三种认证模式"><a href="#一、提供商架构核心与三种认证模式" class="headerlink" title="一、提供商架构核心与三种认证模式"></a>一、提供商架构核心与三种认证模式</h2><h3 id="1-架构分层概述"><a href="#1-架构分层概述" class="headerlink" title="1. 架构分层概述"></a>1. 架构分层概述</h3><p>OpenCode 提供商系统采用三层集成模式，覆盖从简单到复杂的所有扩展场景：</p><ol><li>内置 SDK：面向主流提供商，直接捆绑成熟 SDK 无需额外开发</li><li>自定义加载器：面向具有特殊行为（如区域感知、双 API 切换）的提供商，提供初始化逻辑扩展</li><li>插件化认证：面向复杂 OAuth 流程，支持自定义 UI 交互和外部依赖，实现全生命周期认证管理</li></ol><p>来源：provider.ts, plugin/index.ts, config.ts</p><h3 id="2-三种核心认证模式（适配不同场景）"><a href="#2-三种核心认证模式（适配不同场景）" class="headerlink" title="2. 三种核心认证模式（适配不同场景）"></a>2. 三种核心认证模式（适配不同场景）</h3><h4 id="（1）API-Key-认证：简单高效（静态令牌场景）"><a href="#（1）API-Key-认证：简单高效（静态令牌场景）" class="headerlink" title="（1）API Key 认证：简单高效（静态令牌场景）"></a>（1）API Key 认证：简单高效（静态令牌场景）</h4><p>这是最基础的认证方式，将 API 密钥存储在安全 JSON 文件中，适用于支持静态令牌的提供商，操作简单且无需复杂流程。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 配置 API Key 认证</span></span><br><span class="line"><span class="keyword">await</span> Auth.set(<span class="string">&quot;my-provider&quot;</span>, &#123;</span><br><span class="line">  type: <span class="string">&quot;api&quot;</span>,</span><br><span class="line">  key: <span class="string">&quot;sk-proj-abc123...&quot;</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><ul><li>存储位置：受限权限的安全 JSON 文件，避免明文暴露在项目配置中</li><li>适用场景：个人开发、无需令牌刷新的小型提供商</li><li>核心优势：配置简单，无需额外依赖，快速集成</li></ul><p>来源：auth.ts, provider/auth.ts</p><h4 id="（2）OAuth-2-0-流程：复杂企业场景（支持令牌生命周期管理）"><a href="#（2）OAuth-2-0-流程：复杂企业场景（支持令牌生命周期管理）" class="headerlink" title="（2）OAuth 2.0 流程：复杂企业场景（支持令牌生命周期管理）"></a>（2）OAuth 2.0 流程：复杂企业场景（支持令牌生命周期管理）</h4><p>支持<strong>授权码流程</strong>和<strong>基于 PKCE 的流程</strong>，处理从授权 URL 生成到令牌自动刷新的完整生命周期，适用于企业级提供商（如 GitHub Copilot、Google Vertex）。</p><ul><li>核心能力：<ol><li>生成个性化授权 URL，引导用户完成认证</li><li>处理回调请求，提取授权码并交换访问令牌</li><li>存储访问令牌、刷新令牌和过期时间戳</li><li>令牌过期前自动刷新，维持认证有效性</li></ol></li><li>适用场景：需要用户身份关联、支持令牌刷新的大型提供商/企业内部服务</li></ul><p>来源：provider/auth.ts, auth.ts</p><h4 id="（3）Well-Known-配置：企业集中化部署（远程配置分发）"><a href="#（3）Well-Known-配置：企业集中化部署（远程配置分发）" class="headerlink" title="（3）Well-Known 配置：企业集中化部署（远程配置分发）"></a>（3）Well-Known 配置：企业集中化部署（远程配置分发）</h4><p>遵循 <code>.well-known/opencode</code> 规范，从远程企业端点获取集中化配置，适用于多团队、多部署实例的企业场景。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;type&quot;</span>: <span class="string">&quot;wellknown&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;key&quot;</span>: <span class="string">&quot;https://enterprise.internal&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;token&quot;</span>: <span class="string">&quot;service-account-token&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>核心价值：<ol><li>集中管理提供商配置，无需逐个实例修改</li><li>统一分发凭据和端点设置，简化团队部署</li><li>支持企业级权限管控，批量更新配置</li></ol></li><li>适用场景：大型企业、多项目部署、需要标准化配置的团队</li></ul><p>来源：auth.ts, config.ts</p><h2 id="二、自定义提供商配置：无代码基础集成"><a href="#二、自定义提供商配置：无代码基础集成" class="headerlink" title="二、自定义提供商配置：无代码基础集成"></a>二、自定义提供商配置：无代码基础集成</h2><h3 id="1-核心配置架构（opencode-json-中实现）"><a href="#1-核心配置架构（opencode-json-中实现）" class="headerlink" title="1. 核心配置架构（opencode.json 中实现）"></a>1. 核心配置架构（opencode.json 中实现）</h3><p>通过 <code>provider</code> 节点配置自定义提供商，支持端点覆盖、模型黑白名单、元数据定义等全量自定义，配置示例如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;custom-provider&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;Custom AI Provider&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;env&quot;</span>: [<span class="string">&quot;CUSTOM_API_KEY&quot;</span>], <span class="comment">// 扫描的环境变量列表</span></span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;apiKey&quot;</span>: <span class="string">&quot;sk-...&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;baseURL&quot;</span>: <span class="string">&quot;https://api.custom.com/v1&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;timeout&quot;</span>: <span class="number">300000</span>,</span><br><span class="line">        <span class="attr">&quot;setCacheKey&quot;</span>: <span class="literal">false</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">&quot;whitelist&quot;</span>: [<span class="string">&quot;model-id-1&quot;</span>, <span class="string">&quot;model-id-2&quot;</span>], <span class="comment">// 模型白名单</span></span><br><span class="line">      <span class="attr">&quot;blacklist&quot;</span>: [<span class="string">&quot;deprecated-model&quot;</span>], <span class="comment">// 模型黑名单</span></span><br><span class="line">      <span class="attr">&quot;models&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;custom-model-v1&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;custom-model-v1&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;Custom Model V1&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;cost&quot;</span>: &#123; <span class="comment">// 成本配置（每100万Token）</span></span><br><span class="line">            <span class="attr">&quot;input&quot;</span>: <span class="number">0.001</span>,</span><br><span class="line">            <span class="attr">&quot;output&quot;</span>: <span class="number">0.002</span>,</span><br><span class="line">            <span class="attr">&quot;cache&quot;</span>: &#123; <span class="attr">&quot;read&quot;</span>: <span class="number">0.0001</span>, <span class="attr">&quot;write&quot;</span>: <span class="number">0.0001</span> &#125;</span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="attr">&quot;limit&quot;</span>: &#123; <span class="comment">// 限制配置</span></span><br><span class="line">            <span class="attr">&quot;context&quot;</span>: <span class="number">128000</span>, <span class="comment">// 最大上下文窗口</span></span><br><span class="line">            <span class="attr">&quot;output&quot;</span>: <span class="number">4096</span> <span class="comment">// 最大输出Token数</span></span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="attr">&quot;capabilities&quot;</span>: &#123; <span class="comment">// 功能支持配置</span></span><br><span class="line">            <span class="attr">&quot;temperature&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">            <span class="attr">&quot;reasoning&quot;</span>: <span class="literal">false</span>,</span><br><span class="line">            <span class="attr">&quot;toolcall&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">            <span class="attr">&quot;attachment&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">            <span class="attr">&quot;input&quot;</span>: &#123; <span class="attr">&quot;text&quot;</span>: <span class="literal">true</span>, <span class="attr">&quot;image&quot;</span>: <span class="literal">true</span> &#125;,</span><br><span class="line">            <span class="attr">&quot;output&quot;</span>: &#123; <span class="attr">&quot;text&quot;</span>: <span class="literal">true</span> &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>关键说明：模型的 <code>cost</code>、<code>limit</code>、<code>capabilities</code> 是必填元数据，用于系统的智能选择和成本跟踪。</li></ul><p>来源：config.ts, provider.ts</p><h3 id="2-严格的配置优先级层次（从低到高）"><a href="#2-严格的配置优先级层次（从低到高）" class="headerlink" title="2. 严格的配置优先级层次（从低到高）"></a>2. 严格的配置优先级层次（从低到高）</h3><p>配置系统支持分层覆盖，低优先级配置会被高优先级配置覆盖，数组字段（如插件、指令）会拼接而非替换。</p><div class="table-container"><table><thead><tr><th>优先级</th><th>配置来源</th><th>描述</th></tr></thead><tbody><tr><td>1（最低）</td><td>Remote Well-Known</td><td>企业级默认配置，统一分发</td></tr><tr><td>2</td><td>Global Config</td><td>全局用户配置（<code>~/.opencode/config.json</code>）</td></tr><tr><td>3</td><td>Custom Config Path</td><td>自定义路径配置（<code>OPENCODE_CONFIG</code> 标志指定）</td></tr><tr><td>4</td><td>Project Config</td><td>项目级配置（项目目录 <code>opencode.json</code>）</td></tr><tr><td>5（最高）</td><td>Inline Config</td><td>内联配置（<code>OPENCODE_CONFIG_CONTENT</code> 环境变量）</td></tr></tbody></table></div><ul><li>核心优势：兼顾企业标准化和项目个性化，既可以统一分发基础配置，又能在项目中灵活调整。</li></ul><p>来源：config.ts</p><h2 id="三、自定义加载器实现：高级初始化逻辑扩展"><a href="#三、自定义加载器实现：高级初始化逻辑扩展" class="headerlink" title="三、自定义加载器实现：高级初始化逻辑扩展"></a>三、自定义加载器实现：高级初始化逻辑扩展</h2><p>对于需要特殊行为的提供商（如区域感知、双 API 切换），可通过自定义加载器扩展初始化逻辑，自定义加载器是系统的核心扩展点。</p><h3 id="1-核心接口定义"><a href="#1-核心接口定义" class="headerlink" title="1. 核心接口定义"></a>1. 核心接口定义</h3><p>自定义加载器包含两个核心接口，分别负责模型加载和提供商整体初始化：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 自定义模型加载接口（处理单个模型的加载逻辑）</span></span><br><span class="line"><span class="keyword">type</span> CustomModelLoader = (</span><br><span class="line">  sdk: <span class="built_in">any</span>,</span><br><span class="line">  modelID: <span class="built_in">string</span>,</span><br><span class="line">  options?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;</span><br><span class="line">) =&gt; <span class="built_in">Promise</span>&lt;<span class="built_in">any</span>&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 自定义提供商加载器接口（处理整体初始化逻辑）</span></span><br><span class="line"><span class="keyword">type</span> CustomLoader = (</span><br><span class="line">  provider: Info</span><br><span class="line">) =&gt; <span class="built_in">Promise</span>&lt;&#123;</span><br><span class="line">  autoload: <span class="built_in">boolean</span> <span class="comment">// 是否自动加载该提供商</span></span><br><span class="line">  getModel?: CustomModelLoader <span class="comment">// 可选：覆盖默认模型加载逻辑</span></span><br><span class="line">  options?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt; <span class="comment">// 可选：提供商额外配置选项</span></span><br><span class="line">&#125;&gt;</span><br></pre></td></tr></table></figure><p>来源：provider.ts</p><h3 id="2-基础自定义加载器实现"><a href="#2-基础自定义加载器实现" class="headerlink" title="2. 基础自定义加载器实现"></a>2. 基础自定义加载器实现</h3><p>自定义加载器在 <code>CUSTOM_LOADERS</code> 对象中注册，处理提供商专属需求（如自定义请求头、模型 ID 转换、凭据校验），示例如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> CUSTOM_LOADERS: Record&lt;<span class="built_in">string</span>, CustomLoader&gt; = &#123;</span><br><span class="line">  <span class="string">&quot;my-custom-provider&quot;</span>: <span class="keyword">async</span> (provider) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 1. 校验凭据是否存在（扫描配置的环境变量）</span></span><br><span class="line">    <span class="keyword">const</span> env = Env.all()</span><br><span class="line">    <span class="keyword">const</span> hasKey = provider.env.some(<span class="function">(<span class="params">item</span>) =&gt;</span> env[item])</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 根据认证状态过滤模型（无凭据时移除付费模型）</span></span><br><span class="line">    <span class="keyword">if</span> (!hasKey) &#123;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">const</span> [key, value] <span class="keyword">of</span> <span class="built_in">Object</span>.entries(provider.models)) &#123;</span><br><span class="line">        <span class="keyword">if</span> (value.cost.input &gt; <span class="number">0</span>) &#123;</span><br><span class="line">          <span class="keyword">delete</span> provider.models[key]</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 返回加载配置</span></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      autoload: <span class="built_in">Object</span>.keys(provider.models).length &gt; <span class="number">0</span>, <span class="comment">// 有可用模型则自动加载</span></span><br><span class="line">      options: &#123; <span class="comment">// 自定义请求头等配置</span></span><br><span class="line">        headers: &#123;</span><br><span class="line">          <span class="string">&quot;X-Custom-Header&quot;</span>: <span class="string">&quot;custom-value&quot;</span>,</span><br><span class="line">          <span class="string">&quot;User-Agent&quot;</span>: <span class="string">&quot;opencode-integration&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="comment">// 4. 覆盖默认模型加载逻辑（转换模型ID格式）</span></span><br><span class="line">      <span class="keyword">async</span> <span class="function"><span class="title">getModel</span>(<span class="params">sdk: <span class="built_in">any</span>, modelID: <span class="built_in">string</span>, options?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;</span>)</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> transformedID = modelID.replace(<span class="string">&quot;-&quot;</span>, <span class="string">&quot;_&quot;</span>) <span class="comment">// 模型ID格式转换</span></span><br><span class="line">        <span class="keyword">return</span> sdk.languageModel(transformedID)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：provider.ts</p><h3 id="3-高级自定义加载器模式"><a href="#3-高级自定义加载器模式" class="headerlink" title="3. 高级自定义加载器模式"></a>3. 高级自定义加载器模式</h3><h4 id="（1）区域感知模型加载（适配多区域部署）"><a href="#（1）区域感知模型加载（适配多区域部署）" class="headerlink" title="（1）区域感知模型加载（适配多区域部署）"></a>（1）区域感知模型加载（适配多区域部署）</h4><p>针对需要区域前缀的提供商（如 Amazon Bedrock），动态根据部署区域添加模型 ID 前缀，示例如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;region-aware-provider&quot;</span>: <span class="keyword">async</span> (input) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> config = <span class="keyword">await</span> Config.get()</span><br><span class="line">  <span class="comment">// 从配置中获取区域，默认 us-east-1</span></span><br><span class="line">  <span class="keyword">const</span> region = config.provider?.[<span class="string">&quot;region-aware-provider&quot;</span>]?.options?.region || <span class="string">&quot;us-east-1&quot;</span></span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    autoload: <span class="literal">true</span>,</span><br><span class="line">    options: &#123; region &#125;,</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">getModel</span>(<span class="params">sdk: <span class="built_in">any</span>, modelID: <span class="built_in">string</span>, options?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;</span>)</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> providerRegion = options?.region || region</span><br><span class="line">      <span class="keyword">const</span> regionPrefix = providerRegion.split(<span class="string">&quot;-&quot;</span>)[<span class="number">0</span>]</span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 为 Claude 模型添加区域前缀</span></span><br><span class="line">      <span class="keyword">if</span> (modelID.includes(<span class="string">&quot;claude&quot;</span>) &amp;&amp; regionPrefix === <span class="string">&quot;us&quot;</span>) &#123;</span><br><span class="line">        modelID = <span class="string">`<span class="subst">$&#123;regionPrefix&#125;</span>.<span class="subst">$&#123;modelID&#125;</span>`</span></span><br><span class="line">      &#125;</span><br><span class="line">      </span><br><span class="line">      <span class="keyword">return</span> sdk.languageModel(modelID)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="（2）双-API-动态切换（Response-API-Chat-API）"><a href="#（2）双-API-动态切换（Response-API-Chat-API）" class="headerlink" title="（2）双 API 动态切换（Response API / Chat API）"></a>（2）双 API 动态切换（Response API / Chat API）</h4><p>针对支持多种接口的提供商，根据模型特征动态选择合适的 API（如代码模型用 Response API，对话模型用 Chat API）：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;dual-api-provider&quot;</span>: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    autoload: <span class="literal">false</span>,</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">getModel</span>(<span class="params">sdk: <span class="built_in">any</span>, modelID: <span class="built_in">string</span>, _options?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;</span>)</span> &#123;</span><br><span class="line">      <span class="comment">// 代码模型（含 codex）使用 Response API</span></span><br><span class="line">      <span class="keyword">if</span> (modelID.includes(<span class="string">&quot;codex&quot;</span>)) &#123;</span><br><span class="line">        <span class="keyword">return</span> sdk.responses(modelID)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// 其他模型使用 Chat API</span></span><br><span class="line">      <span class="keyword">return</span> sdk.chat(modelID)</span><br><span class="line">    &#125;,</span><br><span class="line">    options: &#123;&#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：provider.ts</p><h2 id="四、基于插件的认证：复杂-OAuth-流程实现"><a href="#四、基于插件的认证：复杂-OAuth-流程实现" class="headerlink" title="四、基于插件的认证：复杂 OAuth 流程实现"></a>四、基于插件的认证：复杂 OAuth 流程实现</h2><p>对于需要外部依赖或自定义 UI 交互的复杂认证流程，通过插件系统实现，插件提供完整的 <code>auth</code> 钩子，管理 OAuth 全生命周期。</p><h3 id="1-核心插件结构"><a href="#1-核心插件结构" class="headerlink" title="1. 核心插件结构"></a>1. 核心插件结构</h3><p>插件导出一个函数，返回包含 <code>auth</code> 钩子的对象，专门处理特定提供商的认证逻辑：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; Plugin &#125; <span class="keyword">from</span> <span class="string">&quot;@opencode-ai/plugin&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">MyPlugin</span>(<span class="params">input: Plugin.PluginInput</span>): <span class="title">Plugin</span>.<span class="title">Hooks</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">auth</span>(<span class="params">providerID</span>)</span> &#123;</span><br><span class="line">      <span class="comment">// 仅处理目标提供商</span></span><br><span class="line">      <span class="keyword">if</span> (providerID !== <span class="string">&quot;my-provider&quot;</span>) <span class="keyword">return</span> <span class="literal">undefined</span></span><br><span class="line">      </span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        provider: <span class="string">&quot;my-provider&quot;</span>,</span><br><span class="line">        methods: [</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="keyword">type</span>: <span class="string">&quot;oauth&quot;</span>,</span><br><span class="line">            label: <span class="string">&quot;Sign in with MyProvider&quot;</span>,</span><br><span class="line">            <span class="comment">// 初始化 OAuth 授权流程</span></span><br><span class="line">            <span class="keyword">async</span> authorize(): <span class="built_in">Promise</span>&lt;Plugin.AuthOuathResult&gt; &#123;</span><br><span class="line">              <span class="keyword">return</span> &#123;</span><br><span class="line">                url: <span class="string">&quot;https://auth.myprovider.com/authorize&quot;</span>, <span class="comment">// 授权 URL</span></span><br><span class="line">                method: <span class="string">&quot;code&quot;</span>, <span class="comment">// 授权模式（授权码）</span></span><br><span class="line">                instructions: <span class="string">&quot;Complete authorization in your browser&quot;</span>,</span><br><span class="line">                <span class="comment">// 处理回调，交换令牌</span></span><br><span class="line">                callback: <span class="keyword">async</span> (code?: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">                  <span class="keyword">const</span> response = <span class="keyword">await</span> fetch(<span class="string">&quot;https://auth.myprovider.com/token&quot;</span>, &#123;</span><br><span class="line">                    method: <span class="string">&quot;POST&quot;</span>,</span><br><span class="line">                    body: <span class="built_in">JSON</span>.stringify(&#123; code, <span class="attr">client_id</span>: <span class="string">&quot;your-client-id&quot;</span> &#125;)</span><br><span class="line">                  &#125;)</span><br><span class="line">                  <span class="keyword">const</span> tokens = <span class="keyword">await</span> response.json()</span><br><span class="line">                  </span><br><span class="line">                  <span class="keyword">return</span> &#123;</span><br><span class="line">                    <span class="keyword">type</span>: <span class="string">&quot;success&quot;</span>,</span><br><span class="line">                    access: tokens.access_token,</span><br><span class="line">                    refresh: tokens.refresh_token,</span><br><span class="line">                    expires: <span class="built_in">Date</span>.now() + (tokens.expires_in * <span class="number">1000</span>)</span><br><span class="line">                  &#125;</span><br><span class="line">                &#125;</span><br><span class="line">              &#125;</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        ]</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-插件安装与加载"><a href="#2-插件安装与加载" class="headerlink" title="2. 插件安装与加载"></a>2. 插件安装与加载</h3><p>插件可通过 NPM 包或本地文件加载，在 <code>opencode.json</code> 中配置即可：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;plugin&quot;</span>: [</span><br><span class="line">    <span class="string">&quot;@scope/my-auth-plugin@1.0.0&quot;</span>, <span class="comment">// NPM 包插件</span></span><br><span class="line">    <span class="string">&quot;file:///path/to/local/plugin.js&quot;</span> <span class="comment">// 本地文件插件</span></span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>系统会自动处理插件去重和加载顺序，数组字段拼接确保多个插件的 auth 钩子都能生效。</li></ul><p>来源：plugin/index.ts, config.ts</p><h2 id="五、高级配置：模型变体与超时处理"><a href="#五、高级配置：模型变体与超时处理" class="headerlink" title="五、高级配置：模型变体与超时处理"></a>五、高级配置：模型变体与超时处理</h2><h3 id="1-模型变体配置（同一模型多套参数）"><a href="#1-模型变体配置（同一模型多套参数）" class="headerlink" title="1. 模型变体配置（同一模型多套参数）"></a>1. 模型变体配置（同一模型多套参数）</h3><p>为同一基础模型定义多个变体，适配不同任务场景（如快速迭代、深度分析），配置示例：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;custom-provider&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;models&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;base-model&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;variants&quot;</span>: &#123;</span><br><span class="line">            <span class="attr">&quot;fast&quot;</span>: &#123; <span class="comment">// 快速变体：低成本、小窗口</span></span><br><span class="line">              <span class="attr">&quot;cost&quot;</span>: &#123; <span class="attr">&quot;input&quot;</span>: <span class="number">0.001</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">0.001</span> &#125;,</span><br><span class="line">              <span class="attr">&quot;limit&quot;</span>: &#123; <span class="attr">&quot;context&quot;</span>: <span class="number">64000</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">2048</span> &#125;,</span><br><span class="line">              <span class="attr">&quot;options&quot;</span>: &#123; <span class="attr">&quot;max_tokens&quot;</span>: <span class="number">2048</span> &#125;</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="attr">&quot;default&quot;</span>: &#123; <span class="comment">// 默认变体：平衡成本与性能</span></span><br><span class="line">              <span class="attr">&quot;cost&quot;</span>: &#123; <span class="attr">&quot;input&quot;</span>: <span class="number">0.002</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">0.004</span> &#125;,</span><br><span class="line">              <span class="attr">&quot;limit&quot;</span>: &#123; <span class="attr">&quot;context&quot;</span>: <span class="number">128000</span>, <span class="attr">&quot;output&quot;</span>: <span class="number">4096</span> &#125;</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-自定义超时与-Fetch-实现"><a href="#2-自定义超时与-Fetch-实现" class="headerlink" title="2. 自定义超时与 Fetch 实现"></a>2. 自定义超时与 Fetch 实现</h3><p>为每个提供商配置专属超时时间，支持自定义 Fetch 逻辑（如重试、中断控制）：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;provider&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;slow-provider&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;timeout&quot;</span>: <span class="number">600000</span>, <span class="comment">// 10分钟超时</span></span><br><span class="line">        &quot;fetch&quot;: async (input, init) =&gt; &#123;</span><br><span class="line">          const controller = new AbortController()</span><br><span class="line">          <span class="comment">// 自定义超时中断</span></span><br><span class="line">          setTimeout(() =&gt; controller.abort(), 600000)</span><br><span class="line">          return fetch(input, &#123; ...init, signal: controller.signal &#125;)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：config.ts, provider.ts</p><h2 id="六、测试与故障排除"><a href="#六、测试与故障排除" class="headerlink" title="六、测试与故障排除"></a>六、测试与故障排除</h2><h3 id="1-推荐测试策略"><a href="#1-推荐测试策略" class="headerlink" title="1. 推荐测试策略"></a>1. 推荐测试策略</h3><p>开发自定义提供商或认证流程时，需覆盖以下核心场景：</p><ol><li>单元测试：验证自定义加载器的自动加载逻辑、模型 ID 转换是否正确</li><li>集成测试：模拟 OAuth 授权服务器，测试完整的令牌交换流程</li><li>配置测试：验证配置优先级、数组拼接是否符合预期</li><li>SDK 测试：测试动态 NPM 包安装、SDK 实例化是否成功</li><li>错误测试：验证缺失凭据、无效配置时的错误提示是否清晰</li></ol><p>来源：provider/auth.ts</p><h3 id="2-常见问题排查"><a href="#2-常见问题排查" class="headerlink" title="2. 常见问题排查"></a>2. 常见问题排查</h3><h4 id="（1）提供商未加载"><a href="#（1）提供商未加载" class="headerlink" title="（1）提供商未加载"></a>（1）提供商未加载</h4><p>核心排查点：</p><ul><li>提供商未被列入 <code>disabled_providers</code> 列表</li><li>配置中的提供商 ID 与自定义加载器注册的 ID 完全匹配</li><li>凭据可用（环境变量、API Key 或 OAuth 令牌已配置）</li><li>自定义加载器返回 <code>autoload: true</code></li><li>模型元数据（cost、limit、capabilities）完整有效</li></ul><h4 id="（2）OAuth-回调失败"><a href="#（2）OAuth-回调失败" class="headerlink" title="（2）OAuth 回调失败"></a>（2）OAuth 回调失败</h4><p>常见错误及解决方案：</p><ul><li><code>OauthMissing</code>：回调前未启动授权流程，需先调用 <code>authorize()</code></li><li><code>OauthCodeMissing</code>：授权码流程未传递 code，需确认回调 URL 正确返回 code</li><li><code>OauthCallbackFailed</code>：令牌交换失败，需检查 client_id、client_secret 配置，验证授权服务器响应</li></ul><h4 id="（3）模型未找到错误"><a href="#（3）模型未找到错误" class="headerlink" title="（3）模型未找到错误"></a>（3）模型未找到错误</h4><p>核心排查点：</p><ul><li>模型 ID 无拼写错误（系统会提供模糊匹配建议）</li><li>模型已列入提供商的 <code>whitelist</code>（未被 <code>blacklist</code> 排除）</li><li>模型元数据已正确定义在 <code>provider.models</code> 中</li></ul><p>来源：provider.ts, provider/auth.ts</p><h2 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h2><ol><li>扩展 OpenCode 提供商的核心是<strong>分层配置 + 自定义加载器 + 插件化认证</strong>，兼顾简单性与灵活性。</li><li>三种认证模式分别适配静态令牌、企业级 OAuth、集中化部署场景，覆盖全量使用需求。</li><li>自定义加载器是处理特殊逻辑的关键，支持区域感知、双 API 切换等高级功能。</li><li>插件系统实现复杂 OAuth 流程，无需修改核心代码即可扩展认证能力。</li><li>配置优先级、模型变体、超时处理等高级功能，确保自定义提供商的稳定性与可用性。</li></ol>]]></content>
    
    
    <summary type="html">提供商架构：多提供商支持模型
OpenCode 的提供商架构通过统一可扩展接口实现了多 AI 提供商的无缝集成，既抽象了不同提供商的底层复杂性，又保留了自定义配置和提供商专属优化的灵活性，核心支撑了超过 19 个内置提供商的快速接入与自定义提供商的扩展能力。

一、架构核心概述
提供商系统采用分层架构设计，将「配置管理、身份验证、模型发现、运行时执行」四大核心能力整合到内聚框架中，具备两大关键特性：

 1. 内置丰富支持：直接捆绑 SDK 集成 19+ 主流 AI 提供商，消除常见用例的依赖管理成本
 2. 无限扩展能力：通过插件系统支持自定义提供商加载，无需修改核心代码即可扩展新能力
 3</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着OpenCode学智能体设计和开发3：会话管理系统</title>
    <link href="http://qixinbo.github.io/2026/01/18/opencode-3/"/>
    <id>http://qixinbo.github.io/2026/01/18/opencode-3/</id>
    <published>2026-01-18T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="会话生命周期：创建、压缩与持久化"><a href="#会话生命周期：创建、压缩与持久化" class="headerlink" title="会话生命周期：创建、压缩与持久化"></a>会话生命周期：创建、压缩与持久化</h1><p>OpenCode 中的会话生命周期负责编排用户与 AI Agent 之间的对话上下文的创建、维护和演进。这个综合系统通过层级关系管理会话初始化，实施智能压缩以保持上下文效率，并提供带有自动迁移功能的强大持久化机制。</p><h2 id="会话创建与层级结构"><a href="#会话创建与层级结构" class="headerlink" title="会话创建与层级结构"></a>会话创建与层级结构</h2><p>会话创建会建立一个带有元数据跟踪、可选父子关系和可配置自动共享的对话上下文。该系统支持独立会话以及从现有对话派生出的分支会话。</p><p>核心创建过程通过 <code>Session.create()</code> 进行，该方法会委托给 <code>Session.createNext()</code>：</p><p>来源：packages/opencode/src/session/index.ts</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">createNext</span>(<span class="params">input: &#123;</span></span></span><br><span class="line"><span class="function"><span class="params">  id?: <span class="built_in">string</span></span></span></span><br><span class="line"><span class="function"><span class="params">  title?: <span class="built_in">string</span></span></span></span><br><span class="line"><span class="function"><span class="params">  parentID?: <span class="built_in">string</span></span></span></span><br><span class="line"><span class="function"><span class="params">  directory: <span class="built_in">string</span></span></span></span><br><span class="line"><span class="function"><span class="params">  permission?: PermissionNext.Ruleset</span></span></span><br><span class="line"><span class="function"><span class="params">&#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> result: Info = &#123;</span><br><span class="line">    id: Identifier.descending(<span class="string">&quot;session&quot;</span>, input.id),</span><br><span class="line">    version: Installation.VERSION,</span><br><span class="line">    projectID: Instance.project.id,</span><br><span class="line">    directory: input.directory,</span><br><span class="line">    parentID: input.parentID,</span><br><span class="line">    title: input.title ?? createDefaultTitle(!!input.parentID),</span><br><span class="line">    permission: input.permission,</span><br><span class="line">    time: &#123;</span><br><span class="line">      created: <span class="built_in">Date</span>.now(),</span><br><span class="line">      updated: <span class="built_in">Date</span>.now(),</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>会话会被分配唯一的降序标识符，并自动接收时间戳。标题生成功能区分父会话 (“New session - “) 和子会话 (“Child session - “)，并附上 ISO 时间戳后缀。</p><p>来源：packages/opencode/src/session/index.ts</p><p>创建后，会话通过存储层进行持久化，并发布到事件总线以进行实时更新：</p><p>来源：packages/opencode/src/session/index.ts</p><p>当通过 <code>Flag.OPENCODE_AUTO_SHARE</code> 或 <code>Config.share === &quot;auto&quot;</code> 进行配置时，父会话会自动启动自动共享：</p><p>来源：packages/opencode/src/session/index.ts</p><h3 id="会话分支"><a href="#会话分支" class="headerlink" title="会话分支"></a>会话分支</h3><p><code>Session.fork()</code> 函数通过将现有会话中的消息和部分克隆到指定的消息边界来创建子会话。这使得能够在保留共享上下文的同时实现对话分支：</p><p>来源：packages/opencode/src/session/index.ts</p><p>分支通过将原始消息 ID 映射到新的升序标识符来维护消息谱系，确保部分在克隆的会话中引用正确的父消息。</p><h2 id="会话信息模式"><a href="#会话信息模式" class="headerlink" title="会话信息模式"></a>会话信息模式</h2><p>每个会话维护一个全面的 <code>Info</code> 结构：</p><p>来源：packages/opencode/src/session/index.ts</p><div class="table-container"><table><thead><tr><th>字段</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>id</td><td>string</td><td>唯一会话标识符</td></tr><tr><td>projectID</td><td>string</td><td>关联的项目标识符</td></tr><tr><td>directory</td><td>string</td><td>工作目录路径</td></tr><tr><td>parentID</td><td>string</td><td>分支会话的可选父会话</td></tr><tr><td>summary</td><td>object</td><td>聚合统计信息（新增、删除、文件、差异）</td></tr><tr><td>share</td><td>object</td><td>可选的共享 URL 和密钥</td></tr><tr><td>title</td><td>string</td><td>会话显示标题</td></tr><tr><td>version</td><td>string</td><td>OpenCode 安装版本</td></tr><tr><td>time</td><td>object</td><td>时间戳（创建、更新、压缩、归档）</td></tr><tr><td>permission</td><td>object</td><td>会话的权限规则集</td></tr><tr><td>revert</td><td>object</td><td>回滚状态（messageID、partID、快照、差异）</td></tr></tbody></table></div><h2 id="存储架构"><a href="#存储架构" class="headerlink" title="存储架构"></a>存储架构</h2><p>持久化层使用基于文件的 JSON 存储系统，具有项目级隔离和自动迁移支持。所有会话数据都驻留在全局数据目录下的有组织的子目录中。</p><h3 id="存储操作"><a href="#存储操作" class="headerlink" title="存储操作"></a>存储操作</h3><p><code>Storage</code> 命名空间提供带有文件锁定和自动错误处理的原子读/写/更新操作：</p><p>来源：packages/opencode/src/storage/storage.ts</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">write</span>&lt;<span class="title">T</span>&gt;(<span class="params">key: <span class="built_in">string</span>[], content: T</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> dir = <span class="keyword">await</span> state().then(<span class="function">(<span class="params">x</span>) =&gt;</span> x.dir)</span><br><span class="line">  <span class="keyword">const</span> target = path.join(dir, ...key) + <span class="string">&quot;.json&quot;</span></span><br><span class="line">  <span class="keyword">return</span> withErrorHandling(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    using _ = <span class="keyword">await</span> Lock.write(target)</span><br><span class="line">    <span class="keyword">await</span> Bun.write(target, <span class="built_in">JSON</span>.stringify(content, <span class="literal">null</span>, <span class="number">2</span>))</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>存储键遵循层级模式：</p><ul><li>会话：<code>[&quot;session&quot;, projectID, sessionID]</code></li><li>消息：<code>[&quot;message&quot;, sessionID, messageID]</code></li><li>部分：<code>[&quot;part&quot;, messageID, partID]</code></li><li>会话差异：<code>[&quot;session_diff&quot;, sessionID]</code></li><li>共享信息：<code>[&quot;share&quot;, sessionID]</code></li></ul><h3 id="文件锁定"><a href="#文件锁定" class="headerlink" title="文件锁定"></a>文件锁定</h3><p>所有写操作通过 <code>Lock.write()</code> 获取排他锁，而读操作通过 <code>Lock.read()</code> 使用共享锁。这可以防止并发修改，并确保多个 OpenCode 进程之间的数据一致性。</p><p>来源：packages/opencode/src/storage/storage.ts</p><h3 id="迁移系统"><a href="#迁移系统" class="headerlink" title="迁移系统"></a>迁移系统</h3><p>存储层通过版本化函数实现自动迁移：</p><p>来源：packages/opencode/src/storage/storage.ts</p><p>迁移处理遗留数据格式转换，包括：</p><ul><li>项目 ID 从基于目录到基于 Git commit 的标识迁移</li><li>用于差异摘要分离的会话结构更新</li><li>消息和部分位置重组</li></ul><p>迁移状态保存在 <code>migration</code> 文件中，跟踪已应用的迁移。系统仅在初始化时运行待处理的迁移。</p><h2 id="会话压缩"><a href="#会话压缩" class="headerlink" title="会话压缩"></a>会话压缩</h2><p>压缩通过移除过期的工具输出并生成摘要提示词以继续对话，来管理上下文窗口限制。这可以防止上下文溢出，同时为正在进行的工作保留必要信息。</p><h3 id="溢出检测"><a href="#溢出检测" class="headerlink" title="溢出检测"></a>溢出检测</h3><p><code>SessionCompaction.isOverflow()</code> 函数评估当前 Token 使用量是否超过可用上下文窗口：</p><p>来源：packages/opencode/src/session/compaction.ts</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">isOverflow</span>(<span class="params">input: &#123; tokens: MessageV2.Assistant[<span class="string">&quot;tokens&quot;</span>]; model: Provider.Model &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> config = <span class="keyword">await</span> Config.get()</span><br><span class="line">  <span class="keyword">if</span> (config.compaction?.auto === <span class="literal">false</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">  <span class="keyword">const</span> context = input.model.limit.context</span><br><span class="line">  <span class="keyword">if</span> (context === <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">  <span class="keyword">const</span> count = input.tokens.input + input.tokens.cache.read + input.tokens.output</span><br><span class="line">  <span class="keyword">const</span> output = <span class="built_in">Math</span>.min(input.model.limit.output, SessionPrompt.OUTPUT_TOKEN_MAX) || SessionPrompt.OUTPUT_TOKEN_MAX</span><br><span class="line">  <span class="keyword">const</span> usable = context - output</span><br><span class="line">  <span class="keyword">return</span> count &gt; usable</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该计算为最大输出 Token 预留空间，并与模型的上下文限制进行比较。可以通过 <code>config.compaction.auto</code> 或 <code>Flag.OPENCODE_DISABLE_AUTOCOMPACT</code> 标志禁用自动压缩。</p><p>来源：packages/opencode/src/config/config.ts</p><h3 id="工具输出修剪"><a href="#工具输出修剪" class="headerlink" title="工具输出修剪"></a>工具输出修剪</h3><p><code>SessionCompaction.prune()</code> 函数移除旧的工具结果，同时保护最近的上下文：</p><p>来源：packages/opencode/src/session/compaction.ts</p><div class="table-container"><table><thead><tr><th>阈值</th><th>Tokens</th><th>用途</th></tr></thead><tbody><tr><td>PRUNE_MINIMUM</td><td>20,000</td><td>尝试修剪的最小 Token 数</td></tr><tr><td>PRUNE_PROTECT</td><td>40,000</td><td>最近上下文的保护区</td></tr></tbody></table></div><p>修剪按相反的时间顺序遍历消息，跳过：</p><ol><li>最后两次对话轮次</li><li>来自受保护工具类型（目前为 “skill”）的工具输出</li><li>已压缩的输出</li><li>摘要标记之后的消息</li></ol><p>当工具输出被修剪时，会设置其 <code>time.compacted</code> 时间戳，将其标记为从未来的上下文窗口中排除。</p><p>被修剪的工具输出仍持久存储在存储中，并设置了 <code>time.compacted</code>，允许在需要时进行恢复。压缩提示词会明确提到工具输出已被清除。</p><h3 id="压缩处理"><a href="#压缩处理" class="headerlink" title="压缩处理"></a>压缩处理</h3><p><code>SessionCompaction.process()</code> 函数在需要压缩时生成继续对话的提示词：</p><p>来源：packages/opencode/src/session/compaction.ts</p><p>该过程：</p><ol><li>检索触发溢出的用户消息</li><li>以 “compaction” 模式创建 <code>Assistant</code> 消息</li><li>使用默认提示词调用 LLM：“Provide a detailed prompt for continuing our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we’re doing, which files we’re working on, and what we’re going to do next considering new session will not have access to our conversation.”</li><li>如果配置了，可选择插入合成的“如果你有下一步，请继续”用户消息</li><li>发布 <code>SessionCompaction.Event.Compacted</code> 事件</li></ol><p>插件可以通过 <code>experimental.session.compacting</code> 钩子注入自定义上下文或替换压缩提示词。</p><p>来源：packages/opencode/src/session/compaction.ts</p><h3 id="SessionProcessor-集成"><a href="#SessionProcessor-集成" class="headerlink" title="SessionProcessor 集成"></a>SessionProcessor 集成</h3><p>会话处理器将压缩检查集成到主处理循环中：</p><p>来源：packages/opencode/src/session/processor.ts</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="keyword">await</span> SessionCompaction.isOverflow(&#123; <span class="attr">tokens</span>: usage.tokens, <span class="attr">model</span>: input.model &#125;)) &#123;</span><br><span class="line">  needsCompaction = <span class="literal">true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当设置 <code>needsCompaction</code> 时，处理器在完成当前步骤后返回 “compact”，在继续之前触发压缩工作流。</p><p>来源：packages/opencode/src/session/processor.ts</p><h2 id="消息生命周期和持久化"><a href="#消息生命周期和持久化" class="headerlink" title="消息生命周期和持久化"></a>消息生命周期和持久化</h2><p>消息及其组成部分存储为单独的 JSON 文件，从而实现对话历史的高效加载和流式传输。</p><h3 id="消息结构"><a href="#消息结构" class="headerlink" title="消息结构"></a>消息结构</h3><p>消息遵循 V2 格式，并针对用户和 Assistant 角色使用可区分联合：</p><p>来源：packages/opencode/src/session/message-v2.ts</p><h4 id="用户消息包含"><a href="#用户消息包含" class="headerlink" title="用户消息包含"></a>用户消息包含</h4><ul><li>角色：”user”</li><li>创建时间</li><li>可选摘要（标题、正文、差异）</li><li>Agent 标识符</li><li>模型选择（providerID、modelID）</li><li>可选的系统提示词覆盖</li><li>可选的已启用工具映射</li><li>可选的变体标识符</li></ul><h4 id="Assistant-消息包含"><a href="#Assistant-消息包含" class="headerlink" title="Assistant 消息包含"></a>Assistant 消息包含</h4><ul><li>角色：”assistant”</li><li>创建和完成时间（可选）</li><li>可选的错误信息</li><li>父消息 ID</li><li>模型和提供者标识符</li><li>Agent 标识符</li><li>工作路径（cwd、root）</li><li>摘要标记</li><li>总成本和 Token 使用量</li><li>可选的完成原因</li></ul><h3 id="消息部分"><a href="#消息部分" class="headerlink" title="消息部分"></a>消息部分</h3><p>每条消息包含多个代表不同内容类型的部分：</p><p>来源：packages/opencode/src/session/message-v2.ts</p><div class="table-container"><table><thead><tr><th>部分类型</th><th>用途</th></tr></thead><tbody><tr><td>TextPart</td><td>用户或 Assistant 文本内容</td></tr><tr><td>ReasoningPart</td><td>LLM 推理链</td></tr><tr><td>ToolPart</td><td>带有状态的工具调用记录</td></tr><tr><td>FilePart</td><td>带有元数据的文件附件</td></tr><tr><td>SnapshotPart</td><td>项目快照引用</td></tr><tr><td>PatchPart</td><td>文件修改跟踪</td></tr><tr><td>AgentPart</td><td>Agent 特定信息</td></tr><tr><td>SubtaskPart</td><td>子任务委托记录</td></tr><tr><td>RetryPart</td><td>重试尝试信息</td></tr><tr><td>CompactionPart</td><td>压缩触发标记</td></tr><tr><td>StepStartPart</td><td>步骤执行开始标记</td></tr><tr><td>StepFinishPart</td><td>带有指标的步骤完成</td></tr></tbody></table></div><p>工具部分维护状态转换：<code>pending → running → completed</code> 或 <code>error</code>。完成的工具部分跟踪计时信息和可选的压缩时间戳。</p><p>来源：packages/opencode/src/session/message-v2.ts</p><h3 id="消息流式传输"><a href="#消息流式传输" class="headerlink" title="消息流式传输"></a>消息流式传输</h3><p><code>MessageV2.stream()</code> 函数提供一个异步生成器，用于按时间顺序读取消息：</p><p>来源：packages/opencode/src/session/message-v2.ts</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> stream = fn(Identifier.schema(<span class="string">&quot;session&quot;</span>), <span class="keyword">async</span> <span class="function"><span class="keyword">function</span>* (<span class="params">sessionID</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> list = <span class="keyword">await</span> <span class="built_in">Array</span>.fromAsync(<span class="keyword">await</span> Storage.list([<span class="string">&quot;message&quot;</span>, sessionID]))</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = list.length - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">    <span class="keyword">yield</span> <span class="keyword">await</span> get(&#123;</span><br><span class="line">      sessionID,</span><br><span class="line">      messageID: list[i][<span class="number">2</span>],</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>该函数检索按存储字母顺序排序的消息 ID，然后以相反顺序生成以产生时间序列。</p><h3 id="FilterCompacted"><a href="#FilterCompacted" class="headerlink" title="FilterCompacted"></a>FilterCompacted</h3><p><code>MessageV2.filterCompacted()</code> 函数过滤消息以排除最后一个压缩点之前的历史记录：</p><p>来源：packages/opencode/src/session/message-v2.ts</p><p>这通过查找带有 <code>summary: true</code> 的 <code>Assistant</code> 消息和随后包含 <code>CompactionPart</code> 的用户消息来识别边界。</p><h2 id="会话删除和清理"><a href="#会话删除和清理" class="headerlink" title="会话删除和清理"></a>会话删除和清理</h2><p>会话删除会级联处理所有相关数据，包括子会话、共享、消息和部分：</p><p>来源：packages/opencode/src/session/index.ts</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> remove = fn(Identifier.schema(<span class="string">&quot;session&quot;</span>), <span class="keyword">async</span> (sessionID) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> project = Instance.project</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> session = <span class="keyword">await</span> get(sessionID)</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> child <span class="keyword">of</span> <span class="keyword">await</span> children(sessionID)) &#123;</span><br><span class="line">      <span class="keyword">await</span> remove(child.id)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">await</span> unshare(sessionID).catch(<span class="function">() =&gt;</span> &#123;&#125;)</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> msg <span class="keyword">of</span> <span class="keyword">await</span> Storage.list([<span class="string">&quot;message&quot;</span>, sessionID])) &#123;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">const</span> part <span class="keyword">of</span> <span class="keyword">await</span> Storage.list([<span class="string">&quot;part&quot;</span>, msg.at(-<span class="number">1</span>)!])) &#123;</span><br><span class="line">        <span class="keyword">await</span> Storage.remove(part)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">await</span> Storage.remove(msg)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">await</span> Storage.remove([<span class="string">&quot;session&quot;</span>, project.id, sessionID])</span><br><span class="line">    Bus.publish(Event.Deleted, &#123;</span><br><span class="line">      info: session,</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">    log.error(e)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>删除过程：</p><ol><li>递归删除所有子会话</li><li>删除共享元数据（静默忽略错误）</li><li>遍历所有消息及其部分</li><li>删除消息和部分 JSON 文件</li><li>删除会话元数据文件</li><li>发布删除事件</li></ol><h2 id="配置和控制"><a href="#配置和控制" class="headerlink" title="配置和控制"></a>配置和控制</h2><p>会话生命周期行为可通过多个层进行配置：</p><h3 id="配置优先级"><a href="#配置优先级" class="headerlink" title="配置优先级"></a>配置优先级</h3><p>设置按优先级递增的顺序从多个来源合并：</p><p>来源：packages/opencode/src/config/config.ts</p><ol><li>远程/知名配置（组织默认值）</li><li>全局用户配置 (~/.opencode/opencode.json)</li><li>自定义配置路径 (Flag.OPENCODE_CONFIG)</li><li>项目配置（从目录向上搜索的 opencode.json 或 opencode.jsonc）</li><li>内联配置内容 (Flag.OPENCODE_CONFIG_CONTENT)</li></ol><h3 id="压缩设置"><a href="#压缩设置" class="headerlink" title="压缩设置"></a>压缩设置</h3><p>压缩行为的配置选项：</p><div class="table-container"><table><thead><tr><th>设置</th><th>类型</th><th>默认值</th><th>描述</th></tr></thead><tbody><tr><td>compaction.auto</td><td>boolean</td><td>true</td><td>在溢出时启用自动压缩</td></tr><tr><td>compaction.prune</td><td>boolean</td><td>true</td><td>启用工具输出修剪</td></tr><tr><td>share</td><td>string/boolean</td><td>-</td><td>自动共享配置（”auto”、”disabled”或 true 以保持旧版兼容性）</td></tr></tbody></table></div><p>标志覆盖配置：</p><ul><li><code>Flag.OPENCODE_DISABLE_AUTOCOMPACT</code>：禁用自动压缩</li><li><code>Flag.OPENCODE_DISABLE_PRUNE</code>：禁用工具输出修剪</li><li><code>Flag.OPENCODE_AUTO_SHARE</code>：强制新会话自动共享</li></ul><p>来源：packages/opencode/src/config/config.ts</p><h2 id="事件总线集成"><a href="#事件总线集成" class="headerlink" title="事件总线集成"></a>事件总线集成</h2><p>会话生命周期事件发布到全局事件总线，以实现实时 UI 更新和插件集成：</p><p>来源：packages/opencode/src/session/index.ts</p><div class="table-container"><table><thead><tr><th>事件</th><th>负载</th><th>触发器</th></tr></thead><tbody><tr><td>session.created</td><td>{ info: Session.Info }</td><td>创建新会话</td></tr><tr><td>session.updated</td><td>{ info: Session.Info }</td><td>修改会话元数据</td></tr><tr><td>session.deleted</td><td>{ info: Session.Info }</td><td>删除会话</td></tr><tr><td>session.diff</td><td>{ sessionID, diff: Snapshot.FileDiff[] }</td><td>计算文件差异</td></tr><tr><td>session.compacted</td><td>{ sessionID }</td><td>压缩完成</td></tr><tr><td>session.error</td><td>{ sessionID?, error }</td><td>发生错误</td></tr></tbody></table></div><p>消息和部分事件：</p><ul><li><code>message.updated</code>：消息元数据已更改</li><li><code>message.removed</code>：消息已删除</li><li><code>message.part.updated</code>：部分已添加或修改（带有可选增量）</li><li><code>message.part.removed</code>：部分已删除</li></ul><p>来源：packages/opencode/src/session/message-v2.ts</p><hr><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><ol><li>会话生命周期核心是<strong>层级化创建、智能压缩、持久化存储</strong>三大模块，支撑对话上下文的高效管理。</li><li>存储采用<strong>基于文件的 JSON 格式</strong>，配合文件锁保证数据一致性，自动迁移处理遗留格式。</li><li>会话压缩通过<strong>溢出检测、工具输出修剪、摘要生成</strong>，在防止上下文溢出的同时保留关键工作信息。</li><li>消息以 V2 格式存储，通过<strong>多部分结构</strong>支持丰富内容类型，流式传输实现高效历史加载。</li><li>配置支持多来源优先级合并，事件总线提供实时集成能力，删除操作实现级联清理保证数据整洁。</li></ol><h1 id="消息处理：V2-消息格式与处理机制"><a href="#消息处理：V2-消息格式与处理机制" class="headerlink" title="消息处理：V2 消息格式与处理机制"></a>消息处理：V2 消息格式与处理机制</h1><p>V2 消息格式是 OpenCode 管理对话状态、工具交互和 Agent 响应的核心架构演进，它提供了一个精细、类型安全的多部分消息表示体系，具备流式传输、持久化存储和复杂错误处理的完整能力，为对话系统的高效运行奠定了基础。</p><h2 id="一、V2-消息架构概述"><a href="#一、V2-消息架构概述" class="headerlink" title="一、V2 消息架构概述"></a>一、V2 消息架构概述</h2><p>V2 消息格式采用<strong>基于部分的架构</strong>，每条消息由「标准化元数据」和「有序的类型化部分集合」组成。这种设计实现了不同内容类型的独立实时流式传输，支持丰富的工具交互，同时能对执行状态进行精确跟踪。</p><p>核心类型定义在 <code>packages/opencode/src/session/message-v2.ts</code> 中，通过<strong>可辨识联合实现编译时类型安全</strong>，并借助 Zod schemas 完成运行时数据验证，兼顾开发效率和运行稳定性。</p><p>来源：packages/opencode/src/session/message-v2.ts</p><h2 id="二、消息元数据结构"><a href="#二、消息元数据结构" class="headerlink" title="二、消息元数据结构"></a>二、消息元数据结构</h2><p>V2 格式中每条消息都携带标准化元数据，用于跟踪执行上下文、计时信息和资源利用率，且针对「用户消息」和「Assistant 消息」设计了差异化的元数据结构。</p><h3 id="1-用户消息元数据"><a href="#1-用户消息元数据" class="headerlink" title="1. 用户消息元数据"></a>1. 用户消息元数据</h3><p>用户消息捕获每个对话轮次的启动上下文，核心字段包括：</p><ul><li>角色标识：固定为 <code>&quot;user&quot;</code>，用于区分消息发起方</li><li>时间跟踪：创建时间戳，用于消息排序和对话分析</li><li>Agent 分配：指定处理该消息的目标 Agent</li><li>模型配置：LLM 提供者（如 anthropic）和模型 ID（如 claude-sonnet-4-20250514），支持可选变体</li><li>系统指令：针对该特定轮次的可选系统级提示词覆盖</li><li>工具权限：精细的工具启用/禁用标志，控制 Agent 可调用的工具范围</li><li>摘要集成：可选的差异数据和正文内容，用于保证上下文连续性</li></ul><p>来源：packages/opencode/src/session/message-v2.ts</p><h3 id="2-Assistant-消息元数据"><a href="#2-Assistant-消息元数据" class="headerlink" title="2. Assistant 消息元数据"></a>2. Assistant 消息元数据</h3><p>Assistant 消息跟踪执行结果和资源消耗，核心字段包括：</p><ul><li>父引用：链接到对应的响应用户消息，维护对话上下文关联</li><li>模型信息：实际用于生成响应的 LLM 提供者和模型（可能与用户配置有差异）</li><li>Agent 身份：确认实际处理请求的 Agent 实例</li><li>路径上下文：当前工作目录（cwd）和项目根目录，记录执行环境</li><li>资源指标：Token 计数、缓存使用量、推理 Token 消耗，用于成本和性能统计</li><li>成本跟踪：响应生成的累计成本</li><li>错误状态：包含重试元数据的全面错误信息，支持故障排查</li><li>完成状态：响应完成时的时间戳</li><li>结束原因：LLM 提供者停止生成响应的原因（如正常完成、Token 耗尽）</li></ul><p>来源：packages/opencode/src/session/message-v2.ts</p><h2 id="三、消息部分系统"><a href="#三、消息部分系统" class="headerlink" title="三、消息部分系统"></a>三、消息部分系统</h2><p>部分系统是 V2 消息格式的核心，为表示不同类型的内容和操作提供了丰富、可扩展的词汇表。每个部分包含基本标识符和类型特定数据，主要分为三大类：内容部分、工具执行部分、控制和元数据部分。</p><h3 id="1-内容部分"><a href="#1-内容部分" class="headerlink" title="1. 内容部分"></a>1. 内容部分</h3><p>用于承载对话中的核心内容数据，支持流式传输和元数据跟踪。</p><ul><li><p><strong>TextPart</strong>：表示叙述性内容（用户输入、Agent 最终回复）</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="keyword">type</span>: <span class="string">&quot;text&quot;</span>,</span><br><span class="line">  text: <span class="built_in">string</span>,</span><br><span class="line">  synthetic?: <span class="built_in">boolean</span>,      <span class="comment">// 标记是否为系统生成（非用户手动提供）</span></span><br><span class="line">  ignored?: <span class="built_in">boolean</span>,         <span class="comment">// 标记是否从 LLM 上下文中排除</span></span><br><span class="line">  time?: &#123; <span class="attr">start</span>: <span class="built_in">number</span>, end?: <span class="built_in">number</span> &#125;, <span class="comment">// 流式传输计时</span></span><br><span class="line">  metadata?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p><strong>ReasoningPart</strong>：捕获 LLM 的推理链，仅用于具备思考能力的模型，方便调试和理解 Agent 决策过程</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="keyword">type</span>: <span class="string">&quot;reasoning&quot;</span>,</span><br><span class="line">  text: <span class="built_in">string</span>,</span><br><span class="line">  time: &#123; <span class="attr">start</span>: <span class="built_in">number</span>, end?: <span class="built_in">number</span> &#125;,</span><br><span class="line">  metadata?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p><strong>FilePart</strong>：封装二进制和多文件内容，支持 MIME 类型分类，关联文件源信息</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="keyword">type</span>: <span class="string">&quot;file&quot;</span>,</span><br><span class="line">  mime: <span class="built_in">string</span>,</span><br><span class="line">  filename?: <span class="built_in">string</span>,</span><br><span class="line">  url: <span class="built_in">string</span>,</span><br><span class="line">  source?: &#123;</span><br><span class="line">    <span class="keyword">type</span>: <span class="string">&quot;file&quot;</span> | <span class="string">&quot;symbol&quot;</span> | <span class="string">&quot;resource&quot;</span>,</span><br><span class="line">    path?: <span class="built_in">string</span>,</span><br><span class="line">    text: &#123; <span class="attr">value</span>: <span class="built_in">string</span>, <span class="attr">start</span>: <span class="built_in">number</span>, <span class="attr">end</span>: <span class="built_in">number</span> &#125;,</span><br><span class="line">    range?: LSP.Range,      <span class="comment">// 针对符号源的位置信息</span></span><br><span class="line">    name?: <span class="built_in">string</span>,          <span class="comment">// 针对符号源的名称</span></span><br><span class="line">    kind?: <span class="built_in">number</span>,          <span class="comment">// LSP 符号类型</span></span><br><span class="line">    clientName?: <span class="built_in">string</span>,    <span class="comment">// 针对 MCP 资源</span></span><br><span class="line">    uri?: <span class="built_in">string</span>            <span class="comment">// 针对 MCP 资源</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><p>来源：packages/opencode/src/session/message-v2.ts</p><h3 id="2-工具执行部分（ToolPart）"><a href="#2-工具执行部分（ToolPart）" class="headerlink" title="2. 工具执行部分（ToolPart）"></a>2. 工具执行部分（ToolPart）</h3><p>跟踪完整的工具执行生命周期，维护 LLM 工具调用 ID 与内部部分 ID 的双向映射，支持四种状态流转，是 Agent 与外部工具交互的核心载体。</p><div class="table-container"><table><thead><tr><th>状态</th><th>目的</th><th>关键字段</th></tr></thead><tbody><tr><td>pending</td><td>工具已调用，正在解析输入</td><td>input, raw</td></tr><tr><td>running</td><td>工具正在执行中</td><td>input, time.start, metadata</td></tr><tr><td>completed</td><td>工具成功完成</td><td>output, title, attachments, time</td></tr><tr><td>error</td><td>工具执行失败</td><td>error, metadata</td></tr></tbody></table></div><p>核心结构定义：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="keyword">type</span>: <span class="string">&quot;tool&quot;</span>,</span><br><span class="line">  callID: <span class="built_in">string</span>,            <span class="comment">// LLM 的工具调用标识符</span></span><br><span class="line">  tool: <span class="built_in">string</span>,              <span class="comment">// 工具名称（如 bash、read）</span></span><br><span class="line">  state: ToolState,          <span class="comment">// 四种生命周期状态之一</span></span><br><span class="line">  metadata?: Record&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>处理器在消息生成期间，会在运行时映射中跟踪活动的工具调用，实现流式更新与最终存储的同步。</p><p>来源：packages/opencode/src/session/message-v2.ts</p><h3 id="3-控制和元数据部分"><a href="#3-控制和元数据部分" class="headerlink" title="3. 控制和元数据部分"></a>3. 控制和元数据部分</h3><p>用于标记对话流程中的关键节点、记录系统状态，支撑压缩、回滚、分步执行等高级功能：</p><ul><li><strong>StepStartPart/StepFinishPart</strong>：标记 Assistant 消息中的离散推理步骤，实现步骤间快照跟踪</li><li><strong>SnapshotPart</strong>：记录特定时间点的文件系统状态，支持对话回滚和文件差异跟踪</li><li><strong>PatchPart</strong>：表示一组文件修改，基于哈希实现版本控制</li><li><strong>CompactionPart</strong>：标记由上下文压缩产生的消息，区分原始对话和压缩对话</li><li><strong>SubtaskPart</strong>：表示委托给其他 Agent 或工具系统的子工作任务</li><li><strong>RetryPart</strong>：记录失败操作的重试尝试，包含详细错误信息</li></ul><p>来源：packages/opencode/src/session/message-v2.ts</p><h2 id="四、存储架构"><a href="#四、存储架构" class="headerlink" title="四、存储架构"></a>四、存储架构</h2><p>V2 消息实现<strong>分层存储模型</strong>，将消息元数据与部分数据分离存储，针对不同访问模式进行优化，提升数据读写效率。</p><h3 id="1-存储层次结构"><a href="#1-存储层次结构" class="headerlink" title="1. 存储层次结构"></a>1. 存储层次结构</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">storage&#x2F;</span><br><span class="line">├── session&#x2F;&#123;projectID&#125;&#x2F;&#123;sessionID&#125;.json  # 会话元数据</span><br><span class="line">├── message&#x2F;&#123;sessionID&#125;&#x2F;&#123;messageID&#125;.json  # 仅消息元数据（无部分内容）</span><br><span class="line">└── part&#x2F;&#123;messageID&#125;&#x2F;&#123;partID&#125;.json        # 单个消息部分的独立数据</span><br></pre></td></tr></table></figure><p>这种分离存储带来四大优势：</p><ol><li>高效流式传输：部分可以独立写入和读取，无需等待整个消息完成</li><li>定向查询：访问消息元数据时无需加载所有部分，提升查询速度</li><li>选择性更新：修改单个部分仅需重写对应文件，无需重写整个消息</li><li>空间效率：删除部分仅需删除单个文件，清理更轻便，无冗余数据</li></ol><p>来源：packages/opencode/src/storage/storage.ts</p><h3 id="2-消息检索模式"><a href="#2-消息检索模式" class="headerlink" title="2. 消息检索模式"></a>2. 消息检索模式</h3><p>存储系统支持两种核心检索模式：流式传输所有消息、定向检索特定消息的所有部分，且保证返回结果的有序性。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 模式1：按逆时间顺序流式传输某个会话的所有消息</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> stream = fn(Identifier.schema(<span class="string">&quot;session&quot;</span>), <span class="keyword">async</span> <span class="function"><span class="keyword">function</span>* (<span class="params">sessionID</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> list = <span class="keyword">await</span> <span class="built_in">Array</span>.fromAsync(<span class="keyword">await</span> Storage.list([<span class="string">&quot;message&quot;</span>, sessionID]))</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = list.length - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">    <span class="keyword">yield</span> <span class="keyword">await</span> get(&#123;</span><br><span class="line">      sessionID,</span><br><span class="line">      messageID: list[i][<span class="number">2</span>],</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 模式2：检索特定消息的所有部分，并按创建顺序排序</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> parts = fn(Identifier.schema(<span class="string">&quot;message&quot;</span>), <span class="keyword">async</span> (messageID) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> result = [] <span class="keyword">as</span> MessageV2.Part[]</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> item <span class="keyword">of</span> <span class="keyword">await</span> Storage.list([<span class="string">&quot;part&quot;</span>, messageID])) &#123;</span><br><span class="line">    <span class="keyword">const</span> read = <span class="keyword">await</span> Storage.read&lt;MessageV2.Part&gt;(item)</span><br><span class="line">    result.push(read)</span><br><span class="line">  &#125;</span><br><span class="line">  result.sort(<span class="function">(<span class="params">a, b</span>) =&gt;</span> (a.id &gt; b.id ? <span class="number">1</span> : -<span class="number">1</span>))</span><br><span class="line">  <span class="keyword">return</span> result</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/session/message-v2.ts</p><h2 id="五、消息处理流水线"><a href="#五、消息处理流水线" class="headerlink" title="五、消息处理流水线"></a>五、消息处理流水线</h2><p>处理器负责协调 Assistant 消息生成的完整生命周期，处理来自 LLM 提供者的流式响应，管理工具执行，同时实现异常检测和容错。</p><h3 id="1-流式事件处理"><a href="#1-流式事件处理" class="headerlink" title="1. 流式事件处理"></a>1. 流式事件处理</h3><p>处理器为每个 LLM 流式事件配置专用处理程序，实现「事件触发-业务处理-存储更新」的闭环，保证消息状态与执行进度同步。</p><div class="table-container"><table><thead><tr><th>事件类型</th><th>处理程序操作</th><th>存储操作</th></tr></thead><tbody><tr><td>start</td><td>将会话状态设置为忙碌</td><td>无</td></tr><tr><td>reasoning-start</td><td>创建推理部分</td><td>写入新部分</td></tr><tr><td>reasoning-delta</td><td>追加到推理文本</td><td>使用增量更新部分</td></tr><tr><td>reasoning-end</td><td>完成推理元数据</td><td>更新部分</td></tr><tr><td>tool-input-start</td><td>创建待处理工具部分</td><td>写入新部分</td></tr><tr><td>tool-call</td><td>转换工具状态为运行中</td><td>更新部分状态</td></tr><tr><td>tool-result</td><td>标记工具状态为已完成</td><td>使用输出更新部分</td></tr><tr><td>tool-error</td><td>标记工具状态为错误</td><td>使用错误更新部分</td></tr><tr><td>text-start</td><td>创建文本部分</td><td>写入新部分</td></tr><tr><td>text-delta</td><td>追加文本内容</td><td>使用增量更新部分</td></tr><tr><td>text-end</td><td>完成文本元数据</td><td>更新部分</td></tr></tbody></table></div><p>来源：packages/opencode/src/session/processor.ts</p><h3 id="2-Doom-Loop-检测"><a href="#2-Doom-Loop-检测" class="headerlink" title="2. Doom Loop 检测"></a>2. Doom Loop 检测</h3><p>处理器实现复杂的重复工具调用检测系统，防止无限循环（Doom Loop），提升系统稳定性。</p><p>核心逻辑：当同一工具使用完全相同的输入被连续调用 3 次（<code>DOOM_LOOP_THRESHOLD = 3</code>）时，系统会请求用户许可后再继续执行。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (</span><br><span class="line">  lastThree.length === DOOM_LOOP_THRESHOLD &amp;&amp;</span><br><span class="line">  lastThree.every(</span><br><span class="line">    (p) =&gt;</span><br><span class="line">      p.type === <span class="string">&quot;tool&quot;</span> &amp;&amp;</span><br><span class="line">      p.tool === value.toolName &amp;&amp;</span><br><span class="line">      p.state.status !== <span class="string">&quot;pending&quot;</span> &amp;&amp;</span><br><span class="line">      <span class="built_in">JSON</span>.stringify(p.state.input) === <span class="built_in">JSON</span>.stringify(value.input),</span><br><span class="line">  )</span><br><span class="line">) &#123;</span><br><span class="line">  <span class="keyword">await</span> PermissionNext.ask(&#123;</span><br><span class="line">    permission: <span class="string">&quot;doom_loop&quot;</span>,</span><br><span class="line">    patterns: [value.toolName],</span><br><span class="line">    sessionID: input.assistantMessage.sessionID,</span><br><span class="line">    metadata: &#123; <span class="attr">tool</span>: value.toolName, <span class="attr">input</span>: value.input &#125;,</span><br><span class="line">    always: [value.toolName],</span><br><span class="line">    ruleset: agent.permission,</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/session/processor.ts</p><h3 id="3-模型消息转换"><a href="#3-模型消息转换" class="headerlink" title="3. 模型消息转换"></a>3. 模型消息转换</h3><p><code>toModelMessage</code> 函数将 V2 消息转换为 LLM 提供者期望的格式，同时应用一系列过滤和转换规则，保证 LLM 输入的有效性和简洁性：</p><ul><li>过滤空消息（无部分的消息）</li><li>排除标记为 <code>ignored: true</code> 的文本部分</li><li>转换文件类型（纯文本/目录转为文本部分，其他 MIME 类型保留为文件部分）</li><li>注入压缩提示词、子任务提示词</li><li>处理工具附件和错误消息</li></ul><p>来源：packages/opencode/src/session/message-v2.ts</p><h2 id="六、错误处理系统"><a href="#六、错误处理系统" class="headerlink" title="六、错误处理系统"></a>六、错误处理系统</h2><p>V2 消息包含全面的错误分类和标准化处理逻辑，带有结构化元数据，方便调试和重试逻辑实现。</p><h3 id="1-核心错误类型"><a href="#1-核心错误类型" class="headerlink" title="1. 核心错误类型"></a>1. 核心错误类型</h3><div class="table-container"><table><thead><tr><th>错误类型</th><th>用例</th><th>核心元数据</th></tr></thead><tbody><tr><td>OutputLengthError</td><td>响应超过 Token 限制</td><td>无</td></tr><tr><td>AbortedError</td><td>用户中止生成响应</td><td>message</td></tr><tr><td>AuthError</td><td>提供者身份验证失败</td><td>providerID, message</td></tr><tr><td>APIError</td><td>提供者 API 故障</td><td>message, statusCode, isRetryable</td></tr><tr><td>Unknown</td><td>捕获所有其他错误</td><td>message</td></tr></tbody></table></div><h3 id="2-错误规范化转换"><a href="#2-错误规范化转换" class="headerlink" title="2. 错误规范化转换"></a>2. 错误规范化转换</h3><p><code>fromError</code> 函数将不同来源的错误规范化为 V2 错误格式，实现跨 LLM 提供者的一致错误处理：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">fromError</span>(<span class="params">e: unknown, ctx: &#123; providerID: <span class="built_in">string</span> &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">switch</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    <span class="keyword">case</span> e <span class="keyword">instanceof</span> DOMException &amp;&amp; e.name === <span class="string">&quot;AbortError&quot;</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> MessageV2.AbortedError(&#123; <span class="attr">message</span>: e.message &#125;, &#123; <span class="attr">cause</span>: e &#125;).toObject()</span><br><span class="line">      </span><br><span class="line">    <span class="keyword">case</span> LoadAPIKeyError.isInstance(e):</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> MessageV2.AuthError(&#123;</span><br><span class="line">        providerID: ctx.providerID,</span><br><span class="line">        message: e.message,</span><br><span class="line">      &#125;, &#123; <span class="attr">cause</span>: e &#125;).toObject()</span><br><span class="line">      </span><br><span class="line">    <span class="keyword">case</span> APICallError.isInstance(e):</span><br><span class="line">      <span class="keyword">const</span> message = extractErrorMessage(e)</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> MessageV2.APIError(&#123;</span><br><span class="line">        message,</span><br><span class="line">        statusCode: e.statusCode,</span><br><span class="line">        isRetryable: e.isRetryable,</span><br><span class="line">      &#125;, &#123; <span class="attr">cause</span>: e &#125;).toObject()</span><br><span class="line">      </span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> NamedError.Unknown(&#123; <span class="attr">message</span>: <span class="built_in">String</span>(e) &#125;, &#123; <span class="attr">cause</span>: e &#125;).toObject()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/session/message-v2.ts</p><h2 id="七、事件系统集成"><a href="#七、事件系统集成" class="headerlink" title="七、事件系统集成"></a>七、事件系统集成</h2><p>消息更新会发布到全局事件总线，实现实时 UI 更新和跨组件通信，提升用户体验和系统可扩展性。</p><p>核心事件类型：</p><ol><li><code>Message.Updated</code>：消息元数据更改时发布</li><li><code>Message.Removed</code>：删除消息时发布</li><li><code>Message.PartUpdated</code>：修改部分时发布（包含增量数据，提升流式更新效率）</li><li><code>Message.PartRemoved</code>：删除部分时发布</li></ol><p>其中 <code>PartUpdated</code> 中的 <code>delta</code> 字段仅发送增量更改，而非完整部分内容，大幅减少数据传输开销。</p><p>来源：packages/opencode/src/session/message-v2.ts</p><h2 id="八、压缩集成"><a href="#八、压缩集成" class="headerlink" title="八、压缩集成"></a>八、压缩集成</h2><p>V2 消息与上下文压缩系统无缝集成，用于管理长对话中的 Token 限制，防止上下文溢出。</p><ol><li><strong>溢出检测</strong>：压缩系统在每次 Assistant 响应后监控 Token 使用量，计算可用上下文空间，判断是否需要压缩</li><li><strong>压缩过程</strong>：创建标记为 <code>summary: true</code> 的特殊 Assistant 消息，通过压缩 Agent 处理对话历史，生成精简摘要，同时注入压缩标记区分原始内容和压缩内容</li><li><strong>工具结果修剪</strong>：对旧消息的工具输出标记 <code>time.compacted</code> 时间戳，用占位符替换大输出，节省 Token 同时保留执行记录</li></ol><p>来源：packages/opencode/src/session/compaction.ts</p><h2 id="九、V2-与-V1-格式核心对比"><a href="#九、V2-与-V1-格式核心对比" class="headerlink" title="九、V2 与 V1 格式核心对比"></a>九、V2 与 V1 格式核心对比</h2><p>V2 格式相对于 V1 格式实现了重大架构改进，核心差异如下：</p><div class="table-container"><table><thead><tr><th>方面</th><th>V1 格式</th><th>V2 格式</th></tr></thead><tbody><tr><td>结构</td><td>消息元数据中的平铺部分数组</td><td>分离的消息信息和部分存储</td></tr><tr><td>部分类型</td><td>仅限于文本、工具、推理</td><td>11+ 种部分类型，带有丰富元数据</td></tr><tr><td>存储</td><td>每条消息单个 JSON 文件</td><td>分层存储（消息 + N 个独立部分文件）</td></tr><tr><td>流式传输</td><td>有限的增量支持</td><td>精细的每部分独立流式传输</td></tr><tr><td>工具跟踪</td><td>简单状态机</td><td>四状态生命周期 + 完整元数据跟踪</td></tr><tr><td>错误处理</td><td>基本错误字符串</td><td>结构化错误 + 重试元数据</td></tr><tr><td>压缩支持</td><td>仅消息级别压缩</td><td>部分级别修剪 + 消息级摘要压缩</td></tr></tbody></table></div><p>来源：packages/opencode/src/session/message.ts, packages/opencode/src/session/message-v2.ts</p><h2 id="十、核心使用模式"><a href="#十、核心使用模式" class="headerlink" title="十、核心使用模式"></a>十、核心使用模式</h2><h3 id="1-创建用户消息"><a href="#1-创建用户消息" class="headerlink" title="1. 创建用户消息"></a>1. 创建用户消息</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> userMessage = <span class="keyword">await</span> Session.updateMessage(&#123;</span><br><span class="line">  id: Identifier.ascending(<span class="string">&quot;message&quot;</span>),</span><br><span class="line">  role: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">  sessionID,</span><br><span class="line">  time: &#123; <span class="attr">created</span>: <span class="built_in">Date</span>.now() &#125;,</span><br><span class="line">  agent: <span class="string">&quot;builder&quot;</span>,</span><br><span class="line">  model: &#123; <span class="attr">providerID</span>: <span class="string">&quot;anthropic&quot;</span>, <span class="attr">modelID</span>: <span class="string">&quot;claude-sonnet-4-20250514&quot;</span> &#125;,</span><br><span class="line">  tools: &#123; <span class="attr">bash</span>: <span class="literal">true</span>, <span class="attr">edit</span>: <span class="literal">true</span>, <span class="attr">read</span>: <span class="literal">true</span> &#125;,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="2-流式传输文本内容"><a href="#2-流式传输文本内容" class="headerlink" title="2. 流式传输文本内容"></a>2. 流式传输文本内容</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> currentText: MessageV2.TextPart | <span class="literal">undefined</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 文本开始事件</span></span><br><span class="line">currentText = &#123;</span><br><span class="line">  id: Identifier.ascending(<span class="string">&quot;part&quot;</span>),</span><br><span class="line">  messageID,</span><br><span class="line">  sessionID,</span><br><span class="line">  <span class="keyword">type</span>: <span class="string">&quot;text&quot;</span>,</span><br><span class="line">  text: <span class="string">&quot;&quot;</span>,</span><br><span class="line">  time: &#123; <span class="attr">start</span>: <span class="built_in">Date</span>.now() &#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 文本增量事件</span></span><br><span class="line">currentText.text += delta</span><br><span class="line"><span class="keyword">await</span> Session.updatePart(&#123; <span class="attr">part</span>: currentText, delta &#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 文本结束事件</span></span><br><span class="line">currentText.text = currentText.text.trimEnd()</span><br><span class="line"><span class="keyword">await</span> Session.updatePart(currentText)</span><br></pre></td></tr></table></figure><h3 id="3-处理工具执行"><a href="#3-处理工具执行" class="headerlink" title="3. 处理工具执行"></a>3. 处理工具执行</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 接收 LLM 工具调用，创建运行中的工具部分</span></span><br><span class="line"><span class="keyword">const</span> toolPart = <span class="keyword">await</span> Session.updatePart(&#123;</span><br><span class="line">  id: Identifier.ascending(<span class="string">&quot;part&quot;</span>),</span><br><span class="line">  messageID,</span><br><span class="line">  sessionID,</span><br><span class="line">  <span class="keyword">type</span>: <span class="string">&quot;tool&quot;</span>,</span><br><span class="line">  callID: toolCallId,</span><br><span class="line">  tool: toolName,</span><br><span class="line">  state: &#123; <span class="attr">status</span>: <span class="string">&quot;running&quot;</span>, input, <span class="attr">time</span>: &#123; <span class="attr">start</span>: <span class="built_in">Date</span>.now() &#125; &#125;,</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 工具执行完成，更新为已完成状态</span></span><br><span class="line"><span class="keyword">await</span> Session.updatePart(&#123;</span><br><span class="line">  ...toolPart,</span><br><span class="line">  state: &#123;</span><br><span class="line">    status: <span class="string">&quot;completed&quot;</span>,</span><br><span class="line">    input,</span><br><span class="line">    output,</span><br><span class="line">    title,</span><br><span class="line">    metadata,</span><br><span class="line">    time: &#123; <span class="attr">start</span>: toolPart.state.time.start, <span class="attr">end</span>: <span class="built_in">Date</span>.now() &#125;,</span><br><span class="line">    attachments,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h2 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h2><ol><li>V2 消息格式的核心是「<strong>元数据+多类型部分</strong>」的分层架构，兼顾类型安全和可扩展性。</li><li>分离存储和增量流式传输是 V2 格式的性能核心，大幅提升长对话和复杂工具交互的效率。</li><li>完整的生命周期跟踪（工具执行、步骤流转、错误处理）和压缩集成，让 V2 格式能够支撑复杂的 Agent 对话场景。</li><li>标准化的错误处理和事件系统，为系统的可维护性和跨组件集成提供了保障。</li></ol><h1 id="上下文管理：Token-限制与截断策略"><a href="#上下文管理：Token-限制与截断策略" class="headerlink" title="上下文管理：Token 限制与截断策略"></a>上下文管理：Token 限制与截断策略</h1><p>有效的上下文管理是 OpenCode 保障对话在模型 Token 限制内运行、同时保留关键信息的核心能力。该系统通过<strong>Token 估算与限制、工具输出截断、会话压缩、主动修剪</strong>四大核心策略，实现了对长对话和大输出场景的高效管控。</p><h2 id="一、Token-估算与限制"><a href="#一、Token-估算与限制" class="headerlink" title="一、Token 估算与限制"></a>一、Token 估算与限制</h2><p>由于不同 LLM 提供商的 Token 化规则存在差异，且无法通过 SDK 直接获取精确 Token 数，OpenCode 采用<strong>基于字符的近似估算方法</strong>：</p><blockquote><p><strong>核心换算标准</strong>：4 个字符 ≈ 1 个 Token</p></blockquote><h3 id="1-Token-分类跟踪"><a href="#1-Token-分类跟踪" class="headerlink" title="1. Token 分类跟踪"></a>1. Token 分类跟踪</h3><p>每条消息会在以下 5 个类别中，跟踪详细的 Token 使用情况，确保对上下文消耗的精细化管控：</p><div class="table-container"><table><thead><tr><th>Token 类别</th><th>含义</th></tr></thead><tbody><tr><td>Input tokens</td><td>发送给模型的 Token，包含提示词、历史消息、工具输入</td></tr><tr><td>Output tokens</td><td>模型生成的 Token，包含响应文本、推理内容</td></tr><tr><td>Reasoning tokens</td><td>模型用于扩展思考的 Token（仅支持该能力的模型有效）</td></tr><tr><td>Cache read/write tokens</td><td>来自提示词缓存读写操作的 Token</td></tr><tr><td>Total context</td><td>上下文总 Token 数 = Input + Cache.read + Output</td></tr></tbody></table></div><h3 id="2-超限处理"><a href="#2-超限处理" class="headerlink" title="2. 超限处理"></a>2. 超限处理</h3><p>当消息 Token 总数超过模型上下文限制时，系统会触发 <code>MessageV2.OutputLengthError</code> 错误，同时自动启动<strong>截断</strong>或<strong>压缩</strong>流程，避免对话中断。</p><h2 id="二、工具输出截断"><a href="#二、工具输出截断" class="headerlink" title="二、工具输出截断"></a>二、工具输出截断</h2><p>当工具生成大量输出（如读取大文件、批量命令执行结果）时，OpenCode 会自动触发截断机制，在保留关键上下文的同时，防止单次工具输出占用过多 Token。</p><h3 id="1-截断限制参数"><a href="#1-截断限制参数" class="headerlink" title="1. 截断限制参数"></a>1. 截断限制参数</h3><p>截断行为由两个核心参数控制，且支持自定义配置：</p><div class="table-container"><table><thead><tr><th>参数</th><th>默认值</th><th>作用</th></tr></thead><tbody><tr><td>最大行数</td><td>2,000 行</td><td>限制工具输出的最大行数</td></tr><tr><td>最大字节数</td><td>50 KB</td><td>限制工具输出的最大体积</td></tr><tr><td>截断方向</td><td>支持 <code>head</code>（从开头截断）/<code>tail</code>（从末尾截断）</td><td>灵活适配不同场景的内容保留需求</td></tr></tbody></table></div><h3 id="2-截断完整流程"><a href="#2-截断完整流程" class="headerlink" title="2. 截断完整流程"></a>2. 截断完整流程</h3><ol><li>工具执行产生大体积输出，触发截断条件；</li><li>系统将<strong>完整输出内容</strong>持久化到磁盘的 <code>tool-output</code> 目录，保留期限为 <strong>7 天</strong>；</li><li>对返回给 Agent 的内容进行截断，并附加引导性提示，分两种情况：<ul><li><strong>有 Task 工具权限</strong>：<code>使用 Task 工具让子 agent 使用 Grep 和 Read 处理此文件。不要自己读取完整文件——委托处理以保存上下文。</code></li><li><strong>无 Task 工具权限</strong>：<code>使用 Grep 搜索完整内容，或使用带 offset/limit 的 Read 查看特定部分。</code></li></ul></li></ol><p>这种分层提示的设计，既避免了当前 Agent 上下文溢出，又提供了获取完整内容的可行路径。</p><h2 id="三、会话压缩策略"><a href="#三、会话压缩策略" class="headerlink" title="三、会话压缩策略"></a>三、会话压缩策略</h2><p>会话压缩针对<strong>长时间对话</strong>的上下文膨胀问题，采用 <strong>溢出检测+自动压缩+主动修剪</strong> 三种互补策略，确保对话始终运行在模型限制内。</p><h3 id="1-溢出检测"><a href="#1-溢出检测" class="headerlink" title="1. 溢出检测"></a>1. 溢出检测</h3><p>在每次 Assistant 生成响应前，系统会通过 <code>SessionCompaction.isOverflow</code> 函数，判断当前对话是否超出可用上下文容量，核心计算逻辑如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> context = model.limit.context  <span class="comment">// 模型总上下文窗口大小</span></span><br><span class="line"><span class="keyword">const</span> output = model.limit.output || OUTPUT_TOKEN_MAX  <span class="comment">// 预留响应所需 Token</span></span><br><span class="line"><span class="keyword">const</span> usable = context - output  <span class="comment">// 实际可用于输入的 Token 空间</span></span><br><span class="line"><span class="keyword">const</span> total = tokens.input + tokens.cache.read + tokens.output  <span class="comment">// 当前已用 Token 总数</span></span><br><span class="line"><span class="keyword">const</span> isOverflow = total &gt; usable  <span class="comment">// 是否溢出</span></span><br></pre></td></tr></table></figure><p>其中 <code>OUTPUT_TOKEN_MAX</code> 默认为 <strong>32,000 Token</strong>，可通过 <code>OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX</code> 标志修改。</p><h3 id="2-自动压缩"><a href="#2-自动压缩" class="headerlink" title="2. 自动压缩"></a>2. 自动压缩</h3><p>当检测到上下文溢出时，系统会自动创建压缩任务，通过专门的「压缩 Agent」处理对话历史，核心步骤如下：</p><ol><li>在消息流中插入 <code>CompactionPart</code>，标记压缩触发点；</li><li>压缩 Agent 接收专用提示词，分析对话历史：<blockquote><p><code>提供一个详细的提示词，以继续我们上面的对话。重点关注对继续对话有帮助的信息，包括我们做了什么、我们正在做什么、我们正在处理哪些文件，以及考虑到新会话无法访问我们的对话，我们下一步打算做什么。</code></p></blockquote></li><li>生成对话核心内容的简洁摘要，识别关键上下文（编辑中的文件、当前任务、后续步骤）；</li><li>清除上一次压缩点之前的历史消息，仅保留摘要和最新对话；</li><li>对话从压缩后的摘要点恢复，实现上下文「重置」但关键信息不丢失。</li></ol><h3 id="3-主动修剪旧工具输出"><a href="#3-主动修剪旧工具输出" class="headerlink" title="3. 主动修剪旧工具输出"></a>3. 主动修剪旧工具输出</h3><p>主动修剪是在不触发全量压缩的情况下，减少 Token 消耗的轻量化策略，由 <code>SessionCompaction.prune</code> 函数执行，核心规则如下：</p><div class="table-container"><table><thead><tr><th>修剪规则</th><th>具体说明</th></tr></thead><tbody><tr><td>保护区域</td><td>最近 <strong>40,000 Token</strong> 的工具输出不会被修剪，确保当前工作的上下文完整</td></tr><tr><td>最小阈值</td><td>只有修剪操作能回收至少 <strong>20,000 Token</strong> 时，才会执行修剪，避免无效操作</td></tr><tr><td>受保护工具</td><td>特定工具（如 <code>&quot;skill&quot;</code> 类工具）的输出免于修剪</td></tr><tr><td>修剪范围</td><td>仅处理<strong>至少 2 轮对话之前</strong>的、状态为「已完成」的工具调用输出</td></tr></tbody></table></div><h4 id="修剪执行逻辑"><a href="#修剪执行逻辑" class="headerlink" title="修剪执行逻辑"></a>修剪执行逻辑</h4><ol><li>从对话末尾反向遍历消息列表；</li><li>跳过受保护区域内的消息、受保护工具的输出、已压缩的内容；</li><li>对符合条件的工具输出，设置 <code>compacted</code> 时间戳（不删除原始内容）；</li><li>在将消息转换为模型输入时，被修剪的工具输出会被替换为占位文本：<code>[Old tool result content cleared]</code>。</li></ol><blockquote><p>注意：修剪的 Token 计算基于「4 字符≈1 Token」的近似值，实际回收量可能存在小幅偏差。</p></blockquote><h2 id="四、配置与控制"><a href="#四、配置与控制" class="headerlink" title="四、配置与控制"></a>四、配置与控制</h2><p>上下文管理的行为可以通过<strong>配置文件</strong>或<strong>系统标志</strong>灵活自定义，且标志的优先级高于配置文件。</p><h3 id="1-配置文件设置（opencode-json）"><a href="#1-配置文件设置（opencode-json）" class="headerlink" title="1. 配置文件设置（opencode.json）"></a>1. 配置文件设置（opencode.json）</h3><div class="table-container"><table><thead><tr><th>设置项</th><th>类型</th><th>默认值</th><th>描述</th></tr></thead><tbody><tr><td>compaction.auto</td><td>boolean</td><td>true</td><td>上下文溢出时，是否启用自动压缩</td></tr><tr><td>compaction.prune</td><td>boolean</td><td>true</td><td>是否启用旧工具输出的主动修剪</td></tr></tbody></table></div><h4 id="配置示例"><a href="#配置示例" class="headerlink" title="配置示例"></a>配置示例</h4><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;compaction&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;auto&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;prune&quot;</span>: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-系统标志覆盖"><a href="#2-系统标志覆盖" class="headerlink" title="2. 系统标志覆盖"></a>2. 系统标志覆盖</h3><div class="table-container"><table><thead><tr><th>标志</th><th>作用</th></tr></thead><tbody><tr><td>OPENCODE_DISABLE_AUTOCOMPACT</td><td>强制禁用自动压缩，忽略 <code>compaction.auto</code> 配置</td></tr><tr><td>OPENCODE_DISABLE_PRUNE</td><td>强制禁用工具输出修剪，忽略 <code>compaction.prune</code> 配置</td></tr><tr><td>OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX</td><td>自定义最大输出 Token 限制（默认 32,000）</td></tr></tbody></table></div><h2 id="五、与会话处理的集成"><a href="#五、与会话处理的集成" class="headerlink" title="五、与会话处理的集成"></a>五、与会话处理的集成</h2><p>上下文管理功能深度嵌入 <code>SessionPrompt.loop</code> 的会话处理主循环，实现自动化、无感知的上下文管控：</p><ol><li><strong>每次 Assistant 响应完成后</strong>：处理器自动检查当前 Token 使用量是否溢出；</li><li><strong>溢出触发后</strong>：立即创建压缩任务，在下一次循环迭代中优先执行压缩；</li><li><strong>对话完成后</strong>：自动运行修剪流程，清理无效的旧工具输出；</li><li><strong>手动触发方式</strong>：可通过 <code>session_compact</code> 快捷键，或让处理器返回 <code>&quot;compact&quot;</code> 指令，手动启动压缩。</li></ol><p>这种集成方式既保证了对话的连续性，又最大限度减少了对用户工作流的干扰。</p><h2 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h2><p>OpenCode 上下文管理的核心优势在于 <strong>分层治理、智能决策</strong>：</p><ol><li>用<strong>字符近似法</strong>解决了跨平台 Token 估算的一致性问题；</li><li>用<strong>工具输出截断</strong>处理单次大输出，用<strong>会话压缩+主动修剪</strong>解决长对话膨胀，形成互补策略；</li><li>压缩和修剪过程<strong>保留完整原始数据</strong>，仅在模型输入层做精简，兼顾 Token 效率和数据可追溯性；</li><li>支持<strong>配置+标志</strong>的双层自定义，适配不同场景的需求。</li></ol><h1 id="会话回退与回滚机制"><a href="#会话回退与回滚机制" class="headerlink" title="会话回退与回滚机制"></a>会话回退与回滚机制</h1><p>OpenCode 提供的精密版本回退和回滚系统，支持撤销对话进度、恢复文件系统状态，既能实现错误恢复与实验性操作，又能保留探索替代方案的能力，其核心是<strong>两阶段运作模型</strong>与<strong>Git 快照驱动的文件恢复机制</strong>。</p><h2 id="一、回退架构核心设计"><a href="#一、回退架构核心设计" class="headerlink" title="一、回退架构核心设计"></a>一、回退架构核心设计</h2><p>回退系统采用<strong>三阶段生命周期</strong>的设计，允许用户先尝试回退操作，再决定是确认提交还是撤销回退，兼顾灵活性与安全性。</p><ol><li><strong>回退阶段</strong>：标记回滚点，恢复文件系统到目标状态，此时会话消息仍保留，操作可逆</li><li><strong>取消回退阶段</strong>：放弃回退操作，将文件系统和会话状态恢复到回退前的样子</li><li><strong>清理阶段</strong>：永久删除回退点之后的消息和部分，完成回退操作，状态不可逆</li></ol><p>这种设计的核心优势是：<strong>回退操作不立即删除数据</strong>，而是通过标记实现状态管理，给用户留出决策缓冲期。</p><h2 id="二、回退点指定规则"><a href="#二、回退点指定规则" class="headerlink" title="二、回退点指定规则"></a>二、回退点指定规则</h2><p>启动回退时，需要在对话链中指定目标点，系统支持两种指定方式，会智能识别并定位实际回退节点：</p><div class="table-container"><table><thead><tr><th>指定方式</th><th>作用范围</th><th>系统行为</th></tr></thead><tbody><tr><td>仅指定 <code>Message ID</code></td><td>整条目标消息</td><td>1. 若目标是<strong>助手消息</strong>：回退到该消息之前的最后一条用户消息<br>2. 若目标是<strong>用户消息</strong>：直接回退到该用户消息节点<br>最终确保回退到可输入新指令的用户节点</td></tr><tr><td><code>Message ID + Part ID</code></td><td>目标消息内的特定部分</td><td>回退到该部分的位置，且保留同一消息中该部分之前的有效内容（文本/工具类型部分）</td></tr></tbody></table></div><p>系统通过分析对话链的父子关系和消息类型，自动校准回退点，保证回退后的会话处于可继续操作的状态。<br>来源：revert.ts</p><h2 id="三、回滚状态模型"><a href="#三、回滚状态模型" class="headerlink" title="三、回滚状态模型"></a>三、回滚状态模型</h2><p>触发回退时，系统会在会话元数据中记录完整的回滚状态信息，用于跨操作跟踪和状态恢复，核心结构如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">revert: &#123;</span><br><span class="line">  messageID: <span class="built_in">string</span>,      <span class="comment">// 目标回退消息的 ID（必需）</span></span><br><span class="line">  partID?: <span class="built_in">string</span>,        <span class="comment">// 可选：目标部分 ID，用于细粒度回退</span></span><br><span class="line">  snapshot?: <span class="built_in">string</span>,      <span class="comment">// 文件系统状态的 Git 树哈希，用于恢复文件</span></span><br><span class="line">  diff?: <span class="built_in">string</span>           <span class="comment">// 当前状态与快照状态的差异记录</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该元数据存储在会话的 <code>Info</code> 结构中，是实现回退、取消回退、清理操作的核心依据。<br>来源：index.ts</p><h2 id="四、文件系统恢复机制"><a href="#四、文件系统恢复机制" class="headerlink" title="四、文件系统恢复机制"></a>四、文件系统恢复机制</h2><p>回退系统的文件恢复能力基于 <strong>Git 内部操作</strong>实现，通过快照跟踪和补丁回退，确保文件系统精准恢复到目标状态，且不影响无关文件。</p><h3 id="1-快照跟踪原理"><a href="#1-快照跟踪原理" class="headerlink" title="1. 快照跟踪原理"></a>1. 快照跟踪原理</h3><p>在执行文件回退前，系统会创建 Git 树快照来捕获当前文件系统状态，快照与项目主 Git 仓库完全隔离，存储在独立工作树目录中。</p><p><strong>快照创建流程</strong>：</p><ol><li>检查受管位置是否已初始化 Git 仓库，未初始化则自动初始化</li><li>将当前项目的所有文件添加到 Git 索引</li><li>创建代表当前文件系统状态的 Git 树对象</li><li>返回树哈希值，存储到回滚状态的 <code>snapshot</code> 字段中</li></ol><p>快照的核心作用是：<strong>保存文件系统的基准状态</strong>，为后续的恢复或取消回退提供数据支撑。<br>来源：snapshot/index.ts</p><h3 id="2-补丁收集与文件回退"><a href="#2-补丁收集与文件回退" class="headerlink" title="2. 补丁收集与文件回退"></a>2. 补丁收集与文件回退</h3><p>系统在回退过程中，会识别所有由 <code>edit</code>/<code>write</code>/<code>multiedit</code> 等工具执行的<strong>补丁操作</strong>（即文件修改记录），并针对性地恢复文件，具体处理逻辑分两种场景：</p><div class="table-container"><table><thead><tr><th>文件场景</th><th>执行的 Git 命令</th><th>最终结果</th></tr></thead><tbody><tr><td>文件存在于快照中</td><td><code>git checkout &lt;快照哈希&gt; -- &lt;文件路径&gt;</code></td><td>将文件内容精准恢复到快照时的状态</td></tr><tr><td>文件不存在于快照中</td><td>直接删除该文件</td><td>移除回退点之后新建的文件</td></tr></tbody></table></div><p>这种精细化处理的优势是：<strong>仅修改回退点之后变动的文件</strong>，回退点之前的无关更改会被完整保留，避免“一刀切”式的恢复。<br>来源：snapshot/index.ts, revert.ts</p><h2 id="五、回退生命周期操作详解"><a href="#五、回退生命周期操作详解" class="headerlink" title="五、回退生命周期操作详解"></a>五、回退生命周期操作详解</h2><p>回退系统的三个阶段对应三种核心操作，每个操作都有明确的执行步骤和状态影响。</p><h3 id="1-回退操作（触发回滚）"><a href="#1-回退操作（触发回滚）" class="headerlink" title="1. 回退操作（触发回滚）"></a>1. 回退操作（触发回滚）</h3><p>这是启动回退的核心操作，执行步骤如下：</p><ol><li><strong>状态验证</strong>：检查会话是否处于非繁忙状态（未处理命令/提示词），防止冲突</li><li><strong>遍历消息链</strong>：按时间顺序遍历会话消息，定位目标 <code>messageID</code>/<code>partID</code></li><li><strong>识别回退点</strong>：根据指定方式，校准到实际的回退节点（用户消息优先）</li><li><strong>收集补丁</strong>：提取回退点之后所有工具产生的文件修改补丁</li><li><strong>创建快照</strong>：若当前无回退状态，创建当前文件系统的 Git 快照；若已有快照则复用</li><li><strong>恢复文件</strong>：执行 Git checkout 操作，将文件系统恢复到快照状态</li><li><strong>存储状态</strong>：将回滚信息（messageID/partID/snapshot 等）写入会话元数据</li><li><strong>生成差异</strong>：计算当前状态与快照状态的差异，保存到 <code>diff</code> 字段</li></ol><p>来源：revert.ts</p><h3 id="2-取消回退操作（撤销回滚）"><a href="#2-取消回退操作（撤销回滚）" class="headerlink" title="2. 取消回退操作（撤销回滚）"></a>2. 取消回退操作（撤销回滚）</h3><p>当对回退结果不满意时，可执行取消回退，恢复到回退前的状态，步骤如下：</p><ol><li><strong>状态验证</strong>：确认会话非繁忙，且存在活跃的回退状态</li><li><strong>恢复快照</strong>：如果存在快照哈希，通过 <code>git read-tree</code> 和 <code>checkout</code> 恢复文件系统到回退前状态</li><li><strong>清除状态</strong>：从会话元数据中删除 <code>revert</code> 字段，重置回退状态</li></ol><h3 id="3-清理操作（完成回滚）"><a href="#3-清理操作（完成回滚）" class="headerlink" title="3. 清理操作（完成回滚）"></a>3. 清理操作（完成回滚）</h3><p>清理操作会永久删除回退点之后的消息和部分，完成不可逆的回退，步骤如下：</p><ol><li><strong>检查状态</strong>：确认存在活跃的回退状态</li><li><strong>拆分消息链</strong>：根据 <code>messageID</code> 拆分消息列表，区分回退点前后的内容</li><li><strong>删除数据</strong>：<ul><li>全消息回退：删除回退点之后的所有消息及其部分</li><li>部分回退：仅删除目标消息中指定 <code>partID</code> 之后的部分</li></ul></li><li><strong>发布事件</strong>：发送 <code>message.removed</code>/<code>message.part.removed</code> 事件，触发 UI 更新</li><li><strong>清除状态</strong>：删除会话元数据中的 <code>revert</code> 字段，完成回退</li></ol><blockquote><p><strong>关键特性</strong>：清理操作会在<strong>会话压缩前自动执行</strong>，确保压缩摘要基于干净的消息链生成。<br>来源：revert.ts</p></blockquote><h2 id="六、与会话压缩的深度集成"><a href="#六、与会话压缩的深度集成" class="headerlink" title="六、与会话压缩的深度集成"></a>六、与会话压缩的深度集成</h2><p>回退系统与上下文压缩工作流紧密联动，确保状态一致性：</p><ol><li><strong>压缩前置清理</strong>：当请求会话压缩/摘要时，系统会先自动清理所有活跃的回退状态，删除已回退的消息</li><li><strong>避免摘要污染</strong>：防止已回退的无效对话内容被纳入压缩摘要，保证摘要的准确性</li><li><strong>状态同步</strong>：压缩完成后，文件系统状态与消息历史完全匹配，无残留的回退标记</li></ol><p>这种集成让回退操作无需用户手动清理，降低了使用成本。<br>来源：server.ts, compaction.ts</p><h2 id="七、API-端点与事件系统"><a href="#七、API-端点与事件系统" class="headerlink" title="七、API 端点与事件系统"></a>七、API 端点与事件系统</h2><h3 id="1-核心-API-端点"><a href="#1-核心-API-端点" class="headerlink" title="1. 核心 API 端点"></a>1. 核心 API 端点</h3><p>系统提供三个专用 API 端点，支持程序化操作回退功能：</p><div class="table-container"><table><thead><tr><th>端点</th><th>请求方法</th><th>作用</th><th>请求体参数</th><th>响应内容</th></tr></thead><tbody><tr><td><code>/session/:sessionID/revert</code></td><td>POST</td><td>启动回退到指定消息/部分</td><td><code>messageID</code>（必需）、<code>partID</code>（可选）</td><td>更新后的会话信息（含回退状态）</td></tr><tr><td><code>/session/:sessionID/unrevert</code></td><td>POST</td><td>取消活跃的回退操作</td><td>无</td><td>清除回退状态的会话信息</td></tr><tr><td><code>/session/:sessionID/summarize</code></td><td>POST</td><td>启动会话压缩（自动清理回退）</td><td><code>providerID</code>、<code>modelID</code>、<code>auto</code>（可选）</td><td>压缩是否成功的布尔值</td></tr></tbody></table></div><p>来源：server.ts</p><h3 id="2-状态事件通知"><a href="#2-状态事件通知" class="headerlink" title="2. 状态事件通知"></a>2. 状态事件通知</h3><p>回退操作的状态变化会发布到全局事件总线，用于 UI 实时更新和跨组件通信：</p><div class="table-container"><table><thead><tr><th>事件类型</th><th>触发条件</th><th>负载内容</th></tr></thead><tbody><tr><td><code>message.removed</code></td><td>清理操作删除消息</td><td><code>&#123; sessionID, messageID &#125;</code></td></tr><tr><td><code>message.part.removed</code></td><td>清理操作删除部分</td><td><code>&#123; sessionID, messageID, partID &#125;</code></td></tr><tr><td><code>session.updated</code></td><td>回退状态被修改</td><td>更新后的完整会话信息</td></tr></tbody></table></div><p>来源：revert.ts, message-v2.ts</p><h2 id="八、配置与限制"><a href="#八、配置与限制" class="headerlink" title="八、配置与限制"></a>八、配置与限制</h2><h3 id="1-依赖与限制"><a href="#1-依赖与限制" class="headerlink" title="1. 依赖与限制"></a>1. 依赖与限制</h3><ul><li><strong>Git 依赖</strong>：回退系统需要项目目录初始化 Git 仓库，若无 Git，快照机制失效，无法执行文件回退</li><li><strong>繁忙状态保护</strong>：会话处于繁忙状态（处理命令/提示词）时，禁止执行回退/取消回退操作，防止状态冲突</li><li><strong>受保护工具</strong>：<code>skill</code> 类工具的输出在压缩修剪时不会被清除，确保重要上下文在回退/压缩后仍保留</li></ul><p>来源：compaction.ts, revert.ts</p><h3 id="2-测试覆盖"><a href="#2-测试覆盖" class="headerlink" title="2. 测试覆盖"></a>2. 测试覆盖</h3><p>系统包含全面的测试套件，覆盖核心场景：</p><ul><li>消息/部分级别的回退工作流</li><li>压缩前的自动清理集成</li><li>消息/部分移除事件触发</li><li>Git 快照恢复准确性</li><li>部分回退的边缘场景<br>来源：revert-compact.test.ts</li></ul><h2 id="九、最佳实践"><a href="#九、最佳实践" class="headerlink" title="九、最佳实践"></a>九、最佳实践</h2><h3 id="1-回退-vs-分支：场景选择"><a href="#1-回退-vs-分支：场景选择" class="headerlink" title="1. 回退 vs 分支：场景选择"></a>1. 回退 vs 分支：场景选择</h3><div class="table-container"><table><thead><tr><th>操作</th><th>适用场景</th><th>对状态的影响</th></tr></thead><tbody><tr><td><strong>回退</strong></td><td>尝试不同操作方向，需保留“反悔”余地</td><td>修改当前会话，清理前可逆，适合短期实验</td></tr><tr><td><strong>分支</strong></td><td>探索替代方案，同时保留原始对话路径</td><td>创建新的子会话，原会话状态完全不变，适合长期多方案对比</td></tr></tbody></table></div><p>简单总结：<strong>短期试错用回退，长期分支用 fork</strong>。</p><h3 id="2-回退操作时机"><a href="#2-回退操作时机" class="headerlink" title="2. 回退操作时机"></a>2. 回退操作时机</h3><ul><li>最佳时机：<strong>在执行新操作前</strong>启动回退，避免新修改与回退操作冲突</li><li>安全原则：等待当前会话的所有操作完成（非繁忙状态）后再执行回退，不强行操作</li></ul><h3 id="3-清理注意事项"><a href="#3-清理注意事项" class="headerlink" title="3. 清理注意事项"></a>3. 清理注意事项</h3><ul><li>未清理的回退会保留消息数据，频繁回退且不清理可能导致存储膨胀</li><li>压缩操作会自动触发清理，无需手动干预；若需立即释放空间，可手动调用清理 API</li><li>清理操作不可逆，执行前请确认回退结果符合预期</li></ul><p>来源：index.ts</p>]]></content>
    
    
    <summary type="html">会话生命周期：创建、压缩与持久化
OpenCode 中的会话生命周期负责编排用户与 AI Agent 之间的对话上下文的创建、维护和演进。这个综合系统通过层级关系管理会话初始化，实施智能压缩以保持上下文效率，并提供带有自动迁移功能的强大持久化机制。

会话创建与层级结构
会话创建会建立一个带有元数据跟踪、可选父子关系和可配置自动共享的对话上下文。该系统支持独立会话以及从现有对话派生出的分支会话。

核心创建过程通过 Session.create() 进行，该方法会委托给 Session.createNext()：

来源：packages/opencode/src/session/index.</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着OpenCode学智能体设计和开发2：工具系统</title>
    <link href="http://qixinbo.github.io/2026/01/17/opencode-2/"/>
    <id>http://qixinbo.github.io/2026/01/17/opencode-2/</id>
    <published>2026-01-17T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<h1 id="工具注册：内置工具与可扩展性"><a href="#工具注册：内置工具与可扩展性" class="headerlink" title="工具注册：内置工具与可扩展性"></a>工具注册：内置工具与可扩展性</h1><p>工具注册表是 OpenCode 系统中管理所有可用工具的核心组件，为内置工具和自定义工具提供了统一的接口。这种架构使 agents 能够通过一致的 API 访问多样化的功能集，同时通过插件和基于配置的工具定义支持可扩展性。</p><h2 id="工具注册表架构"><a href="#工具注册表架构" class="headerlink" title="工具注册表架构"></a>工具注册表架构</h2><p>工具注册表采用集中式类插件架构，其中工具通过元数据、功能和权限要求进行注册。注册表维护两类工具：随 OpenCode 附带的内置工具，以及可由用户或第三方插件添加的自定义工具。</p><p>内置工具静态注册在 all() 函数中，包括文件操作、搜索功能、bash 执行和 web 交互等核心功能。自定义工具在运行时初始化期间从配置目录和插件中动态发现。</p><p>注册表根据使用的 AI provider 执行过滤——某些工具如 codesearch 和 websearch 仅适用于 OpenCode provider 或通过标志显式启用时。此外，工具在提供给特定 agents 之前会根据 agent 权限进行过滤。</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">all</span>(<span class="params"></span>): <span class="title">Promise</span>&lt;<span class="title">Tool</span>.<span class="title">Info</span>[]&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> custom = <span class="keyword">await</span> state().then(<span class="function">(<span class="params">x</span>) =&gt;</span> x.custom)</span><br><span class="line">  <span class="keyword">const</span> config = <span class="keyword">await</span> Config.get()</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">return</span> [</span><br><span class="line">    InvalidTool,</span><br><span class="line">    ...(Flag.OPENCODE_CLIENT === <span class="string">&quot;cli&quot;</span> ? [QuestionTool] : []),</span><br><span class="line">    BashTool,</span><br><span class="line">    ReadTool,</span><br><span class="line">    GlobTool,</span><br><span class="line">    GrepTool,</span><br><span class="line">    EditTool,</span><br><span class="line">    WriteTool,</span><br><span class="line">    TaskTool,</span><br><span class="line">    WebFetchTool,</span><br><span class="line">    TodoWriteTool,</span><br><span class="line">    TodoReadTool,</span><br><span class="line">    WebSearchTool,</span><br><span class="line">    CodeSearchTool,</span><br><span class="line">    SkillTool,</span><br><span class="line">    ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),</span><br><span class="line">    ...(config.experimental?.batch_tool === <span class="literal">true</span> ? [BatchTool] : []),</span><br><span class="line">    ...custom,</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">ids</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> all().then(<span class="function">(<span class="params">x</span>) =&gt;</span> x.map(<span class="function">(<span class="params">t</span>) =&gt;</span> t.id))</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">tools</span>(<span class="params">providerID: <span class="built_in">string</span>, agent?: Agent.Info</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> tools = <span class="keyword">await</span> all()</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="built_in">Promise</span>.all(</span><br><span class="line">    tools</span><br><span class="line">      .filter(<span class="function">(<span class="params">t</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// Enable websearch/codesearch for zen users OR via enable flag</span></span><br><span class="line">        <span class="keyword">if</span> (t.id === <span class="string">&quot;codesearch&quot;</span> || t.id === <span class="string">&quot;websearch&quot;</span>) &#123;</span><br><span class="line">          <span class="keyword">return</span> providerID === <span class="string">&quot;opencode&quot;</span> || Flag.OPENCODE_ENABLE_EXA</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">      &#125;)</span><br><span class="line">      .map(<span class="keyword">async</span> (t) =&gt; &#123;</span><br><span class="line">        using _ = log.time(t.id)</span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">          id: t.id,</span><br><span class="line">          ...(<span class="keyword">await</span> t.init(&#123; agent &#125;)),</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;),</span><br><span class="line">  )</span><br><span class="line">  <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="工具接口结构"><a href="#工具接口结构" class="headerlink" title="工具接口结构"></a>工具接口结构</h2><p>注册表中的每个工具都实现由 Tool.Info 类型定义的一致接口。该接口确保在整个系统中对工具进行统一处理，并实现适当的验证、执行和输出管理。</p><p>Tool.define() 工厂函数使用 Zod schemas 和输出截断自动验证包装工具实现。当执行工具时，注册表根据工具的 schema 验证输入参数，执行工具逻辑，并在结果超过配置限制时应用输出截断。</p><p>来源：packages/opencode/src/tool/tool.ts, packages/opencode/src/tool/registry.ts</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">namespace</span> Tool &#123;</span><br><span class="line">  <span class="keyword">interface</span> Metadata &#123;</span><br><span class="line">    [key: <span class="built_in">string</span>]: <span class="built_in">any</span></span><br><span class="line">  &#125;</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">interface</span> InitContext &#123;</span><br><span class="line">    agent?: Agent.Info</span><br><span class="line">  &#125;</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">type</span> Context&lt;M <span class="keyword">extends</span> Metadata = Metadata&gt; = &#123;</span><br><span class="line">    sessionID: <span class="built_in">string</span></span><br><span class="line">    messageID: <span class="built_in">string</span></span><br><span class="line">    agent: <span class="built_in">string</span></span><br><span class="line">    abort: AbortSignal</span><br><span class="line">    callID?: <span class="built_in">string</span></span><br><span class="line">    extra?: &#123; [key: <span class="built_in">string</span>]: <span class="built_in">any</span> &#125;</span><br><span class="line">    metadata(input: &#123; title?: <span class="built_in">string</span>; metadata?: M &#125;): <span class="built_in">void</span></span><br><span class="line">    ask(input: Omit&lt;PermissionNext.Request, <span class="string">&quot;id&quot;</span> | <span class="string">&quot;sessionID&quot;</span> | <span class="string">&quot;tool&quot;</span>&gt;): <span class="built_in">Promise</span>&lt;<span class="built_in">void</span>&gt;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">interface</span> Info&lt;Parameters <span class="keyword">extends</span> z.ZodType = z.ZodType, M <span class="keyword">extends</span> Metadata = Metadata&gt; &#123;</span><br><span class="line">    id: <span class="built_in">string</span></span><br><span class="line">    init: <span class="function">(<span class="params">ctx?: InitContext</span>) =&gt;</span> <span class="built_in">Promise</span>&lt;&#123;</span><br><span class="line">      description: <span class="built_in">string</span></span><br><span class="line">      parameters: Parameters</span><br><span class="line">      execute(</span><br><span class="line">        args: z.infer&lt;Parameters&gt;,</span><br><span class="line">        ctx: Context,</span><br><span class="line">      ): <span class="built_in">Promise</span>&lt;&#123;</span><br><span class="line">        title: <span class="built_in">string</span></span><br><span class="line">        metadata: M</span><br><span class="line">        output: <span class="built_in">string</span></span><br><span class="line">        attachments?: MessageV2.FilePart[]</span><br><span class="line">      &#125;&gt;</span><br><span class="line">      formatValidationError?(error: z.ZodError): <span class="built_in">string</span></span><br><span class="line">    &#125;&gt;</span><br><span class="line">  &#125;</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">type</span> InferParameters&lt;T <span class="keyword">extends</span> Info&gt; = T <span class="keyword">extends</span> Info&lt;infer P&gt; ? z.infer&lt;P&gt; : <span class="built_in">never</span></span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">type</span> InferMetadata&lt;T <span class="keyword">extends</span> Info&gt; = T <span class="keyword">extends</span> Info&lt;<span class="built_in">any</span>, infer M&gt; ? M : <span class="built_in">never</span></span><br><span class="line"> </span><br><span class="line">  <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">define</span>&lt;<span class="title">Parameters</span> <span class="title">extends</span> <span class="title">z</span>.<span class="title">ZodType</span>, <span class="title">Result</span> <span class="title">extends</span> <span class="title">Metadata</span>&gt;(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params">    id: <span class="built_in">string</span>,</span></span></span><br><span class="line"><span class="function"><span class="params">    init: Info&lt;Parameters, Result&gt;[<span class="string">&quot;init&quot;</span>] | Awaited&lt;ReturnType&lt;Info&lt;Parameters, Result&gt;[<span class="string">&quot;init&quot;</span>]&gt;&gt;,</span></span></span><br><span class="line"><span class="function"><span class="params">  </span>): <span class="title">Info</span>&lt;<span class="title">Parameters</span>, <span class="title">Result</span>&gt; </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      id,</span><br><span class="line">      init: <span class="keyword">async</span> (initCtx) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> toolInfo = init <span class="keyword">instanceof</span> <span class="built_in">Function</span> ? <span class="keyword">await</span> init(initCtx) : init</span><br><span class="line">        <span class="keyword">const</span> execute = toolInfo.execute</span><br><span class="line">        toolInfo.execute = <span class="keyword">async</span> (args, ctx) =&gt; &#123;</span><br><span class="line">          <span class="keyword">try</span> &#123;</span><br><span class="line">            toolInfo.parameters.parse(args)</span><br><span class="line">          &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">            <span class="keyword">if</span> (error <span class="keyword">instanceof</span> z.ZodError &amp;&amp; toolInfo.formatValidationError) &#123;</span><br><span class="line">              <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(toolInfo.formatValidationError(error), &#123; <span class="attr">cause</span>: error &#125;)</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(</span><br><span class="line">              <span class="string">`The <span class="subst">$&#123;id&#125;</span> tool was called with invalid arguments: <span class="subst">$&#123;error&#125;</span>.\nPlease rewrite the input so it satisfies the expected schema.`</span>,</span><br><span class="line">              &#123; <span class="attr">cause</span>: error &#125;,</span><br><span class="line">            )</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">const</span> result = <span class="keyword">await</span> execute(args, ctx)</span><br><span class="line">          <span class="comment">// skip truncation for tools that handle it themselves</span></span><br><span class="line">          <span class="keyword">if</span> (result.metadata.truncated !== <span class="literal">undefined</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> result</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">const</span> truncated = <span class="keyword">await</span> Truncate.output(result.output, &#123;&#125;, initCtx?.agent)</span><br><span class="line">          <span class="keyword">return</span> &#123;</span><br><span class="line">            ...result,</span><br><span class="line">            output: truncated.content,</span><br><span class="line">            metadata: &#123;</span><br><span class="line">              ...result.metadata,</span><br><span class="line">              truncated: truncated.truncated,</span><br><span class="line">              ...(truncated.truncated &amp;&amp; &#123; <span class="attr">outputPath</span>: truncated.outputPath &#125;),</span><br><span class="line">            &#125;,</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> toolInfo</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="内置工具概述"><a href="#内置工具概述" class="headerlink" title="内置工具概述"></a>内置工具概述</h2><p>OpenCode 附带了一套按功能类别组织的全面内置工具。这些工具涵盖了 AI agents 在处理代码库时需要执行的最常见操作。</p><div class="table-container"><table><thead><tr><th>类别</th><th>工具</th><th>用途</th></tr></thead><tbody><tr><td>文件操作</td><td>read, write, edit, multiedit, patch, batch</td><td>使用各种编辑策略读取、创建、修改文件</td></tr><tr><td>搜索与导航</td><td>grep, glob, codesearch, ls</td><td>搜索文件内容、按模式查找文件、列出目录</td></tr><tr><td>执行</td><td>bash, task</td><td>执行 shell 命令和长时间运行的任务</td></tr><tr><td>Web</td><td>webfetch, websearch</td><td>获取网页和搜索互联网</td></tr><tr><td>LSP 集成</td><td>lsp</td><td>查询语言服务器以获取代码智能</td></tr><tr><td>任务管理</td><td>todo (read/write)</td><td>管理任务列表以跟踪工作</td></tr><tr><td>技能</td><td>skill</td><td>执行专门的 agent 工作流</td></tr><tr><td>系统</td><td>question, invalid</td><td>交互式提示词和错误处理</td></tr></tbody></table></div><h3 id="文件操作工具"><a href="#文件操作工具" class="headerlink" title="文件操作工具"></a>文件操作工具</h3><p>Read Tool：读取文件内容，支持可选的分页功能。支持 offset（基于 0 的行号）和 limit 参数来读取大文件的特定范围。如果目标文件不存在，则提供相似文件名的建议。</p><p>来源：packages/opencode/src/tool/read.ts</p><p>它的description在<code>read.txt</code>中，即：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">Reads a file <span class="keyword">from</span> the local filesystem. You can access <span class="built_in">any</span> file directly by using <span class="built_in">this</span> tool.</span><br><span class="line">Assume <span class="built_in">this</span> tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.</span><br><span class="line"></span><br><span class="line">Usage:</span><br><span class="line">- The filePath parameter must be an absolute path, not a relative path</span><br><span class="line">- By <span class="keyword">default</span>, it reads up to <span class="number">2000</span> lines starting <span class="keyword">from</span> the beginning <span class="keyword">of</span> the file</span><br><span class="line">- You can optionally specify a line offset and limit (especially handy <span class="keyword">for</span> long files), but it<span class="string">&#x27;s recommended to read the whole file by not providing these parameters</span></span><br><span class="line"><span class="string">- Any lines longer than 2000 characters will be truncated</span></span><br><span class="line"><span class="string">- Results are returned using cat -n format, with line numbers starting at 1</span></span><br><span class="line"><span class="string">- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.</span></span><br><span class="line"><span class="string">- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.</span></span><br><span class="line"><span class="string">- You can read image files using this tool.</span></span><br></pre></td></tr></table></figure><p>这是告诉大模型该工具的用途，让大模型可以通过该描述来调用它，实际的用法在<code>read.ts</code>中。</p><p>Edit Tool：使用基于差异的编辑在文件中执行字符串替换。支持 replaceAll 标志以替换所有出现的字符串模式。包括安全检查以防止意外修改，并生成统一差异以供验证。</p><p>来源：packages/opencode/src/tool/edit.txt</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Performs exact <span class="built_in">string</span> replacements <span class="keyword">in</span> files. </span><br><span class="line"></span><br><span class="line">Usage:</span><br><span class="line">- You must use your <span class="string">`Read`</span> tool at least once <span class="keyword">in</span> the conversation before editing. This tool will error <span class="keyword">if</span> you attempt an edit without reading the file. </span><br><span class="line">- When editing text <span class="keyword">from</span> Read tool output, ensure you preserve the exact indentation (tabs/spaces) <span class="keyword">as</span> it appears AFTER the line <span class="built_in">number</span> prefix. The line <span class="built_in">number</span> prefix format is: spaces + line <span class="built_in">number</span> + tab. Everything after that tab is the actual file content to match. Never include <span class="built_in">any</span> part <span class="keyword">of</span> the line <span class="built_in">number</span> prefix <span class="keyword">in</span> the oldString or newString.</span><br><span class="line">- ALWAYS prefer editing existing files <span class="keyword">in</span> the codebase. NEVER write <span class="keyword">new</span> files unless explicitly required.</span><br><span class="line">- Only use emojis <span class="keyword">if</span> the user explicitly requests it. Avoid adding emojis to files unless asked.</span><br><span class="line">- The edit will FAIL <span class="keyword">if</span> <span class="string">`oldString`</span> is not found <span class="keyword">in</span> the file <span class="keyword">with</span> an error <span class="string">&quot;oldString not found in content&quot;</span>.</span><br><span class="line">- The edit will FAIL <span class="keyword">if</span> <span class="string">`oldString`</span> is found multiple times <span class="keyword">in</span> the file <span class="keyword">with</span> an error <span class="string">&quot;oldString found multiple times and requires more code context to uniquely identify the intended match&quot;</span>. Either provide a larger <span class="built_in">string</span> <span class="keyword">with</span> more surrounding context to make it unique or use <span class="string">`replaceAll`</span> to change every instance <span class="keyword">of</span> <span class="string">`oldString`</span>. </span><br><span class="line">- Use <span class="string">`replaceAll`</span> <span class="keyword">for</span> replacing and renaming strings across the file. This parameter is useful <span class="keyword">if</span> you want to rename a variable <span class="keyword">for</span> instance.</span><br></pre></td></tr></table></figure><p>Write Tool：创建新文件或完全覆盖现有文件。用于从头创建新文件或替换整个文件内容时使用。</p><p>MultiEdit Tool：在单个工具调用中应用多个编辑操作，对于需要在多个位置进行协调更改的复杂重构任务很有用。</p><p>Patch Tool：将统一差异补丁应用于文件，支持标准补丁格式以从外部来源导入更改。</p><p>Batch Tool：用于批量文件操作的实验性工具，通过 config.experimental.batch_tool 标志启用。</p><h3 id="搜索和导航工具"><a href="#搜索和导航工具" class="headerlink" title="搜索和导航工具"></a>搜索和导航工具</h3><p>Grep Tool：使用正则表达式模式搜索文件内容，支持文件包含过滤器。底层使用 ripgrep 在大型代码库上进行快速、递归的搜索操作。</p><p>来源：packages/opencode/src/tool/grep.ts</p><p>Glob Tool：查找匹配 glob 模式的文件。支持标准 glob 语法和可选路径规范以在特定目录中搜索。</p><p>来源：packages/opencode/src/tool/glob.ts</p><p>CodeSearch Tool：提供语义代码搜索功能，仅适用于 OpenCode provider 或通过 OPENCODE_ENABLE_EXA 标志启用时可用。</p><p>来源：packages/opencode/src/tool/registry.ts</p><p>Ls Tool：列出目录内容，支持可选过滤，提供了一种探索目录结构的轻量级方式。</p><h3 id="执行工具"><a href="#执行工具" class="headerlink" title="执行工具"></a>执行工具</h3><p>Bash Tool：在持久的 shell 会话中执行 shell 命令，具有全面的安全措施。支持可选超时（默认 2 分钟）和 workdir 参数以在特定目录中执行命令。包括有关避免反模式的广泛指导，例如使用 cd 命令而不是 workdir 参数。</p><p>来源：packages/opencode/src/tool/bash.ts, packages/opencode/src/tool/bash.txt</p><p>Bash 工具包括复杂的 Git 安全协议，可防止强制推送、跳过挂钩或在没有明确用户同意的情况下修改已推送的提交等破坏性操作。它还提供了使用 gh CLI 工具创建 pull requests 的指导，包括正确的格式和分支管理。</p><p>Task Tool：管理可以跨越多个工具调用的长时间运行的任务，适用于需要在多个 agent 步骤之间保持持久状态的操作。</p><h3 id="Web-工具"><a href="#Web-工具" class="headerlink" title="Web 工具"></a>Web 工具</h3><p>WebFetch Tool：从 web URL 获取内容，使 agents 能够访问在线文档、API 参考和其他 web 资源。</p><p>WebSearch Tool：使用 Exa 执行 web 搜索，仅适用于 OpenCode provider 或通过 OPENCODE_ENABLE_EXA 标志启用时可用。对于研究解决方案、查找库或从互联网收集信息很有用。</p><p>来源：packages/opencode/src/tool/registry.ts</p><h3 id="LSP-集成"><a href="#LSP-集成" class="headerlink" title="LSP 集成"></a>LSP 集成</h3><p>Lsp Tool：为代码智能功能提供语言服务器协议 (Language Server Protocol) 服务器的接口。这是一个通过 OPENCODE_EXPERIMENTAL_LSP_TOOL 标志启用的实验性工具，允许 agents 查询符号、定义、引用和其他代码结构信息。</p><p>来源：packages/opencode/src/tool/registry.ts</p><h3 id="任务管理"><a href="#任务管理" class="headerlink" title="任务管理"></a>任务管理</h3><p>TodoWrite Tool / TodoRead Tool：管理任务列表以跟踪跨 agent 会话的工作。支持创建、读取和更新具有结构化元数据的任务，适用于规划和跟踪多步骤操作。</p><h3 id="技能"><a href="#技能" class="headerlink" title="技能"></a>技能</h3><p>Skill Tool：执行专门的 agent 工作流或”技能”，将复杂的多步骤操作打包为可重用的单元。技能根据 agent 权限进行过滤，确保 agents 仅访问适合其权限级别的技能。</p><p>来源：packages/opencode/src/tool/skill.ts</p><h2 id="Agent调用工具的原理"><a href="#Agent调用工具的原理" class="headerlink" title="Agent调用工具的原理"></a>Agent调用工具的原理</h2><p>Agent 通过一个标准化的工具定义和 LLM 的函数调用能力来决定调用哪个工具。整个过程不是 Agent “思考”出来的，而是 LLM 基于工具描述自动推断的。</p><p>工作原理如下：</p><p>工具定义 - 每个工具都有完整的描述和参数定义。例如<br>read 工具L18-L22 定义了：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">id: &quot;read&quot;（唯一标识）</span><br><span class="line">description: 工具功能的详细说明</span><br><span class="line">parameters: 使用 Zod schema 定义的参数结构（filePath, offset, limit 等）</span><br></pre></td></tr></table></figure><p>工具注册与初始化 -<br>ToolRegistry.tools()L120-L140 收集所有可用工具并返回它们的完整信息：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> &#123;</span><br><span class="line">  id: t.id,</span><br><span class="line">  ...(<span class="keyword">await</span> t.init(&#123; agent &#125;)),  <span class="comment">// 包含 description 和 parameters</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>传递给 LLM - 在<br>LLM.stream()L135-L170 中，这些工具被传递给 AI SDK 的 streamText 函数：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> tools = <span class="keyword">await</span> resolveTools(input)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">return</span> streamText(&#123;</span><br><span class="line">  tools,  <span class="comment">// 所有可用工具的完整定义</span></span><br><span class="line">  <span class="comment">// ...其他参数</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>LLM 自动决策 - LLM 会：</p><ol><li>读取每个工具的 description 理解其功能</li><li>查看每个工具的 parameters 了解如何调用</li><li>根据用户请求，自动选择最匹配的工具</li><li>生成符合该工具参数 schema 的调用请求</li></ol><p>执行与反馈 - 工具调用会返回结果给 LLM，LLM 基于结果继续对话或决定是否需要调用其他工具</p><p>关键点： Agent 不需要知道何时调用哪个工具，这个决策完全由 LLM 基于工具的描述和参数定义自动完成。系统通过标准化的工具格式（OpenAI 函数调用兼容）实现了这种能力。</p><h2 id="自定义工具和可扩展性"><a href="#自定义工具和可扩展性" class="headerlink" title="自定义工具和可扩展性"></a>自定义工具和可扩展性</h2><p>工具注册表通过两种主要机制支持可扩展性：基于配置的自定义工具和插件系统集成。</p><h3 id="基于配置的自定义工具"><a href="#基于配置的自定义工具" class="headerlink" title="基于配置的自定义工具"></a>基于配置的自定义工具</h3><p>自定义工具可以在配置目录中定义为 JavaScript 或 TypeScript 文件。注册表扫描 Config.directories() 返回的所有目录中匹配 tool/*.{js,ts} 的文件，允许用户在不修改 OpenCode 代码库的情况下添加自定义工具。</p><p>每个工具文件导出一个具有以下结构的 ToolDefinition 对象：</p><ul><li>description：描述工具功能的字符串</li><li>args：定义工具参数的 Zod schema</li><li>execute：实现工具逻辑的异步函数</li></ul><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> state = Instance.state(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> custom = [] <span class="keyword">as</span> Tool.Info[]</span><br><span class="line">  <span class="keyword">const</span> glob = <span class="keyword">new</span> Bun.Glob(<span class="string">&quot;tool/*.&#123;js,ts&#125;&quot;</span>)</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> dir <span class="keyword">of</span> <span class="keyword">await</span> Config.directories()) &#123;</span><br><span class="line">    <span class="keyword">for</span> <span class="keyword">await</span> (<span class="keyword">const</span> match <span class="keyword">of</span> glob.scan(&#123;</span><br><span class="line">      cwd: dir,</span><br><span class="line">      absolute: <span class="literal">true</span>,</span><br><span class="line">      followSymlinks: <span class="literal">true</span>,</span><br><span class="line">      dot: <span class="literal">true</span>,</span><br><span class="line">    &#125;)) &#123;</span><br><span class="line">      <span class="keyword">const</span> <span class="keyword">namespace</span> = path.basename(match, path.extname(match))</span><br><span class="line">      const mod = await import(match)</span><br><span class="line">      for (const [id, def] of Object.entries&lt;ToolDefinition&gt;(mod)) &#123;</span><br><span class="line">        custom.push(fromPlugin(id === <span class="string">&quot;default&quot;</span> ? <span class="keyword">namespace</span> : `$&#123;<span class="keyword">namespace</span>&#125;_$&#123;id&#125;<span class="string">`, def))</span></span><br><span class="line"><span class="string">      &#125;</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string"> </span></span><br></pre></td></tr></table></figure><p>注册表自动使用输出截断和验证包装这些定义，通过 fromPlugin() 函数将其转换为内部 Tool.Info 格式。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">fromPlugin</span>(<span class="params">id: <span class="built_in">string</span>, def: ToolDefinition</span>): <span class="title">Tool</span>.<span class="title">Info</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    id,</span><br><span class="line">    init: <span class="keyword">async</span> (initCtx) =&gt; (&#123;</span><br><span class="line">      parameters: z.object(def.args),</span><br><span class="line">      description: def.description,</span><br><span class="line">      execute: <span class="keyword">async</span> (args, ctx) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> result = <span class="keyword">await</span> def.execute(args <span class="keyword">as</span> <span class="built_in">any</span>, ctx)</span><br><span class="line">        <span class="keyword">const</span> out = <span class="keyword">await</span> Truncate.output(result, &#123;&#125;, initCtx?.agent)</span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">          title: <span class="string">&quot;&quot;</span>,</span><br><span class="line">          output: out.truncated ? out.content : result,</span><br><span class="line">          metadata: &#123; <span class="attr">truncated</span>: out.truncated, <span class="attr">outputPath</span>: out.truncated ? out.outputPath : <span class="literal">undefined</span> &#125;,</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;),</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/tool/registry.ts, packages/opencode/src/tool/registry.ts</p><h3 id="插件系统集成"><a href="#插件系统集成" class="headerlink" title="插件系统集成"></a>插件系统集成</h3><p>插件可以通过在其插件定义中导出工具来扩展 OpenCode。注册表加载所有可用插件并提取其工具导出，将它们与内置工具和基于配置的自定义工具一起注册。插件系统使用相同的 ToolDefinition 类型，确保所有可扩展机制之间的一致性。</p><p>来源：packages/opencode/src/tool/registry.ts</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">  <span class="keyword">const</span> plugins = <span class="keyword">await</span> Plugin.list()</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> plugin <span class="keyword">of</span> plugins) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> [id, def] <span class="keyword">of</span> <span class="built_in">Object</span>.entries(plugin.tool ?? &#123;&#125;)) &#123;</span><br><span class="line">      custom.push(fromPlugin(id, def))</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">return</span> &#123; custom &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h2 id="动态工具过滤"><a href="#动态工具过滤" class="headerlink" title="动态工具过滤"></a>动态工具过滤</h2><p>工具注册表根据多个因素应用动态过滤，以确保仅将适当的工具提供给特定的 agents 和 providers：</p><p>1、基于 Provider 的过滤：某些工具如 codesearch 和 websearch 仅限于 OpenCode provider 或需要通过 OPENCODE_ENABLE_EXA 标志显式启用。这可以防止在不可用时使用昂贵或特定于 provider 的工具。</p><p>2、基于标志的过滤：实验性和功能标志控制工具可用性：</p><p>LspTool：通过 OPENCODE_EXPERIMENTAL_LSP_TOOL 标志启用<br>BatchTool：通过 config.experimental.batch_tool 设置启用</p><p>3、 客户端特定过滤：QuestionTool 仅在 CLI 模式下运行时包含 (Flag.OPENCODE_CLIENT === “cli”)，因为它在其他上下文中不适用。</p><p>4、基于权限的过滤：工具根据 agent 权限进行过滤，虽然这主要通过 ctx.ask() 权限请求机制在单个工具级别强制执行，而不是在注册表级别。</p><p>来源：packages/opencode/src/tool/registry.ts</p><h2 id="工具执行上下文"><a href="#工具执行上下文" class="headerlink" title="工具执行上下文"></a>工具执行上下文</h2><p>每次工具执行都会收到一个包含有关执行环境信息的上下文对象：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  sessionID: string,      // 唯一会话标识符</span><br><span class="line">  messageID: string,       // 触发工具调用的消息</span><br><span class="line">  agent: string,           // Agent 标识符</span><br><span class="line">  abort: AbortSignal,     // 取消执行的信号</span><br><span class="line">  callID?: string,        // 唯一工具调用标识符</span><br><span class="line">  extra?: object,         // 附加执行元数据</span><br><span class="line">  metadata(),              // 更新工具元数据的函数</span><br><span class="line">  ask()                    // 请求权限的函数</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>metadata() 函数允许工具将元数据附加到其结果以进行 UI 显示和跟踪。ask() 函数实现权限系统，要求工具在访问敏感资源（如文件）或执行命令之前请求权限。</p><p>来源：packages/opencode/src/tool/tool.ts</p><h2 id="输出管理和截断"><a href="#输出管理和截断" class="headerlink" title="输出管理和截断"></a>输出管理和截断</h2><p>工具注册表自动应用输出截断，以防止过多的输出淹没上下文窗口。截断系统支持行数 (MAX_LINES) 和字节大小 (MAX_BYTES) 的可配置限制。</p><p>当输出超过限制时，截断系统：</p><p>将完整输出写入文件<br>返回限制内的截断版本<br>包含指示截断状态和输出文件路径的元数据<br>允许 agents 使用带有 offset/limit 参数的 Read 工具读取特定部分</p><p>某些工具通过设置 metadata.truncated 来处理自己的截断，这会向注册表发出信号以跳过自动截断。这对于需要自定义截断策略或希望对输出格式进行更多控制的工具很有用。</p><p>来源：packages/opencode/src/tool/tool.ts, packages/opencode/src/tool/registry.ts</p><h2 id="工具注册-API"><a href="#工具注册-API" class="headerlink" title="工具注册 API"></a>工具注册 API</h2><p>注册表提供了 register() 函数，用于在运行时动态添加工具。该函数替换具有相同 ID 的现有工具，或者如果不存在则将其添加到注册表。此 API 对于需要在初始注册表初始化后注册工具的插件和扩展很有用。</p><p>来源：packages/opencode/src/tool/registry.ts</p><h1 id="文件操作：编辑、读取、写入及多编辑工具"><a href="#文件操作：编辑、读取、写入及多编辑工具" class="headerlink" title="文件操作：编辑、读取、写入及多编辑工具"></a>文件操作：编辑、读取、写入及多编辑工具</h1><p>文件操作工具构成了 Agent 与本地文件系统交互的基础设施。这些工具——Read、Write、Edit 和 Multi-edit——提供了受控的、具有权限感知的文件访问能力，并具备复杂的 diff 生成、冲突检测和 LSP 集成功能以实现实时代码智能。该架构通过强制写入前读取、并发写入序列化以及灵活的文本匹配策略来处理常见的格式差异，从而强调了安全性。</p><h2 id="工具架构概述"><a href="#工具架构概述" class="headerlink" title="工具架构概述"></a>工具架构概述</h2><p>文件操作工具遵循基于 Tool.define() 框架的统一架构模式。每个工具都实现了一致的接口，包括 Zod schema 验证、参数化执行上下文，以及与权限系统、LSP 服务和事件总线的集成。该架构通过多层验证和并发操作处理优先保障安全性。</p><h2 id="Read-工具"><a href="#Read-工具" class="headerlink" title="Read 工具"></a>Read 工具</h2><p>Read 工具提供了对文件内容的受控访问，支持可配置的分页、二进制文件检测以及对媒体文件的特殊处理。它作为 Agent 在修改之前检查代码和配置文件的基础访问点。</p><h3 id="参数与配置"><a href="#参数与配置" class="headerlink" title="参数与配置"></a>参数与配置</h3><div class="table-container"><table><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>filePath</td><td>string</td><td>Yes</td><td>要读取的文件的绝对路径</td><td>-</td></tr><tr><td>offset</td><td>number</td><td>No</td><td>开始读取的行号（从 0 开始）</td><td>0</td></tr><tr><td>limit</td><td>number</td><td>No</td><td>要读取的行数</td><td>2000</td></tr></tbody></table></div><h3 id="执行流程"><a href="#执行流程" class="headerlink" title="执行流程"></a>执行流程</h3><p>Read 工具在返回文件内容之前会执行全面的验证和处理流程。当找不到文件时，它会基于目录条目提供模糊匹配的智能建议，以帮助 Agent 纠正路径错误。对于二进制文件，工具会检测其类型并返回相应的错误，而图像和 PDF 文件则会转换为 base64 编码的附件以提供预览功能。</p><p>来源：read.ts</p><h3 id="安全性与限制"><a href="#安全性与限制" class="headerlink" title="安全性与限制"></a>安全性与限制</h3><p>该工具实施了多重安全约束以防止资源耗尽并确保稳定运行：</p><ul><li>字节限制：每次读取操作最多 50KB</li><li>行长度限制：超过 2000 个字符的行将被截断并添加 “…” 后缀</li><li>默认行数限制：每次读取请求 2000 行</li><li>二进制检测：自动识别机制防止尝试将二进制文件作为文本读取</li></ul><p>这些限制确保了大文件不会淹没 LLM 的上下文窗口，同时仍为典型的源文件提供全面的访问。当内容被截断时，元数据会包含一个 truncated 标志以指示部分交付。</p><p>来源：read.ts</p><h2 id="Write-工具"><a href="#Write-工具" class="headerlink" title="Write 工具"></a>Write 工具</h2><p>Write 工具支持完整的文件替换，并包含防止意外数据丢失和跟踪修改时间戳的安全机制。它与权限系统集成，确保在执行破坏性操作前获得用户批准。</p><h3 id="参数与约束"><a href="#参数与约束" class="headerlink" title="参数与约束"></a>参数与约束</h3><div class="table-container"><table><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>content</td><td>string</td><td>Yes</td><td>要写入文件的完整内容</td></tr><tr><td>filePath</td><td>string</td><td>Yes</td><td>要写入的文件的绝对路径</td></tr></tbody></table></div><p>关键要求：覆盖现有文件之前必须先读取该文件。FileTime 系统会跟踪读取时间戳，如果文件尚未被读取，或者自上次读取以来文件已被外部修改，则拒绝写入操作。</p><p>来源：write.ts</p><h3 id="诊断集成"><a href="#诊断集成" class="headerlink" title="诊断集成"></a>诊断集成</h3><p>写入成功后，工具会触发 LSP 诊断，以识别由更改引入的任何错误。诊断报告包括：</p><ul><li>文件级错误：每个修改的文件最多 20 个错误</li><li>项目级错误：来自最多 5 个相关文件的错误</li><li>美化输出：带有文件位置的格式化错误消息</li></ul><p>这种即时反馈循环允许 Agent 在继续后续操作之前检测并纠正问题，从而在整个编辑过程中保持代码质量。</p><p>来源：write.ts</p><h3 id="事件发布"><a href="#事件发布" class="headerlink" title="事件发布"></a>事件发布</h3><p>Write 工具通过 Bus 系统发布 File.Event.Edited 事件，使其他系统组件能够对文件更改做出反应。这种事件驱动架构支持实时重载、自动化测试触发器和连接客户端的 UI 更新等功能。</p><p>来源：write.ts</p><h2 id="Edit-工具"><a href="#Edit-工具" class="headerlink" title="Edit 工具"></a>Edit 工具</h2><p>Edit 工具实现了复杂的查找和替换功能，支持多种文本匹配策略。与简单的字符串替换不同，它采用级联的替换器算法，逐步规范化文本以处理常见的格式差异，例如空白字符变化、缩进差异和转义序列。</p><h3 id="参数规范"><a href="#参数规范" class="headerlink" title="参数规范"></a>参数规范</h3><div class="table-container"><table><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>filePath</td><td>string</td><td>Yes</td><td>要修改的文件的绝对路径</td></tr><tr><td>oldString</td><td>string</td><td>Yes</td><td>要替换的文本（必须与上下文完全匹配）</td></tr><tr><td>newString</td><td>string</td><td>Yes</td><td>替换文本</td></tr><tr><td>replaceAll</td><td>boolean</td><td>No</td><td>如果为 true，则替换所有出现的位置；如果为 false，则仅替换第一个匹配项</td></tr></tbody></table></div><p>来源：edit.ts</p><h3 id="替换器策略级联"><a href="#替换器策略级联" class="headerlink" title="替换器策略级联"></a>替换器策略级联</h3><p>Edit 工具采用复杂的回退机制，包含九种不同的替换器算法，按顺序应用，直到找到匹配项为止：</p><p>替换器算法：</p><ul><li>SimpleReplacer：直接字符串匹配</li></ul><ol><li>LineTrimmedReplacer：忽略每行前导/尾随空白字符</li><li>BlockAnchorReplacer：使用 Levenshtein 距离的相似度评分对 3 行以上的代码块进行模糊匹配</li><li>WhitespaceNormalizedReplacer：将多个空白字符折叠为单个空格</li><li>IndentationFlexibleReplacer：移除搜索内容和内容的最小缩进</li><li>EscapeNormalizedReplacer：反转义特殊字符（\n, \t, \ 等）</li><li>TrimmedBoundaryReplacer：在匹配前修剪搜索字符串</li><li>ContextAwareReplacer：使用周围行对 3 行以上的代码块进行模糊匹配</li><li>MultiOccurrenceReplacer：查找所有出现位置以进行 replaceAll 操作</li></ol><p>这种级联方法显著提高了 Agent 尝试匹配具有轻微格式差异的文本时的编辑成功率，这在 LLM 生成的代码与原始代码的空白或缩进略有不同时是一种常见情况。</p><p>来源：edit.ts</p><h3 id="并发写入安全"><a href="#并发写入安全" class="headerlink" title="并发写入安全"></a>并发写入安全</h3><p>Edit 工具使用 FileTime.withLock() 来序列化对同一文件的并发写入操作。此锁定机制确保多个 Agent 或工具无法同时修改同一文件，从而防止竞争条件和数据损坏。锁定系统将 Promise 链接起来，使每个写入操作等待前一个操作完成后再继续。</p><p>来源：edit.ts</p><h3 id="Diff-生成与修剪"><a href="#Diff-生成与修剪" class="headerlink" title="Diff 生成与修剪"></a>Diff 生成与修剪</h3><p>每次编辑后，工具使用 diff 库中的 createTwoFilesPatch() 函数生成统一 diff。trimDiff() 函数随后通过从所有更改行中移除最小公共缩进来规范化 diff 输出中的缩进，从而为用户生成更清晰、更易读的 diff 显示。</p><p>来源：edit.ts</p><h2 id="Multi-edit-工具"><a href="#Multi-edit-工具" class="headerlink" title="Multi-edit 工具"></a>Multi-edit 工具</h2><p>Multi-edit 工具扩展了 Edit 功能，支持在单个原子事务中执行多个查找和替换操作。这对于需要对同一文件进行多个相关更改的重构任务特别有用。</p><h3 id="参数结构"><a href="#参数结构" class="headerlink" title="参数结构"></a>参数结构</h3><div class="table-container"><table><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>filePath</td><td>string</td><td>Yes</td><td>要修改的文件的绝对路径</td></tr><tr><td>edits</td><td>array</td><td>Yes</td><td>编辑操作对象的数组</td></tr><tr><td>edits[].oldString</td><td>string</td><td>Yes</td><td>每个操作中要替换的文本</td></tr><tr><td>edits[].newString</td><td>string</td><td>Yes</td><td>每个操作的替换文本</td></tr><tr><td>edits[].replaceAll</td><td>boolean</td><td>No</td><td>每个操作是否替换所有出现位置</td></tr></tbody></table></div><p>来源：multiedit.ts</p><h3 id="执行语义"><a href="#执行语义" class="headerlink" title="执行语义"></a>执行语义</h3><p>Multi-edit 工具按顺序运行，这对操作规划有重要影响：</p><ul><li>顺序应用：编辑操作按数组中指定的顺序应用</li><li>累积状态：每个编辑操作都在所有先前编辑的结果上进行</li><li>原子回滚：如果任何编辑失败，则不会对文件应用任何更改</li><li>继承 Edit 工具行为：所有编辑操作都使用相同的替换器级联和安全检查</li></ul><p>这种执行模型要求在编辑可能影响后续编辑尝试匹配的文本时进行仔细规划。Agent 必须确保早期的编辑不会使后续编辑的搜索字符串失效。</p><p>来源：multiedit.ts</p><p>使用 Multi-edit 创建文件时，请在第一个编辑操作中使用空的 oldString，并将完整的文件内容作为 newString。随后的编辑操作可以修改新创建的文件，从而允许在单个原子操作中同时进行创建和修改。</p><h2 id="权限系统集成"><a href="#权限系统集成" class="headerlink" title="权限系统集成"></a>权限系统集成</h2><p>所有文件操作工具都通过 ctx.ask() 与权限系统集成，在执行破坏性操作之前提示用户批准。权限请求包括：</p><ul><li>权限类型：Read 工具为 “read”，Write/Edit/Multi-edit 工具为 “edit”</li><li>文件模式：受影响的文件路径</li><li>元数据：附加上下文，包括编辑操作的 diff 预览</li><li>始终允许模式：绕过批准的配置模式（通常为 “*”）</li></ul><p>权限系统支持每会话的批准缓存，允许用户授予对项目中文件的全面访问权限，而无需为每次操作重复确认。</p><p>来源：permission/next.ts</p><h2 id="FileTime-与并发管理"><a href="#FileTime-与并发管理" class="headerlink" title="FileTime 与并发管理"></a>FileTime 与并发管理</h2><p>FileTime 系统通过读取跟踪和写入序列化为文件操作提供核心安全机制：</p><p>核心功能：</p><ol><li>read(sessionID, file)：记录读取文件时的时间戳</li><li>assert(sessionID, file)：验证文件已被读取且未被外部修改</li><li>withLock(filepath, fn)：使用 Promise 链接序列化写入操作</li></ol><p>该系统防止了三种关键故障模式：未先读取文件的意外覆盖、并发写入导致的竞争条件以及静默覆盖外部更改。</p><p>来源：file/time.ts</p><h2 id="错误处理与恢复"><a href="#错误处理与恢复" class="headerlink" title="错误处理与恢复"></a>错误处理与恢复</h2><p>文件操作工具实现了全面的错误处理，提供具体、可操作的错误消息：</p><div class="table-container"><table><thead><tr><th>Error Scenario</th><th>Error Message</th><th>Resolution</th></tr></thead><tbody><tr><td>File not found</td><td>“File not found: {filepath}\n\nDid you mean one of these?”</td><td>检查路径，使用建议</td></tr><tr><td>Binary file read</td><td>“Cannot read binary file: {filepath}”</td><td>使用不同的方法</td></tr><tr><td>Write without read</td><td>“You must read the file before overwriting it”</td><td>先使用 Read 工具</td></tr><tr><td>File modified externally</td><td>“File has been modified since it was last read”</td><td>重新读取文件</td></tr><tr><td>oldString not found</td><td>“oldString not found in content”</td><td>提供带有上下文的精确匹配</td></tr><tr><td>Multiple matches</td><td>“oldString found multiple times and requires more code context”</td><td>提供更多周围行或使用 replaceAll</td></tr></tbody></table></div><p>这些具体的错误消息指导 Agent 采取正确的恢复策略，从而提高自动化编辑工作流程的可靠性。</p><p>来源：read.ts, edit.ts</p><h2 id="LSP-集成-1"><a href="#LSP-集成-1" class="headerlink" title="LSP 集成"></a>LSP 集成</h2><p>所有写入和编辑操作都会触发 Language Server Protocol 集成以实现实时代码智能：</p><ol><li>文件触碰：LSP.touchFile() 通知 LSP 文件更改</li><li>诊断检索：LSP.diagnostics() 从语言服务器收集错误</li><li>错误报告：错误被格式化并包含在工具输出中</li></ol><p>这种集成确保语法错误、类型不匹配和其他问题立即暴露给 Agent，从而在继续执行其他操作之前快速进行更正。诊断限制（每个文件 20 个，5 个项目文件）在全面性和响应时间之间取得了平衡。</p><p>来源：write.ts, edit.ts</p><h2 id="工具注册与发现"><a href="#工具注册与发现" class="headerlink" title="工具注册与发现"></a>工具注册与发现</h2><p>文件操作工具与其他 Agent 功能一起注册在中央工具注册表中。注册表支持：</p><ul><li>内置工具：核心文件、搜索和执行工具</li><li>插件工具：从配置目录动态加载</li><li>自定义工具：用户定义的扩展</li></ul><p>注册表使用优先级系统，其中 InvalidTool 首先出现以处理格式错误的工具请求，随后是按功能分组的操作工具。这种架构在为 Agent 保持稳定接口的同时实现了可扩展性。</p><p>来源：registry.ts</p><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="何时使用每种工具"><a href="#何时使用每种工具" class="headerlink" title="何时使用每种工具"></a>何时使用每种工具</h3><div class="table-container"><table><thead><tr><th>Scenario</th><th>Recommended Tool</th><th>Rationale</th></tr></thead><tbody><tr><td>Inspecting file contents</td><td>ReadTool</td><td>非破坏性，支持分页</td></tr><tr><td>Creating new files</td><td>WriteTool</td><td>直接内容写入</td></tr><tr><td>Replacing existing file</td><td>WriteTool</td><td>完整替换并验证</td></tr><tr><td>Small text changes</td><td>EditTool</td><td>精确的带上下文查找和替换</td></tr><tr><td>Multiple related edits</td><td>MultiEditTool</td><td>原子批处理操作</td></tr><tr><td>Refactoring across file</td><td>EditTool with replaceAll</td><td>全局字符串替换</td></tr></tbody></table></div><h3 id="错误预防策略"><a href="#错误预防策略" class="headerlink" title="错误预防策略"></a>错误预防策略</h3><ol><li>始终先读后写：FileTime 系统强制执行此操作，但 Agent 应在编辑之前显式读取文件</li><li>提供足够的上下文：在 oldString 中包含周围的行以唯一标识匹配项</li><li>使用 Multi-edit 保证原子性：当多个更改必须同时成功或失败时</li><li>检查诊断：在写入/编辑操作后查看 LSP 输出以发现引入的错误</li><li>处理二进制文件：为图像、PDF 和其他二进制格式使用适当的工具</li></ol><h3 id="性能考虑"><a href="#性能考虑" class="headerlink" title="性能考虑"></a>性能考虑</h3><ol><li>大文件：使用 offset 和 limit 参数读取特定部分</li><li>多次编辑：优先使用 Multi-edit 而不是顺序的 Edit 调用以减少锁争用</li><li>诊断：在处理会产生大量错误的文件时，请注意限制（每个文件 20 个）</li></ol><h1 id="搜索与导航：Grep、Glob-及代码搜索工具"><a href="#搜索与导航：Grep、Glob-及代码搜索工具" class="headerlink" title="搜索与导航：Grep、Glob 及代码搜索工具"></a>搜索与导航：Grep、Glob 及代码搜索工具</h1><p>本页面介绍了 OpenCode 的搜索和导航工具，这些工具使 agents 能够高效地探索代码库、查找特定代码模式以及检索外部文档。这些工具构成了跨本地和远程资源进行代码理解和上下文收集的基础。</p><h2 id="工具架构概览"><a href="#工具架构概览" class="headerlink" title="工具架构概览"></a>工具架构概览</h2><p>搜索工具构建在统一的架构之上，集成了权限管理、执行处理和结果格式化。每个工具都遵循 Tool.define() 模式，提供一致的参数验证、权限检查和输出格式化。</p><h2 id="Grep-工具：基于正则表达式的内容搜索"><a href="#Grep-工具：基于正则表达式的内容搜索" class="headerlink" title="Grep 工具：基于正则表达式的内容搜索"></a>Grep 工具：基于正则表达式的内容搜索</h2><p>GrepTool 提供跨任意规模代码库的快速、基于正则表达式的内容搜索。它利用 ripgrep 的性能，同时增加了 OpenCode 特有的功能，如权限控制、修改时间排序和结果截断。</p><h3 id="参数"><a href="#参数" class="headerlink" title="参数"></a>参数</h3><div class="table-container"><table><thead><tr><th>参数</th><th>类型</th><th>描述</th><th>示例</th></tr></thead><tbody><tr><td>pattern</td><td>string</td><td>在文件内容中搜索的正则表达式模式</td><td>“function\s+\w+”</td></tr><tr><td>path</td><td>string</td><td>搜索目录（默认为工作目录）</td><td>“src/utils”</td></tr><tr><td>include</td><td>string</td><td>文件模式过滤器</td><td>“<em>.ts” 或 “</em>.{ts,tsx}”</td></tr></tbody></table></div><h3 id="实现细节"><a href="#实现细节" class="headerlink" title="实现细节"></a>实现细节</h3><p>grep 工具使用特定标志执行 ripgrep 以确保结构化输出：-nH 用于显示行号和文件名，—hidden 和 —follow 用于搜索隐藏文件和跟踪符号链接，—field-match-separator=| 用于分隔输出字段以便解析 packages/opencode/src/tool/grep.ts#L31-L32。</p><p>结果按文件修改时间排序（最新的在前），限制为 100 个匹配项，超过 2000 个字符的行文本会被截断 packages/opencode/src/tool/grep.ts#L10, packages/opencode/src/tool/grep.ts#L89-L96。</p><h3 id="权限集成"><a href="#权限集成" class="headerlink" title="权限集成"></a>权限集成</h3><p>在执行之前，工具会请求搜索模式的权限，允许安全策略控制可搜索的内容 packages/opencode/src/tool/grep.ts#L20-L27。权限检查包括有关搜索范围的元数据，用于审计目的。</p><h3 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h3><p>当你需要查找包含特定模式或代码构造的文件时，grep 工具是理想选择。对于匹配计数或详细分析，请直接使用带有 rg 的 bash 工具。对于需要多轮搜索的开放式探索，Task 工具更为合适 packages/opencode/src/tool/grep.txt#L5-L9。</p><h2 id="Glob-工具：文件模式匹配"><a href="#Glob-工具：文件模式匹配" class="headerlink" title="Glob 工具：文件模式匹配"></a>Glob 工具：文件模式匹配</h2><p>GlobTool 使用 glob 模式提供快速文件发现，基于 ripgrep 的 —files 模式，可跨大型代码库进行高效的模式匹配。</p><h3 id="参数-1"><a href="#参数-1" class="headerlink" title="参数"></a>参数</h3><div class="table-container"><table><thead><tr><th>参数</th><th>类型</th><th>描述</th><th>示例</th></tr></thead><tbody><tr><td>pattern</td><td>string</td><td>用于匹配文件的 glob 模式</td><td>“<strong>/*.js” 或 “src/</strong>/*.ts”</td></tr><tr><td>path</td><td>string</td><td>搜索目录（省略时使用默认值）</td><td>“packages/“</td></tr></tbody></table></div><h3 id="实现细节-1"><a href="#实现细节-1" class="headerlink" title="实现细节"></a>实现细节</h3><p>glob 工具使用 ripgrep 的文件列表功能，通过 —files 标志按 glob 模式过滤并排除 .git 目录 packages/opencode/src/file/ripgrep.ts#L208-L223。结果限制为 100 个文件，并按修改时间排序 packages/opencode/src/tool/glob.ts#L35-L42。</p><p>路径解析处理绝对路径和相对路径，将它们规范化为相对于实例目录的路径 packages/opencode/src/tool/glob.ts#L27-L30。该工具还会验证外部目录以防止未授权访问 packages/opencode/src/tool/glob.ts#L31。</p><h3 id="使用场景-1"><a href="#使用场景-1" class="headerlink" title="使用场景"></a>使用场景</h3><p>glob 工具最适合通过名称模式查找文件，例如定位所有 TypeScript 文件、配置文件或特定目录中的文件。该工具支持批量调用，使得在单个请求中执行多个 glob 搜索变得高效 packages/opencode/src/tool/glob.txt#L6-L7。</p><h2 id="CodeSearch-工具：外部上下文检索"><a href="#CodeSearch-工具：外部上下文检索" class="headerlink" title="CodeSearch 工具：外部上下文检索"></a>CodeSearch 工具：外部上下文检索</h2><p>CodeSearchTool 与 Exa Code API 集成，用于检索库、SDK 和框架的外部文档、示例和 API 参考。</p><h3 id="参数-2"><a href="#参数-2" class="headerlink" title="参数"></a>参数</h3><div class="table-container"><table><thead><tr><th>参数</th><th>类型</th><th>范围</th><th>描述</th><th>示例</th></tr></thead><tbody><tr><td>query</td><td>string</td><td>-</td><td>针对API、库或SDK的搜索查询</td><td>“React useState hook examples”</td></tr><tr><td>tokensNum</td><td>number</td><td>1000-50000</td><td>返回的 token 数量</td><td>5000</td></tr></tbody></table></div><h3 id="实现细节-2"><a href="#实现细节-2" class="headerlink" title="实现细节"></a>实现细节</h3><p>该工具向位于 <a href="https://mcp.exa.ai/mcp">https://mcp.exa.ai/mcp</a> 的 Exa Code API 发送 JSON-RPC 2.0 请求，使用 get_code_context_exa 方法 packages/opencode/src/tool/codesearch.ts#L6-L9, packages/opencode/src/tool/codesearch.ts#L58-L66。请求在 30 秒后超时，以防止挂起 packages/opencode/src/tool/codesearch.ts#L68。</p><p>API 返回服务器发送事件（SSE）响应，工具会解析这些响应，从第一个可用数据行中提取代码上下文 packages/opencode/src/tool/codesearch.ts#L79-L89。</p><h3 id="Token-数量指导"><a href="#Token-数量指导" class="headerlink" title="Token 数量指导"></a>Token 数量指导</h3><p>1000-3000 tokens：针对特定函数或模式的聚焦查询<br>3000-8000 tokens：大多数查询的默认平衡上下文<br>8000-50000 tokens：全面的文档和大量示例 packages/opencode/src/tool/codesearch.txt#L7-L10</p><h3 id="使用场景-2"><a href="#使用场景-2" class="headerlink" title="使用场景"></a>使用场景</h3><p>codesearch 工具专为任何需要外部上下文的编程相关任务而设计，包括 API 文档、框架模式、库使用示例和最佳实践 packages/opencode/src/tool/codesearch.txt#L3-L5。</p><h2 id="Ripgrep-基础设施"><a href="#Ripgrep-基础设施" class="headerlink" title="Ripgrep 基础设施"></a>Ripgrep 基础设施</h2><p>所有本地搜索工具都依赖 Ripgrep 模块，该模块管理跨平台的 ripgrep 二进制文件生命周期。该架构包括自动下载、特定平台安装和错误处理。</p><h3 id="平台支持"><a href="#平台支持" class="headerlink" title="平台支持"></a>平台支持</h3><p>ripgrep 模块支持五种平台组合，从 GitHub releases 下载 14.1.1 版本 packages/opencode/src/file/ripgrep.ts#L91-L100：</p><div class="table-container"><table><thead><tr><th>平台</th><th>架构</th><th>操作系统</th><th>包格式</th></tr></thead><tbody><tr><td>aarch64-apple-darwin</td><td>ARM64</td><td>macOS</td><td>tar.gz</td></tr><tr><td>aarch64-unknown-linux-gnu</td><td>ARM64</td><td>Linux</td><td>tar.gz</td></tr><tr><td>x86_64-apple-darwin</td><td>x64</td><td>macOS</td><td>tar.gz</td></tr><tr><td>x86_64-unknown-linux-musl</td><td>x64</td><td>Linux</td><td>tar.gz</td></tr><tr><td>x86_64-pc-windows-msvc</td><td>x64</td><td>Windows</td><td>zip</td></tr></tbody></table></div><h3 id="搜索-API"><a href="#搜索-API" class="headerlink" title="搜索 API"></a>搜索 API</h3><p>Ripgrep.search() 函数提供结构化的 JSON 输出，包含详细的匹配信息，包括文件路径、行号、文本内容、绝对偏移量和子匹配位置 packages/opencode/src/file/ripgrep.ts#L370-L408。</p><p>结果模式定义了四种事件类型：</p><ul><li>Begin：文件搜索开始通知</li><li>Match：带有行上下文和子匹配的单个匹配项</li><li>End：带有统计信息的文件搜索完成</li><li>Summary：总体搜索指标，包括经过时间、执行的搜索次数、搜索的字节数和匹配计数 packages/opencode/src/file/ripgrep.ts#L14-L90</li></ul><h2 id="工具注册与发现-1"><a href="#工具注册与发现-1" class="headerlink" title="工具注册与发现"></a>工具注册与发现</h2><p>搜索工具在 ToolRegistry 中注册，该注册表管理内置工具、来自配置目录的自定义工具和插件工具。注册表与标志和配置集成，以有条件地启用实验性功能 packages/opencode/src/tool/registry.ts#L70-L98。</p><p>CodeSearchTool 根据提供商（opencode）或 OPENCODE_ENABLE_EXA 标志有条件地启用，允许在部署场景中灵活配置 packages/opencode/src/tool/registry.ts#L106-L110。</p><h2 id="权限模型"><a href="#权限模型" class="headerlink" title="权限模型"></a>权限模型</h2><p>所有搜索工具都与权限系统集成，在访问文件系统或外部资源之前需要明确批准。权限模型使用通配符模式匹配来定义访问规则 packages/opencode/src/permission/next.ts#L1-L270。</p><p>权限操作包括：</p><ul><li>Allow（允许）：继续操作，无需干预</li><li>Ask（询问）：请求用户批准，可选择记住以备将来使用</li><li>Deny（拒绝）：立即阻止访问</li></ul><h2 id="搜索工具对比"><a href="#搜索工具对比" class="headerlink" title="搜索工具对比"></a>搜索工具对比</h2><div class="table-container"><table><thead><tr><th>工具</th><th>范围</th><th>数据源</th><th>用例</th><th>输出格式</th></tr></thead><tbody><tr><td>grep</td><td>本地代码库</td><td>ripgrep</td><td>正则内容搜索</td><td>文件路径、行号、匹配文本</td></tr><tr><td>glob</td><td>本地代码库</td><td>ripgrep —files</td><td>文件模式发现</td><td>按修改时间排序的文件路径</td></tr><tr><td>codesearch</td><td>外部网络</td><td>Exa API</td><td>文档和示例</td><td>代码片段和文档文本</td></tr></tbody></table></div><p>在探索代码库时，在单个请求中批量处理多个 glob 和 grep 调用——agent 可以并行化这些操作以加快结果返回速度 packages/opencode/src/tool/glob.txt#L7。</p><h2 id="错误处理和边缘情况"><a href="#错误处理和边缘情况" class="headerlink" title="错误处理和边缘情况"></a>错误处理和边缘情况</h2><p>搜索工具处理各种边缘情况：</p><ul><li>无效的正则表达式模式：Ripgrep 通过退出代码报告错误，工具将其转换为描述性错误消息 packages/opencode/src/tool/grep.ts#L59-L65</li><li>不存在的目录：外部目录验证器会抛出带有正确错误代码的 ENOENT 错误 packages/opencode/src/file/ripgrep.ts#L226-L233</li><li>API 超时：代码搜索请求在 30 秒后中止，并显示明确的超时错误 packages/opencode/src/tool/codesearch.ts#L68-L70</li><li>结果截断：grep 和 glob 都提供元数据，指示结果何时超过限制，建议使用更具体的查询 packages/opencode/src/tool/grep.ts#L101-L104</li></ul><h1 id="Bash-集成与命令执行"><a href="#Bash-集成与命令执行" class="headerlink" title="Bash 集成与命令执行"></a>Bash 集成与命令执行</h1><p>OpenCode 中的 Bash 集成系统提供安全、基于权限控制的命令执行能力，使 Agent 能够通过 Shell 命令与系统交互，同时保持严格的安全控制。该系统结合了复杂的命令分析、细粒度的权限管理和实时输出流，提供了一个强大的终端执行环境。</p><h2 id="架构概述"><a href="#架构概述" class="headerlink" title="架构概述"></a>架构概述</h2><p>Bash 集成作为一个多层系统运行，连接 AI Agent 请求与实际的 Shell 命令执行。系统的核心使用基于 tree-sitter 的命令解析在执行前分析命令，对命令模式和目录访问应用权限检查，通过适当的清理管理进程生命周期，并将输出实时流式传输回 Agent。</p><p>该架构展示了清晰的关注点分离：解析用于安全，权限检查用于安全，进程管理用于可靠性，输出处理用于可见性。每个组件都可独立测试，并遵循 OpenCode 更广泛的纵深防御安全理念。</p><h2 id="Shell-检测和配置"><a href="#Shell-检测和配置" class="headerlink" title="Shell 检测和配置"></a>Shell 检测和配置</h2><p>在执行任何命令之前，系统会为操作环境确定合适的 Shell。Shell.acceptable() 和 Shell.preferred() 中的 Shell 检测逻辑提供了跨不同平台的智能回退机制。</p><p>来源: shell.ts</p><h3 id="平台特定的-Shell-解析"><a href="#平台特定的-Shell-解析" class="headerlink" title="平台特定的 Shell 解析"></a>平台特定的 Shell 解析</h3><div class="table-container"><table><thead><tr><th>平台</th><th>首选 Shell</th><th>回退行为</th></tr></thead><tbody><tr><td>安装了 Git Bash 的 Windows</td><td>从检测到的 Git 安装路径获取 Git Bash</td><td>通过 COMSPEC 环境变量获取 cmd.exe</td></tr><tr><td>未安装 Git Bash 的 Windows</td><td>cmd.exe</td><td>内置系统 Shell</td></tr><tr><td>macOS</td><td>用户的 SHELL 环境变量</td><td>/bin/zsh</td></tr><tr><td>Linux</td><td>用户的 SHELL 环境变量</td><td>/bin/bash → /bin/sh</td></tr></tbody></table></div><p>系统维护不兼容 Shell（fish, nu）的黑名单，并自动回退到可接受的替代方案。在 Windows 上，它通过检查 Git 可执行文件路径并相对于它构建 bash.exe 路径来智能发现 Git Bash。</p><h3 id="配置选项"><a href="#配置选项" class="headerlink" title="配置选项"></a>配置选项</h3><p>多个标志控制 Bash 工具行为，可在系统级别配置：</p><ul><li>OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: 自定义超时时长（默认: 120,000ms / 2 分钟）</li><li>OPENCODE_GIT_BASH_PATH: Windows 上显式指定 Git Bash 路径（覆盖自动检测）</li></ul><p>来源: bash.ts</p><h2 id="命令分析和权限系统"><a href="#命令分析和权限系统" class="headerlink" title="命令分析和权限系统"></a>命令分析和权限系统</h2><p>Bash 集成最复杂的方面是其命令分析管道，它使用 tree-sitter-bash 解析在执行前理解命令结构。这实现了细粒度的权限控制，可以区分不同的命令类型及其潜在影响。</p><h3 id="命令解析策略"><a href="#命令解析策略" class="headerlink" title="命令解析策略"></a>命令解析策略</h3><p>命令使用带有 Bash 语法的 web-tree-sitter 进行解析，这产生一个抽象语法树（AST），可以分析命令模式、参数结构和潜在的安全问题。解析器被延迟初始化以提高启动性能。</p><p>来源: bash.ts</p><h3 id="权限类别"><a href="#权限类别" class="headerlink" title="权限类别"></a>权限类别</h3><p>系统区分两种主要的权限类型：</p><ol><li>bash Permission: 控制 Agent 可以执行哪些命令模式</li><li>external_directory Permission: 控制对项目根目录之外目录的访问</li></ol><p>在分析命令时，系统从 AST 中提取所有命令节点并对它们进行分类：</p><p>来源: bash.ts</p><h3 id="命令模式匹配"><a href="#命令模式匹配" class="headerlink" title="命令模式匹配"></a>命令模式匹配</h3><p>BashArity 系统通过与常见 CLI 工具及其命令结构的综合字典匹配，来识别人类可理解的命令前缀。这使得权限系统能够理解 <code>npm run dev</code> 和 <code>npm run build</code> 应该被分别跟踪，而 <code>npm install package-a</code> 和 <code>npm install package-b</code> 共享相同的权限模式。</p><p>来源: arity.ts</p><div class="table-container"><table><thead><tr><th>命令前缀</th><th>Arity</th><th>示例命令</th></tr></thead><tbody><tr><td>git</td><td>2</td><td>git checkout main, git log</td></tr><tr><td>git checkout</td><td>2</td><td>与 git 相同</td></tr><tr><td>npm run</td><td>3</td><td>npm run dev, npm run build</td></tr><tr><td>docker compose</td><td>3</td><td>docker compose up, docker compose down</td></tr><tr><td>kubectl rollout</td><td>3</td><td>kubectl rollout restart deploy</td></tr></tbody></table></div><p>Arity 系统遵循<strong>最长匹配前缀优先</strong>的策略，确保特定的子命令（如 <code>npm run</code>）与其父命令具有不同的权限模式。</p><p>权限系统生成特定模式（精确命令匹配）和通配符模式（基于前缀的匹配）。特定模式触发初始权限请求，而通配符模式为类似的未来命令启用“始终允许”功能。这种双重模式方法在安全性和用户便利性之间取得了平衡。</p><h2 id="执行模型和进程管理"><a href="#执行模型和进程管理" class="headerlink" title="执行模型和进程管理"></a>执行模型和进程管理</h2><p>一旦获得权限，Bash 工具使用 Node 的 spawn 函数生成子进程，并精心配置选项以确保安全性和可观察性。</p><p>来源: bash.ts</p><h3 id="进程配置"><a href="#进程配置" class="headerlink" title="进程配置"></a>进程配置</h3><div class="table-container"><table><thead><tr><th>参数</th><th>目的</th><th>实现</th></tr></thead><tbody><tr><td>shell</td><td>Shell 解释器</td><td>通过 Shell.acceptable() 检测</td></tr><tr><td>cwd</td><td>工作目录</td><td>项目目录或指定的 workdir</td></tr><tr><td>env</td><td>环境变量</td><td>父进程环境</td></tr><tr><td>stdio</td><td>流配置</td><td>[“ignore”, “pipe”, “pipe”] 仅用于 stdout/stderr</td></tr><tr><td>detached</td><td>进程组创建</td><td>在非 Windows 上启用以便正确清理</td></tr></tbody></table></div><p>Detached 模式在类 Unix 系统上尤为重要，它支持基于进程组的清理，当父进程被终止时，可以终止整个命令树（包括子进程）。</p><p>来源: bash.ts</p><h3 id="输出流和元数据"><a href="#输出流和元数据" class="headerlink" title="输出流和元数据"></a>输出流和元数据</h3><p>命令输出通过双路径机制实时流式传输：</p><ol><li>累积输出: 所有 stdout/stderr 被累积用于最终返回值</li><li>元数据更新: 通过元数据接口发送给 Agent 的渐进式更新</li></ol><p>这种方法提供了命令进度的即时可见性，同时确保完整输出可用于后续分析。元数据被截断为 30,000 个字符，以防止压垮 Agent 的上下文窗口。</p><p>来源: bash.ts</p><h3 id="超时和终止"><a href="#超时和终止" class="headerlink" title="超时和终止"></a>超时和终止</h3><p>Bash 工具实现了强大的超时和终止机制：</p><p>超时机制使用一个触发进程终止的 setTimeout，而中止信号连接到提供的 AbortSignal。进程终止使用 Shell.killTree()，它实现了特定于平台的清理逻辑：</p><ul><li>类 Unix: 向进程组发送 SIGTERM，等待 200ms，如有必要再发送 SIGKILL</li><li>Windows: 使用 <code>taskkill /pid &lt;pid&gt; /f /t</code> 强制终止进程树</li></ul><p>来源: bash.ts, shell.ts</p><p>200ms 的终止超时是经过精心选择的平衡：它在大多数情况下为优雅关闭提供足够的时间，同时仍确保在进程不响应 SIGTERM 时及时清理。此超时可通过 timeout 参数按命令配置。</p><h2 id="工具参数和用法"><a href="#工具参数和用法" class="headerlink" title="工具参数和用法"></a>工具参数和用法</h2><p>Bash 工具接受几个控制命令执行行为的参数：</p><p>来源: bash.ts</p><div class="table-container"><table><thead><tr><th>参数</th><th>类型</th><th>必需</th><th>描述</th></tr></thead><tbody><tr><td>command</td><td>string</td><td>是</td><td>要执行的 Shell 命令</td></tr><tr><td>timeout</td><td>number</td><td>否</td><td>超时时间（毫秒，默认: 120,000）</td></tr><tr><td>workdir</td><td>string</td><td>否</td><td>工作目录（默认: 项目目录）</td></tr><tr><td>description</td><td>string</td><td>是</td><td>命令的简短描述（5-10 个词）</td></tr></tbody></table></div><h3 id="工具使用的最佳实践"><a href="#工具使用的最佳实践" class="headerlink" title="工具使用的最佳实践"></a>工具使用的最佳实践</h3><p>系统在其工具描述中提供了广泛的指导，强调了几个关键模式：</p><ol><li><p><strong>目录管理</strong>: 使用 workdir 参数而不是 cd 命令。这避免了权限歧义并提供更好的跟踪。</p><ul><li>Good: <code>workdir=&quot;/foo/bar&quot; command=&quot;pytest tests&quot;</code></li><li>Bad: <code>command=&quot;cd /foo/bar &amp;&amp; pytest tests&quot;</code></li></ul></li><li><p><strong>命令链接</strong>: 对依赖命令使用 <code>&amp;&amp;</code>，对独立命令使用 <code>;</code>，对真正独立的操作使用并行的工具调用。</p><ul><li>Sequential (dependent): <code>command=&quot;mkdir build &amp;&amp; cd build &amp;&amp; cmake ..&quot;</code></li><li>Sequential (independent): <code>command=&quot;npm install; npm run build&quot;</code></li><li>Parallel: Separate Bash tool calls in a single message</li></ul></li><li><p><strong>引用</strong>: 始终用双引号引用包含空格的路径。</p></li></ol><p>来源: bash.txt</p><h2 id="交互式终端会话"><a href="#交互式终端会话" class="headerlink" title="交互式终端会话"></a>交互式终端会话</h2><p>除了供 Agent 使用的一次性 Bash 工具外，OpenCode 还通过其 PTY（伪终端）系统提供持久的交互式终端会话。这使用户能够维护具有完整终端仿真的长生命周期的 Shell 会话。</p><h3 id="PTY-会话管理"><a href="#PTY-会话管理" class="headerlink" title="PTY 会话管理"></a>PTY 会话管理</h3><p>PTY 系统管理终端会话的生命周期状态：</p><p>每个会话为断开连接的客户端维护自己的缓冲区，并支持多个并发的 WebSocket 订阅者。缓冲区限制为 2MB 以防止过多的内存使用。</p><p>来源: <a href="/packages/opencode/src/pty/index.ts#L15-L16, #L66-L154">pty/index.ts</a></p><h3 id="会话能力"><a href="#会话能力" class="headerlink" title="会话能力"></a>会话能力</h3><div class="table-container"><table><thead><tr><th>功能</th><th>描述</th></tr></thead><tbody><tr><td>缓冲区管理</td><td>2MB 限制，64KB 分块传输</td></tr><tr><td>多订阅者</td><td>多个 WebSocket 客户端可以连接到同一个 PTY</td></tr><tr><td>进程组</td><td>会话终止时正确的进程树清理</td></tr><tr><td>终端仿真</td><td>xterm-256color 支持丰富的终端功能</td></tr><tr><td>动态调整大小</td><td>运行时终端大小调整</td></tr></tbody></table></div><h3 id="前端集成"><a href="#前端集成" class="headerlink" title="前端集成"></a>前端集成</h3><p>Web 应用程序提供了一个终端组件，通过 WebSockets 连接到 PTY 会话，支持具有主题、序列化和调整大小处理的完整终端仿真。</p><p>来源: <a href="/packages/app/src/components/terminal.tsx#L1-L50, /packages/app/src/context/terminal.tsx#L1-L100">terminal.tsx</a></p><h2 id="安全考虑"><a href="#安全考虑" class="headerlink" title="安全考虑"></a>安全考虑</h2><p>Bash 集成实现了多层安全性，以防止滥用和保护系统资源：</p><h3 id="权限执行"><a href="#权限执行" class="headerlink" title="权限执行"></a>权限执行</h3><ul><li>基于模式的授权: 命令必须匹配批准的模式才能执行</li><li>目录边界: 访问外部目录需要单独的权限授予</li><li>始终允许机制: 用户可以预先批准命令前缀以方便使用，而不影响安全性</li></ul><h3 id="资源保护"><a href="#资源保护" class="headerlink" title="资源保护"></a>资源保护</h3><ul><li>超时限制: 命令在配置的超时后自动终止</li><li>输出截断: 大输出被截断以防止内存耗尽</li><li>进程清理: 正确的进程树终止防止僵尸进程</li></ul><h3 id="平台特定的保护"><a href="#平台特定的保护" class="headerlink" title="平台特定的保护"></a>平台特定的保护</h3><p>系统考虑了进程管理中的平台差异：</p><ul><li>Windows: 使用 taskkill 进行进程树终止</li><li>类 Unix: 使用进程组和 SIGTERM/SIGKILL 升级</li><li>路径规范化: 在 Windows 上将 Git Bash Unix 风格路径转换为 Windows 格式</li></ul><p>来源: <a href="/packages/opencode/src/tool/bash.ts#L116-L134, #L217-L259">bash.ts</a></p><h2 id="测试和验证"><a href="#测试和验证" class="headerlink" title="测试和验证"></a>测试和验证</h2><p>Bash 集成包括跨多个场景的全面测试覆盖：</p><p>来源: bash.test.ts</p><div class="table-container"><table><thead><tr><th>测试类别</th><th>示例</th></tr></thead><tbody><tr><td>基本执行</td><td>简单的 echo 命令，退出代码验证</td></tr><tr><td>权限请求</td><td>单个和多个命令的模式匹配</td></tr><tr><td>目录检测</td><td>cd ../ 和 /tmp workdir 的外部目录权限</td></tr><tr><td>项目边界</td><td>项目目录内操作不需要外部权限</td></tr><tr><td>自动批准</td><td>类似命令的“始终”模式</td></tr></tbody></table></div><p>测试策略验证了快乐路径（具有适当权限的成功执行）和边缘情况（目录遍历、命令链接、权限边界）。</p><h2 id="与工具系统集成"><a href="#与工具系统集成" class="headerlink" title="与工具系统集成"></a>与工具系统集成</h2><p>Bash 工具与其他内置工具一起在中央工具注册表中注册，可供所有支持工具使用的 Agent 使用。</p><p>来源: registry.ts</p><h3 id="工具注册表位置"><a href="#工具注册表位置" class="headerlink" title="工具注册表位置"></a>工具注册表位置</h3><p>Bash 工具出现在工具列表的早期，反映了其基本重要性。它始终启用（不像 LSP 或 Batch 等实验性工具），表明其作为核心能力而不是可选功能的角色。</p><h3 id="工具定义结构"><a href="#工具定义结构" class="headerlink" title="工具定义结构"></a>工具定义结构</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  id: &quot;bash&quot;,</span><br><span class="line">  description: Tool description with dynamic parameters,</span><br><span class="line">  parameters: Zod schema for validation,</span><br><span class="line">  execute: Async function implementing command lifecycle</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此结构遵循标准工具契约，在 Agent 框架内实现一致的处理。</p><h2 id="高级功能"><a href="#高级功能" class="headerlink" title="高级功能"></a>高级功能</h2><h3 id="命令分析实现"><a href="#命令分析实现" class="headerlink" title="命令分析实现"></a>命令分析实现</h3><p>基于 tree-sitter 的命令分析通过遍历 AST 和识别特定节点类型来提取文件操作并构建权限请求：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> node <span class="keyword">of</span> tree.rootNode.descendantsOfType(<span class="string">&quot;command&quot;</span>)) &#123;</span><br><span class="line">  <span class="comment">// Extract command components</span></span><br><span class="line">  <span class="comment">// Check if file operation (cd, rm, cp, mv, etc.)</span></span><br><span class="line">  <span class="comment">// Resolve file paths and check boundaries</span></span><br><span class="line">  <span class="comment">// Generate permission patterns</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源: bash.ts</p><h3 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h3><p>系统通过结果元数据提供详细的错误信息：</p><ul><li>Timeout: 超过超时时的清晰指示</li><li>Aborted: 用户发起的取消通知</li><li>Exit Codes: 元数据中可用的进程退出代码</li><li>Truncated Output: 为元数据截断输出时的指示器</li></ul><p>来源: bash.ts</p><h3 id="Git-集成"><a href="#Git-集成" class="headerlink" title="Git 集成"></a>Git 集成</h3><p>工具描述包括 Git 操作的广泛指导，包括：</p><ul><li>基于仓库历史记录起草提交消息</li><li>破坏性操作的安全协议</li><li>Pre-commit hook 处理</li><li>使用 gh CLI 创建 Pull Request 的工作流</li></ul><p>这种专门的指导使 Agent 能够有效地参与版本控制工作流，同时遵守仓库约定。</p><p>来源: bash.txt</p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>Bash 集成系统代表了一种安全地将 AI Agent 与 Shell 命令执行连接起来的复杂方法。通过结合基于 AST 的命令分析、细粒度的权限控制、强大的进程管理和实时输出流，它为 Agent 通过终端操作与系统交互提供了安全的基础。</p><p>系统的架构反映了 OpenCode 的核心原则：通过解析确保安全，通过权限确保安全，通过流输出和智能默认值确保可用性。当你探索更高级的工具使用时，请考虑查看 权限系统 以深入了解权限如何在平台上工作，或 文件操作 以了解何时应优先使用 Bash 命令而不是专门的文件工具。</p><p>对于希望扩展系统的开发者，开发自定义工具 文档提供了实现遵循相同安全模型的额外命令执行工具的模式。</p><h1 id="开发自定义工具：工具定义与实现"><a href="#开发自定义工具：工具定义与实现" class="headerlink" title="开发自定义工具：工具定义与实现"></a>开发自定义工具：工具定义与实现</h1><p>自定义工具通过添加针对特定工作流、领域知识或集成需求的专业能力，扩展了 OpenCode Agent 系统。工具是模块化、经过验证的函数，Agent 在其问题解决过程中可以调用这些函数，从而提供对系统资源和外部服务的受控访问。</p><h2 id="工具架构概述-1"><a href="#工具架构概述-1" class="headerlink" title="工具架构概述"></a>工具架构概述</h2><p>工具系统采用基于插件的架构，其中工具通过中央注册表被发现、初始化并提供给 Agent。每个工具定义其输入架构、描述和执行逻辑，使 Agent 能够理解何时以及如何有效地使用它。</p><p>工具与多个系统组件集成，包括用于安全检查的权限系统、用于代码智能的 LSP、用于事件通信的总线以及用于上下文管理的会话系统。这种集成确保工具在当前 Agent 和会话上下文的安全边界内运行。</p><p>来源：tool.ts，registry.ts</p><h2 id="工具定义结构-1"><a href="#工具定义结构-1" class="headerlink" title="工具定义结构"></a>工具定义结构</h2><p>核心工具定义使用 TypeScript 泛型和 Zod 架构进行类型安全的参数验证。Tool.define() 函数创建工具定义，包含自动参数解析、验证错误格式化和输出截断功能。</p><h3 id="工具接口"><a href="#工具接口" class="headerlink" title="工具接口"></a>工具接口</h3><p>Tool.Info 接口定义了所有工具的契约：</p><div class="table-container"><table><thead><tr><th>属性</th><th>类型</th><th>目的</th></tr></thead><tbody><tr><td>id</td><td>string</td><td>工具的唯一标识符</td></tr><tr><td>init</td><td>function</td><td>返回工具元数据的异步初始化</td></tr><tr><td>description</td><td>string</td><td>面向 Agent 的工具用途描述</td></tr><tr><td>parameters</td><td>ZodType</td><td>用于输入验证的 Zod 架构</td></tr><tr><td>execute</td><td>function</td><td>返回结果的核心执行逻辑</td></tr><tr><td>formatValidationError</td><td>function?</td><td>用于无效输入的自定义错误格式化</td></tr></tbody></table></div><h3 id="执行上下文"><a href="#执行上下文" class="headerlink" title="执行上下文"></a>执行上下文</h3><p>工具接收一个丰富的上下文对象，提供对会话状态和系统功能的访问：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Context&lt;M <span class="keyword">extends</span> Metadata = Metadata&gt; = &#123;</span><br><span class="line">  sessionID: <span class="built_in">string</span>          <span class="comment">// 当前会话标识符</span></span><br><span class="line">  messageID: <span class="built_in">string</span>          <span class="comment">// 当前消息标识符  </span></span><br><span class="line">  agent: <span class="built_in">string</span>              <span class="comment">// 调用工具的 Agent</span></span><br><span class="line">  abort: AbortSignal         <span class="comment">// 取消信号</span></span><br><span class="line">  callID?: <span class="built_in">string</span>           <span class="comment">// 唯一调用标识符</span></span><br><span class="line">  extra?: &#123; [key: <span class="built_in">string</span>]: <span class="built_in">any</span> &#125;  <span class="comment">// 附加元数据</span></span><br><span class="line">  metadata: <span class="function">(<span class="params">input: &#123; title?: <span class="built_in">string</span>; metadata?: M &#125;</span>) =&gt;</span> <span class="built_in">void</span>  <span class="comment">// 更新工具元数据</span></span><br><span class="line">  ask: <span class="function">(<span class="params">input: PermissionRequest</span>) =&gt;</span> <span class="built_in">Promise</span>&lt;<span class="built_in">void</span>&gt;  <span class="comment">// 请求权限</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>metadata() 函数允许工具在执行期间更新其结果元数据，而 ask() 方法则支持在敏感操作之前进行权限检查。</p><p>来源：tool.ts</p><h2 id="创建自定义工具"><a href="#创建自定义工具" class="headerlink" title="创建自定义工具"></a>创建自定义工具</h2><h3 id="基本工具定义"><a href="#基本工具定义" class="headerlink" title="基本工具定义"></a>基本工具定义</h3><p>最简单的工具定义遵循以下模式：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; Tool &#125; <span class="keyword">from</span> <span class="string">&quot;./tool&quot;</span></span><br><span class="line"><span class="keyword">import</span> z <span class="keyword">from</span> <span class="string">&quot;zod&quot;</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> MyCustomTool = Tool.define(<span class="string">&quot;my_custom_tool&quot;</span>, &#123;</span><br><span class="line">  description: <span class="string">&quot;对用户数据执行专门操作&quot;</span>,</span><br><span class="line">  parameters: z.object(&#123;</span><br><span class="line">    input: z.string().describe(<span class="string">&quot;主要输入数据&quot;</span>),</span><br><span class="line">    options: z.record(z.string()).optional().describe(<span class="string">&quot;可选配置&quot;</span>),</span><br><span class="line">  &#125;),</span><br><span class="line">  <span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 工具逻辑在这里</span></span><br><span class="line">    <span class="keyword">const</span> result = processInput(params.input, params.options)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      title: <span class="string">&quot;自定义操作已完成&quot;</span>,</span><br><span class="line">      output: <span class="built_in">JSON</span>.stringify(result),</span><br><span class="line">      metadata: &#123;</span><br><span class="line">        processedAt: <span class="keyword">new</span> <span class="built_in">Date</span>().toISOString(),</span><br><span class="line">        inputLength: params.input.length</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>Tool.define() 包装器自动处理参数验证和错误格式化。当 Agent 使用无效参数调用工具时，系统会提供描述验证失败的清晰错误消息。</p><p>来源：tool.ts</p><h3 id="工具结果结构"><a href="#工具结果结构" class="headerlink" title="工具结果结构"></a>工具结果结构</h3><p>工具必须返回特定的结果结构：</p><div class="table-container"><table><thead><tr><th>字段</th><th>类型</th><th>必需</th><th>描述</th></tr></thead><tbody><tr><td>title</td><td>string</td><td>是</td><td>操作的可读摘要</td></tr><tr><td>output</td><td>string</td><td>是</td><td>主要输出内容（返回给 Agent）</td></tr><tr><td>metadata</td><td>Metadata</td><td>是</td><td>关于结果的结构化元数据</td></tr><tr><td>attachments</td><td>FilePart[]</td><td>否</td><td>用于丰富结果的可选文件附件</td></tr></tbody></table></div><p>元数据字段是可扩展的，并且当输出超过限制时会自动包含截断信息。工具可以添加自定义元数据字段以进行跟踪、调试或提供额外上下文。</p><p>来源：tool.ts</p><h2 id="高级工具功能"><a href="#高级工具功能" class="headerlink" title="高级工具功能"></a>高级工具功能</h2><h3 id="异步初始化"><a href="#异步初始化" class="headerlink" title="异步初始化"></a>异步初始化</h3><p>工具可以通过向 Tool.define() 提供函数而不是对象来执行异步初始化工作：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> AsyncTool = Tool.define(<span class="string">&quot;async_tool&quot;</span>, <span class="keyword">async</span> (initCtx) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 根据 Agent 或环境执行异步设置</span></span><br><span class="line">  <span class="keyword">const</span> capabilities = <span class="keyword">await</span> detectCapabilities(initCtx?.agent)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    description: <span class="string">`具有以下功能的工具: <span class="subst">$&#123;capabilities.join(<span class="string">&quot;, &quot;</span>)&#125;</span>`</span>,</span><br><span class="line">    parameters: z.object(&#123; <span class="comment">/* schema */</span> &#125;),</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123;</span><br><span class="line">      <span class="comment">// 执行逻辑</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>InitContext 提供可选的 Agent 信息，使工具能够根据使用它们的 Agent 自定义其行为。这对于具有权限感知的工具或特定于 Agent 的优化非常有用。</p><p>来源：skill.ts</p><h3 id="权限请求"><a href="#权限请求" class="headerlink" title="权限请求"></a>权限请求</h3><p>工具可以使用 ask() 方法在敏感操作之前请求权限：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123;</span><br><span class="line">  <span class="keyword">await</span> ctx.ask(&#123;</span><br><span class="line">    permission: <span class="string">&quot;read&quot;</span>,</span><br><span class="line">    patterns: [<span class="string">&quot;/etc/passwd&quot;</span>],</span><br><span class="line">    always: [<span class="string">&quot;*&quot;</span>],</span><br><span class="line">    metadata: &#123;&#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 继续执行敏感操作</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>权限系统支持基于模式的访问控制，允许工具指定它们需要访问的资源。系统在允许执行之前，会根据 Agent 的权限配置评估这些请求。</p><p>来源：read.ts</p><h3 id="输出截断"><a href="#输出截断" class="headerlink" title="输出截断"></a>输出截断</h3><p>大型输出会通过可配置的限制自动截断。工具可以通过元数据控制截断行为：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> &#123;</span><br><span class="line">  title: <span class="string">&quot;结果&quot;</span>,</span><br><span class="line">  output: largeContent,</span><br><span class="line">  metadata: &#123;</span><br><span class="line">    truncated: <span class="literal">undefined</span>  <span class="comment">// 让系统处理截断</span></span><br><span class="line">    <span class="comment">// 或者</span></span><br><span class="line">    truncated: <span class="literal">true</span>,      <span class="comment">// 工具处理了自己的截断</span></span><br><span class="line">    outputPath: <span class="string">&quot;/path/to/full/output&quot;</span>  <span class="comment">// 可选引用</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当超过限制时，截断系统会将完整输出保留在文件中，允许 Agent 使用偏移量/限制参数读取特定部分。这确保 Agent 可以访问大型结果而不会淹没上下文窗口。</p><p>来源：bash.ts，truncation.ts</p><h2 id="工具注册方法"><a href="#工具注册方法" class="headerlink" title="工具注册方法"></a>工具注册方法</h2><h3 id="方法-1：项目目录工具"><a href="#方法-1：项目目录工具" class="headerlink" title="方法 1：项目目录工具"></a>方法 1：项目目录工具</h3><p>将工具文件放在项目内的 tool/ 子目录中：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">project&#x2F;</span><br><span class="line">├── tool&#x2F;</span><br><span class="line">│   ├── my_tool.ts</span><br><span class="line">│   └── another_tool.ts</span><br><span class="line">├── src&#x2F;</span><br><span class="line">└── package.json</span><br></pre></td></tr></table></figure><p>每个文件应导出工具作为命名导出：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// tool/my_tool.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; Tool &#125; <span class="keyword">from</span> <span class="string">&quot;@opencode-ai/tool&quot;</span></span><br><span class="line"><span class="keyword">import</span> z <span class="keyword">from</span> <span class="string">&quot;zod&quot;</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> myTool = Tool.define(<span class="string">&quot;my_tool&quot;</span>, &#123;</span><br><span class="line">  description: <span class="string">&quot;一个自定义项目工具&quot;</span>,</span><br><span class="line">  parameters: z.object(&#123; <span class="comment">/* ... */</span> &#125;),</span><br><span class="line">  <span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 实现</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  myTool</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注册表使用匹配配置目录中 tool/*.{js,ts} 文件的 glob 模式自动发现这些工具。工具在初始化时加载，并可供所有 Agent 使用。</p><p>来源：registry.ts</p><h3 id="方法-2：插件系统"><a href="#方法-2：插件系统" class="headerlink" title="方法 2：插件系统"></a>方法 2：插件系统</h3><p>创建一个导出工具定义的 npm 包：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// my-plugin/src/index.ts</span></span><br><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; PluginInput, Hooks &#125; <span class="keyword">from</span> <span class="string">&quot;@opencode-ai/plugin&quot;</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">myPlugin</span>(<span class="params">input: PluginInput</span>): <span class="title">Hooks</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">tool</span>(<span class="params">def, ctx</span>)</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        my_plugin_tool: &#123;</span><br><span class="line">          description: <span class="string">&quot;由 my-plugin 提供的工具&quot;</span>,</span><br><span class="line">          args: &#123;</span><br><span class="line">            input: &#123; <span class="attr">type</span>: <span class="string">&quot;string&quot;</span> &#125;</span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">args, ctx</span>)</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="string">&quot;工具执行结果&quot;</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过配置安装插件：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// opencode.config.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  plugin: [</span><br><span class="line">    <span class="string">&quot;my-plugin@latest&quot;</span></span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>插件系统提供对项目上下文、服务器客户端和执行环境的访问。插件可以导出多个工具，并与身份验证和事件处理等其他钩子点集成。</p><p>来源：plugin/index.ts，registry.ts</p><h3 id="方法-3：编程式注册"><a href="#方法-3：编程式注册" class="headerlink" title="方法 3：编程式注册"></a>方法 3：编程式注册</h3><p>直接使用 ToolRegistry.register() 函数注册工具：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; ToolRegistry &#125; <span class="keyword">from</span> <span class="string">&quot;@opencode-ai/tool&quot;</span></span><br><span class="line"><span class="keyword">import</span> &#123; Tool &#125; <span class="keyword">from</span> <span class="string">&quot;@opencode-ai/tool&quot;</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">const</span> customTool = Tool.define(<span class="string">&quot;custom&quot;</span>, &#123;</span><br><span class="line">  description: <span class="string">&quot;以编程方式注册的工具&quot;</span>,</span><br><span class="line">  parameters: z.object(&#123; <span class="comment">/* ... */</span> &#125;),</span><br><span class="line">  <span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 实现</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">await</span> ToolRegistry.register(customTool)</span><br></pre></td></tr></table></figure><p>此方法适用于需要有条件地注册或基于运行时配置注册的工具。具有相同 ID 的现有工具将被替换，从而允许工具覆盖。</p><p>来源：registry.ts</p><h2 id="内置工具示例"><a href="#内置工具示例" class="headerlink" title="内置工具示例"></a>内置工具示例</h2><h3 id="文件读取工具"><a href="#文件读取工具" class="headerlink" title="文件读取工具"></a>文件读取工具</h3><p>ReadTool 演示了具有权限检查和二进制文件处理的文件访问：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> ReadTool = Tool.define(<span class="string">&quot;read&quot;</span>, &#123;</span><br><span class="line">  description: DESCRIPTION,</span><br><span class="line">  parameters: z.object(&#123;</span><br><span class="line">    filePath: z.string().describe(<span class="string">&quot;要读取的文件路径&quot;</span>),</span><br><span class="line">    offset: z.coerce.number().describe(<span class="string">&quot;开始读取的行号（从0开始）&quot;</span>).optional(),</span><br><span class="line">    limit: z.coerce.number().describe(<span class="string">&quot;要读取的行数（默认为 2000）&quot;</span>).optional(),</span><br><span class="line">  &#125;),</span><br><span class="line">  <span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 路径解析和验证</span></span><br><span class="line">    <span class="keyword">let</span> filepath = params.filePath</span><br><span class="line">    <span class="keyword">if</span> (!path.isAbsolute(filepath)) &#123;</span><br><span class="line">      filepath = path.join(process.cwd(), filepath)</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 权限请求</span></span><br><span class="line">    <span class="keyword">await</span> ctx.ask(&#123;</span><br><span class="line">      permission: <span class="string">&quot;read&quot;</span>,</span><br><span class="line">      patterns: [filepath],</span><br><span class="line">      always: [<span class="string">&quot;*&quot;</span>],</span><br><span class="line">      metadata: &#123;&#125;</span><br><span class="line">    &#125;)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 带有二进制检测的文件读取</span></span><br><span class="line">    <span class="keyword">const</span> file = Bun.file(filepath)</span><br><span class="line">    <span class="keyword">if</span> (!(<span class="keyword">await</span> file.exists())) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">`File not found: <span class="subst">$&#123;filepath&#125;</span>`</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 内容读取和格式化</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>ReadTool 包含复杂的错误处理，为拼写错误提供文件未找到建议，并支持图像/PDF 预览附件。</p><p>来源：read.ts</p><h3 id="Bash-执行工具"><a href="#Bash-执行工具" class="headerlink" title="Bash 执行工具"></a>Bash 执行工具</h3><p>BashTool 演示了命令解析、权限推断和安全协议：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> BashTool = Tool.define(<span class="string">&quot;bash&quot;</span>, <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> shell = Shell.acceptable()</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    description: DESCRIPTION,</span><br><span class="line">    parameters: z.object(&#123;</span><br><span class="line">      command: z.string().describe(<span class="string">&quot;要执行的命令&quot;</span>),</span><br><span class="line">      timeout: z.number().describe(<span class="string">&quot;可选超时（毫秒）&quot;</span>).optional(),</span><br><span class="line">      workdir: z.string().describe(<span class="string">&quot;运行命令的工作目录&quot;</span>).optional(),</span><br><span class="line">      description: z.string().describe(<span class="string">&quot;对该命令作用的清晰简明描述（5-10个词）&quot;</span>).optional(),</span><br><span class="line">    &#125;),</span><br><span class="line">    <span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123;</span><br><span class="line">      <span class="comment">// 使用 tree-sitter 解析命令</span></span><br><span class="line">      <span class="keyword">const</span> tree = <span class="keyword">await</span> parser().then(<span class="function">(<span class="params">p</span>) =&gt;</span> p.parse(params.command))</span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 从命令结构推断权限</span></span><br><span class="line">      <span class="keyword">const</span> directories = <span class="keyword">new</span> <span class="built_in">Set</span>&lt;<span class="built_in">string</span>&gt;()</span><br><span class="line">      <span class="keyword">const</span> patterns = <span class="keyword">new</span> <span class="built_in">Set</span>&lt;<span class="built_in">string</span>&gt;()</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">const</span> node <span class="keyword">of</span> tree.rootNode.descendantsOfType(<span class="string">&quot;command&quot;</span>)) &#123;</span><br><span class="line">        <span class="comment">// 分析命令参数以获取文件/目录引用</span></span><br><span class="line">      &#125;</span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 请求推断的权限</span></span><br><span class="line">      <span class="keyword">await</span> ctx.ask(&#123;</span><br><span class="line">        permission: <span class="string">&quot;bash&quot;</span>,</span><br><span class="line">        patterns: <span class="built_in">Array</span>.from(patterns),</span><br><span class="line">        always: [<span class="string">&quot;*&quot;</span>],</span><br><span class="line">        metadata: &#123;</span><br><span class="line">          directories: <span class="built_in">Array</span>.from(directories)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 执行并处理超时</span></span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>BashTool 使用 tree-sitter 解析根据命令结构智能推断所需权限，从而实现细粒度的安全性，而无需为每个命令手动指定权限。</p><p>来源：bash.ts</p><h2 id="最佳实践-1"><a href="#最佳实践-1" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="参数设计"><a href="#参数设计" class="headerlink" title="参数设计"></a>参数设计</h3><p>使用具有清晰描述性的 Zod 架构：</p><div class="table-container"><table><thead><tr><th>实践</th><th>示例</th><th>基本原理</th></tr></thead><tbody><tr><td>使用特定类型</td><td>z.coerce.number()</td><td>自动类型转换</td></tr><tr><td>提供描述</td><td>.describe(“文件的绝对路径”)</td><td>Agent 理解</td></tr><tr><td>标记可选字段</td><td>.optional()</td><td>灵活调用</td></tr><tr><td>验证约束</td><td>.max(1000)</td><td>早期错误检测</td></tr></tbody></table></div><h3 id="错误处理-1"><a href="#错误处理-1" class="headerlink" title="错误处理"></a>错误处理</h3><p>提供上下文丰富的错误消息：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> riskyOperation()</span><br><span class="line">  <span class="keyword">return</span> &#123; <span class="attr">title</span>: <span class="string">&quot;成功&quot;</span>, <span class="attr">output</span>: result, <span class="attr">metadata</span>: &#123;&#125; &#125;</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">  <span class="keyword">if</span> (error <span class="keyword">instanceof</span> NotFoundError) &#123;</span><br><span class="line">    <span class="keyword">const</span> suggestions = <span class="keyword">await</span> findAlternatives(error.resource)</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(</span><br><span class="line">      <span class="string">`资源未找到: <span class="subst">$&#123;error.resource&#125;</span>\n\n`</span> +</span><br><span class="line">      <span class="string">`你的意思是这些中的一个吗?\n<span class="subst">$&#123;suggestions.join(<span class="string">&quot;\n&quot;</span>)&#125;</span>`</span></span><br><span class="line">    )</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">throw</span> error</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="性能考虑-1"><a href="#性能考虑-1" class="headerlink" title="性能考虑"></a>性能考虑</h3><ol><li>使用取消信号：对于长时间运行的操作，检查 ctx.abort</li><li>最小化输出：对于大型结果使用 limit 和 offset</li><li>批量操作：尽可能组合多个操作</li><li>缓存结果：在元数据中存储昂贵的计算以供重用</li></ol><p>来源：read.ts</p><p>在处理之前始终验证外部输入。使用 Zod 的内置验证进行参数验证，并添加对文件路径、URL 或其他外部资源的额外验证，以防止安全漏洞和注入攻击。</p><h2 id="完整示例：天气工具"><a href="#完整示例：天气工具" class="headerlink" title="完整示例：天气工具"></a>完整示例：天气工具</h2><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; Tool &#125; <span class="keyword">from</span> <span class="string">&quot;./tool&quot;</span></span><br><span class="line"><span class="keyword">import</span> z <span class="keyword">from</span> <span class="string">&quot;zod&quot;</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">interface</span> WeatherMetadata &#123;</span><br><span class="line">  location: <span class="built_in">string</span></span><br><span class="line">  timestamp: <span class="built_in">string</span></span><br><span class="line">  source: <span class="built_in">string</span></span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> WeatherTool = Tool.define(</span><br><span class="line">  <span class="string">&quot;weather&quot;</span>,</span><br><span class="line">  <span class="keyword">async</span> (initCtx) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 可以在这里加载 API 密钥或配置</span></span><br><span class="line">    <span class="keyword">const</span> apiKey = process.env.WEATHER_API_KEY</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      description: <span class="string">&quot;获取给定位置的当前天气信息&quot;</span>,</span><br><span class="line">      parameters: z.object(&#123;</span><br><span class="line">        location: z.string().describe(<span class="string">&quot;城市名称或坐标（纬度,经度）&quot;</span>),</span><br><span class="line">        units: z.enum([<span class="string">&quot;celsius&quot;</span>, <span class="string">&quot;fahrenheit&quot;</span>]).optional().describe(<span class="string">&quot;温度单位&quot;</span>),</span><br><span class="line">      &#125;),</span><br><span class="line">      <span class="function"><span class="title">formatValidationError</span>(<span class="params">error: z.ZodError</span>)</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> issues = error.issues.map(<span class="function"><span class="params">i</span> =&gt;</span> <span class="string">`- <span class="subst">$&#123;i.path.join(<span class="string">&quot;.&quot;</span>)&#125;</span>: <span class="subst">$&#123;i.message&#125;</span>`</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="string">`天气工具参数无效:\n<span class="subst">$&#123;issues.join(<span class="string">&quot;\n&quot;</span>)&#125;</span>`</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="keyword">async</span> execute(</span><br><span class="line">        params: z.infer&lt;<span class="keyword">typeof</span> <span class="built_in">this</span>.parameters&gt;,</span><br><span class="line">        ctx: Tool.Context&lt;WeatherMetadata&gt;</span><br><span class="line">      ) &#123;</span><br><span class="line">        <span class="comment">// 请求外部 API 调用的权限</span></span><br><span class="line">        <span class="keyword">await</span> ctx.ask(&#123;</span><br><span class="line">          permission: <span class="string">&quot;network&quot;</span>,</span><br><span class="line">          patterns: [<span class="string">`api.weather.com/*`</span>],</span><br><span class="line">          always: [],</span><br><span class="line">          metadata: &#123;&#125;</span><br><span class="line">        &#125;)</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 获取天气数据</span></span><br><span class="line">        <span class="keyword">const</span> response = <span class="keyword">await</span> fetch(</span><br><span class="line">          <span class="string">`https://api.weather.com/current?location=<span class="subst">$&#123;<span class="built_in">encodeURIComponent</span>(params.location)&#125;</span>&amp;units=<span class="subst">$&#123;params.units || <span class="string">&#x27;celsius&#x27;</span>&#125;</span>`</span>,</span><br><span class="line">          &#123; </span><br><span class="line">            headers: &#123; <span class="string">&#x27;Authorization&#x27;</span>: <span class="string">`Bearer <span class="subst">$&#123;process.env.WEATHER_API_KEY&#125;</span>`</span> &#125;,</span><br><span class="line">            signal: ctx.abort </span><br><span class="line">          &#125;</span><br><span class="line">        )</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (!response.ok) &#123;</span><br><span class="line">          <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">`天气 API 错误: <span class="subst">$&#123;response.status&#125;</span> <span class="subst">$&#123;response.statusText&#125;</span>`</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">const</span> data = <span class="keyword">await</span> response.json()</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 使用操作详细信息更新元数据</span></span><br><span class="line">        ctx.metadata(&#123;</span><br><span class="line">          title: <span class="string">`<span class="subst">$&#123;params.location&#125;</span> 的天气`</span>,</span><br><span class="line">          metadata: &#123;</span><br><span class="line">            location: params.location,</span><br><span class="line">            timestamp: <span class="keyword">new</span> <span class="built_in">Date</span>().toISOString(),</span><br><span class="line">            source: <span class="string">&quot;天气 API&quot;</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;)</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">          title: <span class="string">`<span class="subst">$&#123;params.location&#125;</span> 的当前天气`</span>,</span><br><span class="line">          output: <span class="built_in">JSON</span>.stringify(data, <span class="literal">null</span>, <span class="number">2</span>),</span><br><span class="line">          metadata: &#123;</span><br><span class="line">            location: params.location,</span><br><span class="line">            timestamp: <span class="keyword">new</span> <span class="built_in">Date</span>().toISOString(),</span><br><span class="line">            source: <span class="string">&quot;天气 API&quot;</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>这个完整的示例演示了：</p><ul><li>带环境配置的异步初始化</li><li>带枚举约束的参数验证</li><li>自定义错误格式化</li><li>外部网络访问的权限请求</li><li>取消信号支持</li><li>丰富的元数据跟踪</li><li>外部 API 集成</li></ul><h2 id="工具生命周期"><a href="#工具生命周期" class="headerlink" title="工具生命周期"></a>工具生命周期</h2><p>工具生命周期从定义开始，经过注册、被 Agent 发现、参数验证、权限检查、执行，最后到输出处理（包括必要时进行截断）。每个阶段都提供了自定义行为和错误处理的钩子。</p><p>来源：registry.ts，tool.ts</p>]]></content>
    
    
    <summary type="html">工具注册：内置工具与可扩展性
工具注册表是 OpenCode 系统中管理所有可用工具的核心组件，为内置工具和自定义工具提供了统一的接口。这种架构使 agents 能够通过一致的 API 访问多样化的功能集，同时通过插件和基于配置的工具定义支持可扩展性。

工具注册表架构
工具注册表采用集中式类插件架构，其中工具通过元数据、功能和权限要求进行注册。注册表维护两类工具：随 OpenCode 附带的内置工具，以及可由用户或第三方插件添加的自定义工具。

内置工具静态注册在 all() 函数中，包括文件操作、搜索功能、bash 执行和 web 交互等核心功能。自定义工具在运行时初始化期间从配置目录和插</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着OpenCode学智能体设计和开发1：Agent系统</title>
    <link href="http://qixinbo.github.io/2026/01/12/opencode-1/"/>
    <id>http://qixinbo.github.io/2026/01/12/opencode-1/</id>
    <published>2026-01-12T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<p>OpenCode Agent系统是一个多智能体架构，通过定义Agent结构，使用Task工具实现Agent间调用，集成Permission权限系统进行访问控制，通过Session会话处理器处理交互，并使用Tool工具系统提供可扩展能力。</p><h1 id="Agent-类型和模式：主-Agent、子-Agent-和隐藏-Agent"><a href="#Agent-类型和模式：主-Agent、子-Agent-和隐藏-Agent" class="headerlink" title="Agent 类型和模式：主 Agent、子 Agent 和隐藏 Agent"></a>Agent 类型和模式：主 Agent、子 Agent 和隐藏 Agent</h1><p>本部分解释了 OpenCode 中 Agent 的架构组织，涵盖了三种不同的 Agent 类型（主 Agent、子 Agent 和隐藏 Agent）、它们的运行模式、配置机制以及它们如何在会话管理系统内进行交互。</p><h2 id="架构概述"><a href="#架构概述" class="headerlink" title="架构概述"></a>架构概述</h2><p>OpenCode 的 Agent 系统围绕 <code>Agent.Info</code> 结构中 mode 字段定义的分层分类构建。此分类决定了 Agent 如何向用户展示、如何被调用以及需要什么权限。该系统通过专门处理开发工作不同方面的 Agent（从代码探索到任务执行和会话维护）来实现模块化的 AI 辅助。</p><p>来源: <code>agent.ts</code></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">namespace</span> Agent &#123;</span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">const</span> Info = z</span><br><span class="line">    .object(&#123;</span><br><span class="line">      name: z.string(),</span><br><span class="line">      description: z.string().optional(),</span><br><span class="line">      mode: z.enum([<span class="string">&quot;subagent&quot;</span>, <span class="string">&quot;primary&quot;</span>, <span class="string">&quot;all&quot;</span>]),</span><br><span class="line">      native: z.boolean().optional(),</span><br><span class="line">      hidden: z.boolean().optional(),</span><br><span class="line">      topP: z.number().optional(),</span><br><span class="line">      temperature: z.number().optional(),</span><br><span class="line">      color: z.string().optional(),</span><br><span class="line">      permission: PermissionNext.Ruleset,</span><br><span class="line">      model: z</span><br><span class="line">        .object(&#123;</span><br><span class="line">          modelID: z.string(),</span><br><span class="line">          providerID: z.string(),</span><br><span class="line">        &#125;)</span><br><span class="line">        .optional(),</span><br><span class="line">      prompt: z.string().optional(),</span><br><span class="line">      options: z.record(z.string(), z.any()),</span><br><span class="line">      steps: z.number().int().positive().optional(),</span><br><span class="line">    &#125;)</span><br><span class="line">    .meta(&#123;</span><br><span class="line">      ref: <span class="string">&quot;Agent&quot;</span>,</span><br><span class="line">    &#125;)</span><br></pre></td></tr></table></figure><h2 id="Agent-分类系统"><a href="#Agent-分类系统" class="headerlink" title="Agent 分类系统"></a>Agent 分类系统</h2><h3 id="主-Agent"><a href="#主-Agent" class="headerlink" title="主 Agent"></a>主 Agent</h3><p>主 Agent 作为 OpenCode 中用户交互的主要入口点。这些 Agent 可以通过用户界面直接访问，并可以被选为会话的活动 Agent。系统提供了两个内置的主 Agent：</p><p>构建 Agent (Build Agent)：默认 Agent，专用于执行修改代码库的任务。它拥有广泛的权限，并启用了问题审批机制，允许其在需要时请求用户确认。构建 Agent 可以读写文件、执行 bash 命令以及使用系统中的大多数工具。</p><p>计划 Agent (Plan Agent)：专注于创建和管理实施计划的专用 Agent。它具有受限的写入权限，仅允许修改 .opencode/plan/*.md 目录下的文件。这种设计鼓励 Agent 生成文档和计划，而不是直接修改源代码。</p><h3 id="子-Agent"><a href="#子-Agent" class="headerlink" title="子 Agent"></a>子 Agent</h3><p>子 Agent 是为被其他 Agent 调用（而非直接由用户调用）而设计的专用助手。它们处理更大工作流中的特定任务，使主 Agent 能够委派专门的工作。OpenCode 包含两个原生的子 Agent：</p><p>通用子 Agent (General Subagent)：用于执行并行工作单元和研究复杂问题的多用途助手。它无法读取或写入待办事项（todoread/todowrite 被拒绝），使其适合执行不影响项目跟踪系统的任务。</p><p>探索子 Agent (Explore Subagent)：专用于代码库探索的快速、专用 Agent。它擅长通过模式查找文件、搜索代码内容以及回答有关代码库的结构性问题。探索子 Agent 具有严格限制的权限——仅允许读取操作、grep、glob、列表、bash 命令和 Web 操作。这种集中的权限集确保了安全的探索，同时防止了意外的修改。<br>探索子Agent的Prompt在<code>explore.txt</code>文件中：</p><figure class="highlight md"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">You are a file search specialist. You excel at thoroughly navigating and exploring codebases.</span><br><span class="line"></span><br><span class="line">Your strengths:</span><br><span class="line"><span class="bullet">-</span> Rapidly finding files using glob patterns</span><br><span class="line"><span class="bullet">-</span> Searching code and text with powerful regex patterns</span><br><span class="line"><span class="bullet">-</span> Reading and analyzing file contents</span><br><span class="line"></span><br><span class="line">Guidelines:</span><br><span class="line"><span class="bullet">-</span> Use Glob for broad file pattern matching</span><br><span class="line"><span class="bullet">-</span> Use Grep for searching file contents with regex</span><br><span class="line"><span class="bullet">-</span> Use Read when you know the specific file path you need to read</span><br><span class="line"><span class="bullet">-</span> Use Bash for file operations like copying, moving, or listing directory contents</span><br><span class="line"><span class="bullet">-</span> Adapt your search approach based on the thoroughness level specified by the caller</span><br><span class="line"><span class="bullet">-</span> Return file paths as absolute paths in your final response</span><br><span class="line"><span class="bullet">-</span> For clear communication, avoid using emojis</span><br><span class="line"><span class="bullet">-</span> Do not create any files, or run bash commands that modify the user&#x27;s system state in any way</span><br><span class="line"></span><br><span class="line">Complete the user&#x27;s search request efficiently and report your findings clearly.</span><br></pre></td></tr></table></figure><h3 id="隐藏-Agent"><a href="#隐藏-Agent" class="headerlink" title="隐藏 Agent"></a>隐藏 Agent</h3><p>隐藏 Agent 是处理内部维护任务的系统 Agent，从不直接向用户展示。它们响应特定的系统事件自动运行：</p><p>压缩 Agent (Compaction Agent)：管理会话历史压缩以控制 token 限制并保留上下文。当会话超过 token 阈值时，此 Agent 分析对话历史并生成浓缩的摘要，以在减小上下文大小的同时保留基本信息。它的Prompt在<code>compaction.txt</code>中：<br><figure class="highlight md"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">You are a helpful AI assistant tasked with summarizing conversations.</span><br><span class="line"></span><br><span class="line">When asked to summarize, provide a detailed but concise summary of the conversation. </span><br><span class="line">Focus on information that would be helpful for continuing the conversation, including:</span><br><span class="line"><span class="bullet">-</span> What was done</span><br><span class="line"><span class="bullet">-</span> What is currently being worked on</span><br><span class="line"><span class="bullet">-</span> Which files are being modified</span><br><span class="line"><span class="bullet">-</span> What needs to be done next</span><br><span class="line"><span class="bullet">-</span> Key user requests, constraints, or preferences that should persist</span><br><span class="line"><span class="bullet">-</span> Important technical decisions and why they were made</span><br><span class="line"></span><br><span class="line">Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.</span><br></pre></td></tr></table></figure></p><p>标题 Agent (Title Agent)：根据对话内容自动为会话生成有意义的标题。这在会话完成或用户请求生成标题时运行，使用较低的 temperature (0.5) 以获得更确定的输出。它的Prompt在<code>title.txt</code>中：</p><figure class="highlight md"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">You are a title generator. You output ONLY a thread title. Nothing else.</span><br><span class="line"></span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">task</span>&gt;</span></span></span><br><span class="line">Generate a brief title that would help the user find this conversation later.</span><br><span class="line"></span><br><span class="line">Follow all rules in <span class="xml"><span class="tag">&lt;<span class="name">rules</span>&gt;</span></span></span><br><span class="line">Use the <span class="xml"><span class="tag">&lt;<span class="name">examples</span>&gt;</span></span> so you know what a good title looks like.</span><br><span class="line">Your output must be:</span><br><span class="line"><span class="bullet">-</span> A single line</span><br><span class="line"><span class="bullet">-</span> ≤50 characters</span><br><span class="line"><span class="bullet">-</span> No explanations</span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">task</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">rules</span>&gt;</span></span></span><br><span class="line"><span class="bullet">-</span> Title must be grammatically correct and read naturally - no word salad</span><br><span class="line"><span class="bullet">-</span> Never include tool names in the title (e.g. &quot;read tool&quot;, &quot;bash tool&quot;, &quot;edit tool&quot;)</span><br><span class="line"><span class="bullet">-</span> Focus on the main topic or question the user needs to retrieve</span><br><span class="line"><span class="bullet">-</span> Vary your phrasing - avoid repetitive patterns like always starting with &quot;Analyzing&quot;</span><br><span class="line"><span class="bullet">-</span> When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it</span><br><span class="line"><span class="bullet">-</span> Keep exact: technical terms, numbers, filenames, HTTP codes</span><br><span class="line"><span class="bullet">-</span> Remove: the, this, my, a, an</span><br><span class="line"><span class="bullet">-</span> Never assume tech stack</span><br><span class="line"><span class="bullet">-</span> Never use tools</span><br><span class="line"><span class="bullet">-</span> NEVER respond to questions, just generate a title for the conversation</span><br><span class="line"><span class="bullet">-</span> The title should NEVER include &quot;summarizing&quot; or &quot;generating&quot; when generating a title</span><br><span class="line"><span class="bullet">-</span> DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT</span><br><span class="line"><span class="bullet">-</span> Always output something meaningful, even if the input is minimal.</span><br><span class="line"><span class="bullet">-</span> If the user message is short or conversational (e.g. &quot;hello&quot;, &quot;lol&quot;, &quot;what&#x27;s up&quot;, &quot;hey&quot;):</span><br><span class="line">  → create a title that reflects the user&#x27;s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)</span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">rules</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">examples</span>&gt;</span></span></span><br><span class="line">&quot;debug 500 errors in production&quot; → Debugging production 500 errors</span><br><span class="line">&quot;refactor user service&quot; → Refactoring user service</span><br><span class="line">&quot;why is app.js failing&quot; → app.js failure investigation</span><br><span class="line">&quot;implement rate limiting&quot; → Rate limiting implementation</span><br><span class="line">&quot;how do I connect postgres to my API&quot; → Postgres API connection</span><br><span class="line">&quot;best practices for React hooks&quot; → React hooks best practices</span><br><span class="line">&quot;@src/auth.ts can you add refresh token support&quot; → Auth refresh token support</span><br><span class="line">&quot;@utils/parser.ts this is broken&quot; → Parser bug fix</span><br><span class="line">&quot;look at @config.json&quot; → Config review</span><br><span class="line">&quot;@App.tsx add dark mode toggle&quot; → Dark mode toggle in App</span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">examples</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>摘要 Agent (Summary Agent)：创建会话摘要以供历史参考和快速上下文检索。与压缩 Agent 一样，它在没有任何工具权限的情况下运行，仅分析对话内容。它的Prompt在<code>summary.txt</code>中：<br><figure class="highlight md"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Summarize what was done in this conversation. Write like a pull request description.</span><br><span class="line"></span><br><span class="line">Rules:</span><br><span class="line"><span class="bullet">-</span> 2-3 sentences max</span><br><span class="line"><span class="bullet">-</span> Describe the changes made, not the process</span><br><span class="line"><span class="bullet">-</span> Do not mention running tests, builds, or other validation steps</span><br><span class="line"><span class="bullet">-</span> Do not explain what the user asked for</span><br><span class="line"><span class="bullet">-</span> Write in first person (I added..., I fixed...)</span><br><span class="line"><span class="bullet">-</span> Never ask questions or add new questions</span><br><span class="line"><span class="bullet">-</span> If the conversation ends with an unanswered question to the user, preserve that exact question</span><br><span class="line"><span class="bullet">-</span> If the conversation ends with an imperative statement or request to the user (e.g. &quot;Now please run the command and paste the console output&quot;), always include that exact request in the summary</span><br></pre></td></tr></table></figure></p><h3 id="Build、Plan和General-3个Agent的提示词"><a href="#Build、Plan和General-3个Agent的提示词" class="headerlink" title="Build、Plan和General 3个Agent的提示词"></a>Build、Plan和General 3个Agent的提示词</h3><p>这3个Agent没有固定的Prompt，是通过以下的Prompt添加机制实现：</p><h4 id="1-Agent定义阶段"><a href="#1-Agent定义阶段" class="headerlink" title="1. Agent定义阶段"></a>1. Agent定义阶段</h4><p>在<code>agent.ts:66-L110</code>中，这三个agent的定义都没有设置prompt字段：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">build: &#123;</span><br><span class="line">  name: <span class="string">&quot;build&quot;</span>,</span><br><span class="line">  options: &#123;&#125;,</span><br><span class="line">  permission: ...,</span><br><span class="line">  mode: <span class="string">&quot;primary&quot;</span>,</span><br><span class="line">  native: <span class="literal">true</span>,</span><br><span class="line">  <span class="comment">// 没有 prompt 字段！</span></span><br><span class="line">&#125;,</span><br><span class="line">plan: &#123; <span class="comment">/* 同样没有 prompt 字段 */</span> &#125;,</span><br><span class="line">general: &#123; <span class="comment">/* 同样没有 prompt 字段 */</span> &#125;</span><br></pre></td></tr></table></figure><p>而其他agent如explore、summary等都有明确的prompt：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">explore: &#123;</span><br><span class="line">  ...</span><br><span class="line">  prompt: PROMPT_EXPLORE,  <span class="comment">// 明确指定了prompt</span></span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2-Prompt组装逻辑"><a href="#2-Prompt组装逻辑" class="headerlink" title="2. Prompt组装逻辑"></a>2. Prompt组装逻辑</h4><p>关键在<code>llm.ts:65-L77</code>的stream函数中：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> system = SystemPrompt.header(input.model.providerID)</span><br><span class="line">system.push(</span><br><span class="line">  [</span><br><span class="line">    <span class="comment">// 使用agent的prompt，如果没有则使用provider的prompt</span></span><br><span class="line">    ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),</span><br><span class="line">    <span class="comment">// 自定义传入的prompt</span></span><br><span class="line">    ...input.system,</span><br><span class="line">    <span class="comment">// 用户消息中的自定义prompt</span></span><br><span class="line">    ...(input.user.system ? [input.user.system] : []),</span><br><span class="line">  ]</span><br><span class="line">    .filter(<span class="function">(<span class="params">x</span>) =&gt;</span> x)</span><br><span class="line">    .join(<span class="string">&quot;\n&quot;</span>),</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p><strong>核心逻辑</strong>：</p><ul><li>如果<code>input.agent.prompt</code>存在，使用agent的prompt</li><li>否则，使用<code>SystemPrompt.provider(input.model)</code>根据模型类型选择prompt</li></ul><h4 id="3-根据模型类型选择Prompt"><a href="#3-根据模型类型选择Prompt" class="headerlink" title="3. 根据模型类型选择Prompt"></a>3. 根据模型类型选择Prompt</h4><p><code>SystemPrompt.provider</code>在<code>system.ts:28-L34</code>中定义：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">provider</span>(<span class="params">model: Provider.Model</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (model.api.id.includes(<span class="string">&quot;gpt-5&quot;</span>)) <span class="keyword">return</span> [PROMPT_CODEX]</span><br><span class="line">  <span class="keyword">if</span> (model.api.id.includes(<span class="string">&quot;gpt-&quot;</span>) || model.api.id.includes(<span class="string">&quot;o1&quot;</span>) || model.api.id.includes(<span class="string">&quot;o3&quot;</span>))</span><br><span class="line">    <span class="keyword">return</span> [PROMPT_BEAST]</span><br><span class="line">  <span class="keyword">if</span> (model.api.id.includes(<span class="string">&quot;gemini-&quot;</span>)) <span class="keyword">return</span> [PROMPT_GEMINI]</span><br><span class="line">  <span class="keyword">if</span> (model.api.id.includes(<span class="string">&quot;claude&quot;</span>)) <span class="keyword">return</span> [PROMPT_ANTHROPIC]</span><br><span class="line">  <span class="keyword">return</span> [PROMPT_ANTHROPIC_WITHOUT_TODO]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><div class="table-container"><table><thead><tr><th>Agent</th><th>Prompt来源</th><th>机制</th></tr></thead><tbody><tr><td>build</td><td>根据模型类型动态选择</td><td>没有设置prompt字段，fallback到<code>SystemPrompt.provider()</code></td></tr><tr><td>plan</td><td>根据模型类型动态选择 + plan模式特殊处理</td><td>同上，但在plan模式会额外注入<a href="packages/opencode/src/session/prompt/plan.txt">plan.txt</a>的只读限制</td></tr><tr><td>general</td><td>根据模型类型动态选择</td><td>同build</td></tr></tbody></table></div><p>这种设计的好处是：</p><ol><li><strong>灵活性</strong>：同一个agent可以根据使用的不同模型自动适配对应的prompt</li><li><strong>可维护性</strong>：不需要为每个agent复制粘贴相同的prompt模板</li><li><strong>统一性</strong>：确保所有使用相同模型的agent行为一致</li></ol><h3 id="主Agent和子Agent的关系"><a href="#主Agent和子Agent的关系" class="headerlink" title="主Agent和子Agent的关系"></a>主Agent和子Agent的关系</h3><h4 id="执行模式：同步阻塞（而非并行）"><a href="#执行模式：同步阻塞（而非并行）" class="headerlink" title="执行模式：同步阻塞（而非并行）"></a>执行模式：同步阻塞（而非并行）</h4><p><strong>主agent调用子agent时会阻塞，等待子agent完全执行完成后才继续。</strong></p><p>具体来说：</p><ol><li><p><strong>独立的Session，但顺序执行</strong></p><ul><li>子agent在新的session中运行（<a href="packages/opencode/src/tool/task.ts#L58">TaskTool.execute L58-88</a>）</li><li>新session的parentID指向主agent的session</li><li>但调用是<code>await</code>的，主agent的执行循环会暂停等待</li></ul></li><li><p><strong>阻塞调用链</strong></p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 主agent的loop中（packages/opencode/src/session/prompt.ts L317-478）</span></span><br><span class="line"><span class="keyword">if</span> (task?.type === <span class="string">&quot;subtask&quot;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> taskTool = <span class="keyword">await</span> TaskTool.init()</span><br><span class="line">  <span class="comment">// 执行TaskTool，这里会await子agent的整个执行过程</span></span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> taskTool.execute(taskArgs, taskCtx)</span><br><span class="line">  <span class="comment">// 子agent完成后才继续</span></span><br><span class="line">  <span class="keyword">continue</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p><strong>TaskTool的实现</strong>（<a href="packages/opencode/src/tool/task.ts#L139">packages/opencode/src/tool/task.ts L139</a>）</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> result = <span class="keyword">await</span> SessionPrompt.prompt(&#123;</span><br><span class="line">  sessionID: session.id,  <span class="comment">// 子agent的session</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;)</span><br><span class="line"><span class="comment">// 这个await会等待子agent的整个loop完成</span></span><br></pre></td></tr></table></figure></li></ol><h4 id="Session隔离和协作"><a href="#Session隔离和协作" class="headerlink" title="Session隔离和协作"></a>Session隔离和协作</h4><div class="table-container"><table><thead><tr><th>特性</th><th>主Agent Session</th><th>子Agent Session</th></tr></thead><tbody><tr><td><strong>独立性</strong></td><td>有独立的loop</td><td>有自己独立的loop</td></tr><tr><td><strong>并发</strong></td><td>❌ 只有一个active loop</td><td>❌ 只有一个active loop</td></tr><tr><td><strong>父子关系</strong></td><td>作为parentID被引用</td><td>parentID指向主session</td></tr><tr><td><strong>执行</strong></td><td>await子agent完成后继续</td><td>完成自主任务后返回结果</td></tr></tbody></table></div><h4 id="为什么是阻塞而非并行？"><a href="#为什么是阻塞而非并行？" class="headerlink" title="为什么是阻塞而非并行？"></a>为什么是阻塞而非并行？</h4><ol><li><strong>因果依赖</strong>：主agent需要子agent的结果才能继续</li><li><strong>资源管理</strong>：避免同时运行多个LLM调用导致不可控的成本</li><li><strong>简化状态</strong>：更容易追踪和理解执行流程</li><li><strong>可调试性</strong>：顺序执行更容易排查问题</li></ol><h4 id="执行时序图"><a href="#执行时序图" class="headerlink" title="执行时序图"></a>执行时序图</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">时间 →</span><br><span class="line"></span><br><span class="line">主Session A Loop:</span><br><span class="line">├── Step 1: LLM生成 -&gt; 调用TaskTool(explore)</span><br><span class="line">│   └── 等待...</span><br><span class="line">│</span><br><span class="line">│   子Session B Loop (作为TaskTool的一部分):</span><br><span class="line">│   ├── SubStep 1: LLM生成 -&gt; 调用grep&#x2F;glob工具</span><br><span class="line">│   ├── SubStep 2: 处理工具结果</span><br><span class="line">│   ├── SubStep 3: LLM继续 -&gt; 完成</span><br><span class="line">│   └── 返回结果给Session A</span><br><span class="line">│</span><br><span class="line">├── Step 2: 收到子agent结果，LLM继续</span><br><span class="line">└── Step 3: 完成任务</span><br></pre></td></tr></table></figure><h4 id="特殊情况：多个子Agent"><a href="#特殊情况：多个子Agent" class="headerlink" title="特殊情况：多个子Agent"></a>特殊情况：多个子Agent</h4><p>如果主agent需要调用多个子agent：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 示例：主agent可能的行为</span></span><br><span class="line"><span class="number">1.</span> 调用 explore agent（等待结果）</span><br><span class="line"><span class="number">2.</span> 根据结果调用 general agent（等待结果）</span><br><span class="line"><span class="number">3.</span> 综合两个agent的输出</span><br></pre></td></tr></table></figure><p>这是<strong>顺序的</strong>，不是并行的。LLM会自然地按顺序调用不同的task工具。</p><h4 id="“单线程”的真正含义"><a href="#“单线程”的真正含义" class="headerlink" title="“单线程”的真正含义"></a>“单线程”的真正含义</h4><ul><li><strong>Session级别</strong>：每个session同时只能有一个active的loop</li><li><strong>进程级别</strong>：Node.js事件循环是单线程的，所有异步操作通过事件驱动</li><li><strong>执行级别</strong>：主agent和子agent的loop不会<strong>同时</strong>运行，而是串行的</li></ul><hr><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><ul><li><strong>主agent调用子agent = 阻塞等待</strong>，不是并行处理</li><li>子agent在独立session中运行，有自己的loop</li><li>但主agent的loop会暂停，等待子agent完成</li><li>这简化了执行流程，使结果可预测、可调试</li><li>多个子agent调用也是顺序的，由LLM决定何时调用哪个</li></ul><p>需要注意的是在<code>oh-my-opencode</code>插件中实现了非阻塞的多agent的编排。</p><h2 id="Agent-配置结构"><a href="#Agent-配置结构" class="headerlink" title="Agent 配置结构"></a>Agent 配置结构</h2><p>Agent.Info 结构定义了所有 Agent 类型的完整配置结构：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  name: string                    &#x2F;&#x2F; 唯一标识符</span><br><span class="line">  description?: string            &#x2F;&#x2F; 人类可读的描述</span><br><span class="line">  mode: &quot;subagent&quot; | &quot;primary&quot; | &quot;all&quot;  &#x2F;&#x2F; Agent 分类</span><br><span class="line">  native?: boolean                &#x2F;&#x2F; 是否为内置 Agent</span><br><span class="line">  hidden?: boolean                &#x2F;&#x2F; 是否从 UI 中隐藏</span><br><span class="line">  topP?: number                   &#x2F;&#x2F; Nucleus 采样参数</span><br><span class="line">  temperature?: number            &#x2F;&#x2F; 创造性&#x2F;随机性设置</span><br><span class="line">  color?: string                  &#x2F;&#x2F; UI 显示颜色</span><br><span class="line">  permission: Ruleset             &#x2F;&#x2F; 权限配置</span><br><span class="line">  model?: &#123;                       &#x2F;&#x2F; 模型覆盖</span><br><span class="line">    modelID: string</span><br><span class="line">    providerID: string</span><br><span class="line">  &#125;</span><br><span class="line">  prompt?: string                 &#x2F;&#x2F; 自定义系统提示词</span><br><span class="line">  options: Record&lt;string, any&gt;    &#x2F;&#x2F; Agent 特定选项</span><br><span class="line">  steps?: number                  &#x2F;&#x2F; 最大执行步数</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>来源: agent.ts</p><h2 id="Agent-模式字段和可见性"><a href="#Agent-模式字段和可见性" class="headerlink" title="Agent 模式字段和可见性"></a>Agent 模式字段和可见性</h2><p>mode 字段是决定 Agent 可见性和行为的主要因素：</p><div class="table-container"><table><thead><tr><th>模式</th><th>UI 可见性</th><th>调用方式</th><th>用例</th></tr></thead><tbody><tr><td>primary</td><td>在 Agent 选择器中可见</td><td>直接用户选择</td><td>主要交互 Agent (build, plan)</td></tr><tr><td>subagent</td><td>在选择器中隐藏</td><td>任务工具调用</td><td>专用助手 (general, explore)</td></tr><tr><td>all</td><td>在选择器中可见</td><td>直接和任务调用</td><td>用户定义的自定义 Agent</td></tr></tbody></table></div><p>hidden 布尔字段提供了额外的控制——当设置为 true 时，Agent 将被排除在模式列表之外，无论其 mode 设置如何。这用于绝不应出现在用户界面中的内部维护 Agent。</p><p>来源: agent.ts, acp/agent.ts</p><h2 id="权限系统集成"><a href="#权限系统集成" class="headerlink" title="权限系统集成"></a>权限系统集成</h2><p>每个 Agent 维护一个独特的权限规则集，确定它可以访问哪些工具和操作。权限按优先级顺序从多个来源合并：</p><p>默认权限：应用于所有 Agent 的基准规则<br>Agent 特定默认值：特定于模式的权限覆盖<br>用户配置：来自配置文件的项目级自定义<br>外部目录强制：确保对截断目录的访问<br>权限系统使用通配符模式和动作（allow、deny、ask）来创建灵活的安全边界。例如，探索子 Agent 的权限明确拒绝大多数操作，同时允许只读工具如 grep、glob 和 read。</p><p>来源: agent.ts, agent.ts</p><h2 id="Agent-发现和过滤"><a href="#Agent-发现和过滤" class="headerlink" title="Agent 发现和过滤"></a>Agent 发现和过滤</h2><p>系统提供了多种根据用例发现和过滤 Agent 的方法：<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 列出所有 Agent（按 default_agent 优先级排序）</span></span><br><span class="line"><span class="keyword">await</span> Agent.list()</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 按名称获取特定 Agent</span></span><br><span class="line"><span class="keyword">await</span> Agent.get(<span class="string">&quot;explore&quot;</span>)</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 获取默认 Agent（排序列表中的第一个）</span></span><br><span class="line"><span class="keyword">await</span> Agent.defaultAgent()</span><br></pre></td></tr></table></figure><br>在构建 UI 元素或创建任务工具描述时，Agent 按模式过滤。ACP 集成专门从可用模式列表中排除子 Agent 和隐藏 Agent：<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> availableModes = agents</span><br><span class="line">  .filter(<span class="function">(<span class="params">agent</span>) =&gt;</span> agent.mode !== <span class="string">&quot;subagent&quot;</span> &amp;&amp; !agent.hidden)</span><br><span class="line">  .map(<span class="function">(<span class="params">agent</span>) =&gt;</span> (&#123;</span><br><span class="line">    id: agent.name,</span><br><span class="line">    name: agent.name,</span><br><span class="line">    description: agent.description,</span><br><span class="line">  &#125;))</span><br></pre></td></tr></table></figure><br>来源: agent.ts, acp/agent.ts</p><h2 id="基于Task任务的子-Agent-调用"><a href="#基于Task任务的子-Agent-调用" class="headerlink" title="基于Task任务的子 Agent 调用"></a>基于Task任务的子 Agent 调用</h2><p>Task工具是OpenCode中实现agent间协作的关键机制，实现在<a href="packages/opencode/src/tool/task.ts#L1-L182">task.ts</a>中。<br>它使主 Agent 能够通过结构化的调用机制将工作委派给子 Agent。当 Agent 调用任务工具时：</p><p>1、权限验证：检查调用 Agent 的权限，以查看其是否允许调用目标子 Agent<br>2、会话创建：使用子 Agent 的配置创建一个新的分支会话<br>3、隔离执行：子 Agent 在受限权限下执行其任务<br>4、结果聚合：子 Agent 的输出返回给调用 Agent<br>任务工具动态生成描述，列出所有可用的子 Agent，并根据调用 Agent 的权限对其进行过滤。这使得可以进行上下文相关的委派，Agent 只能调用其有权使用的子 Agent。</p><p>通过源码具体分析上述过程：</p><h3 id="1-工具注册和权限过滤"><a href="#1-工具注册和权限过滤" class="headerlink" title="1. 工具注册和权限过滤"></a>1. 工具注册和权限过滤</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> TaskTool = Tool.define(<span class="string">&quot;task&quot;</span>, <span class="keyword">async</span> (ctx) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 获取所有非primary模式的agent</span></span><br><span class="line">  <span class="keyword">const</span> agents = <span class="keyword">await</span> Agent.list().then(<span class="function">(<span class="params">x</span>) =&gt;</span> x.filter(<span class="function">(<span class="params">a</span>) =&gt;</span> a.mode !== <span class="string">&quot;primary&quot;</span>))</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 根据调用者的权限过滤可访问的agent</span></span><br><span class="line">  <span class="keyword">const</span> caller = ctx?.agent</span><br><span class="line">  <span class="keyword">const</span> accessibleAgents = caller</span><br><span class="line">    ? agents.filter(<span class="function">(<span class="params">a</span>) =&gt;</span> PermissionNext.evaluate(<span class="string">&quot;task&quot;</span>, a.name, caller.permission).action !== <span class="string">&quot;deny&quot;</span>)</span><br><span class="line">    : agents</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    description: DESCRIPTION.replace(<span class="string">&quot;&#123;agents&#125;&quot;</span>, ...),  <span class="comment">// 动态生成可用agent列表</span></span><br><span class="line">    parameters,</span><br><span class="line">    <span class="function"><span class="title">execute</span>(<span class="params">params, ctx</span>)</span> &#123; ... &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><strong>关键点</strong>：</p><ul><li>只有<code>mode !== &quot;primary&quot;</code>的agent才能被调用（即subagent）</li><li>权限系统控制agent间调用关系</li><li>动态生成agent描述，AI能看到可用的agent列表</li></ul><h3 id="2-权限检查流程"><a href="#2-权限检查流程" class="headerlink" title="2. 权限检查流程"></a>2. 权限检查流程</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="title">execute</span>(<span class="params">params: z.infer&lt;<span class="keyword">typeof</span> parameters&gt;, ctx</span>)</span> &#123;</span><br><span class="line">  <span class="comment">// 跳过权限检查的特殊情况（用户通过@或命令触发）</span></span><br><span class="line">  <span class="keyword">if</span> (!ctx.extra?.bypassAgentCheck) &#123;</span><br><span class="line">    <span class="keyword">await</span> ctx.ask(&#123;</span><br><span class="line">      permission: <span class="string">&quot;task&quot;</span>,</span><br><span class="line">      patterns: [params.subagent_type],</span><br><span class="line">      always: [<span class="string">&quot;*&quot;</span>],</span><br><span class="line">      metadata: &#123;</span><br><span class="line">        description: params.description,</span><br><span class="line">        subagent_type: params.subagent_type,</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>权限评估逻辑</strong>（在<a href="packages/opencode/src/permission/next.ts#L107-L125">next.ts:107-L125</a>）：<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">permission: <span class="built_in">string</span>, pattern: <span class="built_in">string</span>, ruleset: Ruleset, approved: Ruleset</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 1. 检查用户批准的规则</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> rule <span class="keyword">of</span> approved) &#123;</span><br><span class="line">    <span class="keyword">if</span> (Wildcard.match(permission, rule.permission) &amp;&amp; Wildcard.match(pattern, rule.pattern)) &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">action</span>: <span class="string">&quot;allow&quot;</span> &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 2. 检查默认规则集</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> rule <span class="keyword">of</span> ruleset) &#123;</span><br><span class="line">    <span class="keyword">if</span> (Wildcard.match(permission, rule.permission) &amp;&amp; Wildcard.match(pattern, rule.pattern)) &#123;</span><br><span class="line">      <span class="keyword">return</span> rule</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> &#123; <span class="attr">action</span>: <span class="string">&quot;deny&quot;</span> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><strong>三种权限</strong>：</p><ul><li><code>allow</code>：直接允许调用</li><li><code>deny</code>：拒绝调用，抛出异常</li><li><code>ask</code>：向用户请求批准</li></ul><h3 id="3-子Agent会话创建"><a href="#3-子Agent会话创建" class="headerlink" title="3. 子Agent会话创建"></a>3. 子Agent会话创建</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> session = <span class="keyword">await</span> iife(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (params.session_id) &#123;</span><br><span class="line">    <span class="comment">// 继续已有会话</span></span><br><span class="line">    <span class="keyword">const</span> found = <span class="keyword">await</span> Session.get(params.session_id).catch(<span class="function">() =&gt;</span> &#123;&#125;)</span><br><span class="line">    <span class="keyword">if</span> (found) <span class="keyword">return</span> found</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 创建新会话，设置严格权限</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">await</span> Session.create(&#123;</span><br><span class="line">    parentID: ctx.sessionID,  <span class="comment">// 建立父子关系</span></span><br><span class="line">    title: params.description + <span class="string">` (@<span class="subst">$&#123;agent.name&#125;</span> subagent)`</span>,</span><br><span class="line">    permission: [</span><br><span class="line">      <span class="comment">// 禁止嵌套调用task（防止无限递归）</span></span><br><span class="line">      &#123; <span class="attr">permission</span>: <span class="string">&quot;task&quot;</span>, <span class="attr">pattern</span>: <span class="string">&quot;*&quot;</span>, <span class="attr">action</span>: <span class="string">&quot;deny&quot;</span> &#125;,</span><br><span class="line">      <span class="comment">// 禁止创建和管理todos</span></span><br><span class="line">      &#123; <span class="attr">permission</span>: <span class="string">&quot;todowrite&quot;</span>, <span class="attr">pattern</span>: <span class="string">&quot;*&quot;</span>, <span class="attr">action</span>: <span class="string">&quot;deny&quot;</span> &#125;,</span><br><span class="line">      &#123; <span class="attr">permission</span>: <span class="string">&quot;todoread&quot;</span>, <span class="attr">pattern</span>: <span class="string">&quot;*&quot;</span>, <span class="attr">action</span>: <span class="string">&quot;deny&quot;</span> &#125;,</span><br><span class="line">      <span class="comment">// 允许配置的primary tools</span></span><br><span class="line">      ...(config.experimental?.primary_tools?.map(<span class="function">(<span class="params">t</span>) =&gt;</span> (&#123;</span><br><span class="line">        pattern: <span class="string">&quot;*&quot;</span>,</span><br><span class="line">        action: <span class="string">&quot;allow&quot;</span> <span class="keyword">as</span> <span class="keyword">const</span>,</span><br><span class="line">        permission: t,</span><br><span class="line">      &#125;)) ?? []),</span><br><span class="line">    ],</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><strong>安全隔离</strong>：</p><ul><li>嵌套调用限制：禁止子agent再调用task（防递归）</li><li>工具限制：子agent只能使用配置允许的工具</li><li>会话隔离：独立的会话状态和上下文</li></ul><h3 id="4-实时进度反馈"><a href="#4-实时进度反馈" class="headerlink" title="4. 实时进度反馈"></a>4. 实时进度反馈</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> parts: Record&lt;<span class="built_in">string</span>, &#123;...&#125;&gt; = &#123;&#125;</span><br><span class="line"><span class="keyword">const</span> unsub = Bus.subscribe(MessageV2.Event.PartUpdated, <span class="keyword">async</span> (evt) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (evt.properties.part.sessionID !== session.id) <span class="keyword">return</span></span><br><span class="line">  <span class="keyword">if</span> (evt.properties.part.type !== <span class="string">&quot;tool&quot;</span>) <span class="keyword">return</span></span><br><span class="line">  </span><br><span class="line">  parts[part.id] = &#123;</span><br><span class="line">    id: part.id,</span><br><span class="line">    tool: part.tool,</span><br><span class="line">    state: &#123; <span class="attr">status</span>: part.state.status, <span class="attr">title</span>: ... &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 更新调用方的元数据</span></span><br><span class="line">  ctx.metadata(&#123;</span><br><span class="line">    title: params.description,</span><br><span class="line">    metadata: &#123;</span><br><span class="line">      summary: <span class="built_in">Object</span>.values(parts),</span><br><span class="line">      sessionId: session.id,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><strong>事件流</strong>：</p><ul><li>子agent的工具调用通过事件系统广播</li><li>调用方实时接收进度更新</li><li>用户可以看到agent的工作状态</li></ul><h3 id="5-Agent执行和结果返回"><a href="#5-Agent执行和结果返回" class="headerlink" title="5. Agent执行和结果返回"></a>5. Agent执行和结果返回</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> result = <span class="keyword">await</span> SessionPrompt.prompt(&#123;</span><br><span class="line">  messageID,</span><br><span class="line">  sessionID: session.id,</span><br><span class="line">  model: agent.model ?? &#123; <span class="attr">modelID</span>: msg.info.modelID, <span class="attr">providerID</span>: msg.info.providerID &#125;,</span><br><span class="line">  agent: agent.name,</span><br><span class="line">  tools: &#123; <span class="attr">todowrite</span>: <span class="literal">false</span>, <span class="attr">todoread</span>: <span class="literal">false</span>, <span class="attr">task</span>: <span class="literal">false</span>, ... &#125;,  <span class="comment">// 禁用特定工具</span></span><br><span class="line">  parts: promptParts,</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">unsub()  <span class="comment">// 取消事件订阅</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 生成输出，包含元数据</span></span><br><span class="line"><span class="keyword">const</span> output = text + <span class="string">&quot;\n\n&quot;</span> + [</span><br><span class="line">  <span class="string">&quot;&lt;task_metadata&gt;&quot;</span>,</span><br><span class="line">  <span class="string">`session_id: <span class="subst">$&#123;session.id&#125;</span>`</span>,</span><br><span class="line">  <span class="string">&quot;&lt;/task_metadata&gt;&quot;</span></span><br><span class="line">].join(<span class="string">&quot;\n&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> &#123; title, <span class="attr">metadata</span>: &#123; summary, <span class="attr">sessionId</span>: session.id &#125;, output &#125;</span><br></pre></td></tr></table></figure><h3 id="6-Agent调用链"><a href="#6-Agent调用链" class="headerlink" title="6. Agent调用链"></a>6. Agent调用链</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">User Prompt</span><br><span class="line">    ↓</span><br><span class="line">Primary Agent (build&#x2F;plan)</span><br><span class="line">    ↓ Task(subagent_type&#x3D;&quot;explore&quot;, prompt&#x3D;&quot;find auth logic&quot;)</span><br><span class="line">Subagent (explore) [独立会话]</span><br><span class="line">    ├─ Grep(&quot;authentication&quot;)</span><br><span class="line">    ├─ Read(&quot;src&#x2F;auth.ts&quot;)</span><br><span class="line">    └─ Return: &quot;Auth is in src&#x2F;auth.ts:45&quot;</span><br><span class="line">    ↓</span><br><span class="line">Primary Agent 接收结果</span><br><span class="line">    ↓</span><br><span class="line">Continue with result</span><br></pre></td></tr></table></figure><h3 id="关键设计要点"><a href="#关键设计要点" class="headerlink" title="关键设计要点"></a>关键设计要点</h3><h4 id="安全机制"><a href="#安全机制" class="headerlink" title="安全机制"></a>安全机制</h4><ol><li><strong>权限隔离</strong>：每个agent有独立的权限集</li><li><strong>嵌套限制</strong>：禁止子agent递归调用task</li><li><strong>工具限制</strong>：子agent只能使用允许的工具</li><li><strong>用户审批</strong>：敏感操作需要用户确认</li></ol><h4 id="会话管理"><a href="#会话管理" class="headerlink" title="会话管理"></a>会话管理</h4><ol><li><strong>父子关系</strong>：<code>parentID</code>建立会话层级</li><li><strong>独立状态</strong>：子agent有自己的消息历史</li><li><strong>状态隔离</strong>：修改不会影响父会话</li></ol><h4 id="通信机制"><a href="#通信机制" class="headerlink" title="通信机制"></a>通信机制</h4><ol><li><strong>事件总线</strong>：通过Bus系统传递进度</li><li><strong>实时反馈</strong>：调用方可以监控子agent状态</li><li><strong>结果汇总</strong>：工具调用状态会被汇总返回</li></ol><h4 id="灵活性"><a href="#灵活性" class="headerlink" title="灵活性"></a>灵活性</h4><ol><li><strong>动态agent列表</strong>：工具描述包含可用agent</li><li><strong>会话续接</strong>：通过<code>session_id</code>可以继续之前的任务</li><li><strong>配置化权限</strong>：<code>config.experimental.primary_tools</code>控制可用工具</li></ol><p>这种设计实现了agent间的安全协作，同时保持了系统的灵活性和可扩展性。</p><h2 id="Agent-生命周期和状态管理"><a href="#Agent-生命周期和状态管理" class="headerlink" title="Agent 生命周期和状态管理"></a>Agent 生命周期和状态管理</h2><p>Agent 通过从多个来源合并配置的延迟加载状态系统进行配置：</p><p>内置定义：具有默认配置的原生 Agent（build、plan、general、explore、compaction、title、summary）<br>用户扩展：在 .opencode 目录或项目配置中定义的自定义 Agent<br>运行时覆盖：通过配置文件或程序化更改进行的修改<br>用户定义的 Agent 可以扩展或覆盖内置配置。当用户配置指定了与内置 Agent 同名的 Agent 时，用户配置将与内置定义合并并优先于内置定义。用户还可以通过在配置中设置 disable: true 来完全禁用 Agent。</p><p>来源: agent.ts</p><p>创建自定义 Agent 时，使用 mode: “all” 使其既可用于直接用户选择，也可通过任务工具由其他 Agent 调用。这在保持 UI 中清晰可见性的同时提供了最大的灵活性。</p><h2 id="Agent-模式交互模式"><a href="#Agent-模式交互模式" class="headerlink" title="Agent 模式交互模式"></a>Agent 模式交互模式</h2><p>主 Agent 通过将专门任务委派给子 Agent 来编排复杂的工作流。这种模式实现了高效的任务分解：</p><p>1、用户交互：用户选择一个主 Agent（例如 build）并提供高级请求<br>2、委派：主 Agent 分析请求并将子任务委派给适当的子 Agent（例如，用于代码库分析的 explore）<br>3、并行执行：多个子 Agent 可以同时在问题的不同方面工作<br>4、聚合：主 Agent 综合结果并协调最终执行<br>隐藏 Agent 独立于此模式运行，响应系统事件而非直接请求。例如，当超过 token 限制时，压缩 Agent 会自动触发，无论哪个主 Agent 处于活动状态。</p><h2 id="会话与-Agent-模式的集成"><a href="#会话与-Agent-模式的集成" class="headerlink" title="会话与 Agent 模式的集成"></a>会话与 Agent 模式的集成</h2><p>创建会话时，系统将每个会话与特定的 Agent 关联。这种关联决定了：</p><ul><li>可用工具：Agent 可以基于其权限规则集访问哪些工具</li><li>系统提示词：特定 Agent 的自定义提示词配置</li><li>模型选择：Agent 是使用默认模型还是配置的覆盖模型</li><li>执行限制：控制 Agent 行为的步数限制和 temperature 设置</li></ul><p>会话可以使用不同的 Agent 进行分支，从而实现计划 Agent 创建实施计划，然后构建 Agent 在分支会话中执行它的工作流。</p><p>来源: prompt.ts, processor.ts</p><h2 id="创建自定义-Agent"><a href="#创建自定义-Agent" class="headerlink" title="创建自定义 Agent"></a>创建自定义 Agent</h2><p>自定义 Agent 可以通过 .opencode 目录中的配置文件或通过程序化配置来定义。系统支持扩展内置 Agent 和创建全新的 Agent：<br><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;agent&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;code-review&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;专用于代码审查和重构的 Agent&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;mode&quot;</span>: <span class="string">&quot;primary&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;temperature&quot;</span>: <span class="number">0.7</span>,</span><br><span class="line">      <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;edit&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;explorer&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;快速代码库探索&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;mode&quot;</span>: <span class="string">&quot;subagent&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;grep&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;glob&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>自定义 Agent 自动继承默认权限规则集，可以通过 permission 字段有选择地覆盖。系统还确保所有 Agent 都可以访问截断目录以进行上下文管理，除非明确拒绝。</p><p>来源: agent.ts, config.ts</p><h2 id="Agent-专业化示例"><a href="#Agent-专业化示例" class="headerlink" title="Agent 专业化示例"></a>Agent 专业化示例</h2><p>探索子 Agent 展示了子 Agent 如何针对特定任务进行专业化：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">explore: &#123;</span><br><span class="line">  name: <span class="string">&quot;explore&quot;</span>,</span><br><span class="line">  permission: PermissionNext.merge(</span><br><span class="line">    defaults,</span><br><span class="line">    PermissionNext.fromConfig(&#123;</span><br><span class="line">      <span class="string">&quot;*&quot;</span>: <span class="string">&quot;deny&quot;</span>,                    <span class="comment">// 默认拒绝所有</span></span><br><span class="line">      grep: <span class="string">&quot;allow&quot;</span>,                 <span class="comment">// 允许代码搜索</span></span><br><span class="line">      glob: <span class="string">&quot;allow&quot;</span>,                 <span class="comment">// 允许文件模式匹配</span></span><br><span class="line">      list: <span class="string">&quot;allow&quot;</span>,                 <span class="comment">// 允许目录列表</span></span><br><span class="line">      bash: <span class="string">&quot;allow&quot;</span>,                 <span class="comment">// 允许 shell 命令</span></span><br><span class="line">      webfetch: <span class="string">&quot;allow&quot;</span>,             <span class="comment">// 允许 web 获取</span></span><br><span class="line">      websearch: <span class="string">&quot;allow&quot;</span>,            <span class="comment">// 允许 web 搜索</span></span><br><span class="line">      codesearch: <span class="string">&quot;allow&quot;</span>,           <span class="comment">// 允许代码搜索</span></span><br><span class="line">      read: <span class="string">&quot;allow&quot;</span>,                 <span class="comment">// 允许文件读取</span></span><br><span class="line">      external_directory: &#123;</span><br><span class="line">        [Truncate.DIR]: <span class="string">&quot;allow&quot;</span>,    <span class="comment">// 允许截断目录</span></span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;),</span><br><span class="line">    user,</span><br><span class="line">  ),</span><br><span class="line">  description: <span class="string">`专用于探索代码库的快速 Agent...`</span>,</span><br><span class="line">  prompt: PROMPT_EXPLORE,</span><br><span class="line">  options: &#123;&#125;,</span><br><span class="line">  mode: <span class="string">&quot;subagent&quot;</span>,</span><br><span class="line">  native: <span class="literal">true</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>此配置创建了一个只能执行读取操作的 Agent，使其可以安全地探索不熟悉的代码库，而不会有意外修改的风险。</p><p>来源: agent.ts, explore.txt</p><p>隐藏 Agent 应始终设置 hidden: true 并配置 permission: “*”: “deny”，以确保它们无法执行任何工具操作。它们唯一的交互应通过内部系统调用，而不是面向用户的工具。</p><h2 id="Agent-模式最佳实践"><a href="#Agent-模式最佳实践" class="headerlink" title="Agent 模式最佳实践"></a>Agent 模式最佳实践</h2><p>设计 Agent 配置时，请考虑以下准则：</p><ol><li>主 Agent 应具有广泛的权限集，但对破坏性操作使用 “ask” 动作。这在保持安全的同时实现了灵活性。</li><li>子 Agent 应具有严格限制的范围和最少的权限。每个子 Agent 应专注于特定的能力。</li><li>隐藏 Agent 不得公开工具或写入操作。它们应该是仅处理现有数据的纯分析 Agent。</li><li>自定义 Agent 当你希望同时具有直接用户访问和任务工具调用能力时，应指定 mode: “all”。</li><li>Temperature 调整会影响行为——对于确定的 Agent（如 title/summary）使用较低的 temperature (0.3-0.5)，对于创造性 Agent 使用较高的 temperature (0.7-1.0)。</li></ol><h1 id="权限系统"><a href="#权限系统" class="headerlink" title="权限系统"></a>权限系统</h1><p>OpenCode 权限系统提供了一个灵活且可配置的安全层，用于控制 Agent 对工具和系统资源的访问。该系统支持配置驱动的策略、基于通配符的模式匹配以及运行时用户审批工作流，从而对 Agent 可执行的操作实现精细化的控制。</p><h2 id="安全模型架构"><a href="#安全模型架构" class="headerlink" title="安全模型架构"></a>安全模型架构</h2><p>权限系统基于分层安全模型运行，结合了声明式配置与运行时授权检查。当 Agent 尝试使用工具时，系统会在继续执行前根据配置的规则评估请求，确保需要用户干预的操作显示明确的审批对话框，而常规操作则可以自动进行。</p><p>权限系统维护了两个并行的实现：旧系统（packages/opencode/src/permission/index.ts）和下一代系统（packages/opencode/src/permission/next.ts）。新系统提供了增强的功能，包括规则集评估、更好的模式匹配以及更复杂的错误处理。</p><p>来源：permission/index.ts, permission/next.ts</p><h2 id="核心权限概念"><a href="#核心权限概念" class="headerlink" title="核心权限概念"></a>核心权限概念</h2><h3 id="权限类型和操作"><a href="#权限类型和操作" class="headerlink" title="权限类型和操作"></a>权限类型和操作</h3><p>系统识别三种主要的操作，用于确定如何处理权限请求：</p><ul><li><code>allow</code> (允许)：无需用户干预自动批准工具执行</li><li><code>deny</code> (拒绝)：立即拒绝工具执行并报错</li><li><code>ask</code> (询问)：在运行时提示用户进行批准（当没有匹配规则时的默认行为）</li></ul><p>这些操作可以应用于不同的粒度级别，从全局工具策略到特定的路径或命令模式。</p><p>来源：config/config.ts</p><h3 id="工具权限"><a href="#工具权限" class="headerlink" title="工具权限"></a>工具权限</h3><p>以下工具类别可以通过权限系统进行控制：</p><div class="table-container"><table><thead><tr><th>权限类型</th><th>关联工具</th><th>描述</th></tr></thead><tbody><tr><td>edit</td><td>edit, write, patch, multiedit</td><td>文件修改操作</td></tr><tr><td>read</td><td>read, ls, glob</td><td>文件读取和发现</td></tr><tr><td>bash</td><td>bash</td><td>Shell 命令执行</td></tr><tr><td>grep</td><td>grep</td><td>代码和文本搜索</td></tr><tr><td>task</td><td>task</td><td>Subagent 执行</td></tr><tr><td>external_directory</td><td>external_directory</td><td>项目目录外的操作</td></tr><tr><td>webfetch, websearch, codesearch</td><td>基于网络的工具</td><td>外部数据访问</td></tr><tr><td>lsp</td><td>lsp</td><td>语言服务器操作</td></tr><tr><td>todoread, todowrite</td><td>todo</td><td>任务列表管理</td></tr><tr><td>question</td><td>question</td><td>交互式查询</td></tr></tbody></table></div><p>来源：config/config.ts</p><h2 id="权限配置"><a href="#权限配置" class="headerlink" title="权限配置"></a>权限配置</h2><h3 id="配置结构"><a href="#配置结构" class="headerlink" title="配置结构"></a>配置结构</h3><p>权限在 opencode.json 或 opencode.jsonc 文件中通过 permission 字段进行配置。系统支持多个配置层及其优先级：远程/已知配置（最低）→ 全局用户配置 → 项目配置 → 命令行标志（最高）。</p><p>来源：config/config.ts</p><h3 id="基本配置示例"><a href="#基本配置示例" class="headerlink" title="基本配置示例"></a>基本配置示例</h3><p>最简单的形式是为权限类型分配单个操作：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;edit&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了进行更多控制，可以使用对象语法来指定模式：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;edit&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;*.md&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;src/*.ts&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;node_modules/**&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;git*&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm install&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;rm -rf&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：config/config.ts</p><h3 id="通配符模式匹配"><a href="#通配符模式匹配" class="headerlink" title="通配符模式匹配"></a>通配符模式匹配</h3><p>权限系统使用通配符模式来灵活地匹配文件路径和命令。模式引擎支持：</p><ul><li><code>*</code>：匹配任意字符序列</li><li><code>?</code>：匹配任意单个字符</li><li>标准正则表达式特殊字符将被转义以进行字面匹配</li></ul><p>模式示例：</p><ul><li><code>*.md</code> - 匹配任意 Markdown 文件</li><li><code>src/**/*.ts</code> - 匹配 src 层级结构中的任意 TypeScript 文件</li><li><code>git checkout *</code> - 匹配任意 git checkout 命令</li><li><code>npm run *</code> - 匹配任意 npm run 命令</li></ul><p>系统使用最长前缀匹配，其中更具体的模式优先于通用模式。</p><p>来源：util/wildcard.ts</p><p>配置通配符模式时，请在权限对象内将规则按从最具体到最不具体的顺序排列。评估引擎使用 findLast() 进行匹配，这意味着最后一个匹配的规则获胜。这允许您使用特定的例外覆盖通用规则。</p><h3 id="Agent-特定权限"><a href="#Agent-特定权限" class="headerlink" title="Agent 特定权限"></a>Agent 特定权限</h3><p>可以为每个 Agent 配置权限，以对不同 Agent 的行为进行精细控制：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;agent&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;coder&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;edit&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;bash&quot;</span>: &#123;</span><br><span class="line">          <span class="attr">&quot;git*&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">          <span class="attr">&quot;npm install&quot;</span>: <span class="string">&quot;ask&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;reviewer&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;edit&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Agent 特定权限会覆盖该 Agent 的全局设置，使您能够创建具有不同安全配置文件的 Agent。</p><p>来源：config/config.ts</p><h2 id="权限评估流程"><a href="#权限评估流程" class="headerlink" title="权限评估流程"></a>权限评估流程</h2><h3 id="请求生成"><a href="#请求生成" class="headerlink" title="请求生成"></a>请求生成</h3><p>当调用工具时，它会生成一个包含以下内容的权限请求：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;id&quot;</span>: <span class="string">&quot;string&quot;</span>,              <span class="comment">// 唯一的权限请求 ID</span></span><br><span class="line">  <span class="attr">&quot;sessionID&quot;</span>: <span class="string">&quot;string&quot;</span>,       <span class="comment">// 当前会话标识符</span></span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: <span class="string">&quot;string&quot;</span>,      <span class="comment">// 权限类型（例如 &quot;edit&quot;, &quot;bash&quot;）</span></span><br><span class="line">  <span class="attr">&quot;patterns&quot;</span>: [<span class="string">&quot;string&quot;</span>],      <span class="comment">// 此请求匹配的模式</span></span><br><span class="line">  <span class="attr">&quot;metadata&quot;</span>: &#123;&#125;,  <span class="comment">// 附加上下文（文件路径、命令等）</span></span><br><span class="line">  <span class="attr">&quot;always&quot;</span>: [<span class="string">&quot;string&quot;</span>],        <span class="comment">// 如果用户选择“总是允许”则自动批准的模式</span></span><br><span class="line">  <span class="attr">&quot;tool&quot;</span>: &#123;                 <span class="comment">// 工具调用上下文</span></span><br><span class="line">    <span class="attr">&quot;messageID&quot;</span>: <span class="string">&quot;string&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;callID&quot;</span>: <span class="string">&quot;string&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：permission/next.ts</p><h3 id="规则评估"><a href="#规则评估" class="headerlink" title="规则评估"></a>规则评估</h3><p>系统使用以下逻辑根据配置的规则集评估请求：</p><p>来源：permission/next.ts, permission/next.ts</p><h3 id="评估算法"><a href="#评估算法" class="headerlink" title="评估算法"></a>评估算法</h3><p><code>evaluate()</code> 函数合并所有规则集，并使用通配符匹配找到最后一个匹配的规则：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">evaluate</span>(<span class="params">permission: <span class="built_in">string</span>, pattern: <span class="built_in">string</span>, ...rulesets: Ruleset[]</span>): <span class="title">Rule</span> </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> merged = merge(...rulesets)</span><br><span class="line">  <span class="keyword">const</span> match = merged.findLast(</span><br><span class="line">    (rule) =&gt; Wildcard.match(permission, rule.permission) &amp;&amp; Wildcard.match(pattern, rule.pattern)</span><br><span class="line">  )</span><br><span class="line">  <span class="keyword">return</span> match ?? &#123; <span class="attr">action</span>: <span class="string">&quot;ask&quot;</span>, permission, <span class="attr">pattern</span>: <span class="string">&quot;*&quot;</span> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通配符匹配确保了像 <code>src/**/*.ts</code> 这样的模式能正确匹配嵌套目录结构。</p><p>来源：permission/next.ts</p><h2 id="用户响应处理"><a href="#用户响应处理" class="headerlink" title="用户响应处理"></a>用户响应处理</h2><h3 id="响应类型"><a href="#响应类型" class="headerlink" title="响应类型"></a>响应类型</h3><p>用户可以通过三个选项响应权限请求：</p><div class="table-container"><table><thead><tr><th>响应</th><th>行为</th><th>用例</th></tr></thead><tbody><tr><td>once (仅此一次)</td><td>仅批准此单个请求</td><td>对一次性操作的临时批准</td></tr><tr><td>always (总是允许)</td><td>批准并为将来的匹配保存规则</td><td>您信任的重复操作</td></tr><tr><td>reject (拒绝)</td><td>拒绝此请求并停止执行</td><td>您想要阻止的操作</td></tr></tbody></table></div><p>当选择“总是允许”时，系统会自动批准所有与保存的模式匹配的待处理请求。</p><p>来源：permission/next.ts</p><h3 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h3><p>系统为不同的拒绝场景提供了三种不同的错误类型：</p><ul><li><code>DeniedError</code>：由配置规则自动拒绝，包含匹配的规则集以供参考</li><li><code>RejectedError</code>：用户拒绝且未提供消息，使用默认消息停止执行</li><li><code>CorrectedError</code>：用户拒绝并提供了反馈消息，为使用不同参数重试提供指导</li></ul><p>这种区别有助于 Agent 理解它们应该重试、修改方法还是完全停止。</p><p>来源：permission/next.ts</p><h2 id="工具集成示例"><a href="#工具集成示例" class="headerlink" title="工具集成示例"></a>工具集成示例</h2><h3 id="文件编辑工具"><a href="#文件编辑工具" class="headerlink" title="文件编辑工具"></a>文件编辑工具</h3><p>编辑工具在修改文件之前请求权限：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> ctx.ask(&#123;</span><br><span class="line">  permission: <span class="string">&quot;edit&quot;</span>,</span><br><span class="line">  patterns: [path.relative(Instance.worktree, filePath)],</span><br><span class="line">  always: [<span class="string">&quot;*&quot;</span>],</span><br><span class="line">  metadata: &#123;</span><br><span class="line">    filepath: filePath,</span><br><span class="line">    diff: <span class="string">&quot;generated diff...&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这允许用户批准特定文件或像 <code>src/**/*.ts</code> 这样的模式以进行自动批准。</p><p>来源：tool/edit.ts</p><h3 id="Bash-工具"><a href="#Bash-工具" class="headerlink" title="Bash 工具"></a>Bash 工具</h3><p>Bash 工具执行复杂的命令解析以确定权限：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 解析命令结构</span></span><br><span class="line"><span class="keyword">const</span> tree = <span class="keyword">await</span> parser().then(<span class="function">(<span class="params">p</span>) =&gt;</span> p.parse(params.command))</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 为不同的命令类型提取模式</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> node <span class="keyword">of</span> tree.rootNode.descendantsOfType(<span class="string">&quot;command&quot;</span>)) &#123;</span><br><span class="line">  <span class="keyword">const</span> command = extractCommandTokens(node)</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 检查文件系统操作</span></span><br><span class="line">  <span class="keyword">if</span> ([<span class="string">&quot;rm&quot;</span>, <span class="string">&quot;cp&quot;</span>, <span class="string">&quot;mv&quot;</span>, <span class="string">&quot;mkdir&quot;</span>].includes(command[<span class="number">0</span>])) &#123;</span><br><span class="line">    directories.add(resolvedPath)</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 添加命令模式以进行权限检查</span></span><br><span class="line">  patterns.add(command.join(<span class="string">&quot; &quot;</span>))</span><br><span class="line">  always.add(BashArity.prefix(command).join(<span class="string">&quot; &quot;</span>) + <span class="string">&quot;*&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>系统使用命令元数检测来识别“人类可理解”的命令部分。例如，<code>npm install package-name</code> 被识别为 <code>npm install*</code> 用于权限模式。</p><p>来源：tool/bash.ts, permission/arity.ts</p><p>Bash 工具包含一个全面的元数字典，涵盖 150 多个常用命令（git, npm, docker, kubectl 等）。这确保了 <code>npm run dev</code> 和 <code>npm install</code> 被视为不同的模式，从而允许精细的权限控制。</p><h3 id="命令元数字典"><a href="#命令元数字典" class="headerlink" title="命令元数字典"></a>命令元数字典</h3><p>系统包含一个预构建的字典，将命令前缀映射到它们的元数（定义命令的标记数量）：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> ARITY: Record&lt;<span class="built_in">string</span>, <span class="built_in">number</span>&gt; = &#123;</span><br><span class="line">  git: <span class="number">2</span>,           <span class="comment">// git checkout, git commit</span></span><br><span class="line">  <span class="string">&quot;git config&quot;</span>: <span class="number">3</span>,   <span class="comment">// git config user.name</span></span><br><span class="line">  npm: <span class="number">2</span>,           <span class="comment">// npm install</span></span><br><span class="line">  <span class="string">&quot;npm run&quot;</span>: <span class="number">3</span>,     <span class="comment">// npm run dev</span></span><br><span class="line">  docker: <span class="number">2</span>,        <span class="comment">// docker run</span></span><br><span class="line">  <span class="string">&quot;docker compose&quot;</span>: <span class="number">3</span>,  <span class="comment">// docker compose up</span></span><br><span class="line">  kubectl: <span class="number">2</span>,       <span class="comment">// kubectl get pods</span></span><br><span class="line">  <span class="comment">// ... 150+ 命令</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这实现了基于模式的权限，可以理解命令语义。</p><p>来源：permission/arity.ts</p><h2 id="插件集成"><a href="#插件集成" class="headerlink" title="插件集成"></a>插件集成</h2><h3 id="权限-Hooks"><a href="#权限-Hooks" class="headerlink" title="权限 Hooks"></a>权限 Hooks</h3><p>插件可以通过 <code>permission.ask</code> hook 拦截权限请求，从而实现自定义授权逻辑：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 插件实现</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> MyPlugin: Plugin = <span class="keyword">async</span> (input) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    permission: &#123;</span><br><span class="line">      ask: <span class="keyword">async</span> (request, output) =&gt; &#123;</span><br><span class="line">        <span class="comment">// 自定义逻辑以确定响应</span></span><br><span class="line">        <span class="keyword">if</span> (shouldAutoApprove(request)) &#123;</span><br><span class="line">          output.status = <span class="string">&quot;allow&quot;</span></span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (shouldAutoDeny(request)) &#123;</span><br><span class="line">          output.status = <span class="string">&quot;deny&quot;</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          output.status = <span class="string">&quot;ask&quot;</span>  <span class="comment">// 让用户决定</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这允许插件实现特定领域的安全策略，与外部授权系统集成，或根据上下文因素提供自动批准。</p><p>来源：plugin/index.ts, permission/index.ts</p><h2 id="旧系统迁移"><a href="#旧系统迁移" class="headerlink" title="旧系统迁移"></a>旧系统迁移</h2><h3 id="从-Tools-字段迁移"><a href="#从-Tools-字段迁移" class="headerlink" title="从 Tools 字段迁移"></a>从 Tools 字段迁移</h3><p>系统会自动将旧版 <code>tools</code> 配置迁移到新的 <code>permission</code> 格式：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 旧格式（已弃用）</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;tools&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;edit&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: <span class="literal">false</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 自动转换为：</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;edit&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样，已弃用的 <code>autoshare</code> 字段会迁移到 <code>share</code> 字段。这种向后兼容性确保现有配置无需手动更新即可继续工作。</p><p>来源：config/config.ts, config/config.ts</p><h2 id="高级配置模式"><a href="#高级配置模式" class="headerlink" title="高级配置模式"></a>高级配置模式</h2><h3 id="特定环境权限"><a href="#特定环境权限" class="headerlink" title="特定环境权限"></a>特定环境权限</h3><p>为不同的环境配置不同的权限集：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;git*&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm run dev&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm run build&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;docker-compose -f docker-compose.dev.yml *&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;docker-compose -f docker-compose.prod.yml *&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="基于目录的安全性"><a href="#基于目录的安全性" class="headerlink" title="基于目录的安全性"></a>基于目录的安全性</h3><p>将操作限制在特定的项目区域：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;edit&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;src/**&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;tests/**&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;docs/**&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;node_modules/**&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;.git/**&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;external_directory&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;/tmp/**&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;/home/user/**&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="命令白名单"><a href="#命令白名单" class="headerlink" title="命令白名单"></a>命令白名单</h3><p>为生产环境实施严格的命令白名单：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;git status&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;git diff&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;git log -10&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm run test&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm run lint&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;*&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种默认拒绝的方法确保只有明确批准的命令才能执行。</p><p>来源：config/config.ts</p><h2 id="状态管理和持久化"><a href="#状态管理和持久化" class="headerlink" title="状态管理和持久化"></a>状态管理和持久化</h2><h3 id="权限状态"><a href="#权限状态" class="headerlink" title="权限状态"></a>权限状态</h3><p>系统维护权限状态，包括：</p><ul><li>待处理请求：当前等待用户批准</li><li>已批准规则：在会话期间保存的自动批准模式</li><li>会话隔离：权限范围限定在每个会话</li></ul><p>已批准的规则会持久化到 <code>[&quot;permission&quot;, projectID]</code> 下的存储中，允许规则在会话之间持久化（目前处于注释状态，等待 UI 管理实现）。</p><p>来源：permission/next.ts, permission/next.ts</p><h3 id="会话清理"><a href="#会话清理" class="headerlink" title="会话清理"></a>会话清理</h3><p>会话终止时，系统会自动拒绝所有待处理的权限请求，以防止孤立操作：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> (state) =&gt; &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> pending <span class="keyword">of</span> <span class="built_in">Object</span>.values(state.pending)) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> item <span class="keyword">of</span> <span class="built_in">Object</span>.values(pending)) &#123;</span><br><span class="line">      item.reject(<span class="keyword">new</span> RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这确保了清晰的会话边界，并防止工具在会话结束后执行。</p><p>来源：permission/index.ts</p><h2 id="事件系统"><a href="#事件系统" class="headerlink" title="事件系统"></a>事件系统</h2><h3 id="权限事件"><a href="#权限事件" class="headerlink" title="权限事件"></a>权限事件</h3><p>系统发布事件以与 UI 和其他组件集成：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> Event = &#123;</span><br><span class="line">  Asked: BusEvent.define(<span class="string">&quot;permission.asked&quot;</span>, Request),</span><br><span class="line">  Replied: BusEvent.define(</span><br><span class="line">    <span class="string">&quot;permission.replied&quot;</span>,</span><br><span class="line">    z.object(&#123;</span><br><span class="line">      sessionID: z.string(),</span><br><span class="line">      requestID: z.string(),</span><br><span class="line">      reply: Reply</span><br><span class="line">    &#125;)</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>组件可以订阅这些事件以：</p><ul><li>向用户显示权限对话框</li><li>跟踪权限使用情况以进行分析</li><li>实现自定义审批工作流</li><li>监控安全态势</li></ul><p>来源：permission/next.ts, permission/index.ts</p><h3 id="禁用工具检测"><a href="#禁用工具检测" class="headerlink" title="禁用工具检测"></a>禁用工具检测</h3><p>系统提供了一个实用函数，用于识别通过配置全局禁用的工具：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">disabled</span>(<span class="params">tools: <span class="built_in">string</span>[], ruleset: Ruleset</span>): <span class="title">Set</span>&lt;<span class="title">string</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">new</span> <span class="built_in">Set</span>&lt;<span class="built_in">string</span>&gt;()</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> tool <span class="keyword">of</span> tools) &#123;</span><br><span class="line">    <span class="keyword">const</span> permission = EDIT_TOOLS.includes(tool) ? <span class="string">&quot;edit&quot;</span> : tool</span><br><span class="line">    <span class="keyword">const</span> rule = ruleset.findLast(<span class="function">(<span class="params">r</span>) =&gt;</span> Wildcard.match(permission, r.permission))</span><br><span class="line">    <span class="keyword">if</span> (!rule) <span class="keyword">continue</span></span><br><span class="line">    <span class="keyword">if</span> (rule.pattern === <span class="string">&quot;*&quot;</span> &amp;&amp; rule.action === <span class="string">&quot;deny&quot;</span>) result.add(tool)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这允许 UI 组件根据配置隐藏或禁用不可用的工具，通过防止挫败感来改善用户体验。</p><p>来源：permission/next.ts</p><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="安全考虑"><a href="#安全考虑" class="headerlink" title="安全考虑"></a>安全考虑</h3><ol><li>默认询问：对于开发环境，使用 “ask” 作为默认操作，以保持对 Agent 操作的了解</li><li>生产白名单：在生产环境中，使用 “deny” 作为默认值，并为受信任的操作设置明确的允许规则</li><li>模式具体性：将模式按从具体到通用的顺序排列，以确保正确的覆盖行为</li><li>外部访问：始终要求对 external_directory 和网络工具进行批准</li><li>破坏性命令：明确拒绝危险模式，如 rm -rf 或 docker rm *</li></ol><h3 id="配置建议"><a href="#配置建议" class="headerlink" title="配置建议"></a>配置建议</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;edit&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;git*&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;npm*&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;rm -rf&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;*&quot;</span>: <span class="string">&quot;ask&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;external_directory&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;webfetch&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;websearch&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种平衡的方法允许安全的读取操作，同时要求对修改和外部访问进行监督。</p><p>来源：config/config.ts</p><h3 id="Agent-权限配置文件"><a href="#Agent-权限配置文件" class="headerlink" title="Agent 权限配置文件"></a>Agent 权限配置文件</h3><p>不同的 Agent 应具有适当的权限配置文件：</p><div class="table-container"><table><thead><tr><th>Agent 类型</th><th>推荐权限</th></tr></thead><tbody><tr><td>代码生成器</td><td>edit: ask, read: allow, bash: deny</td></tr><tr><td>重构 Agent</td><td>edit: allow 针对 src/**, read: allow, bash: allow 针对 git 命令</td></tr><tr><td>测试 Agent</td><td>edit: deny, read: allow, bash: allow 针对 npm test</td></tr><tr><td>文档 Agent</td><td>edit: ask 针对 docs/**, read: allow, bash: deny</td></tr></tbody></table></div><h1 id="Agent生命周期"><a href="#Agent生命周期" class="headerlink" title="Agent生命周期"></a>Agent生命周期</h1><p>OpenCode 中的 Agent 生命周期涵盖了从 Agent 注册、执行、状态转换到终止的完整旅程。该系统支持多种 Agent 类型（主 Agent、子 Agent、隐藏 Agent），并具备复杂的权限管理和实时状态跟踪功能。</p><h2 id="1-初始化阶段-Initialization"><a href="#1-初始化阶段-Initialization" class="headerlink" title="1. 初始化阶段 (Initialization)"></a>1. 初始化阶段 (Initialization)</h2><p>当用户发起对话时：</p><ol><li><p><strong>创建或获取Session</strong></p><ul><li>通过<a href="packages/opencode/src/session/index.ts#L126">Session.create</a>创建新会话，或通过<a href="packages/opencode/src/session/index.ts#L230">Session.get</a>获取已有会话</li><li>Session包含projectID、directory、parentID（如果是子会话）、权限配置等元数据</li></ul><p>具体源码见:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">  <span class="keyword">export</span> <span class="keyword">const</span> create = fn(</span><br><span class="line">    z</span><br><span class="line">      .object(&#123;</span><br><span class="line">        parentID: Identifier.schema(<span class="string">&quot;session&quot;</span>).optional(),</span><br><span class="line">...</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">const</span> get = fn(Identifier.schema(<span class="string">&quot;session&quot;</span>), <span class="keyword">async</span> (id) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> read = <span class="keyword">await</span> Storage.read&lt;Info&gt;([<span class="string">&quot;session&quot;</span>, Instance.project.id, id])</span><br><span class="line">    <span class="keyword">return</span> read <span class="keyword">as</span> Info</span><br><span class="line">  &#125;)</span><br></pre></td></tr></table></figure></li><li><p><strong>选择Agent</strong></p><ul><li>通过<a href="packages/opencode/src/agent/agent.ts#L230">Agent.get</a>获取指定的agent配置（默认是”build” agent）</li><li>Agent配置包含：名称、权限规则集、prompt、model配置、步骤限制等</li></ul></li><li><p><strong>创建用户消息</strong></p><ul><li><a href="packages/opencode/src/session/prompt.ts#L804">createUserMessage</a>构建MessageV2.User对象<br>源码见:</li></ul></li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">createUserMessage</span>(<span class="params">input: PromptInput</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> agent = <span class="keyword">await</span> Agent.get(input.agent ?? (<span class="keyword">await</span> Agent.defaultAgent()))</span><br><span class="line">  <span class="keyword">const</span> info: MessageV2.Info = &#123;</span><br><span class="line">    id: input.messageID ?? Identifier.ascending(<span class="string">&quot;message&quot;</span>),</span><br><span class="line">    role: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">    sessionID: input.sessionID,</span><br><span class="line">    time: &#123;</span><br><span class="line">      created: <span class="built_in">Date</span>.now(),</span><br><span class="line">    &#125;,</span><br><span class="line">    tools: input.tools,</span><br><span class="line">    agent: agent.name,</span><br><span class="line">    model: input.model ?? agent.model ?? (<span class="keyword">await</span> lastModel(input.sessionID)),</span><br><span class="line">    system: input.system,</span><br><span class="line">    variant: input.variant,</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><ul><li>处理文件附件、@agent引用等parts</li><li>将消息持久化存储</li></ul><h2 id="2-执行循环阶段-Execution-Loop"><a href="#2-执行循环阶段-Execution-Loop" class="headerlink" title="2. 执行循环阶段 (Execution Loop)"></a>2. 执行循环阶段 (Execution Loop)</h2><p><a href="packages/opencode/src/session/prompt.ts#L257">SessionPrompt.loop</a>是核心驱动器，它是一个无限循环：</p><h4 id="循环开始"><a href="#循环开始" class="headerlink" title="循环开始"></a>循环开始</h4><ul><li>检查最后一条用户消息和助手消息</li><li>如果助手已完成（finish状态不是tool-calls/unknown），退出循环</li></ul><h4 id="处理待办任务（优先级从高到低）"><a href="#处理待办任务（优先级从高到低）" class="headerlink" title="处理待办任务（优先级从高到低）"></a>处理待办任务（优先级从高到低）</h4><p><strong>a. Subtask处理</strong> (L317-478)</p><ul><li>如果有挂起的<a href="packages/opencode/src/session/prompt.ts#L317">subtask</a>，优先执行</li><li>Subtask是通过TaskTool调用的其他agent（如explore agent）</li><li>创建assistant消息，执行TaskTool，处理结果</li></ul><p><strong>b. Compaction处理</strong> (L482-492)</p><ul><li>如果有挂起的<a href="packages/opencode/src/session/prompt.ts#L482">压缩任务</a>，执行上下文压缩</li><li>调用<a href="packages/opencode/src/session/compaction.ts">SessionCompaction.process</a>减少token使用</li></ul><p><strong>c. 上下文溢出检查</strong> (L495-507)</p><ul><li>检查最后一条完成的消息是否导致token溢出</li><li>如果溢出，自动创建compaction任务并继续</li></ul><h4 id="正常处理-L509-620"><a href="#正常处理-L509-620" class="headerlink" title="正常处理 (L509-620)"></a>正常处理 (L509-620)</h4><p><strong>a. 创建Processor</strong></p><ul><li>通过<a href="packages/opencode/src/session/processor.ts#L26">SessionProcessor.create</a>创建处理器</li><li>创建新的assistant消息，关联到用户消息</li></ul><p><strong>b. 解析工具</strong></p><ul><li><a href="packages/opencode/src/session/prompt.ts#L641">resolveTools</a>根据agent权限和session权限合并工具集</li><li>从ToolRegistry获取可用工具</li><li>包装MCP工具</li></ul><p><strong>c. LLM流式调用</strong></p><ul><li>调用<a href="packages/opencode/src/session/processor.ts#L45">processor.process</a>开始处理</li><li>传入system prompt、历史消息、工具定义</li></ul><h2 id="3-LLM流式处理阶段-Streaming-Processing"><a href="#3-LLM流式处理阶段-Streaming-Processing" class="headerlink" title="3. LLM流式处理阶段 (Streaming Processing)"></a>3. LLM流式处理阶段 (Streaming Processing)</h2><p><a href="packages/opencode/src/session/processor.ts#L45">SessionProcessor.process</a>处理LLM流式响应：</p><h3 id="事件类型处理"><a href="#事件类型处理" class="headerlink" title="事件类型处理"></a>事件类型处理</h3><ol><li><p><strong>start</strong> (L58)</p><ul><li>设置session状态为busy</li></ul></li><li><p><strong>reasoning系列事件</strong> (L62-101)</p><ul><li>推理内容的开始、增量、结束</li><li>实时更新ReasoningPart</li></ul></li><li><p><strong>tool-input系列事件</strong> (L103-124)</p><ul><li>工具输入准备（当前未实际使用）</li></ul></li><li><p><strong>tool-call</strong> (L126-171)</p><ul><li>执行工具调用前</li><li><strong>Doom Loop检测</strong> (L146-168)：如果最近3次调用相同工具用相同参数，触发权限询问</li><li>创建ToolPart，状态设为running</li><li>执行工具</li></ul></li><li><p><strong>tool-result</strong> (L172-194)</p><ul><li>工具执行成功</li><li>更新ToolPart状态为completed，保存输出</li></ul></li><li><p><strong>tool-error</strong> (L196-221)</p><ul><li>工具执行失败</li><li>如果是权限拒绝错误，设置blocked标志（可能导致循环终止）</li><li>更新ToolPart状态为error</li></ul></li><li><p><strong>start-step / finish-step</strong> (L225-277)</p><ul><li>步骤边界标记</li><li>追踪文件快照变化，生成patch</li><li>更新token和成本统计</li><li>触发<a href="packages/opencode/src/session/summary.ts">SessionSummary.summarize</a></li><li>检查是否需要compaction</li></ul></li><li><p><strong>text系列事件</strong> (L279-326)</p><ul><li>文本内容的开始、增量、结束</li><li>实时更新TextPart</li></ul></li><li><p><strong>error</strong> (L222-223)</p><ul><li>抛出错误，进入重试逻辑</li></ul></li></ol><h3 id="错误处理和重试-L339-363"><a href="#错误处理和重试-L339-363" class="headerlink" title="错误处理和重试 (L339-363)"></a>错误处理和重试 (L339-363)</h3><ul><li>捕获异常，判断是否可重试（<a href="packages/opencode/src/session/retry.ts">SessionRetry.retryable</a>）</li><li>计算重试延迟，更新session状态</li><li>重试最多3次后终止</li></ul><h3 id="循环终止条件-L397-400"><a href="#循环终止条件-L397-400" class="headerlink" title="循环终止条件 (L397-400)"></a>循环终止条件 (L397-400)</h3><ul><li>如果需要compaction，返回”compact”</li><li>如果工具被权限拒绝，返回”stop”</li><li>如果有错误，返回”stop”</li><li>否则返回”continue”</li></ul><h2 id="4-状态管理和持久化"><a href="#4-状态管理和持久化" class="headerlink" title="4. 状态管理和持久化"></a>4. 状态管理和持久化</h2><p>在整个生命周期中：</p><ul><li><strong>SessionStatus</strong> - 实时状态更新（idle/busy/retry）</li><li><strong>MessageV2</strong> - 消息持久化（通过<a href="packages/opencode/src/session/index.ts#L337">Session.updateMessage</a>）</li><li><strong>Parts</strong> - 消息部分持久化（通过<a href="packages/opencode/src/session/index.ts#L389">Session.updatePart</a>）</li><li><strong>Snapshot</strong> - 文件变更追踪（通过<a href="packages/opencode/src/snapshot/index.ts">Snapshot.track</a>）</li><li><strong>事件总线</strong> - 发布事件供UI或其他组件订阅</li></ul><h2 id="5-清理和完成"><a href="#5-清理和完成" class="headerlink" title="5. 清理和完成"></a>5. 清理和完成</h2><p>当循环结束后：</p><ol><li><a href="packages/opencode/src/session/compaction.ts">SessionCompaction.prune</a> - 清理过期的压缩记录</li><li>返回最后一条assistant消息给调用者</li><li>触发相应的事件（如完成、错误）</li></ol><h2 id="关键特性"><a href="#关键特性" class="headerlink" title="关键特性"></a>关键特性</h2><ul><li><strong>单线程处理</strong>：每个session同时只能有一个active的循环（通过<a href="packages/opencode/src/session/index.ts#L453">BusyError</a>保护）</li><li><strong>渐进式执行</strong>：工具调用流式处理，实时更新UI</li><li><strong>智能压缩</strong>：自动检测token溢出并触发压缩</li><li><strong>循环保护</strong>：检测并阻止doom loop（重复调用相同工具）</li><li><strong>权限控制</strong>：每个工具调用前都会检查权限（<a href="packages/opencode/src/permission/next.ts">PermissionNext.ask</a>）</li><li><strong>成本追踪</strong>：精确统计每次调用的token和成本</li></ul><p>这就是agent从出生到完成任务的完整旅程。整个过程是异步的、流式的、可中断的，充满了状态检查和错误恢复机制。</p><h2 id="其他关键点"><a href="#其他关键点" class="headerlink" title="其他关键点"></a>其他关键点</h2><h3 id="Doom-Loop-预防"><a href="#Doom-Loop-预防" class="headerlink" title="Doom Loop 预防"></a>Doom Loop 预防</h3><p>系统实现了自动 doom loop 检测，以防止无限的工具调用循环：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> parts = <span class="keyword">await</span> MessageV2.parts(input.assistantMessage.id)</span><br><span class="line"><span class="keyword">const</span> lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span> (</span><br><span class="line">  lastThree.length === DOOM_LOOP_THRESHOLD &amp;&amp;</span><br><span class="line">  lastThree.every(</span><br><span class="line">    (p) =&gt;</span><br><span class="line">      p.type === <span class="string">&quot;tool&quot;</span> &amp;&amp;</span><br><span class="line">      p.tool === value.toolName &amp;&amp;</span><br><span class="line">      p.state.status !== <span class="string">&quot;pending&quot;</span> &amp;&amp;</span><br><span class="line">      <span class="built_in">JSON</span>.stringify(p.state.input) === <span class="built_in">JSON</span>.stringify(value.input),</span><br><span class="line">  )</span><br><span class="line">) &#123;</span><br><span class="line">  <span class="keyword">await</span> PermissionNext.ask(&#123;</span><br><span class="line">    permission: <span class="string">&quot;doom_loop&quot;</span>,</span><br><span class="line">    patterns: [value.toolName],</span><br><span class="line">    sessionID: input.assistantMessage.sessionID,</span><br><span class="line">    metadata: &#123; <span class="attr">tool</span>: value.toolName, <span class="attr">input</span>: value.input &#125;,</span><br><span class="line">    always: [value.toolName],</span><br><span class="line">    ruleset: agent.permission,</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：processor.ts</p><h4 id="核心原理"><a href="#核心原理" class="headerlink" title="核心原理"></a>核心原理</h4><p>系统监控连续的工具调用，如果检测到相同工具以相同参数被重复调用，就会触发用户确认。阈值设为 3 次。</p><h4 id="具体实现步骤"><a href="#具体实现步骤" class="headerlink" title="具体实现步骤"></a>具体实现步骤</h4><p>参见 <code>processor.tsL20-L168</code>：</p><ol><li><p><strong>设定阈值（第 20 行）</strong>：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> DOOM_LOOP_THRESHOLD = <span class="number">3</span></span><br></pre></td></tr></table></figure></li><li><p><strong>检测时机</strong>：每次执行工具调用时（tool-call 事件）</p></li><li><p><strong>检测逻辑（第 143-168 行）</strong>：</p><ul><li>获取当前消息的所有部分：<code>const parts = await MessageV2.parts(input.assistantMessage.id)</code></li><li>检查最后 3 个工具调用：<code>const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)</code></li><li>判断条件：只有当满足以下所有条件时才视为 doom loop：<ol><li>恰好有 3 个工具调用</li><li>都是 tool 类型</li><li>使用相同的工具名称</li><li>不是 pending 状态（说明已经执行过）</li><li>输入参数完全相同（通过 JSON 序列化比较）</li></ol></li></ul></li><li><p><strong>触发权限询问</strong>：</p><ul><li>调用 <code>PermissionNext.ask()</code> 请求用户确认</li><li>提供 <code>doom_loop</code> 权限类型</li><li>传递工具名称和输入参数作为元数据</li></ul></li></ol><h4 id="处理机制"><a href="#处理机制" class="headerlink" title="处理机制"></a>处理机制</h4><p>如果用户拒绝继续：</p><p>参见 <code>processor.tsL212-L217</code>：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 工具会被标记为错误状态</span></span><br><span class="line"><span class="comment">// 如果配置允许（continue_loop_on_deny），会继续执行；否则停止处理</span></span><br><span class="line"><span class="comment">// 返回 &quot;stop&quot; 状态终止循环</span></span><br></pre></td></tr></table></figure><p>这种设计既防止了无限循环浪费资源，又给用户提供了必要的控制权——如果是合理的重复操作，用户可以选择继续。</p><h3 id="系统持久化机制详解"><a href="#系统持久化机制详解" class="headerlink" title="系统持久化机制详解"></a>系统持久化机制详解</h3><p>系统的持久化机制设计得相当优雅。</p><h4 id="存储类型"><a href="#存储类型" class="headerlink" title="存储类型"></a>存储类型</h4><p>基于文件系统的 JSON 存储，参见 <code>storage.tsL143-L158</code>：</p><ul><li>所有数据存储在 <code>&#123;Global.Path.data&#125;/storage/</code> 目录</li><li>每个实体都是独立的 <code>.json</code> 文件</li><li>使用读写锁机制保证并发安全（参见 <code>LockL172</code>）</li></ul><h4 id="存储格式与目录结构"><a href="#存储格式与目录结构" class="headerlink" title="存储格式与目录结构"></a>存储格式与目录结构</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">storage&#x2F;</span><br><span class="line">├── session&#x2F;           # 会话信息</span><br><span class="line">│   └── &lt;projectID&gt;&#x2F;</span><br><span class="line">│       └── &lt;sessionID&gt;.json</span><br><span class="line">├── message&#x2F;           # 消息</span><br><span class="line">│   └── &lt;sessionID&gt;&#x2F;</span><br><span class="line">│       └── &lt;messageID&gt;.json</span><br><span class="line">├── part&#x2F;              # 消息部分（文本、工具调用等）</span><br><span class="line">│   └── &lt;messageID&gt;&#x2F;</span><br><span class="line">│       └── &lt;partID&gt;.json</span><br><span class="line">├── session_diff&#x2F;      # 会话变更历史</span><br><span class="line">│   └── &lt;sessionID&gt;.json</span><br><span class="line">├── share&#x2F;             # 分享信息</span><br><span class="line">│   └── &lt;sessionID&gt;.json</span><br><span class="line">├── project&#x2F;           # 项目信息</span><br><span class="line">│   └── &lt;projectID&gt;.json</span><br><span class="line">└── migration          # 迁移版本号</span><br></pre></td></tr></table></figure><h4 id="核心数据结构"><a href="#核心数据结构" class="headerlink" title="核心数据结构"></a>核心数据结构</h4><h5 id="1-会话（Session）"><a href="#1-会话（Session）" class="headerlink" title="1. 会话（Session）"></a>1. 会话（Session）</h5><p>参见 <code>session/index.tsL39-L79</code>：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Session.Info = &#123;</span><br><span class="line">  id: <span class="built_in">string</span>                    <span class="comment">// 会话 ID</span></span><br><span class="line">  projectID: <span class="built_in">string</span>             <span class="comment">// 所属项目</span></span><br><span class="line">  directory: <span class="built_in">string</span>             <span class="comment">// 工作目录</span></span><br><span class="line">  parentID?: <span class="built_in">string</span>             <span class="comment">// 父会话（分叉场景）</span></span><br><span class="line">  title: <span class="built_in">string</span>                 <span class="comment">// 会话标题</span></span><br><span class="line">  version: <span class="built_in">string</span>               <span class="comment">// 创建时的版本</span></span><br><span class="line">  summary?: &#123;                   <span class="comment">// 变更摘要</span></span><br><span class="line">    additions: <span class="built_in">number</span></span><br><span class="line">    deletions: <span class="built_in">number</span></span><br><span class="line">    files: <span class="built_in">number</span></span><br><span class="line">    diffs?: FileDiff[]</span><br><span class="line">  &#125;</span><br><span class="line">  share?: &#123; <span class="attr">url</span>: <span class="built_in">string</span> &#125;       <span class="comment">// 分享 URL</span></span><br><span class="line">  time: &#123;                       <span class="comment">// 时间戳</span></span><br><span class="line">    created: <span class="built_in">number</span></span><br><span class="line">    updated: <span class="built_in">number</span></span><br><span class="line">    compacting?: <span class="built_in">number</span></span><br><span class="line">    archived?: <span class="built_in">number</span></span><br><span class="line">  &#125;</span><br><span class="line">  permission?: Ruleset          <span class="comment">// 权限规则集</span></span><br><span class="line">  revert?: RevertInfo           <span class="comment">// 回滚信息</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="2-消息（MessageV2）"><a href="#2-消息（MessageV2）" class="headerlink" title="2. 消息（MessageV2）"></a>2. 消息（MessageV2）</h5><p>参见 <code>message-v2.tsL298-L390</code>：</p><h6 id="用户消息："><a href="#用户消息：" class="headerlink" title="用户消息："></a>用户消息：</h6><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User = &#123;</span><br><span class="line">  id: <span class="built_in">string</span></span><br><span class="line">  sessionID: <span class="built_in">string</span></span><br><span class="line">  role: <span class="string">&quot;user&quot;</span></span><br><span class="line">  time: &#123; <span class="attr">created</span>: <span class="built_in">number</span> &#125;</span><br><span class="line">  agent: <span class="built_in">string</span>                 <span class="comment">// 使用的 Agent</span></span><br><span class="line">  model: &#123;                      <span class="comment">// 模型配置</span></span><br><span class="line">    providerID: <span class="built_in">string</span></span><br><span class="line">    modelID: <span class="built_in">string</span></span><br><span class="line">  &#125;</span><br><span class="line">  system?: <span class="built_in">string</span>               <span class="comment">// 系统提示</span></span><br><span class="line">  tools?: Record&lt;<span class="built_in">string</span>, <span class="built_in">boolean</span>&gt;  <span class="comment">// 工具启用状态</span></span><br><span class="line">  variant?: <span class="built_in">string</span>              <span class="comment">// 模型变体</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h6 id="助手消息："><a href="#助手消息：" class="headerlink" title="助手消息："></a>助手消息：</h6><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Assistant = &#123;</span><br><span class="line">  id: <span class="built_in">string</span></span><br><span class="line">  sessionID: <span class="built_in">string</span></span><br><span class="line">  role: <span class="string">&quot;assistant&quot;</span></span><br><span class="line">  parentID: <span class="built_in">string</span>              <span class="comment">// 关联的用户消息 ID</span></span><br><span class="line">  modelID: <span class="built_in">string</span></span><br><span class="line">  providerID: <span class="built_in">string</span></span><br><span class="line">  agent: <span class="built_in">string</span></span><br><span class="line">  mode: <span class="built_in">string</span>                  <span class="comment">// @deprecated</span></span><br><span class="line">  path: &#123; <span class="attr">cwd</span>: <span class="built_in">string</span>, <span class="attr">root</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  cost: <span class="built_in">number</span>                  <span class="comment">// 成本</span></span><br><span class="line">  tokens: &#123;                     <span class="comment">// Token 使用统计</span></span><br><span class="line">    input: <span class="built_in">number</span></span><br><span class="line">    output: <span class="built_in">number</span></span><br><span class="line">    reasoning: <span class="built_in">number</span></span><br><span class="line">    cache: &#123; <span class="attr">read</span>: <span class="built_in">number</span>, <span class="attr">write</span>: <span class="built_in">number</span> &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  finish?: <span class="built_in">string</span>               <span class="comment">// 完成原因</span></span><br><span class="line">  time: &#123;</span><br><span class="line">    created: <span class="built_in">number</span></span><br><span class="line">    completed?: <span class="built_in">number</span></span><br><span class="line">  &#125;</span><br><span class="line">  error?: <span class="built_in">Error</span>                 <span class="comment">// 错误信息</span></span><br><span class="line">  summary?: <span class="built_in">boolean</span>             <span class="comment">// 是否为摘要消息</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="3-消息部分（Parts）"><a href="#3-消息部分（Parts）" class="headerlink" title="3. 消息部分（Parts）"></a>3. 消息部分（Parts）</h5><p>消息由多个部分组成，支持多种类型，参见 <code>message-v2.tsL323-L341</code>：</p><div class="table-container"><table><thead><tr><th>类型</th><th>用途</th></tr></thead><tbody><tr><td>text</td><td>文本内容（助手回复或用户输入）</td></tr><tr><td>tool</td><td>工具调用（输入、输出、错误）</td></tr><tr><td>reasoning</td><td>推理过程（思维链）</td></tr><tr><td>file</td><td>文件附件</td></tr><tr><td>snapshot</td><td>文件快照</td></tr><tr><td>patch</td><td>代码补丁</td></tr><tr><td>step-start/finish</td><td>执行步骤标记</td></tr><tr><td>compaction</td><td>会话压缩标记</td></tr><tr><td>subtask</td><td>子任务</td></tr><tr><td>retry</td><td>重试信息</td></tr><tr><td>agent</td><td>Agent 信息</td></tr></tbody></table></div><h6 id="工具部分详解（最复杂）："><a href="#工具部分详解（最复杂）：" class="headerlink" title="工具部分详解（最复杂）："></a>工具部分详解（最复杂）：</h6><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> ToolPart = &#123;</span><br><span class="line">  id: <span class="built_in">string</span></span><br><span class="line">  sessionID: <span class="built_in">string</span></span><br><span class="line">  messageID: <span class="built_in">string</span></span><br><span class="line">  callID: <span class="built_in">string</span>                <span class="comment">// LLM 的工具调用 ID</span></span><br><span class="line">  tool: <span class="built_in">string</span>                  <span class="comment">// 工具名称</span></span><br><span class="line">  state: ToolState              <span class="comment">// 状态机</span></span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">type</span> ToolState = </span><br><span class="line">  | &#123; <span class="attr">status</span>: <span class="string">&quot;pending&quot;</span>, <span class="attr">input</span>: <span class="built_in">any</span>, <span class="attr">raw</span>: <span class="built_in">string</span> &#125;</span><br><span class="line">  | &#123; <span class="attr">status</span>: <span class="string">&quot;running&quot;</span>, <span class="attr">input</span>: <span class="built_in">any</span>, <span class="attr">time</span>: &#123; <span class="attr">start</span>: <span class="built_in">number</span> &#125; &#125;</span><br><span class="line">  | &#123; <span class="attr">status</span>: <span class="string">&quot;completed&quot;</span>, </span><br><span class="line">      input: <span class="built_in">any</span>,</span><br><span class="line">      output: <span class="built_in">string</span>,</span><br><span class="line">      title: <span class="built_in">string</span>,</span><br><span class="line">      time: &#123; <span class="attr">start</span>: <span class="built_in">number</span>, <span class="attr">end</span>: <span class="built_in">number</span>, compacted?: <span class="built_in">number</span> &#125;,</span><br><span class="line">      attachments?: FilePart[] &#125;</span><br><span class="line">  | &#123; <span class="attr">status</span>: <span class="string">&quot;error&quot;</span>,</span><br><span class="line">      input: <span class="built_in">any</span>,</span><br><span class="line">      error: <span class="built_in">string</span>,</span><br><span class="line">      time: &#123; <span class="attr">start</span>: <span class="built_in">number</span>, <span class="attr">end</span>: <span class="built_in">number</span> &#125; &#125;</span><br></pre></td></tr></table></figure><h4 id="持久化操作"><a href="#持久化操作" class="headerlink" title="持久化操作"></a>持久化操作</h4><p>核心 API 在 <code>storage.tsL160-L226</code>：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 读取</span></span><br><span class="line">Storage.read&lt;T&gt;(key: <span class="built_in">string</span>[]) -&gt; T</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 写入（覆盖）</span></span><br><span class="line">Storage.write&lt;T&gt;(key: <span class="built_in">string</span>[], <span class="attr">content</span>: T) -&gt; <span class="built_in">void</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">// 更新（读取-修改-写入）</span></span><br><span class="line">Storage.update&lt;T&gt;(key: <span class="built_in">string</span>[], <span class="attr">fn</span>: <span class="function">(<span class="params">draft: T</span>) =&gt;</span> <span class="built_in">void</span>) -&gt; T</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 删除</span></span><br><span class="line">Storage.remove(key: <span class="built_in">string</span>[]) -&gt; <span class="built_in">void</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">// 列举</span></span><br><span class="line">Storage.list(prefix: <span class="built_in">string</span>[]) -&gt; <span class="built_in">string</span>[][]</span><br></pre></td></tr></table></figure><p>使用示例（来自 <code>session/index.tsL209</code>）：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建会话</span></span><br><span class="line"><span class="keyword">await</span> Storage.write(</span><br><span class="line">  [<span class="string">&quot;session&quot;</span>, Instance.project.id, result.id], </span><br><span class="line">  result</span><br><span class="line">)</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 更新消息</span></span><br><span class="line"><span class="keyword">await</span> Storage.write([<span class="string">&quot;message&quot;</span>, msg.sessionID, msg.id], msg)</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 更新部分</span></span><br><span class="line"><span class="keyword">await</span> Storage.write([<span class="string">&quot;part&quot;</span>, part.messageID, part.id], part)</span><br></pre></td></tr></table></figure><h4 id="数据迁移"><a href="#数据迁移" class="headerlink" title="数据迁移"></a>数据迁移</h4><p>系统支持 schema 迁移，参见 <code>storage.tsL23-L141</code>：</p><ul><li>迁移脚本在 <code>MIGRATIONS</code> 数组中</li><li>每次启动时检查 <code>migration</code> 文件</li><li>按顺序执行未执行的迁移</li></ul><h4 id="会话压缩（优化存储）"><a href="#会话压缩（优化存储）" class="headerlink" title="会话压缩（优化存储）"></a>会话压缩（优化存储）</h4><p>当会话接近上下文限制时，系统会进行压缩，参见 <code>compaction.tsL30-L39</code>：</p><h5 id="主动修剪："><a href="#主动修剪：" class="headerlink" title="主动修剪："></a>主动修剪：</h5><ul><li>从旧工具调用中删除输出（保留调用信息）</li><li>保护最近的 40,000 tokens 和特定工具（如 skill）</li><li>只删除超过 20,000 tokens 的内容</li></ul><h5 id="会话压缩："><a href="#会话压缩：" class="headerlink" title="会话压缩："></a>会话压缩：</h5><ul><li>使用 LLM 生成对话摘要</li><li>在压缩点插入 <code>compaction</code> part</li><li>后续消息可以基于摘要恢复上下文</li></ul><p>这种设计既保证了数据的完整性和可追溯性，又通过文件系统和 JSON 格式保持了简单性和可维护性。</p><h3 id="生命周期事件"><a href="#生命周期事件" class="headerlink" title="生命周期事件"></a>生命周期事件</h3><p>系统通过事件总线发布全面的事件，用于实时监控和 UI 更新：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> Event = &#123;</span><br><span class="line">  Created: BusEvent.define(<span class="string">&quot;session.created&quot;</span>, z.object(&#123; <span class="attr">info</span>: Info &#125;)),</span><br><span class="line">  Updated: BusEvent.define(<span class="string">&quot;session.updated&quot;</span>, z.object(&#123; <span class="attr">info</span>: Info &#125;)),</span><br><span class="line">  Deleted: BusEvent.define(<span class="string">&quot;session.deleted&quot;</span>, z.object(&#123; <span class="attr">info</span>: Info &#125;)),</span><br><span class="line">  Diff: BusEvent.define(<span class="string">&quot;session.diff&quot;</span>, z.object(&#123;</span><br><span class="line">    sessionID: z.string(),</span><br><span class="line">    diff: Snapshot.FileDiff.array(),</span><br><span class="line">  &#125;)),</span><br><span class="line">  <span class="built_in">Error</span>: BusEvent.define(<span class="string">&quot;session.error&quot;</span>, z.object(&#123;</span><br><span class="line">    sessionID: z.string().optional(),</span><br><span class="line">    error: MessageV2.Assistant.shape.error,</span><br><span class="line">  &#125;)),</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：index.ts</p><h1 id="创建自定义-Agent：配置与最佳实践"><a href="#创建自定义-Agent：配置与最佳实践" class="headerlink" title="创建自定义 Agent：配置与最佳实践"></a>创建自定义 Agent：配置与最佳实践</h1><p>本指南涵盖了在 OpenCode 中创建和配置自定义 agent 的完整流程，从基础设置到高级配置模式和最佳实践。</p><h2 id="Agent-配置基础"><a href="#Agent-配置基础" class="headerlink" title="Agent 配置基础"></a>Agent 配置基础</h2><p>OpenCode 中的自定义 agent 通过一个支持多种配置来源和分层合并的声明式系统进行配置。核心 agent schema 定义了所有 agent（无论是内置还是自定义）必须遵循的结构。</p><h3 id="配置-Schema-概述"><a href="#配置-Schema-概述" class="headerlink" title="配置 Schema 概述"></a>配置 Schema 概述</h3><p>每个 agent 配置都遵循 <code>Agent.Info</code> schema，并包含以下核心属性：</p><div class="table-container"><table><thead><tr><th>属性</th><th>类型</th><th>必需</th><th>描述</th></tr></thead><tbody><tr><td>name</td><td>string</td><td>是</td><td>agent 的唯一标识符</td></tr><tr><td>description</td><td>string</td><td>否</td><td>描述何时使用该 agent 的可读说明</td></tr><tr><td>mode</td><td>“subagent” \</td><td>“primary” \</td><td>“all”</td><td>否</td><td>决定 agent 何时可用</td></tr><tr><td>prompt</td><td>string</td><td>否</td><td>定义 agent 行为的系统提示词</td></tr><tr><td>model</td><td>object</td><td>否</td><td>特定模型配置 (providerID, modelID)</td></tr><tr><td>temperature</td><td>number</td><td>否</td><td>采样温度 (0.0-1.0)</td></tr><tr><td>topP</td><td>number</td><td>否</td><td>核采样参数</td></tr><tr><td>permission</td><td>PermissionObject</td><td>否</td><td>工具权限覆盖</td></tr><tr><td>hidden</td><td>boolean</td><td>否</td><td>从 @ 自动补全菜单中隐藏（仅限 subagent）</td></tr><tr><td>color</td><td>string</td><td>否</td><td>UI 显示的十六进制颜色代码 (#RRGGBB)</td></tr><tr><td>steps</td><td>number</td><td>否</td><td>纯文本响应前的最大 agent 迭代次数</td></tr><tr><td>disable</td><td>boolean</td><td>否</td><td>完全禁用该 agent</td></tr></tbody></table></div><p>来源：packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts</p><h3 id="配置加载层级"><a href="#配置加载层级" class="headerlink" title="配置加载层级"></a>配置加载层级</h3><p>OpenCode 按照特定的优先级顺序从多个来源加载 agent 配置，后加载的来源会覆盖先前的来源：</p><p>Agent 文件通过扫描多个目录的 glob 模式发现：</p><ul><li><code>.opencode/agent/**/*.md</code>: 项目特定的 agent</li><li><code>.opencode/mode/**/*.md</code> :模式特定的 agent（已弃用，使用 mode: primary）</li><li>全局配置目录中的 agent</li><li>向上至工作树根目录的祖先 <code>.opencode</code> 目录</li></ul><p>来源：packages/opencode/src/config/config.ts, packages/opencode/src/config/config.ts</p><h2 id="创建你的第一个自定义-Agent"><a href="#创建你的第一个自定义-Agent" class="headerlink" title="创建你的第一个自定义 Agent"></a>创建你的第一个自定义 Agent</h2><h3 id="方法-1：Markdown-文件配置"><a href="#方法-1：Markdown-文件配置" class="headerlink" title="方法 1：Markdown 文件配置"></a>方法 1：Markdown 文件配置</h3><p>创建自定义 agent 的推荐方法是使用带有 frontmatter 配置的 Markdown 文件：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: code-reviewer</span><br><span class="line">description: 分析代码变更的质量、安全漏洞和最佳实践违规</span><br><span class="line">mode: subagent</span><br><span class="line">temperature: 0.3</span><br><span class="line">permission:</span><br><span class="line">  read: allow</span><br><span class="line">  grep: allow</span><br><span class="line">  websearch: deny</span><br><span class="line">---</span><br><span class="line"></span><br><span class="line">你是一位代码审查专家。你的专业能力包括：</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 安全漏洞检测</span><br><span class="line"><span class="bullet">-</span> 性能优化机会识别</span><br><span class="line"><span class="bullet">-</span> 代码风格和最佳实践执行</span><br><span class="line"><span class="bullet">-</span> 架构模式分析</span><br><span class="line"></span><br><span class="line">在审查代码时：</span><br><span class="line"><span class="bullet">1.</span> 识别潜在的安全问题</span><br><span class="line"><span class="bullet">2.</span> 检查常见的反模式</span><br><span class="line"><span class="bullet">3.</span> 提出可读性改进建议</span><br><span class="line"><span class="bullet">4.</span> 验证适当的错误处理</span><br><span class="line"><span class="bullet">5.</span> 评估测试覆盖需求</span><br><span class="line"></span><br><span class="line">在适当的地方提供带有具体代码示例的建设性反馈。</span><br></pre></td></tr></table></figure><p>将此文件保存为项目目录中的 <code>.opencode/agent/code-reviewer.md</code>。</p><p>来源：packages/opencode/src/config/config.ts</p><h3 id="方法-2：JSON-配置"><a href="#方法-2：JSON-配置" class="headerlink" title="方法 2：JSON 配置"></a>方法 2：JSON 配置</h3><p>对于程序化配置，你可以直接在 <code>opencode.json</code> 中定义 agent：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;agent&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;code-reviewer&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;code-reviewer&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;分析代码变更的质量和安全性&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;mode&quot;</span>: <span class="string">&quot;subagent&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;temperature&quot;</span>: <span class="number">0.3</span>,</span><br><span class="line">      <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;grep&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;websearch&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">&quot;prompt&quot;</span>: <span class="string">&quot;你是一位代码审查专家...&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/config/config.ts</p><h3 id="方法-3：AI-辅助生成"><a href="#方法-3：AI-辅助生成" class="headerlink" title="方法 3：AI 辅助生成"></a>方法 3：AI 辅助生成</h3><p>OpenCode 提供了一个内置的 agent 生成功能，使用 AI 根据自然语言描述创建 agent 配置：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> result = <span class="keyword">await</span> Agent.generate(&#123;</span><br><span class="line">  description: <span class="string">&quot;一个专注于将遗留 JavaScript 代码重构为现代 TypeScript 的 agent&quot;</span>,</span><br><span class="line">  model: &#123; <span class="attr">providerID</span>: <span class="string">&quot;anthropic&quot;</span>, <span class="attr">modelID</span>: <span class="string">&quot;claude-3-sonnet-20240229&quot;</span> &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回：&#123; identifier, whenToUse, systemPrompt &#125;</span></span><br></pre></td></tr></table></figure><p>此函数会验证生成的标识符不与现有 agent 冲突，并生成完整的可供使用的配置。</p><p>来源：packages/opencode/src/agent/agent.ts</p><h2 id="Agent-模式和使用模式"><a href="#Agent-模式和使用模式" class="headerlink" title="Agent 模式和使用模式"></a>Agent 模式和使用模式</h2><p>理解 agent 模式对于正确的 agent 设计和用户体验至关重要。</p><h3 id="模式类型"><a href="#模式类型" class="headerlink" title="模式类型"></a>模式类型</h3><div class="table-container"><table><thead><tr><th>模式</th><th>可用性</th><th>用例</th><th>示例</th></tr></thead><tbody><tr><td>primary</td><td>用户直接选择</td><td>主要工作流，面向用户的 agent</td><td>build, plan</td></tr><tr><td>subagent</td><td>仅限委托</td><td>专门任务，@提及</td><td>general, explore, 自定义专家</td></tr><tr><td>all</td><td>直接和委托均可</td><td>可充当任一角色的多功能 agent</td><td>（罕见，通常首选显式模式）</td></tr></tbody></table></div><p>来源：packages/opencode/src/agent/agent.ts</p><h3 id="内置-Agent-模式"><a href="#内置-Agent-模式" class="headerlink" title="内置 Agent 模式"></a>内置 Agent 模式</h3><p>系统包括几个展示不同模式的内置 agent：</p><h4 id="Explore-Agent-快速代码库导航专家"><a href="#Explore-Agent-快速代码库导航专家" class="headerlink" title="Explore Agent - 快速代码库导航专家"></a>Explore Agent - 快速代码库导航专家</h4><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: explore</span><br><span class="line">mode: subagent</span><br><span class="line">permission:</span><br><span class="line">  &quot;<span class="emphasis">*&quot;: deny</span></span><br><span class="line"><span class="emphasis">  grep: allow</span></span><br><span class="line"><span class="emphasis">  glob: allow</span></span><br><span class="line"><span class="emphasis">  list: allow</span></span><br><span class="line"><span class="emphasis">  bash: allow</span></span><br><span class="line"><span class="emphasis">  webfetch: allow</span></span><br><span class="line"><span class="emphasis">  websearch: allow</span></span><br><span class="line"><span class="emphasis">  codesearch: allow</span></span><br><span class="line"><span class="emphasis">  read: allow</span></span><br><span class="line"><span class="emphasis">---</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">你是一位文件搜索专家。你擅长彻底地导航和探索代码库。</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">你的强项：</span></span><br><span class="line"><span class="emphasis">- 使用 glob 模式快速查找文件</span></span><br><span class="line"><span class="emphasis">- 使用强大的正则表达式搜索代码和文本</span></span><br><span class="line"><span class="emphasis">- 阅读和分析文件内容</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">指导原则：</span></span><br><span class="line"><span class="emphasis">- 使用 Glob 进行广泛的文件模式匹配</span></span><br><span class="line"><span class="emphasis">- 使用 Grep 通过正则表达式搜索文件内容</span></span><br><span class="line"><span class="emphasis">- 当你知道需要阅读的具体文件路径时，使用 Read</span></span><br><span class="line"><span class="emphasis">- 在最终响应中以绝对路径返回文件路径</span></span><br><span class="line"><span class="emphasis">- 为了清晰沟通，请避免使用表情符号</span></span><br><span class="line"><span class="emphasis">- 不要创建任何文件，或运行修改用户系统状态的 bash 命令</span></span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/agent/prompt/explore.txt, packages/opencode/src/agent/agent.ts</p><h4 id="Build-Agent-用于代码生成的主要-agent"><a href="#Build-Agent-用于代码生成的主要-agent" class="headerlink" title="Build Agent - 用于代码生成的主要 agent"></a>Build Agent - 用于代码生成的主要 agent</h4><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">build: &#123;</span><br><span class="line">  name: <span class="string">&quot;build&quot;</span>,</span><br><span class="line">  options: &#123;&#125;,</span><br><span class="line">  permission: PermissionNext.merge(</span><br><span class="line">    defaults,</span><br><span class="line">    PermissionNext.fromConfig(&#123;</span><br><span class="line">      question: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    &#125;),</span><br><span class="line">    user,</span><br><span class="line">  ),</span><br><span class="line">  mode: <span class="string">&quot;primary&quot;</span>,</span><br><span class="line">  native: <span class="literal">true</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/agent/agent.ts</p><h2 id="权限系统集成-1"><a href="#权限系统集成-1" class="headerlink" title="权限系统集成"></a>权限系统集成</h2><p>自定义 agent 可以覆盖全局权限配置，根据 agent 的预期用途限制或扩展工具访问。</p><h3 id="权限配置结构"><a href="#权限配置结构" class="headerlink" title="权限配置结构"></a>权限配置结构</h3><p>权限定义为一个分层对象，其中每个工具映射到一个操作：</p><div class="table-container"><table><thead><tr><th>操作</th><th>行为</th><th>用例</th></tr></thead><tbody><tr><td>allow</td><td>始终允许</td><td>受信任环境下的安全工具</td></tr><tr><td>deny</td><td>始终阻止</td><td>危险或不适当的工具</td></tr><tr><td>ask</td><td>提示用户</td><td>需要确认的工具</td></tr></tbody></table></div><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;edit&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;*.js&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;*.env&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;node_modules/**&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;bash&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;websearch&quot;</span>: <span class="string">&quot;deny&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/config/config.ts, packages/opencode/src/permission/next.ts</p><h3 id="权限合并行为"><a href="#权限合并行为" class="headerlink" title="权限合并行为"></a>权限合并行为</h3><p>定义 agent 权限时，它们会按特定顺序与系统默认值合并：</p><ol><li>系统默认权限</li><li>Agent 特定的权限覆盖</li><li>用户配置的基础权限</li><li>外部目录访问许可（除非明确拒绝，否则始终允许 Truncate.DIR）</li></ol><p>来源：packages/opencode/src/agent/agent.ts, packages/opencode/src/agent/agent.ts</p><h3 id="权限最佳实践"><a href="#权限最佳实践" class="headerlink" title="权限最佳实践"></a>权限最佳实践</h3><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: security-scanner</span><br><span class="line">description: 扫描代码中的安全漏洞</span><br><span class="line">mode: subagent</span><br><span class="line">permission:</span><br><span class="line">  &quot;<span class="emphasis">*&quot;: deny</span></span><br><span class="line"><span class="emphasis">  read: allow</span></span><br><span class="line"><span class="emphasis">  grep: allow</span></span><br><span class="line"><span class="emphasis">  glob: allow</span></span><br><span class="line"><span class="emphasis">  webfetch: allow</span></span><br><span class="line"><span class="emphasis">  bash: deny</span></span><br><span class="line"><span class="emphasis">  edit: deny</span></span><br><span class="line"><span class="emphasis">---</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">你是一位安全专家。仅专注于阅读和分析代码。</span></span><br><span class="line"><span class="emphasis">在安全分析期间切勿执行任意命令或修改文件。</span></span><br></pre></td></tr></table></figure><p>对于专用 agent，始终以 <code>*: deny</code> 开始限制，然后仅显式允许 agent 目的所需的工具。这可以防止意外副作用和安全风险。</p><h2 id="高级配置模式-1"><a href="#高级配置模式-1" class="headerlink" title="高级配置模式"></a>高级配置模式</h2><h3 id="温度和创意控制"><a href="#温度和创意控制" class="headerlink" title="温度和创意控制"></a>温度和创意控制</h3><p>通过温度设置微调 agent 行为：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;creative-writer&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;temperature&quot;</span>: <span class="number">0.9</span>,</span><br><span class="line">  <span class="attr">&quot;topP&quot;</span>: <span class="number">0.95</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;api-designer&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;temperature&quot;</span>: <span class="number">0.2</span>,</span><br><span class="line">  <span class="attr">&quot;topP&quot;</span>: <span class="number">0.7</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="table-container"><table><thead><tr><th>Agent 类型</th><th>温度范围</th><th>基本原理</th></tr></thead><tbody><tr><td>创意/探索性</td><td>0.7-0.9</td><td>鼓励多样化的输出</td></tr><tr><td>分析/调试</td><td>0.1-0.4</td><td>专注、确定性的响应</td></tr><tr><td>代码生成</td><td>0.3-0.5</td><td>正确性和多样性的平衡</td></tr></tbody></table></div><p>来源：packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts</p><h3 id="步骤限制以实现受控执行"><a href="#步骤限制以实现受控执行" class="headerlink" title="步骤限制以实现受控执行"></a>步骤限制以实现受控执行</h3><p>使用 <code>steps</code> 参数防止无限循环或过度使用工具：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;quick-audit&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;mode&quot;</span>: <span class="string">&quot;subagent&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;steps&quot;</span>: <span class="number">3</span>,</span><br><span class="line">  <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;具有有限迭代的快速代码审计&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这会强制 agent 在指定次数的工具调用后提供纯文本响应，使其适合时间敏感的操作。</p><p>来源：packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts</p><h3 id="每个-Agent-的模型选择"><a href="#每个-Agent-的模型选择" class="headerlink" title="每个 Agent 的模型选择"></a>每个 Agent 的模型选择</h3><p>不同的 agent 可以根据其需求使用不同的模型：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;agent&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;explore&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-3-haiku-20240307&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;使用轻量级模型快速探索&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">&quot;code-analysis&quot;</span>: &#123;</span><br><span class="line">      <span class="attr">&quot;model&quot;</span>: <span class="string">&quot;anthropic/claude-3-opus-20240229&quot;</span>,</span><br><span class="line">      <span class="attr">&quot;description&quot;</span>: <span class="string">&quot;使用最强大的模型进行深入分析&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts</p><h3 id="自定义选项和元数据"><a href="#自定义选项和元数据" class="headerlink" title="自定义选项和元数据"></a>自定义选项和元数据</h3><p>通过 <code>options</code> 字段传递自定义配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;test-generator&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;options&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;framework&quot;</span>: <span class="string">&quot;jest&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;coverageThreshold&quot;</span>: <span class="number">80</span>,</span><br><span class="line">    <span class="attr">&quot;preferAsync&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">&quot;mockExternalApis&quot;</span>: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>frontmatter 中的自定义属性会自动合并到 <code>options</code> 对象中：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: custom-agent</span><br><span class="line">framework: react</span><br><span class="line">testRunner: vitest</span><br><span class="line">---</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/config/config.ts, packages/opencode/src/agent/agent.ts</p><h2 id="Agent-生命周期和状态管理-1"><a href="#Agent-生命周期和状态管理-1" class="headerlink" title="Agent 生命周期和状态管理"></a>Agent 生命周期和状态管理</h2><h3 id="初始化和状态"><a href="#初始化和状态" class="headerlink" title="初始化和状态"></a>初始化和状态</h3><p>Agent 通过 <code>Agent.state()</code> 函数延迟加载，该函数将默认配置与用户覆盖合并：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> state = Instance.state(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> cfg = <span class="keyword">await</span> Config.get()</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 定义默认权限</span></span><br><span class="line">  <span class="keyword">const</span> defaults = PermissionNext.fromConfig(&#123;</span><br><span class="line">    <span class="string">&quot;*&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    doom_loop: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">    external_directory: &#123;</span><br><span class="line">      <span class="string">&quot;*&quot;</span>: <span class="string">&quot;ask&quot;</span>,</span><br><span class="line">      [Truncate.DIR]: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    question: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">    read: &#123;</span><br><span class="line">      <span class="string">&quot;*&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">      <span class="string">&quot;*.env&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">      <span class="string">&quot;*.env.*&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">      <span class="string">&quot;*.env.example&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">const</span> user = PermissionNext.fromConfig(cfg.permission ?? &#123;&#125;)</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 定义内置 agent</span></span><br><span class="line">  <span class="keyword">const</span> result: Record&lt;<span class="built_in">string</span>, Info&gt; = &#123;</span><br><span class="line">    <span class="comment">// ... 内置 agent 定义</span></span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 合并用户定义的 agent</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> [key, value] <span class="keyword">of</span> <span class="built_in">Object</span>.entries(cfg.agent ?? &#123;&#125;)) &#123;</span><br><span class="line">    <span class="keyword">if</span> (value.disable) &#123;</span><br><span class="line">      <span class="keyword">delete</span> result[key]</span><br><span class="line">      <span class="keyword">continue</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 合并配置</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> result</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/agent/agent.ts</p><h3 id="Agent-发现和列表"><a href="#Agent-发现和列表" class="headerlink" title="Agent 发现和列表"></a>Agent 发现和列表</h3><p>通过 <code>list</code> 函数检索可用的 agent：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> agents = <span class="keyword">await</span> Agent.list()</span><br><span class="line"><span class="comment">// 返回已排序的 agent，default_agent 在前</span></span><br></pre></td></tr></table></figure><p>来源：packages/opencode/src/agent/agent.ts</p><h2 id="最佳实践和常见模式"><a href="#最佳实践和常见模式" class="headerlink" title="最佳实践和常见模式"></a>最佳实践和常见模式</h2><h3 id="1-专用-Subagent-模式"><a href="#1-专用-Subagent-模式" class="headerlink" title="1. 专用 Subagent 模式"></a>1. 专用 Subagent 模式</h3><p>为特定任务创建专注的 agent：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: database-migrator</span><br><span class="line">mode: subagent</span><br><span class="line">description: 生成和验证数据库迁移脚本</span><br><span class="line">permission:</span><br><span class="line">  &quot;<span class="emphasis">*&quot;: deny</span></span><br><span class="line"><span class="emphasis">  read: allow</span></span><br><span class="line"><span class="emphasis">  glob: allow</span></span><br><span class="line"><span class="emphasis">  grep: allow</span></span><br><span class="line"><span class="emphasis">  write: allow</span></span><br><span class="line"><span class="emphasis">---</span></span><br></pre></td></tr></table></figure><h3 id="2-分层-Agent-组织"><a href="#2-分层-Agent-组织" class="headerlink" title="2. 分层 Agent 组织"></a>2. 分层 Agent 组织</h3><p>在子目录中组织 agent 以适应复杂项目：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">.opencode&#x2F;</span><br><span class="line">├── agent&#x2F;</span><br><span class="line">│   ├── frontend&#x2F;</span><br><span class="line">│   │   ├── react-component.md</span><br><span class="line">│   │   └── css-reviewer.md</span><br><span class="line">│   ├── backend&#x2F;</span><br><span class="line">│   │   ├── api-endpoint.md</span><br><span class="line">│   │   └── database-schema.md</span><br><span class="line">│   └── devops&#x2F;</span><br><span class="line">│       └── ci-pipeline.md</span><br></pre></td></tr></table></figure><p>嵌套路径将成为 agent 名称的一部分：<code>frontend/react-component</code>。</p><p>来源：packages/opencode/src/config/config.ts</p><h3 id="3-渐进式权限升级"><a href="#3-渐进式权限升级" class="headerlink" title="3. 渐进式权限升级"></a>3. 渐进式权限升级</h3><p>从限制性权限开始，并根据 agent 需求进行扩展：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;permission&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;*&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;read&quot;</span>: <span class="string">&quot;allow&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;grep&quot;</span>: <span class="string">&quot;allow&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后随着测试发现需求，添加更多工具。</p><h3 id="4-Agent-的提示词工程"><a href="#4-Agent-的提示词工程" class="headerlink" title="4. Agent 的提示词工程"></a>4. Agent 的提示词工程</h3><p>构建清晰的、有效的 agent 提示词：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: documentation-writer</span><br><span class="line">---</span><br><span class="line"></span><br><span class="line">你是一位技术文档专家。</span><br><span class="line"></span><br><span class="line"><span class="section">## 核心原则</span></span><br><span class="line"><span class="bullet">-</span> 为你的受众写作：开发者、用户或两者兼有</span><br><span class="line"><span class="bullet">-</span> 简洁但全面</span><br><span class="line"><span class="bullet">-</span> 为描述的每个 API 包含代码示例</span><br><span class="line"><span class="bullet">-</span> 使用清晰、一致的术语</span><br><span class="line"></span><br><span class="line"><span class="section">## 你擅长的文档类型</span></span><br><span class="line"><span class="bullet">-</span> API 参考文档</span><br><span class="line"><span class="bullet">-</span> 入门指南</span><br><span class="line"><span class="bullet">-</span> 教程内容</span><br><span class="line"><span class="bullet">-</span> 架构概述</span><br><span class="line"></span><br><span class="line"><span class="section">## 输出格式</span></span><br><span class="line">生成文档时，将其结构化为：</span><br><span class="line"><span class="bullet">1.</span> 简要描述（是什么和为什么）</span><br><span class="line"><span class="bullet">2.</span> 先决条件</span><br><span class="line"><span class="bullet">3.</span> 快速示例</span><br><span class="line"><span class="bullet">4.</span> 详细解释</span><br><span class="line"><span class="bullet">5.</span> 常见陷阱</span><br><span class="line"><span class="bullet">6.</span> 相关资源</span><br><span class="line"></span><br><span class="line"><span class="section">## 质量检查</span></span><br><span class="line">在定稿文档之前，验证：</span><br><span class="line"><span class="bullet">-</span> 所有代码示例都可运行</span><br><span class="line"><span class="bullet">-</span> 没有未定义的术语或缩略词</span><br><span class="line"><span class="bullet">-</span> 层次清晰，标题级别一致</span><br><span class="line"><span class="bullet">-</span> 语法和拼写正确</span><br></pre></td></tr></table></figure><h3 id="5-内部使用的隐藏-Agent"><a href="#5-内部使用的隐藏-Agent" class="headerlink" title="5. 内部使用的隐藏 Agent"></a>5. 内部使用的隐藏 Agent</h3><p>从 @ 自动补全菜单中隐藏专用 agent：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;name&quot;</span>: <span class="string">&quot;internal-formatter&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;mode&quot;</span>: <span class="string">&quot;subagent&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;hidden&quot;</span>: <span class="literal">true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>隐藏的 agent 仍然可供直接调用或被其他 agent 委托，但不会出现在面向用户的 agent 列表中。</p><p>来源：packages/opencode/src/config/config.ts</p><p>对于主要由其他 agent 以编程方式调用而非由用户直接调用的 agent，请使用 <code>hidden: true</code> 标志。这可以减少认知负荷并防止对可用选项的混淆。</p>]]></content>
    
    
    <summary type="html">OpenCode Agent系统是一个多智能体架构，通过定义Agent结构，使用Task工具实现Agent间调用，集成Permission权限系统进行访问控制，通过Session会话处理器处理交互，并使用Tool工具系统提供可扩展能力。

Agent 类型和模式：主 Agent、子 Agent 和隐藏 Agent
本部分解释了 OpenCode 中 Agent 的架构组织，涵盖了三种不同的 Agent 类型（主 Agent、子 Agent 和隐藏 Agent）、它们的运行模式、配置机制以及它们如何在会话管理系统内进行交互。

架构概述
OpenCode 的 Agent 系统围绕 Agent.I</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>跟着OpenCode学智能体设计和开发0：核心架构</title>
    <link href="http://qixinbo.github.io/2026/01/11/opencode-0/"/>
    <id>http://qixinbo.github.io/2026/01/11/opencode-0/</id>
    <published>2026-01-11T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.909Z</updated>
    
    <content type="html"><![CDATA[<p>OpenCode最近很火，它不仅仅是另一个用于代码的 AI 聊天机器人——它是一个综合性的开发环境，能够理解你的项目结构，执行命令，并在你的监督下进行真正的代码更改。通过它可以学习一个强大的智能体是怎么设计和开发。</p><p>接下来，通过分析OpenCode的源码，来深入学习，这里大量用到了另外2个代码仓智能分析工具<a href="https://zread.ai/">Zread</a>和<a href="https://deepwiki.com/">DeepWiki</a>。</p><h1 id="核心架构"><a href="#核心架构" class="headerlink" title="核心架构"></a>核心架构</h1><p>OpenCode 的核心采用了客户端/服务器架构，其中强大的后端服务器负责管理 AI 交互、文件操作和工具执行，而各种客户端界面（CLI、桌面应用、Web）则提供了与这些功能无缝交互的方式 <code>packages/opencode/src/index.ts#L1-L50</code>。</p><p>OpenCode 的架构围绕几个相互连接的系统构建，这些系统协同工作以提供无缝的 AI 辅助开发体验：<br><img src="https://private-user-images.githubusercontent.com/6218739/534614898-71137b5f-d5e1-4d31-9d3e-f25f2bd5e0a2.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Njg2MTY0MDIsIm5iZiI6MTc2ODYxNjEwMiwicGF0aCI6Ii82MjE4NzM5LzUzNDYxNDg5OC03MTEzN2I1Zi1kNWUxLTRkMzEtOWQzZS1mMjVmMmJkNWUwYTIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MDExNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjAxMTdUMDIxNTAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9N2Y2NjZmMzIzYzVkZjgyNjlhMTUwNDQ0NjU5YzIwOGI4YzQ1ZmRlYTY3ZTA0NWUyMTNmZGRjMTEwNjliNmIzMiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.eRpx_iuy2ZLVFUvedamtSjpFxafN9NSx2X1z-S3lUuo" alt="arch"></p><p>系统利用 Bun 的运行时能力进行高性能服务器操作，使用 SolidJS 驱动 Web 界面，并借助 Tauri 实现跨平台桌面体验。</p><h2 id="服务器核心"><a href="#服务器核心" class="headerlink" title="服务器核心"></a>服务器核心</h2><h3 id="服务器基础"><a href="#服务器基础" class="headerlink" title="服务器基础"></a>服务器基础</h3><p>服务器是 OpenCode 的心脏，使用 Hono 和 Bun 构建，以提供高性能的 HTTP 和 WebSocket 端点。它管理所有 AI 交互、工具执行以及与客户端的实时通信 <code>packages/opencode/src/server/server.ts#L59-L95</code>。服务器既暴露了用于直接集成的 REST API，也提供了用于实时更新和流式 AI 响应的 WebSocket 流。</p><p>服务器的主要职责包括：</p><ul><li><strong>会话管理</strong>：创建、分支和管理具有完整状态持久化的对话会话  </li><li><strong>工具编排</strong>：协调文件操作、代码搜索、bash 命令和其他开发工具  </li><li><strong>AI 提供商集成</strong>：管理与多个 AI 提供商（Anthropic、OpenAI、Google 等）的连接  </li><li><strong>权限系统</strong>：处理敏感操作的用户批准工作流  </li><li><strong>事件流</strong>：通过服务器发送事件进行实时更新，以实现响应式 UI 更新  </li></ul><p>服务器架构采用单文件路由定义方法 (server.ts)，在整合所有端点的同时，通过中间件链和路由组保持清晰的代码组织。这种设计允许对所有 API 操作进行集中的错误处理、CORS 管理和日志记录。<br><img src="https://private-user-images.githubusercontent.com/6218739/535636515-e8822e74-8755-46e7-bea4-ba8d9873f8f5.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Njg2MTY0MDIsIm5iZiI6MTc2ODYxNjEwMiwicGF0aCI6Ii82MjE4NzM5LzUzNTYzNjUxNS1lODgyMmU3NC04NzU1LTQ2ZTctYmVhNC1iYThkOTg3M2Y4ZjUucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MDExNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjAxMTdUMDIxNTAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MTIzNzM1NGJhOGU4NmE1MzA0MzY4Y2RjMWIxYjllMGZjNGQ1YzlkMmEwZTk0MWJlZDliZGVjNTEwNmY1NjE1YyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.lFLrkmk1tgw4zUuC0sjKvcy6ybCQptJQR1tqxBcUJ-A" alt="arch1"></p><p>服务器监听可配置的端口，并支持 mDNS 发现功能，能够在本地网络上自动发现服务。默认配置使用端口 4096 进行本地主机连接，当默认端口不可用时会回退到端口 0 (server.ts)。</p><h3 id="实例管理"><a href="#实例管理" class="headerlink" title="实例管理"></a>实例管理</h3><p>核心架构创新之一是实例管理系统，它为不同的项目目录提供隔离的上下文。每个项目目录都有自己的实例，拥有专属的状态、配置和资源。这种设计使 OpenCode 能够同时管理多个项目而不会发生冲突。</p><p>实例系统通过自定义的 Context 实现使用异步上下文传播 (instance.ts)，确保请求中的所有操作自动继承正确的项目上下文。实例生命周期通过缓存和显式销毁进行管理，并在实例终止时自动清理。<br><img src="https://private-user-images.githubusercontent.com/6218739/535669161-46ecfdb0-7e4d-4203-9a85-a64054eaef6b.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Njg2MTY0MDIsIm5iZiI6MTc2ODYxNjEwMiwicGF0aCI6Ii82MjE4NzM5LzUzNTY2OTE2MS00NmVjZmRiMC03ZTRkLTQyMDMtOWE4NS1hNjQwNTRlYWVmNmIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MDExNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjAxMTdUMDIxNTAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ODQwNTJlODk3ZWZkNWNhY2ZmNDljNGMyM2Q0MmU1Zjk1OTM5ZGRiYmI1NzIwZTYyMzhmYjAyMjM3Njk4MTNmNCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.NuBrjc4bY_DnI-QZ9QJ-HY5bHWoR-2k_wtKq0nvm6JQ" alt="instance"></p><h3 id="客户端实现"><a href="#客户端实现" class="headerlink" title="客户端实现"></a>客户端实现</h3><p>OpenCode 提供了多种客户端实现，每种都针对特定用例量身定制，同时使用相同的服务器 API：</p><p>Web 客户端：使用 SolidJS 构建 (app.tsx)，Web 客户端提供一个响应式的基于浏览器的界面，并通过服务器发送事件 (SSE) 进行实时更新。它包括会话管理、文件树导航和交互式终端仿真。</p><p>桌面应用程序：基于 Tauri (index.tsx)，桌面应用程序嵌入了 Web 客户端，并具有原生平台集成功能，包括文件对话框、系统通知和自动更新。它包含一个服务器门控组件，在渲染界面之前等待嵌入的服务器初始化。</p><p>CLI/TUI 界面：命令行界面提供基于终端的交互，具备完整的服务器功能，包括用于可视化导航的 TUI 模式。CLI 还可以运行在仅服务器模式下，用于无头操作 (serve.ts)。</p><p>SDK 客户端：TypeScript SDK (packages/sdk/js/v2/client) 提供对 OpenCode 功能的编程访问，支持第三方集成和自动化脚本。</p><h2 id="事件驱动架构"><a href="#事件驱动架构" class="headerlink" title="事件驱动架构"></a>事件驱动架构</h2><h3 id="内部事件系统"><a href="#内部事件系统" class="headerlink" title="内部事件系统"></a>内部事件系统</h3><p>OpenCode 实现了一个全面的事件总线系统，促进不同子系统之间的通信。事件系统使用基于类型架构的方法，所有事件都使用 Zod 架构定义，以确保编译时的类型安全 (bus-event.ts)。</p><p>事件作用于实例范围，这意味着当实例被销毁时，订阅会自动清理。事件系统支持特定事件订阅和通配符订阅，用于监控所有事件。<br><img src="https://private-user-images.githubusercontent.com/6218739/535671364-9fe4f9c3-f45b-40ab-9ef3-ce5ab79e0e19.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Njg2MTY0MDIsIm5iZiI6MTc2ODYxNjEwMiwicGF0aCI6Ii82MjE4NzM5LzUzNTY3MTM2NC05ZmU0ZjljMy1mNDViLTQwYWItOWVmMy1jZTVhYjc5ZTBlMTkucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MDExNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjAxMTdUMDIxNTAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9OTliNTk2OWNjYmJmODhhYWJjZjlmYmFkZGJiOTNhN2Q3MzViYWYzMDAyZDZlOWUwMTg5ZjRkM2VhYzQ2NjNhMCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.4xreUCk5v_3sTtS_AkQK5BvEJphMmovteSRivJNTBj8" alt="event"><br>总线实现包括自动负载验证和类型推断，确保事件处理程序接收正确类型的数据。事件异步发布，总线收集所有订阅承诺并等待完成 (bus/index.ts)。</p><h3 id="实时更新"><a href="#实时更新" class="headerlink" title="实时更新"></a>实时更新</h3><p>服务器发送事件 (SSE) 向客户端提供实时更新，实现对 AI 生成、文件监视和状态更改等长时间运行操作的实时反馈。服务器为全局事件和特定于实例的事件维护单独的 SSE 连接，并自动生成心跳以防止连接超时。</p><p>全局事件包括实例销毁信号、系统运行状况更新和配置更改。实例事件包括会话状态更改、消息完成和文件修改。客户端可以根据需要订阅任一流 (server.ts)。</p><h2 id="Agent-系统"><a href="#Agent-系统" class="headerlink" title="Agent 系统"></a>Agent 系统</h2><p>OpenCode 引入了一个复杂的 Agent 系统，允许你切换具有专门功能的不同 AI 个性。每个 Agent 都有独特的权限、行为和用例 <code>packages/opencode/src/agent/agent.ts#L66-L136</code>：</p><div class="table-container"><table><thead><tr><th>Agent</th><th>模式</th><th>描述</th><th>最适用于</th></tr></thead><tbody><tr><td>build</td><td>主要</td><td>具有完整文件权限的全访问开发 Agent</td><td>主动开发、重构、实现</td></tr><tr><td>plan</td><td>主要</td><td>默认拒绝编辑的只读分析 Agent</td><td>代码探索、理解不熟悉的代码库</td></tr><tr><td>general</td><td>子 Agent</td><td>用于复杂搜索和并行执行的多任务 Agent</td><td>研究、多步骤规划任务</td></tr><tr><td>explore</td><td>子 Agent</td><td>快速代码库探索专家</td><td>快速查找文件、模式搜索、代码理解</td></tr></tbody></table></div><p>build 和 plan Agent 是你可以使用 Tab 键切换的主要 Agent，而像 general 和 explore 这样的子 Agent 则由系统自动调用以处理专门任务 <code>README.md#L33-L45</code>。</p><h2 id="工具生态系统"><a href="#工具生态系统" class="headerlink" title="工具生态系统"></a>工具生态系统</h2><p>OpenCode 提供了一套全面的工具包，AI Agent 可以利用这些工具与你的代码库进行交互。工具注册表管理内置工具，并允许自定义工具扩展 <code>packages/opencode/src/tool/registry.ts#L90-L114</code>：</p><h3 id="文件操作"><a href="#文件操作" class="headerlink" title="文件操作"></a>文件操作</h3><ul><li><strong>Read</strong>：读取文件内容，支持行范围和语言检测  </li><li><strong>Write</strong>：创建新文件或完全替换现有文件  </li><li><strong>Edit</strong>：对特定行进行精确编辑，支持冲突检测  </li><li><strong>Multi-edit</strong>：跨文件进行多个协调编辑  </li></ul><h3 id="代码导航"><a href="#代码导航" class="headerlink" title="代码导航"></a>代码导航</h3><ul><li><strong>Grep</strong>：使用 ripgrep 在整个代码库中搜索文本模式  </li><li><strong>Glob</strong>：查找匹配 glob 模式的文件（例如 <code>src/components/**/*.tsx</code>）  </li><li><strong>Code Search</strong>：用于查找函数、类和模式的语义代码搜索  </li><li><strong>LS</strong>：列出包含丰富元数据的目录内容  </li></ul><h3 id="开发工具"><a href="#开发工具" class="headerlink" title="开发工具"></a>开发工具</h3><ul><li><strong>Bash</strong>：执行 shell 命令，支持实时输出流  </li><li><strong>LSP</strong>：获取代码智能、符号定义和引用  </li><li><strong>Todo</strong>：创建和管理与会话关联的任务列表  </li><li><strong>Skills</strong>：执行自定义技能和可重用的工作流  </li></ul><p>所有工具都在权限系统内运行，该系统赋予你控制 AI 可以执行的操作的权利。你可以根据安全要求，为文件访问、命令执行和外部 API 调用配置细粒度的权限。</p><h2 id="多提供商-AI-集成"><a href="#多提供商-AI-集成" class="headerlink" title="多提供商 AI 集成"></a>多提供商 AI 集成</h2><p>OpenCode 支持广泛的 AI 提供商目录，使你可以灵活地选择最适合你需求的模型 <code>packages/opencode/src/provider/provider.ts#L43-L65</code>：</p><p>支持的提供商：</p><ul><li><strong>Anthropic</strong>：具有高级代码功能的 Claude 模型  </li><li><strong>OpenAI</strong>：具有广泛工具支持的 GPT 模型  </li><li><strong>Google</strong>：Gemini 和 Vertex AI 模型  </li><li><strong>Amazon Bedrock</strong>：支持区域部署的企业级模型  </li><li><strong>GitHub Copilot</strong>：专用于开发的代码专用模型  </li><li><strong>以及更多</strong>：Groq、Mistral、Cerebras、Perplexity、Vercel、OpenRouter 和自定义提供商  </li></ul><p>提供商系统处理身份验证、模型发现、成本跟踪和自动故障转移。每个模型都根据其功能、定价和上下文限制进行编录，使 OpenCode 能够智能地为每项任务选择合适的工具 <code>packages/opencode/src/provider/provider.ts#L443-L511</code>。</p><h2 id="会话管理"><a href="#会话管理" class="headerlink" title="会话管理"></a>会话管理</h2><p>OpenCode 中的所有交互都在会话内进行，该会话维护完整的对话历史、上下文和状态。会话支持强大的功能：</p><ul><li><strong>分支</strong>：从对话的任何点创建替代分支，以探索不同的方法  </li><li><strong>压缩</strong>：自动汇总较旧的对话以保持在上下文限制内，同时保留重要信息  </li><li><strong>回滚</strong>：通过回滚特定消息或整个会话来撤销更改  </li><li><strong>共享</strong>：通过生成的链接与协作者共享会话  </li><li><strong>持久化</strong>：所有会话都本地存储，具有完整的离线功能  </li></ul><p>会话系统跟踪消息交换、工具执行、文件更改和差异，为每次对话提供完整的可重现性 <code>packages/opencode/src/session/index.ts#L39-L79</code>。</p><h1 id="项目结构"><a href="#项目结构" class="headerlink" title="项目结构"></a>项目结构</h1><p>了解 OpenCode 的 monorepo 结构有助于贡献和自定义：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">packages/</span><br><span class="line">├── opencode/        ├── app/            # Next.js Web 应用程序</span><br><span class="line">├── console/        # 无服务器控制台应用程序</span><br><span class="line">├── desktop/        # 基于 Tauri 的桌面应用程序</span><br><span class="line">├── ui/             # 共享 React UI 组件</span><br><span class="line">├── sdk/            # JavaScript/TypeScript SDK</span><br><span class="line">├── util/           # 共享实用程序库</span><br><span class="line">└── extensions/     # IDE 扩展（Zed、VS Code）</span><br></pre></td></tr></table></figure><p>核心功能位于 <code>packages/opencode/</code> 中，包含服务器实现、Agent 系统、工具注册表和提供商集成 <code>packages/opencode/src/index.ts#L1-L50</code>。</p><h1 id="主要功能"><a href="#主要功能" class="headerlink" title="主要功能"></a>主要功能</h1><h2 id="代码智能"><a href="#代码智能" class="headerlink" title="代码智能"></a>代码智能</h2><ul><li><strong>LSP 集成</strong>：连接到语言服务器以进行准确的代码导航和理解  </li><li><strong>符号搜索</strong>：在整个代码库中查找函数、类和变量  </li><li><strong>代码格式化</strong>：与流行的格式化程序集成，以保持一致的代码风格  </li><li><strong>文件监视</strong>：自动跟踪文件更改并更新上下文  </li></ul><h2 id="安全与控制"><a href="#安全与控制" class="headerlink" title="安全与控制"></a>安全与控制</h2><ul><li><strong>权限系统</strong>：对 AI Agent 可以执行的操作进行细粒度控制  </li><li><strong>会话回滚</strong>：使用单个命令撤销任何 AI 生成的更改  </li><li><strong>审计跟踪</strong>：所有操作和更改的完整历史记录  </li><li><strong>沙盒化</strong>：使用 Git worktrees 进行安全实验  </li></ul><h2 id="开发者体验"><a href="#开发者体验" class="headerlink" title="开发者体验"></a>开发者体验</h2><ul><li><strong>丰富的终端 UI</strong>：美观、响应迅速的终端界面，支持键盘快捷键  </li><li><strong>流式响应</strong>：在生成 AI 响应时实时查看  </li><li><strong>上下文管理</strong>：智能截断和汇总以保持在限制范围内  </li><li><strong>多项目支持</strong>：使用独立的会话处理多个项目  </li></ul>]]></content>
    
    
    <summary type="html">OpenCode最近很火，它不仅仅是另一个用于代码的 AI 聊天机器人——它是一个综合性的开发环境，能够理解你的项目结构，执行命令，并在你的监督下进行真正的代码更改。通过它可以学习一个强大的智能体是怎么设计和开发。

接下来，通过分析OpenCode的源码，来深入学习，这里大量用到了另外2个代码仓智能分析工具Zread和DeepWiki。

核心架构
OpenCode 的核心采用了客户端/服务器架构，其中强大的后端服务器负责管理 AI 交互、文件操作和工具执行，而各种客户端界面（CLI、桌面应用、Web）则提供了与这些功能无缝交互的方式 packages/opencode/src/index.</summary>
    
    
    
    <category term="coding" scheme="http://qixinbo.github.io/categories/coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>【翻译】Cursor的动态上下文发现</title>
    <link href="http://qixinbo.github.io/2026/01/10/agent-context-cursor/"/>
    <id>http://qixinbo.github.io/2026/01/10/agent-context-cursor/</id>
    <published>2026-01-10T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.889Z</updated>
    
    <content type="html"><![CDATA[<p>之前研究过一篇LangChain的工程师Lance Martin和Manus的联合创始人Peak（季逸超）关于上下文工程的研讨，博文见<a href="https://www.qixinbo.info/2025/11/23/agent-context/">这里</a>，最近Cursor的工程师Jediah Katz也分享了一篇关于Cursor中关于上下文工程的优秀实践，这里对该文章进行翻译学习：Cursor原文在<a href="https://cursor.com/en-US/blog/dynamic-context-discovery">这里</a></p><hr><p><strong>发布时间</strong>：2026 年 1 月 6 日<br><strong>作者</strong>：Jediah Katz<br><strong>来源</strong>：Cursor 官方博客（Research）</p><p>编码 Agent（智能体）正在快速改变软件构建的方式。它们的快速进步既来自模型本身能力的提升，也来自更优的上下文工程设计，用以更好地引导其行为。</p><p>Cursor 的 agent harness（即我们为模型提供的指令集与工具体系）会针对所支持的每一个最新前沿模型进行单独优化。但与此同时，仍然存在一类对所有在该 harness 中运行的模型都通用的上下文工程改进空间，例如：我们应如何收集上下文，以及如何在长时间交互过程中更高效地使用 token。</p><p>随着模型作为 agent 的能力不断增强，我们发现，<strong>预先提供更少的细节，反而能让 agent 更容易在需要时主动拉取相关上下文</strong>。我们将这种模式称为<strong>动态上下文发现（Dynamic Context Discovery）</strong>，以区别于传统始终被注入的<strong>静态上下文（Static Context）</strong>。</p><hr><h1 id="用于动态上下文发现的文件"><a href="#用于动态上下文发现的文件" class="headerlink" title="用于动态上下文发现的文件"></a>用于动态上下文发现的文件</h1><p>动态上下文发现具有极高的 token 使用效率，因为只有在确有必要时，相关数据才会被拉入上下文窗口。同时，它还能减少上下文中可能引入的噪声、冲突或无关信息，从而提升 agent 的响应质量。</p><p>在 Cursor 中，我们主要通过以下方式实践动态上下文发现：</p><ol><li>将冗长的工具响应转化为文件</li><li>在摘要阶段允许引用完整聊天历史</li><li>支持 Agent Skills 开放标准</li><li>仅在需要时加载 MCP 工具</li><li>将所有集成终端会话视为文件</li></ol><hr><h1 id="将冗长的工具响应转换为文件"><a href="#将冗长的工具响应转换为文件" class="headerlink" title="将冗长的工具响应转换为文件"></a>将冗长的工具响应转换为文件</h1><p>当工具调用返回体量巨大的 JSON 响应时，会显著推高上下文窗口的占用。</p><p>对于 Cursor 内部的一方工具（例如文件编辑、代码库搜索），我们可以通过精心设计的工具接口和最小化的响应格式来避免上下文膨胀。然而，对于第三方工具（如 shell 命令或 MCP 调用），默认情况下并不具备这种控制能力。</p><p>多数编码 agent 的常见做法是直接截断过长的 shell 或 MCP 输出，但这种方式可能导致关键信息丢失，而这些信息往往正是后续推理所必需的。</p><p>在 Cursor 中，我们选择将这些冗长输出写入文件，并赋予 agent 读取这些文件的能力。agent 可以通过 <code>tail</code> 查看文件末尾内容，并在需要时继续向前读取更多内容。</p><p>这种策略显著减少了在接近上下文窗口上限时触发不必要摘要的情况。</p><hr><h1 id="在摘要过程中引用聊天历史"><a href="#在摘要过程中引用聊天历史" class="headerlink" title="在摘要过程中引用聊天历史"></a>在摘要过程中引用聊天历史</h1><p>当上下文窗口被填满时，Cursor 会触发一次摘要步骤，为 agent 提供一个新的上下文窗口，其中包含截至当前工作的整体摘要。</p><p>然而，摘要本质上是一种有损压缩。一旦完成摘要，agent 可能会遗忘部分关键细节，从而影响后续任务的正确性。</p><p>为此，Cursor 将完整的聊天历史以文件形式保存，并在摘要完成后将该历史文件的引用提供给 agent。</p><p>在达到上下文上限或用户手动触发摘要后，agent 可以在需要时主动搜索聊天历史文件，以找回摘要中未包含但仍然重要的细节信息。</p><hr><h1 id="支持-Agent-Skills-开放标准"><a href="#支持-Agent-Skills-开放标准" class="headerlink" title="支持 Agent Skills 开放标准"></a>支持 Agent Skills 开放标准</h1><p>Cursor 支持 <strong>Agent Skills</strong>，这是一种用于通过专门能力扩展编码 agent 的开放标准。</p><p>类似于规则配置，Skills 通过文件进行定义，这些文件描述了 agent 在特定领域任务中应如何执行操作。</p><p>Skills 文件本身包含名称与简要描述，这部分内容会作为“静态上下文”注入系统提示中。随后，agent 可以通过动态上下文发现机制，使用诸如 <code>grep</code> 或 Cursor 的语义搜索工具，按需查找并加载相关 Skills 的具体内容。</p><p>此外，Skills 还可以打包与任务相关的可执行文件或脚本。由于它们本质上都是文件，agent 能够非常自然地发现并理解与某一 Skill 相关的全部资源。</p><hr><h1 id="高效地仅加载所需的-MCP-工具"><a href="#高效地仅加载所需的-MCP-工具" class="headerlink" title="高效地仅加载所需的 MCP 工具"></a>高效地仅加载所需的 MCP 工具</h1><p>MCP（Model Context Protocol）用于访问受 OAuth 保护的资源，例如生产日志、外部设计文件，或企业内部的上下文与文档。</p><p>一些 MCP 服务器包含大量工具，并附带冗长而详细的描述。这些描述如果被全部注入上下文，会显著占用 token，但其中大多数工具在实际任务中并不会被使用。随着同时接入多个 MCP 服务器，这一问题会被进一步放大。</p><p>我们认为，指望每个 MCP 服务器自行优化其上下文占用并不现实。降低上下文成本应当由编码 agent 本身来承担。</p><p>在 Cursor 中，我们通过将 MCP 工具的描述同步到一个目录中，从而实现对 MCP 的动态上下文发现支持。agent 在初始阶段只接收到极少量的静态信息（例如工具名称），并在确有需要时再主动查找对应工具的完整描述。</p><p>在一次 A/B 测试中，我们发现，在涉及 MCP 工具调用的场景下，这种策略将 agent 的总体 token 消耗降低了 <strong>46.9%</strong>。这一结果具有统计学显著性，且具体节省幅度会随已安装 MCP 数量的不同而有所变化。</p><p>这种基于文件的设计还使 agent 能够感知 MCP 工具的状态。例如，过去当某个 MCP 服务器需要重新认证时，agent 会“完全遗忘”这些工具，从而让用户感到困惑；现在，agent 可以主动提醒用户需要重新进行认证。</p><hr><h1 id="将所有集成终端会话视为文件"><a href="#将所有集成终端会话视为文件" class="headerlink" title="将所有集成终端会话视为文件"></a>将所有集成终端会话视为文件</h1><p>传统上，用户需要将终端会话的输出手动复制粘贴到 agent 的输入中。现在，Cursor 会自动将集成终端的输出同步到本地文件系统。</p><p>这使得用户可以直接提出诸如“为什么我的命令失败了？”之类的问题，而 agent 则能够准确理解其所引用的终端输出内容。由于终端历史往往非常冗长，agent 可以通过 <code>grep</code> 等工具仅检索相关片段，这在分析长期运行的进程日志（例如服务器日志）时尤为有用。</p><p>这种模式与基于 CLI 的编码 agent 所面对的环境非常相似：它们同样能够访问历史 shell 输出，只不过这里采用的是动态发现，而非静态注入。</p><hr><h1 id="简单抽象"><a href="#简单抽象" class="headerlink" title="简单抽象"></a>简单抽象</h1><p>目前尚不清楚，文件是否会成为基于 LLM 的工具交互的最终接口形式。</p><p>但可以确定的是，随着编码 agent 能力的快速演进，文件作为一种抽象原语，既简单又强大，并且相比于设计一种可能无法适应未来需求的新抽象层，更加稳妥可靠。</p><p>未来我们还将分享更多在这一方向上的进展。这些改进将在接下来的几周内逐步向所有用户推出。</p><p>本文中描述的成果来自多位 Cursor 员工的共同努力，包括 Lukas Moller、Yash Gaitonde、Wilson Lin、Jason Ma、Devang Jhabakh 和 Jediah Katz。如果你有兴趣使用 AI 解决最困难、最具挑战性的编码问题，我们非常期待与你交流。</p>]]></content>
    
    
    <summary type="html">之前研究过一篇LangChain的工程师Lance Martin和Manus的联合创始人Peak（季逸超）关于上下文工程的研讨，博文见这里，最近Cursor的工程师Jediah Katz也分享了一篇关于Cursor中关于上下文工程的优秀实践，这里对该文章进行翻译学习：Cursor原文在这里




发布时间：2026 年 1 月 6 日
作者：Jediah Katz
来源：Cursor 官方博客（Research）

编码 Agent（智能体）正在快速改变软件构建的方式。它们的快速进步既来自模型本身能力的提升，也来自更优的上下文工程设计，用以更好地引导其行为。

Cursor 的 agent </summary>
    
    
    
    <category term="Vibe coding" scheme="http://qixinbo.github.io/categories/Vibe-coding/"/>
    
    
    <category term="Agent" scheme="http://qixinbo.github.io/tags/Agent/"/>
    
  </entry>
  
  <entry>
    <title>一步步从0开发一个微信小程序</title>
    <link href="http://qixinbo.github.io/2026/01/02/weixin-small-program/"/>
    <id>http://qixinbo.github.io/2026/01/02/weixin-small-program/</id>
    <published>2026-01-02T00:00:00.000Z</published>
    <updated>2026-04-12T14:29:51.925Z</updated>
    
    <content type="html"><![CDATA[<p>微信小程序以其轻量、便捷、跨平台的特性，成为了连接线上线下的重要工具。本教程将根据微信小程序最新的开发文档，从零开始，一步步开发一个功能完整的 “待办事项 (Todo List)” 小程序。</p><h1 id="目标功能"><a href="#目标功能" class="headerlink" title="目标功能"></a>目标功能</h1><p>我们将开发的Todo List小程序将具备以下功能：</p><ol><li><strong>添加待办事项</strong>：用户可以在输入框中输入新的待办事项并添加。</li><li><strong>显示待办事项列表</strong>：所有待办事项以列表形式展示。</li><li><strong>标记完成/未完成</strong>：每个事项可以被标记为已完成或未完成。</li><li><strong>删除待办事项</strong>：可以从列表中删除某个事项。</li><li><strong>数据持久化</strong>：关闭小程序后，数据不会丢失（使用本地存储）。</li></ol><h1 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h1><p>在开始开发之前，需要准备以下工具和账号：</p><ol><li><p><strong>微信开发者账号 (个人或企业)</strong>：</p><ul><li>访问 <a href="https://mp.weixin.qq.com/">微信公众平台</a>。</li><li>注册一个服务号、订阅号、小程序或企业微信，并选择注册小程序。（这一步注意小程序与公众号的区别，一定是小程序，且注册了公众号的邮箱不能用作小程序的注册）</li><li>注册成功后，登录后台，在 “开发” -&gt; “开发设置” 中获取您的 <strong>AppID(小程序ID)</strong>。这个ID在开发和发布小程序时非常重要。</li></ul></li><li><p><strong>微信开发者工具</strong>：</p><ul><li>这是开发微信小程序的官方IDE。</li><li>下载地址：<a href="https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html">微信小程序开发工具</a></li><li>安装并登录。</li></ul></li><li><p><strong>基础前端知识</strong>：</p><ul><li>熟悉 HTML、CSS、JavaScript 基本语法。</li><li>理解数据绑定和事件处理等概念。</li></ul></li></ol><h1 id="核心概念回顾"><a href="#核心概念回顾" class="headerlink" title="核心概念回顾"></a>核心概念回顾</h1><p>在深入开发之前，我们先简单回顾一下小程序的核心文件类型和作用：</p><ul><li><strong><code>项目目录/app.js</code></strong>: 小程序逻辑文件。注册小程序应用，监听生命周期，声明全局变量等。</li><li><strong><code>项目目录/app.json</code></strong>: 小程序全局配置文件。配置所有页面路径、窗口表现、网络超时、底部 TabBar 等。</li><li><strong><code>项目目录/app.wxss</code></strong>: 小程序全局样式文件。类似于 CSS，作用于所有页面。</li><li><strong><code>项目目录/pages/页面名/页面名.js</code></strong>: 页面逻辑文件。注册页面，声明数据、生命周期、事件处理函数等。</li><li><strong><code>项目目录/pages/页面名/页面名.json</code></strong>: 页面配置文件。配置当前页面的窗口表现。优先级高于 <code>app.json</code>。</li><li><strong><code>项目目录/pages/页面名/页面名.wxml</code></strong>: 页面结构文件。类似 HTML，用于描述页面结构。</li><li><strong><code>项目目录/pages/页面名/页面名.wxss</code></strong>: 页面样式文件。类似 CSS，作用于当前页面。优先级高于 <code>app.wxss</code>。</li></ul><p><strong>特别说明：</strong></p><ul><li><strong>WXML</strong>：微信自己的模板语言，结合基础组件、事件系统，可以构建出页面的结构。</li><li><strong>WXSS</strong>：微信自己的样式语言，基于 CSS，做了一些扩充和优化，例如：<code>rpx</code>（响应式像素）。</li><li><strong>JS</strong>：页面逻辑文件，负责页面的数据管理、事件响应等。</li><li><strong>JSON</strong>：配置文件，控制小程序和页面的表现。</li></ul><hr><h1 id="第一阶段：项目初始化与基本结构"><a href="#第一阶段：项目初始化与基本结构" class="headerlink" title="第一阶段：项目初始化与基本结构"></a>第一阶段：项目初始化与基本结构</h1><h2 id="步骤-1-创建新项目"><a href="#步骤-1-创建新项目" class="headerlink" title="步骤 1: 创建新项目"></a>步骤 1: 创建新项目</h2><ol><li>打开 <strong>微信开发者工具</strong>。</li><li>点击左上角的 <strong><code>+</code></strong> 号，选择 <strong><code>新建项目</code></strong>。</li><li>填写项目信息：<ul><li><strong>项目名称</strong>：<code>Todo List</code></li><li><strong>项目目录</strong>：选择一个您想存放项目的文件夹。</li><li><strong>AppID</strong>：填写您在微信公众平台获取的 AppID。</li><li><strong>后端服务</strong>：选择 <code>不使用云服务</code> (本教程不涉及云开发)。</li><li><strong>语言</strong>：<code>JavaScript</code> (默认)。</li><li><strong>模板</strong>：<code>小程序基础模板</code> (默认)。</li></ul></li><li>点击 <strong><code>新建</code></strong>。</li></ol><p>现在，您应该能看到一个默认的项目结构，并在模拟器中显示一个简单的页面。</p><h2 id="步骤-2-理解初始文件结构"><a href="#步骤-2-理解初始文件结构" class="headerlink" title="步骤 2: 理解初始文件结构"></a>步骤 2: 理解初始文件结构</h2><p>创建成功后，项目目录会包含以下主要文件：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">├── project.config.json  &#x2F;&#x2F; 项目配置文件</span><br><span class="line">├── app.js               &#x2F;&#x2F; 小程序入口逻辑文件</span><br><span class="line">├── app.json             &#x2F;&#x2F; 小程序全局配置</span><br><span class="line">├── app.wxss             &#x2F;&#x2F; 小程序全局样式</span><br><span class="line">├── sitemap.json         &#x2F;&#x2F; 站点地图</span><br><span class="line">└── pages&#x2F;               &#x2F;&#x2F; 存放所有页面的目录</span><br><span class="line">    └── index&#x2F;           &#x2F;&#x2F; 首页目录</span><br><span class="line">        ├── index.js     &#x2F;&#x2F; 首页逻辑</span><br><span class="line">        ├── index.json   &#x2F;&#x2F; 首页配置</span><br><span class="line">        ├── index.wxml   &#x2F;&#x2F; 首页结构</span><br><span class="line">        └── index.wxss   &#x2F;&#x2F; 首页样式</span><br></pre></td></tr></table></figure><h2 id="步骤-3-配置-app-json-全局配置"><a href="#步骤-3-配置-app-json-全局配置" class="headerlink" title="步骤 3: 配置 app.json (全局配置)"></a>步骤 3: 配置 <code>app.json</code> (全局配置)</h2><p>修改 <code>app.json</code> 文件，配置小程序的窗口表现。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">&quot;pages&quot;</span>: [</span><br><span class="line">    <span class="string">&quot;pages/index/index&quot;</span></span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">&quot;window&quot;</span>: &#123;</span><br><span class="line">    <span class="attr">&quot;backgroundTextStyle&quot;</span>: <span class="string">&quot;light&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;navigationBarBackgroundColor&quot;</span>: <span class="string">&quot;#007bff&quot;</span>, </span><br><span class="line">    <span class="attr">&quot;navigationBarTitleText&quot;</span>: <span class="string">&quot;我的待办事项&quot;</span>, </span><br><span class="line">    <span class="attr">&quot;navigationBarTextStyle&quot;</span>: <span class="string">&quot;white&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">&quot;style&quot;</span>: <span class="string">&quot;v2&quot;</span>,</span><br><span class="line">  <span class="attr">&quot;sitemapLocation&quot;</span>: <span class="string">&quot;sitemap.json&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>保存后，您会看到模拟器的导航栏颜色和标题发生了变化。</p><h2 id="步骤-4-清理默认-index-页面内容"><a href="#步骤-4-清理默认-index-页面内容" class="headerlink" title="步骤 4: 清理默认 index 页面内容"></a>步骤 4: 清理默认 <code>index</code> 页面内容</h2><p>为了从零开始，我们先清空 <code>pages/index/</code> 下的 <code>wxml</code> 和 <code>wxss</code> 文件内容。</p><ul><li><strong><code>pages/index/index.wxml</code></strong>：清空所有内容。</li><li><strong><code>pages/index/index.wxss</code></strong>：清空所有内容。</li><li><strong><code>pages/index/index.js</code></strong>：保留 <code>Page(&#123;&#125;)</code> 结构，清空 <code>data</code> 和 <code>methods</code>，只留下基本骨架。</li></ul><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// pages/index/index.js</span></span><br><span class="line">Page(&#123;</span><br><span class="line">  data: &#123;</span><br><span class="line">    <span class="comment">// 页面数据</span></span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="title">onLoad</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 页面加载时触发</span></span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 其他事件处理函数</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><hr><h1 id="第二阶段：构建-UI-界面-WXML-amp-WXSS"><a href="#第二阶段：构建-UI-界面-WXML-amp-WXSS" class="headerlink" title="第二阶段：构建 UI 界面 (WXML &amp; WXSS)"></a>第二阶段：构建 UI 界面 (WXML &amp; WXSS)</h1><p>现在我们开始构建 Todo List 的界面。</p><h2 id="步骤-1-设计-pages-index-index-wxml-页面结构"><a href="#步骤-1-设计-pages-index-index-wxml-页面结构" class="headerlink" title="步骤 1: 设计 pages/index/index.wxml (页面结构)"></a>步骤 1: 设计 <code>pages/index/index.wxml</code> (页面结构)</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- pages/index/index.wxml --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">view</span> <span class="attr">class</span>=<span class="string">&quot;container&quot;</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- 1. 添加新待办事项区域 --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">view</span> <span class="attr">class</span>=<span class="string">&quot;input-area&quot;</span>&gt;</span></span><br><span class="line">    &lt;input</span><br><span class="line">      class=&quot;todo-input&quot;</span><br><span class="line">      placeholder=&quot;请输入待办事项&quot;</span><br><span class="line">      bindinput=&quot;handleInputChange&quot;</span><br><span class="line">      value=&quot;&#123;&#123;newTodo&#125;&#125;&quot;</span><br><span class="line">      confirm-type=&quot;done&quot;</span><br><span class="line">      bindconfirm=&quot;addTodo&quot;</span><br><span class="line">    /&gt;</span><br><span class="line">    <span class="tag">&lt;<span class="name">button</span> <span class="attr">class</span>=<span class="string">&quot;add-button&quot;</span> <span class="attr">bindtap</span>=<span class="string">&quot;addTodo&quot;</span>&gt;</span>添加<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">view</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">&lt;!-- 2. 待办事项列表 --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">view</span> <span class="attr">class</span>=<span class="string">&quot;todo-list&quot;</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- 使用 wx:for 遍历 todos 数组 --&gt;</span></span><br><span class="line">    &lt;view</span><br><span class="line">      wx:for=&quot;&#123;&#123;todos&#125;&#125;&quot;</span><br><span class="line">      wx:key=&quot;id&quot;</span><br><span class="line">      class=&quot;todo-item &#123;&#123;item.completed ? &#x27;completed&#x27; : &#x27;&#x27;&#125;&#125;&quot;</span><br><span class="line">      data-id=&quot;&#123;&#123;item.id&#125;&#125;&quot;</span><br><span class="line">      bindtap=&quot;toggleTodoStatus&quot;</span><br><span class="line">    &gt;</span><br><span class="line">      <span class="tag">&lt;<span class="name">text</span> <span class="attr">class</span>=<span class="string">&quot;todo-text&quot;</span>&gt;</span>&#123;&#123;item.text&#125;&#125;<span class="tag">&lt;/<span class="name">text</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">text</span> <span class="attr">class</span>=<span class="string">&quot;delete-button&quot;</span> <span class="attr">data-id</span>=<span class="string">&quot;&#123;&#123;item.id&#125;&#125;&quot;</span> <span class="attr">catchtap</span>=<span class="string">&quot;deleteTodo&quot;</span>&gt;</span>×<span class="tag">&lt;/<span class="name">text</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">view</span>&gt;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">&lt;!-- 列表为空时的提示 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">view</span> <span class="attr">wx:if</span>=<span class="string">&quot;&#123;&#123;todos.length === 0&#125;&#125;&quot;</span> <span class="attr">class</span>=<span class="string">&quot;empty-tip&quot;</span>&gt;</span></span><br><span class="line">      暂无待办事项，快来添加吧！</span><br><span class="line">    <span class="tag">&lt;/<span class="name">view</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">view</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">view</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>代码解释：</strong></p><ul><li><code>&lt;view&gt;</code>：小程序的容器组件，类似 <code>div</code>。</li><li><code>&lt;input&gt;</code>：输入框组件。<ul><li><code>placeholder</code>：提示文本。</li><li><code>bindinput</code>：当输入框内容改变时触发的事件，用于更新 <code>newTodo</code> 数据。</li><li><code>value=&quot;&#123;&#123;newTodo&#125;&#125;&quot;</code>：数据绑定，将输入框的值绑定到 <code>newTodo</code> 变量。</li><li><code>confirm-type=&quot;done&quot;</code>：键盘右下角按钮显示“完成”。</li><li><code>bindconfirm=&quot;addTodo&quot;</code>：当用户点击键盘上的“完成”按钮时触发，用于添加待办。</li></ul></li><li><code>&lt;button&gt;</code>：按钮组件。<ul><li><code>bindtap=&quot;addTodo&quot;</code>：当按钮被点击时触发 <code>addTodo</code> 方法。</li></ul></li><li><code>wx:for=&quot;&#123;&#123;todos&#125;&#125;&quot;</code>：列表渲染，遍历 <code>todos</code> 数组。<ul><li><code>wx:key=&quot;id&quot;</code>：指定列表中项目的唯一标识符，提高渲染效率，避免不必要的组件重新渲染。</li><li><code>class=&quot;todo-item &#123;&#123;item.completed ? 'completed' : ''&#125;&#125;&quot;</code>：动态类名，根据 <code>item.completed</code> 状态添加 <code>completed</code> 类。</li><li><code>data-id=&quot;&#123;&#123;item.id&#125;&#125;&quot;</code>：自定义属性，用于在事件处理函数中获取当前事项的 <code>id</code>。</li><li><code>bindtap=&quot;toggleTodoStatus&quot;</code>：点击整个事项区域时，切换其完成状态。</li></ul></li><li><code>&lt;text&gt;</code>：文本组件。<ul><li><code>catchtap=&quot;deleteTodo&quot;</code>：阻止事件冒泡 (避免点击删除按钮时同时触发 <code>toggleTodoStatus</code>)，用于删除事项。</li></ul></li><li><code>wx:if=&quot;&#123;&#123;todos.length === 0&#125;&#125;&quot;</code>：条件渲染，当 <code>todos</code> 数组为空时显示提示信息。</li></ul><h2 id="步骤-2-设计-pages-index-index-wxss-页面样式"><a href="#步骤-2-设计-pages-index-index-wxss-页面样式" class="headerlink" title="步骤 2: 设计 pages/index/index.wxss (页面样式)"></a>步骤 2: 设计 <code>pages/index/index.wxss</code> (页面样式)</h2><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* pages/index/index.wxss */</span></span><br><span class="line"><span class="selector-class">.container</span> &#123;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">20</span>rpx;</span><br><span class="line">  <span class="attribute">font-family</span>: Arial, sans-serif;</span><br><span class="line">  <span class="attribute">display</span>: flex;</span><br><span class="line">  <span class="attribute">flex-direction</span>: column;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">100vh</span>; <span class="comment">/* 让容器撑满整个视口高度 */</span></span><br><span class="line">  <span class="attribute">box-sizing</span>: border-box;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 输入区域样式 */</span></span><br><span class="line"><span class="selector-class">.input-area</span> &#123;</span><br><span class="line">  <span class="attribute">display</span>: flex;</span><br><span class="line">  <span class="attribute">margin-bottom</span>: <span class="number">30</span>rpx;</span><br><span class="line">  <span class="attribute">align-items</span>: center;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.todo-input</span> &#123;</span><br><span class="line">  <span class="attribute">flex</span>: <span class="number">1</span>; <span class="comment">/* 占据剩余空间 */</span></span><br><span class="line">  <span class="attribute">height</span>: <span class="number">80</span>rpx;</span><br><span class="line">  <span class="attribute">border</span>: <span class="number">2</span>rpx solid <span class="number">#eee</span>;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">0</span> <span class="number">20</span>rpx;</span><br><span class="line">  <span class="attribute">border-radius</span>: <span class="number">10</span>rpx;</span><br><span class="line">  <span class="attribute">font-size</span>: <span class="number">32</span>rpx;</span><br><span class="line">  <span class="attribute">margin-right</span>: <span class="number">20</span>rpx;</span><br><span class="line">  <span class="attribute">box-sizing</span>: border-box; <span class="comment">/* 确保内边距不会增加宽度 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.add-button</span> &#123;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">160</span>rpx;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">80</span>rpx;</span><br><span class="line">  <span class="attribute">line-height</span>: <span class="number">80</span>rpx;</span><br><span class="line">  <span class="attribute">font-size</span>: <span class="number">32</span>rpx;</span><br><span class="line">  <span class="attribute">background-color</span>: <span class="number">#007bff</span>;</span><br><span class="line">  <span class="attribute">color</span>: white;</span><br><span class="line">  <span class="attribute">border-radius</span>: <span class="number">10</span>rpx;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">0</span>; <span class="comment">/* 移除默认内边距 */</span></span><br><span class="line">  <span class="attribute">margin</span>: <span class="number">0</span>; <span class="comment">/* 移除默认外边距 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 待办事项列表样式 */</span></span><br><span class="line"><span class="selector-class">.todo-list</span> &#123;</span><br><span class="line">  <span class="attribute">flex</span>: <span class="number">1</span>; <span class="comment">/* 列表占据剩余空间 */</span></span><br><span class="line">  <span class="attribute">overflow-y</span>: auto; <span class="comment">/* 允许列表内容滚动 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.todo-item</span> &#123;</span><br><span class="line">  <span class="attribute">display</span>: flex;</span><br><span class="line">  <span class="attribute">align-items</span>: center;</span><br><span class="line">  <span class="attribute">justify-content</span>: space-between;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">25</span>rpx <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">border-bottom</span>: <span class="number">1</span>rpx solid <span class="number">#f0f0f0</span>;</span><br><span class="line">  <span class="attribute">font-size</span>: <span class="number">32</span>rpx;</span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#333</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.todo-item</span><span class="selector-pseudo">:last-child</span> &#123;</span><br><span class="line">  <span class="attribute">border-bottom</span>: none; <span class="comment">/* 最后一个item没有下边框 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.todo-text</span> &#123;</span><br><span class="line">  <span class="attribute">flex</span>: <span class="number">1</span>;</span><br><span class="line">  <span class="attribute">margin-right</span>: <span class="number">20</span>rpx;</span><br><span class="line">  <span class="attribute">word-break</span>: break-word; <span class="comment">/* 文本过长时自动换行 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 已完成事项的样式 */</span></span><br><span class="line"><span class="selector-class">.todo-item</span><span class="selector-class">.completed</span> <span class="selector-class">.todo-text</span> &#123;</span><br><span class="line">  <span class="attribute">text-decoration</span>: line-through; <span class="comment">/* 删除线 */</span></span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#999</span>; <span class="comment">/* 颜色变灰 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 删除按钮样式 */</span></span><br><span class="line"><span class="selector-class">.delete-button</span> &#123;</span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#ff4d4f</span>;</span><br><span class="line">  <span class="attribute">font-size</span>: <span class="number">40</span>rpx;</span><br><span class="line">  <span class="attribute">font-weight</span>: bold;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">0</span> <span class="number">10</span>rpx;</span><br><span class="line">  <span class="attribute">cursor</span>: pointer;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 列表为空时的提示 */</span></span><br><span class="line"><span class="selector-class">.empty-tip</span> &#123;</span><br><span class="line">  <span class="attribute">text-align</span>: center;</span><br><span class="line">  <span class="attribute">color</span>: <span class="number">#999</span>;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">50</span>rpx <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">font-size</span>: <span class="number">30</span>rpx;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>代码解释：</strong></p><ul><li><code>rpx</code>：微信小程序特有的尺寸单位，可以根据屏幕宽度进行自适应。例如，在 iPhone6 上，1rpx = 0.5px。</li><li><code>display: flex;</code>、<code>flex-direction</code>、<code>align-items</code>、<code>justify-content</code>：使用 Flexbox 布局实现元素的排列和对齐。</li><li><code>.todo-item.completed</code>：当 <code>todo-item</code> 同时拥有 <code>completed</code> 类时生效。</li></ul><p>保存这两个文件后，模拟器中会显示出 Todo List 的基本布局，但还不能进行交互。</p><hr><h1 id="第三阶段：实现核心逻辑-JS"><a href="#第三阶段：实现核心逻辑-JS" class="headerlink" title="第三阶段：实现核心逻辑 (JS)"></a>第三阶段：实现核心逻辑 (JS)</h1><p>现在我们为 Todo List 添加交互逻辑和数据管理。</p><h2 id="步骤-1-修改-pages-index-index-js"><a href="#步骤-1-修改-pages-index-index-js" class="headerlink" title="步骤 1: 修改 pages/index/index.js"></a>步骤 1: 修改 <code>pages/index/index.js</code></h2><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// pages/index/index.js</span></span><br><span class="line">Page(&#123;</span><br><span class="line">  data: &#123;</span><br><span class="line">    newTodo: <span class="string">&#x27;&#x27;</span>, <span class="comment">// 存储输入框的值</span></span><br><span class="line">    todos: []    <span class="comment">// 存储待办事项列表，每个事项包含 id, text, completed</span></span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 页面加载时触发：从本地存储加载数据</span></span><br><span class="line">  <span class="function"><span class="title">onLoad</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.loadTodos();</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 页面隐藏或退出时触发：保存数据到本地存储</span></span><br><span class="line">  <span class="function"><span class="title">onHide</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.saveTodos();</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 监听输入框内容变化</span></span><br><span class="line">  <span class="function"><span class="title">handleInputChange</span>(<span class="params">e</span>)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.setData(&#123;</span><br><span class="line">      newTodo: e.detail.value <span class="comment">// 更新 newTodo 数据</span></span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 添加待办事项</span></span><br><span class="line">  <span class="function"><span class="title">addTodo</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> newTodoText = <span class="built_in">this</span>.data.newTodo.trim(); <span class="comment">// 获取输入框内容并去除首尾空格</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (newTodoText) &#123; <span class="comment">// 如果输入内容不为空</span></span><br><span class="line">      <span class="keyword">const</span> newTodoItem = &#123;</span><br><span class="line">        id: <span class="built_in">Date</span>.now(), <span class="comment">// 使用时间戳作为唯一ID</span></span><br><span class="line">        text: newTodoText,</span><br><span class="line">        completed: <span class="literal">false</span></span><br><span class="line">      &#125;;</span><br><span class="line"></span><br><span class="line">      <span class="built_in">this</span>.setData(&#123;</span><br><span class="line">        todos: [...this.data.todos, newTodoItem], <span class="comment">// 将新事项添加到 todos 数组</span></span><br><span class="line">        newTodo: <span class="string">&#x27;&#x27;</span> <span class="comment">// 清空输入框</span></span><br><span class="line">      &#125;, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.saveTodos(); <span class="comment">// 数据更新后保存</span></span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 切换待办事项的完成状态</span></span><br><span class="line">  <span class="function"><span class="title">toggleTodoStatus</span>(<span class="params">e</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> id = e.currentTarget.dataset.id; <span class="comment">// 获取通过 data-id 传递过来的事项ID</span></span><br><span class="line">    <span class="keyword">const</span> todos = <span class="built_in">this</span>.data.todos.map(<span class="function"><span class="params">item</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (item.id === id) &#123;</span><br><span class="line">        <span class="keyword">return</span> &#123; ...item, <span class="attr">completed</span>: !item.completed &#125;; <span class="comment">// 切换完成状态</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> item;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">this</span>.setData(&#123; todos &#125;, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="built_in">this</span>.saveTodos(); <span class="comment">// 数据更新后保存</span></span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 删除待办事项</span></span><br><span class="line">  <span class="function"><span class="title">deleteTodo</span>(<span class="params">e</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// 阻止事件冒泡，避免同时触发 toggleTodoStatus</span></span><br><span class="line">    <span class="comment">// e.stopPropagation(); </span></span><br><span class="line">    <span class="keyword">const</span> id = e.currentTarget.dataset.id; <span class="comment">// 获取事项ID</span></span><br><span class="line">    <span class="keyword">const</span> todos = <span class="built_in">this</span>.data.todos.filter(<span class="function"><span class="params">item</span> =&gt;</span> item.id !== id); <span class="comment">// 过滤掉要删除的事项</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">this</span>.setData(&#123; todos &#125;, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="built_in">this</span>.saveTodos(); <span class="comment">// 数据更新后保存</span></span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 保存待办事项到本地存储</span></span><br><span class="line">  <span class="function"><span class="title">saveTodos</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    wx.setStorageSync(<span class="string">&#x27;todos&#x27;</span>, <span class="built_in">this</span>.data.todos); <span class="comment">// 将 todos 数组存储到本地</span></span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&#x27;Todos saved:&#x27;</span>, <span class="built_in">this</span>.data.todos);</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 从本地存储加载待办事项</span></span><br><span class="line">  <span class="function"><span class="title">loadTodos</span>(<span class="params"></span>)</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> todos = wx.getStorageSync(<span class="string">&#x27;todos&#x27;</span>); <span class="comment">// 从本地获取 todos 数组</span></span><br><span class="line">    <span class="keyword">if</span> (todos) &#123; <span class="comment">// 如果存在数据</span></span><br><span class="line">      <span class="built_in">this</span>.setData(&#123; todos &#125;); <span class="comment">// 更新页面数据</span></span><br><span class="line">      <span class="built_in">console</span>.log(<span class="string">&#x27;Todos loaded:&#x27;</span>, todos);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>在微信开发者工具的右上角，点击 “详情” 按钮（图标通常是一个 i 或者类似列表的图标）。<br>在弹出的面板中，点击 “本地设置” 选项卡。<br>勾选 “开启条件编译” (Enable Enhanced Compilation)。<br>注意：如果已经勾选，尝试先取消勾选，编译一次，然后再重新勾选上。<br>同时确保 “ES6 转 ES5” 也是勾选状态。<br>点击工具栏上的 “编译” 按钮，查看报错是否消失。</p><p><strong>代码解释：</strong></p><ul><li><strong><code>data</code> 对象</strong>：<ul><li><code>newTodo</code>：用于存储用户在输入框中实时输入的内容。</li><li><code>todos</code>：一个数组，存储所有待办事项。每个事项是一个对象 <code>&#123; id: number, text: string, completed: boolean &#125;</code>。</li></ul></li><li><strong><code>onLoad()</code> 生命周期</strong>：页面加载时执行，我们在此调用 <code>loadTodos()</code> 从本地存储加载数据。</li><li><strong><code>onHide()</code> 生命周期</strong>：当页面被隐藏（如用户切换到其他页面、退出小程序）时执行。我们在此调用 <code>saveTodos()</code> 将当前数据保存到本地存储，实现数据持久化。</li><li><strong><code>handleInputChange(e)</code></strong>：<ul><li><code>e</code> 是事件对象，<code>e.detail.value</code> 获取输入框的当前值。</li><li><code>this.setData(&#123; key: value &#125;)</code>：这是小程序更新页面数据的唯一方法。它会将数据从逻辑层发送到视图层，同时改变对应的 <code>this.data</code> 值。</li></ul></li><li><strong><code>addTodo()</code></strong>：<ul><li><code>trim()</code> 方法去除输入字符串两端的空白字符。</li><li><code>Date.now()</code>：生成一个唯一的 ID (毫秒时间戳)。</li><li><code>[...this.data.todos, newTodoItem]</code>：使用 ES6 的展开运算符，创建一个包含所有旧事项和新事项的新数组，然后通过 <code>setData</code> 更新。</li></ul></li><li><strong><code>toggleTodoStatus(e)</code></strong>：<ul><li><code>e.currentTarget.dataset.id</code>：通过 <code>data-id</code> 属性获取当前点击元素的自定义数据。</li><li><code>map()</code> 方法遍历 <code>todos</code> 数组，找到匹配 ID 的事项并切换其 <code>completed</code> 状态。<code>&#123; ...item, completed: !item.completed &#125;</code> 也是 ES6 语法，用于创建一个新对象并覆盖 <code>completed</code> 属性。</li></ul></li><li><strong><code>deleteTodo(e)</code></strong>：<ul><li><code>e.stopPropagation()</code>：非常重要！防止点击删除按钮时，事件冒泡到父级 <code>todo-item</code> 元素，从而触发 <code>toggleTodoStatus</code>。</li><li><code>filter()</code> 方法遍历 <code>todos</code> 数组，过滤掉 ID 匹配的事项，从而实现删除。</li></ul></li><li><strong><code>saveTodos()</code> 和 <code>loadTodos()</code></strong>：<ul><li><code>wx.setStorageSync(key, data)</code>：将数据同步写入本地缓存。</li><li><code>wx.getStorageSync(key)</code>：从本地缓存中同步获取指定 <code>key</code> 的内容。</li><li>这些是微信小程序提供的本地存储API，类似于浏览器的 <code>localStorage</code>。</li></ul></li></ul><p>保存所有文件后，现在Todo List小程序应该可以正常工作了！可以尝试添加、标记完成、删除事项，并关闭小程序再打开，看看数据是否被保存。</p><hr><h1 id="第四阶段：调试与预览"><a href="#第四阶段：调试与预览" class="headerlink" title="第四阶段：调试与预览"></a>第四阶段：调试与预览</h1><ol><li><p><strong>在开发者工具中调试</strong>：</p><ul><li>在开发者工具中，您可以直接看到模拟器的运行效果。</li><li>点击工具栏上的 <strong><code>调试器</code></strong> 按钮，可以打开调试面板，类似于 Chrome 开发者工具。<ul><li><strong><code>Console</code></strong>：查看 <code>console.log</code> 输出，排查 JS 错误。</li><li><strong><code>Elements</code></strong>：查看 WXML 结构和 WXSS 样式，动态修改样式。</li><li><strong><code>Sources</code></strong>：调试 JS 代码，设置断点。</li><li><strong><code>Storage</code></strong>：查看本地缓存数据 (<code>wx.setStorageSync</code> 存储的数据)。</li><li><strong><code>Network</code></strong>：查看网络请求。</li></ul></li></ul></li><li><p><strong>真机预览</strong>：</p><ul><li>点击开发者工具右上角的 <strong><code>预览</code></strong> 按钮。</li><li>微信会生成一个二维码，用您的微信扫描该二维码，即可在真机上预览小程序。这有助于检查不同设备上的兼容性和实际体验。</li></ul></li></ol><hr><h1 id="第五阶段：发布与上线-简述"><a href="#第五阶段：发布与上线-简述" class="headerlink" title="第五阶段：发布与上线 (简述)"></a>第五阶段：发布与上线 (简述)</h1><p>当完成开发并测试无误后，就可以考虑发布小程序了：</p><ol><li><strong>上传代码</strong>：在开发者工具右上角点击 <strong><code>上传</code></strong> 按钮，填写版本号和项目备注。代码将上传到微信公众平台。</li><li><strong>提交审核</strong>：登录微信公众平台后台，进入 “开发管理” -&gt; “版本管理”，会看到刚才上传的版本。点击 <strong><code>提交审核</code></strong>。根据微信的审核规范，填写相关信息并等待审核。</li><li><strong>发布上线</strong>：审核通过后，您可以选择 <strong><code>发布</code></strong>，小程序就会正式上线，用户可以在微信中搜索到并使用。</li></ol><hr><h1 id="总结与下一步"><a href="#总结与下一步" class="headerlink" title="总结与下一步"></a>总结与下一步</h1><p>上面已经成功从零开发了一个具备基本功能的 Todo List 微信小程序，并了解了小程序的开发流程、核心概念和常用API。</p><p><strong>可以继续探索和优化：</strong></p><ul><li><strong>编辑功能</strong>：点击事项文本后，允许用户编辑内容。</li><li><strong>事项分类/筛选</strong>：添加分类功能，或按完成状态筛选显示。</li><li><strong>排序功能</strong>：例如按创建时间、重要程度排序。</li><li><strong>界面美化</strong>：使用更丰富的 WXSS 样式和动画效果。</li><li><strong>组件化</strong>：将待办事项列表项封装成自定义组件，提高代码复用性。</li><li><strong>用户登录/云开发</strong>：如果需要用户特定数据，可以结合微信登录和云开发（或自有后端服务）实现更复杂的功能。</li><li><strong>API 调用</strong>：学习如何使用 <code>wx.request</code> 进行网络请求，与后端交互。</li></ul><p><strong>重要资源：</strong></p><ul><li><strong>微信小程序官方文档</strong>：<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/">https://developers.weixin.qq.com/miniprogram/dev/framework/</a> (务必多查阅！)</li><li><strong>微信开放社区</strong>：<a href="https://developers.weixin.qq.com/community/miniprogram">https://developers.weixin.qq.com/community/miniprogram</a> (获取帮助和交流经验)</li></ul>]]></content>
    
    
    <summary type="html">微信小程序以其轻量、便捷、跨平台的特性，成为了连接线上线下的重要工具。本教程将根据微信小程序最新的开发文档，从零开始，一步步开发一个功能完整的 “待办事项 (Todo List)” 小程序。

目标功能
我们将开发的Todo List小程序将具备以下功能：

 1. 添加待办事项：用户可以在输入框中输入新的待办事项并添加。
 2. 显示待办事项列表：所有待办事项以列表形式展示。
 3. 标记完成/未完成：每个事项可以被标记为已完成或未完成。
 4. 删除待办事项：可以从列表中删除某个事项。
 5. 数据持久化：关闭小程序后，数据不会丢失（使用本地存储）。

准备工作
在开始开发之前，需要准备以</summary>
    
    
    
    <category term="Vibe coding" scheme="http://qixinbo.github.io/categories/Vibe-coding/"/>
    
    
    <category term="LLM" scheme="http://qixinbo.github.io/tags/LLM/"/>
    
  </entry>
  
</feed>
