<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>ILOFT · 我的阁楼</title>
  
  
  <link href="https://www.iloft.xyz/atom.xml" rel="self"/>
  
  <link href="https://www.iloft.xyz/"/>
  <updated>2026-04-10T10:50:27.641Z</updated>
  <id>https://www.iloft.xyz/</id>
  
  <author>
    <name>Yu</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>从零开始写 AI Agent：一个 for 循环的演进</title>
    <link href="https://www.iloft.xyz/archives/0-1-AI-Agent.html"/>
    <id>https://www.iloft.xyz/archives/0-1-AI-Agent.html</id>
    <published>2026-04-10T17:32:00.000Z</published>
    <updated>2026-04-10T10:50:27.641Z</updated>
    
    <content type="html"><![CDATA[<h3 id="为什么要从零写一个-Agent？"><a href="#为什么要从零写一个-Agent？" class="headerlink" title="为什么要从零写一个 Agent？"></a>为什么要从零写一个 Agent？</h3><p>AI 时代信息爆炸——MCP、RAG、Multi-Agent、Agentic Workflow……新概念一个接一个,每隔几周就有新的框架冒出来。很容易陷进去,感觉 Agent 是一个高深莫测的东西,离自己很远。</p><p>但如果你亲手写过一遍(在 AI 帮助下,只需要一个上午),就会发现:<strong>Agent 本身的代码比大佬们写的控制器和转发面简单多了。</strong> 核心逻辑就是一个 for 循环,加上几个工具调用。</p><p>这个 Workshop 的目的就是<strong>祛魅</strong>——把地基翻出来看清楚。从一个 223 行的最小 Agent 出发,一步步演进到具备权限控制、任务规划、技能加载、上下文压缩的完整系统。每一版只解决一个问题,每一行代码都有来处。</p><p>看完之后,那些概念还会存在,但它们背后的地基你已经摸清楚了。雾里看花,变成近在眼前。</p><span id="more"></span> <h3 id="v1-LLM-调用-Agent-Loop-单工具-223行"><a href="#v1-LLM-调用-Agent-Loop-单工具-223行" class="headerlink" title="v1: LLM 调用 + Agent Loop + 单工具 (223行)"></a>v1: LLM 调用 + Agent Loop + 单工具 (223行)</h3><p>最简单的 Agent 实现:一次 LLM 调用 + 一个 while 循环 + 一个计算器工具。核心公式:<strong>Agent &#x3D; LLM调用 + Loop + 工具执行 + 上下文累积</strong>。</p><h4 id="1-LLM-调用接口-glm-go"><a href="#1-LLM-调用接口-glm-go" class="headerlink" title="1. LLM 调用接口 (glm.go)"></a>1. LLM 调用接口 (glm.go)</h4><p>我们用标准 HTTP POST + JSON body 与 LLM 交互,本质上是遵循 <strong>OpenAI Chat Completions API 规范</strong>——这是业界事实标准,GLM、Claude、GPT 等主流模型都兼容它。我们把 <code>messages</code>(对话历史)和 <code>tools</code>(工具描述)序列化成 JSON 发过去,LLM 服务端收到的只是一段普通的 HTTP 请求体。</p><p>有意思的地方在于:<strong>我们从未在 prompt 里告诉 LLM “请用 JSON 格式回复”</strong>,但当我们传入 <code>tools</code> 参数时,模型会自动在响应里输出结构化的 <code>tool_calls</code> 字段。这是因为模型在训练阶段(SFT + RLHF)就已经被大量”工具调用示例”微调过——它学会了:<em>一旦上下文里出现工具定义,就应该用约定格式声明调用意图,而不是用自然语言描述</em>。这个行为是编码在模型权重里的,不依赖任何运行时的格式指令。因此,<code>json.Unmarshal</code> 能稳定解析出 <code>tool_calls</code>,不是因为我们约束了输出,而是因为模型自己知道该怎么做。这两个 struct 的字段名和 JSON tag 与 API 规范一一对应,Go 的标准库会自动完成映射:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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> Message <span class="keyword">struct</span> &#123;</span><br><span class="line">Role      <span class="type">string</span>     <span class="string">`json:&quot;role&quot;`</span></span><br><span class="line">Content   <span class="type">string</span>     <span class="string">`json:&quot;content&quot;`</span></span><br><span class="line">ToolCalls []ToolCall <span class="string">`json:&quot;tool_calls,omitempty&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> ToolCall <span class="keyword">struct</span> &#123;</span><br><span class="line">ID       <span class="type">string</span> <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">Type     <span class="type">string</span> <span class="string">`json:&quot;type&quot;`</span></span><br><span class="line">Function <span class="keyword">struct</span> &#123;</span><br><span class="line">Name      <span class="type">string</span> <span class="string">`json:&quot;name&quot;`</span></span><br><span class="line">Arguments <span class="type">string</span> <span class="string">`json:&quot;arguments&quot;`</span></span><br><span class="line">&#125; <span class="string">`json:&quot;function&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="params">(c *GLMClient)</span></span> Chat(ctx context.Context, messages []Message, tools []<span class="keyword">map</span>[<span class="type">string</span>]<span class="keyword">interface</span>&#123;&#125;) (Message, <span class="type">error</span>) &#123;</span><br><span class="line">reqBody := <span class="keyword">map</span>[<span class="type">string</span>]<span class="keyword">interface</span>&#123;&#125;&#123;</span><br><span class="line"><span class="string">&quot;model&quot;</span>:    <span class="string">&quot;glm-5&quot;</span>,</span><br><span class="line"><span class="string">&quot;messages&quot;</span>: messages,</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(tools) &gt; <span class="number">0</span> &#123;</span><br><span class="line">reqBody[<span class="string">&quot;tools&quot;</span>] = tools</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">jsonData, _ := json.Marshal(reqBody)</span><br><span class="line">req, _ := http.NewRequestWithContext(ctx, <span class="string">&quot;POST&quot;</span>, c.url, bytes.NewBuffer(jsonData))</span><br><span class="line">req.Header.Set(<span class="string">&quot;Content-Type&quot;</span>, <span class="string">&quot;application/json&quot;</span>)</span><br><span class="line">req.Header.Set(<span class="string">&quot;Authorization&quot;</span>, <span class="string">&quot;Bearer &quot;</span>+c.token)</span><br><span class="line"></span><br><span class="line">resp, err := http.DefaultClient.Do(req)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> Message&#123;&#125;, err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line"></span><br><span class="line">body, _ := io.ReadAll(resp.Body)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> resp.StatusCode != <span class="number">200</span> &#123;</span><br><span class="line"><span class="keyword">return</span> Message&#123;&#125;, fmt.Errorf(<span class="string">&quot;API error: %s&quot;</span>, <span class="type">string</span>(body))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> result <span class="keyword">struct</span> &#123;</span><br><span class="line">Choices []<span class="keyword">struct</span> &#123;</span><br><span class="line">Message Message <span class="string">`json:&quot;message&quot;`</span></span><br><span class="line">&#125; <span class="string">`json:&quot;choices&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line">json.Unmarshal(body, &amp;result)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> result.Choices[<span class="number">0</span>].Message, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2-Agent-主循环-main-go"><a href="#2-Agent-主循环-main-go" class="headerlink" title="2. Agent 主循环 (main.go)"></a>2. Agent 主循环 (main.go)</h4><p>这里有两层循环,职责完全不同。外层是<strong>对话轮次</strong>——用户每发一条消息触发一次;内层才是真正的 <strong>Agent Loop</strong>,处理单条消息内可能发生的多轮工具调用。</p><p>LLM 不一定一次就给出最终答案。比如”先算 10+5,再把结果乘以 2”,它需要先调用一次 calculator、拿到结果、再调用第二次才能回答。内层 <code>for i := 0; i &lt; 10</code> 就是这个循环:有 <code>tool_calls</code> 就执行工具、把结果追加进 messages、继续下一轮;没有 <code>tool_calls</code> 说明 LLM 已经给出最终答案,直接 return。上限 10 是安全兜底,防止工具持续报错时无限循环——正常路径根本跑不到 10 次。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">// 外层:对话轮次循环(main 函数里)</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">handleUserMessage(ctx, client, &amp;messages, input)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 内层:单条消息的 Agent Loop</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleUserMessage</span><span class="params">(...)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10</span>; i++ &#123;  <span class="comment">// 上限 10 次,安全兜底</span></span><br><span class="line">response, _ := client.Chat(ctx, *messages, tools)</span><br><span class="line">*messages = <span class="built_in">append</span>(*messages, response)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(response.ToolCalls) == <span class="number">0</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;AI: %s\n\n&quot;</span>, response.Content)</span><br><span class="line"><span class="keyword">return</span>  <span class="comment">// 无工具调用 = 最终答案,退出</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 执行工具,把结果追加进 messages,继续下一轮</span></span><br><span class="line"><span class="keyword">for</span> _, tc := <span class="keyword">range</span> response.ToolCalls &#123;</span><br><span class="line">result := calculator(tc.Function.Arguments)</span><br><span class="line">*messages = <span class="built_in">append</span>(*messages, Message&#123;Role: <span class="string">&quot;tool&quot;</span>, Content: result&#125;)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(<span class="string">&quot;超过最大轮次&quot;</span>)  <span class="comment">// 兜底,正常不会触发</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h3 id="v2-多工具系统-410行"><a href="#v2-多工具系统-410行" class="headerlink" title="v2: 多工具系统 (410行)"></a>v2: 多工具系统 (410行)</h3><h4 id="问题-LLM-只会”说”-不会”干”"><a href="#问题-LLM-只会”说”-不会”干”" class="headerlink" title="问题:LLM 只会”说”,不会”干”"></a>问题:LLM 只会”说”,不会”干”</h4><p>v1 的 Agent 只有一个计算器——LLM 可以推理、可以规划,但没有办法真正操作文件、执行命令、读取外部数据。它的能力边界就是它的上下文窗口,干不了任何需要副作用的事。</p><p>v2 的思路:引入工具系统,让 LLM 通过结构化的 Function Call 驱动真实操作。抽出统一的 <code>ExecuteTool</code> 路由层——新增工具只改 tools.go,Agent Loop 完全不动。</p><h4 id="工具路由接口-tools-go"><a href="#工具路由接口-tools-go" class="headerlink" title="工具路由接口 (tools.go)"></a>工具路由接口 (tools.go)</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ExecuteTool</span><span class="params">(toolName <span class="type">string</span>, arguments <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">switch</span> toolName &#123;</span><br><span class="line"><span class="keyword">case</span> <span class="string">&quot;calculator&quot;</span>:</span><br><span class="line"><span class="keyword">var</span> args <span class="keyword">struct</span> &#123;</span><br><span class="line">Expression <span class="type">string</span> <span class="string">`json:&quot;expression&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line">json.Unmarshal([]<span class="type">byte</span>(arguments), &amp;args)</span><br><span class="line"><span class="keyword">return</span> calculator(args.Expression)</span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> <span class="string">&quot;read_file&quot;</span>:</span><br><span class="line"><span class="keyword">var</span> args <span class="keyword">struct</span> &#123;</span><br><span class="line">Path <span class="type">string</span> <span class="string">`json:&quot;path&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line">json.Unmarshal([]<span class="type">byte</span>(arguments), &amp;args)</span><br><span class="line"><span class="keyword">return</span> readFile(args.Path)</span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> <span class="string">&quot;write_file&quot;</span>:</span><br><span class="line"><span class="keyword">var</span> args <span class="keyword">struct</span> &#123;</span><br><span class="line">Path    <span class="type">string</span> <span class="string">`json:&quot;path&quot;`</span></span><br><span class="line">Content <span class="type">string</span> <span class="string">`json:&quot;content&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line">json.Unmarshal([]<span class="type">byte</span>(arguments), &amp;args)</span><br><span class="line"><span class="keyword">return</span> writeFile(args.Path, args.Content)</span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> <span class="string">&quot;edit_file&quot;</span>:</span><br><span class="line"><span class="keyword">var</span> args <span class="keyword">struct</span> &#123;</span><br><span class="line">Path    <span class="type">string</span> <span class="string">`json:&quot;path&quot;`</span></span><br><span class="line">OldText <span class="type">string</span> <span class="string">`json:&quot;old_text&quot;`</span></span><br><span class="line">NewText <span class="type">string</span> <span class="string">`json:&quot;new_text&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line">json.Unmarshal([]<span class="type">byte</span>(arguments), &amp;args)</span><br><span class="line"><span class="keyword">return</span> editFile(args.Path, args.OldText, args.NewText)</span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> <span class="string">&quot;bash&quot;</span>:</span><br><span class="line"><span class="keyword">var</span> args <span class="keyword">struct</span> &#123;</span><br><span class="line">Command <span class="type">string</span> <span class="string">`json:&quot;command&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line">json.Unmarshal([]<span class="type">byte</span>(arguments), &amp;args)</span><br><span class="line"><span class="keyword">return</span> runBash(args.Command)</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> fmt.Sprintf(<span class="string">&quot;未知工具: %s&quot;</span>, toolName)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键特性</strong>:统一的 switch 路由,每个工具独立解析 JSON 参数,返回字符串结果。支持 5 种工具:计算器、文件读写、编辑、命令执行。</p><h4 id="两个最重要的工具-bash-与-edit-file"><a href="#两个最重要的工具-bash-与-edit-file" class="headerlink" title="两个最重要的工具:bash 与 edit_file"></a>两个最重要的工具:bash 与 edit_file</h4><p>有了 <code>bash</code>,Agent 拥有了和人类工程师几乎等价的操作能力——编译、运行测试、调用 CLI 工具、查询系统状态,任何能在终端里做的事它都能做。这是从”玩具 Agent”到”能干活的 Agent”的分水岭。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="title">runBash</span><span class="params">(command <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">cmd := exec.Command(<span class="string">&quot;bash&quot;</span>, <span class="string">&quot;-c&quot;</span>, command)</span><br><span class="line">output, err := cmd.CombinedOutput()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;执行失败: %v\n输出: %s&quot;</span>, err, <span class="type">string</span>(output))</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="type">string</span>(output)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>既然有了 bash,为什么还要单独提供 <code>edit_file</code>?直接让 Agent 用 <code>sed</code> 或 <code>echo &gt;&gt;</code> 改文件不行吗?</p><p>实践中不行。<strong>LLM 生成的 shell 转义极不可靠</strong>——多行内容、特殊字符、引号嵌套,稍有偏差就静默写错甚至清空文件,而且错误往往难以复现。<code>edit_file</code> 把”找到原文、替换为新文”抽象成一个原子操作,参数是结构化的 JSON 字符串而不是需要转义的 shell 命令,LLM 生成起来可靠得多。本质上是给 LLM 设计<strong>适合它使用的接口</strong>,而不是把人类用的工具直接暴露出去。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">func</span> <span class="title">editFile</span><span class="params">(path, oldText, newText <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">content, _ := os.ReadFile(path)</span><br><span class="line"><span class="keyword">if</span> !strings.Contains(<span class="type">string</span>(content), oldText) &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;错误:在文件中找不到指定的文本&quot;</span>)  <span class="comment">// 明确报错,不静默失败</span></span><br><span class="line">&#125;</span><br><span class="line">newContent := strings.Replace(<span class="type">string</span>(content), oldText, newText, <span class="number">1</span>)</span><br><span class="line">os.WriteFile(path, []<span class="type">byte</span>(newContent), <span class="number">0644</span>)</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;成功编辑文件: %s&quot;</span>, path)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意 <code>strings.Contains</code> 的前置检查——找不到原文直接报错,而不是什么都不做返回成功。这对 Agent 很重要:<strong>模糊失败比明确报错更难恢复</strong>,LLM 看到错误信息才能在下一轮修正。</p><h4 id="集成到-Agent-Loop-main-go"><a href="#集成到-Agent-Loop-main-go" class="headerlink" title="集成到 Agent Loop (main.go)"></a>集成到 Agent Loop (main.go)</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="comment">// v1: 硬编码 calculator</span></span><br><span class="line"><span class="comment">// result := calculator(args.Expression)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// v2: 通用工具路由 ⭐</span></span><br><span class="line"><span class="keyword">for</span> _, tc := <span class="keyword">range</span> response.ToolCalls &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;[调用工具: %s]\n&quot;</span>, tc.Function.Name)</span><br><span class="line">fmt.Printf(<span class="string">&quot;[参数: %s]\n&quot;</span>, tc.Function.Arguments)</span><br><span class="line"></span><br><span class="line"><span class="comment">// ⭐ 关键变化:统一调用 ExecuteTool,不再硬编码工具名</span></span><br><span class="line">result := ExecuteTool(tc.Function.Name, tc.Function.Arguments)</span><br><span class="line">fmt.Printf(<span class="string">&quot;[工具结果: %s]\n&quot;</span>, result)</span><br><span class="line"></span><br><span class="line">*messages = <span class="built_in">append</span>(*messages, Message&#123;</span><br><span class="line">Role:    <span class="string">&quot;tool&quot;</span>,</span><br><span class="line">Content: result,</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><strong>核心变化</strong>:Agent Loop 本身几乎不变,只是把 <code>calculator(args.Expression)</code> 替换为 <code>ExecuteTool(toolName, arguments)</code>。这实现了<strong>开闭原则</strong>:新增工具只需修改 tools.go,main.go 无需改动。</p><hr><h3 id="v3-权限系统-607行"><a href="#v3-权限系统-607行" class="headerlink" title="v3: 权限系统 (607行)"></a>v3: 权限系统 (607行)</h3><h4 id="问题-Agent-拿到了-bash-但没有任何约束"><a href="#问题-Agent-拿到了-bash-但没有任何约束" class="headerlink" title="问题:Agent 拿到了 bash,但没有任何约束"></a>问题:Agent 拿到了 bash,但没有任何约束</h4><p>v2 给了 Agent bash 和文件写入能力,但这意味着一条错误的指令就能删文件、跑危险命令。权限系统要解决的核心问题是:<strong>Agent 应该在什么情况下可以自主行动,什么情况下必须先问人</strong>。</p><p>v3 定义了 5 种权限模式,覆盖从”完全信任”到”完全只读”的整个光谱:</p><table><thead><tr><th>模式</th><th>行为</th><th>适用场景</th></tr></thead><tbody><tr><td><code>readonly</code></td><td>只允许读取和计算</td><td>代码审查、只读分析</td></tr><tr><td><code>ask_on_write</code></td><td>读自动通过,写操作弹出确认</td><td>日常使用推荐默认值</td></tr><tr><td><code>auto_write</code></td><td>文件读写自动通过,bash 执行需确认</td><td>批量文件处理</td></tr><tr><td><code>allow_all</code></td><td>全部放行,无任何拦截</td><td>完全信任的自动化流水线</td></tr><tr><td><code>plan_mode</code></td><td>禁止一切写操作,只允许读和分析</td><td>让 Agent 先规划再执行(v4 会用到)</td></tr></tbody></table><p>另外还有一层路径检查:所有文件操作必须在指定 workspace 目录内,<code>filepath.Rel</code> 检测到 <code>..</code> 路径穿越直接拒绝。这是最基本的沙箱隔离。</p><p><strong>这是简化版本。</strong>生产级权限系统会复杂得多:细化到单个工具粒度的白名单、操作审计日志、会话级权限动态升降级、人工审批工作流……我们这里的 5 模式分层已经能覆盖大多数教学和原型场景,但如果是面向真实用户的 Agent,权限设计本身会是一个独立的工程课题。</p><h4 id="权限检查逻辑-permission-go"><a href="#权限检查逻辑-permission-go" class="headerlink" title="权限检查逻辑 (permission.go)"></a>权限检查逻辑 (permission.go)</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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 class="function"><span class="keyword">func</span> <span class="params">(p *Permission)</span></span> Check(opType OperationType, toolName <span class="type">string</span>, details <span class="type">string</span>) (<span class="type">bool</span>, <span class="type">string</span>) &#123;</span><br><span class="line"><span class="keyword">switch</span> p.Mode &#123;</span><br><span class="line"><span class="keyword">case</span> ModeReadOnly:</span><br><span class="line"><span class="keyword">if</span> opType == OpSafe || opType == OpRead &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>, <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">false</span>, fmt.Sprintf(<span class="string">&quot;权限拒绝:当前为只读模式,不允许 %s 操作&quot;</span>, opType)</span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> ModeAskOnWrite:</span><br><span class="line"><span class="keyword">if</span> opType == OpSafe || opType == OpRead &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>, <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> p.askUser(toolName, details)</span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> ModeAutoWrite:</span><br><span class="line"><span class="keyword">if</span> opType == OpExecute &#123;</span><br><span class="line"><span class="keyword">return</span> p.askUser(toolName, details)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>, <span class="string">&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> ModeAllowAll:</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>, <span class="string">&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">case</span> ModePlan:</span><br><span class="line"><span class="keyword">if</span> opType == OpSafe || opType == OpRead &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>, <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">false</span>, fmt.Sprintf(<span class="string">&quot;权限拒绝:计划模式禁止 %s 操作&quot;</span>, opType)</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="literal">false</span>, <span class="string">&quot;未知权限模式&quot;</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>核心机制</strong>:策略模式,5 种模式分别对应不同安全等级。ModeAskOnWrite 需交互确认,ModePlan 完全禁止修改。返回 (bool, string) 元组:布尔值表示是否允许,字符串为拒绝理由。</p><h4 id="集成到-Agent-Loop"><a href="#集成到-Agent-Loop" class="headerlink" title="集成到 Agent Loop"></a>集成到 Agent Loop</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">// Agent Loop 代码与 v2 完全相同!权限检查在工具函数内部透明处理</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// tools.go - 以 writeFile 为例</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">writeFile</span><span class="params">(path, content <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line"><span class="comment">// ⭐ 步骤1:检查路径是否在工作区内</span></span><br><span class="line"><span class="keyword">if</span> err := globalPermission.CheckPath(path); err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;权限错误: %v&quot;</span>, err)</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">allowed, reason := globalPermission.Check(</span><br><span class="line">OpWrite,</span><br><span class="line"><span class="string">&quot;write_file&quot;</span>,</span><br><span class="line">fmt.Sprintf(<span class="string">&quot;写入文件: %s (内容长度: %d 字节)&quot;</span>, path, <span class="built_in">len</span>(content)),</span><br><span class="line">)</span><br><span class="line"><span class="keyword">if</span> !allowed &#123;</span><br><span class="line"><span class="keyword">return</span> reason  <span class="comment">// 返回拒绝原因,如 &quot;权限拒绝:只读模式&quot;</span></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">err := os.WriteFile(path, []<span class="type">byte</span>(content), <span class="number">0644</span>)</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>:权限检查对 Agent Loop <strong>完全透明</strong>。Loop 调用 <code>ExecuteTool()</code>,工具内部自行处理权限,被拒绝时返回错误信息而非执行。这是<strong>职责分离</strong>的典范。</p><hr><h3 id="v4-Todo-Plan-Mode-1119行"><a href="#v4-Todo-Plan-Mode-1119行" class="headerlink" title="v4: Todo + Plan Mode (1119行)"></a>v4: Todo + Plan Mode (1119行)</h3><h4 id="问题-多步骤任务里-LLM-容易迷失"><a href="#问题-多步骤任务里-LLM-容易迷失" class="headerlink" title="问题:多步骤任务里 LLM 容易迷失"></a>问题:多步骤任务里 LLM 容易迷失</h4><p>v3 的 Agent 面对复杂任务时,往往走几步就忘了目标,或者反复做同样的事。根本原因是:<strong>LLM 没有跨轮次的持久记忆</strong>,每轮对话只能靠 context 里的历史消息推断”我做到哪了”——历史越长越贵,越短越容易失忆。</p><p>v4 的思路:引入 todo 工具把任务状态外化到结构化列表,再通过 System Prompt 和提醒注入告诉 LLM 如何使用它。</p><h4 id="1-Plan-Mode-的起点-System-Prompt-注入-main-go"><a href="#1-Plan-Mode-的起点-System-Prompt-注入-main-go" class="headerlink" title="1. Plan Mode 的起点:System Prompt 注入 (main.go)"></a>1. Plan Mode 的起点:System Prompt 注入 (main.go)</h4><p>v3 的 <code>plan_mode</code> 只是权限层的拦截——写操作会被拒绝,但 LLM 自己不知道它处于”规划模式”,只会看到一堆权限错误然后困惑地重试。v4 补上这个缺口:<strong>启动时根据当前模式动态构建 System Prompt</strong>,把规划流程写进 LLM 的行为指令。权限拦截是”墙”,Prompt 是”路牌”——告诉 LLM 应该怎么走,而不只是哪里不能去。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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 class="comment">// main.go — 根据模式动态构建 system prompt</span></span><br><span class="line">basePrompt := <span class="string">`你是一个助手,可以使用工具:calculator、read_file、write_file、edit_file、bash、todo。</span></span><br><span class="line"><span class="string">重要:对于复杂任务,使用 todo 工具进行任务分解和进度跟踪:</span></span><br><span class="line"><span class="string">1. 收到复杂任务时,先用 todo add 添加子任务</span></span><br><span class="line"><span class="string">2. 开始任务前,用 todo start 标记</span></span><br><span class="line"><span class="string">3. 完成任务后,用 todo complete 标记`</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> systemPrompt <span class="type">string</span></span><br><span class="line"><span class="keyword">if</span> mode == ModePlan &#123;</span><br><span class="line">systemPrompt = basePrompt + <span class="string">`</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">特别注意 - Plan Mode(计划模式):</span></span><br><span class="line"><span class="string">Plan mode 已激活。你必须遵循以下三阶段流程:</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">重要约束:只能使用 calculator、read_file、todo</span></span><br><span class="line"><span class="string">禁止使用:write_file、edit_file、bash</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Phase 1 - 理解需求:使用 read_file 阅读相关代码,有疑问直接提问,不要猜测</span></span><br><span class="line"><span class="string">Phase 2 - 任务分解:用 todo add 添加子任务,要求具体明确、原子化、可验证</span></span><br><span class="line"><span class="string">  ✅ &quot;在 snake.html 中创建 canvas 元素,设置 id=&#x27;gameCanvas&#x27;,宽 800px 高 600px&quot;</span></span><br><span class="line"><span class="string">  ❌ &quot;创建游戏界面&quot;(太笼统)</span></span><br><span class="line"><span class="string">Phase 3 - 向用户确认:说明任务数和思路,告知使用 /execute 执行,等待确认`</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">systemPrompt = basePrompt</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">messages := []Message&#123;</span><br><span class="line">&#123;Role: <span class="string">&quot;system&quot;</span>, Content: systemPrompt&#125;,  <span class="comment">// ⭐ 运行时注入</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2-为什么需要-todo-工具"><a href="#2-为什么需要-todo-工具" class="headerlink" title="2. 为什么需要 todo 工具?"></a>2. 为什么需要 todo 工具?</h4><p>Prompt 告诉 LLM”先制定计划”,但有一个根本问题:<strong>LLM 没有跨轮次的持久记忆</strong>。每次调用都只能看到 messages 里的内容——如果计划只存在于某条 assistant 消息的文字里,随着对话推进它会被”淹没”,LLM 很容易遗忘或跳过还没完成的任务。</p><p>解决方案是把”记住任务”这件事变成一个<strong>工具调用行为</strong>,而不是依赖 LLM 的上下文注意力。LLM 主动调用 <code>todo add</code> 把任务写入外部状态,调用 <code>todo start/complete</code> 更新进度——状态由 Go 程序维护,不会消失,还可以在需要时重新注入回 messages。</p><h4 id="3-Todo-管理器-todo-go"><a href="#3-Todo-管理器-todo-go" class="headerlink" title="3. Todo 管理器 (todo.go)"></a>3. Todo 管理器 (todo.go)</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">type</span> TodoManager <span class="keyword">struct</span> &#123;</span><br><span class="line">Items               []TodoItem</span><br><span class="line">NextID              <span class="type">int</span></span><br><span class="line">RoundsSinceLastTodo <span class="type">int</span>  <span class="comment">// 追踪多少轮未用 todo 工具</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tm *TodoManager)</span></span> AddItem(description <span class="type">string</span>) TodoItem &#123;</span><br><span class="line">item := TodoItem&#123;ID: tm.NextID, Description: description, Status: <span class="string">&quot;pending&quot;</span>&#125;</span><br><span class="line">tm.Items = <span class="built_in">append</span>(tm.Items, item)</span><br><span class="line">tm.NextID++</span><br><span class="line">tm.RoundsSinceLastTodo = <span class="number">0</span>  <span class="comment">// 重置计数</span></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="comment">// pending → doing → done 三阶段流转</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tm *TodoManager)</span></span> StartItem(id <span class="type">int</span>) <span class="type">error</span> &#123; ... &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tm *TodoManager)</span></span> CompleteItem(id <span class="type">int</span>) <span class="type">error</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>状态完全在 Go 侧维护,LLM 通过工具调用读写,不依赖上下文记忆。</p><h4 id="4-提醒注入到-Agent-Loop-main-go-⭐-核心变化"><a href="#4-提醒注入到-Agent-Loop-main-go-⭐-核心变化" class="headerlink" title="4. 提醒注入到 Agent Loop (main.go) ⭐ 核心变化"></a>4. 提醒注入到 Agent Loop (main.go) ⭐ 核心变化</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">// v4 是唯一真正修改 Agent Loop 的版本!</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, tc := <span class="keyword">range</span> response.ToolCalls &#123;</span><br><span class="line">result := ExecuteTool(tc.Function.Name, tc.Function.Arguments)</span><br><span class="line"></span><br><span class="line"><span class="comment">// ⭐ 如果 3 轮未使用 todo 工具,把待办状态注入到工具结果里</span></span><br><span class="line"><span class="comment">// LLM 下一轮会读到它,从而想起来更新任务进度</span></span><br><span class="line"><span class="keyword">if</span> globalTodoManager.ShouldRemind() &amp;&amp; tc.Function.Name != <span class="string">&quot;todo&quot;</span> &#123;</span><br><span class="line">result += globalTodoManager.GetReminder()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">*messages = <span class="built_in">append</span>(*messages, Message&#123;Role: <span class="string">&quot;tool&quot;</span>, Content: result, ...&#125;)</span><br><span class="line">&#125;</span><br><span class="line">globalTodoManager.IncrementRound()</span><br></pre></td></tr></table></figure><p>注入的内容是什么?就是下面这段拼出来的纯文本,追加在工具结果末尾,LLM 读 tool message 时会自然看到它:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">func</span> <span class="params">(tm *TodoManager)</span></span> ShouldRemind() <span class="type">bool</span> &#123;</span><br><span class="line"><span class="keyword">return</span> tm.RoundsSinceLastTodo &gt;= <span class="number">3</span> &amp;&amp; <span class="built_in">len</span>(tm.Items) &gt; <span class="number">0</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tm *TodoManager)</span></span> GetReminder() <span class="type">string</span> &#123;</span><br><span class="line">hasPending, hasDoing := <span class="literal">false</span>, <span class="literal">false</span></span><br><span class="line"><span class="keyword">for</span> _, item := <span class="keyword">range</span> tm.Items &#123;</span><br><span class="line"><span class="keyword">if</span> item.Status == <span class="string">&quot;pending&quot;</span> &#123; hasPending = <span class="literal">true</span> &#125;</span><br><span class="line"><span class="keyword">if</span> item.Status == <span class="string">&quot;doing&quot;</span>   &#123; hasDoing = <span class="literal">true</span> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> sb strings.Builder</span><br><span class="line">sb.WriteString(<span class="string">&quot;\n🔔 待办提醒:\n&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> hasDoing   &#123; sb.WriteString(<span class="string">&quot;  有任务正在进行中,记得更新状态\n&quot;</span>) &#125;</span><br><span class="line"><span class="keyword">if</span> hasPending &#123; sb.WriteString(<span class="string">&quot;  有待处理的任务,记得开始执行\n&quot;</span>) &#125;</span><br><span class="line">sb.WriteString(<span class="string">&quot;  使用 todo 工具查看完整列表\n&quot;</span>)</span><br><span class="line"><span class="keyword">return</span> sb.String()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>提醒注入是闭环的最后一环:Prompt 建立初始行为 → todo 工具外化状态 → 提醒注入在 LLM 走神时把状态送回它的视野。三者缺一不可——只有 Prompt 容易遗忘,只有 todo 工具没有提醒 LLM 会忽视,只有提醒没有工具则状态无处存放。</p><hr><h3 id="v5-Skill-系统-1341行"><a href="#v5-Skill-系统-1341行" class="headerlink" title="v5: Skill 系统 (1341行)"></a>v5: Skill 系统 (1341行)</h3><h4 id="问题-System-Prompt-的扩展瓶颈"><a href="#问题-System-Prompt-的扩展瓶颈" class="headerlink" title="问题:System Prompt 的扩展瓶颈"></a>问题:System Prompt 的扩展瓶颈</h4><p>随着 Agent 能力扩展,一个自然的冲动是把更多知识塞进 System Prompt——“如何处理 PDF”、”代码审查标准”、”数据库操作规范”。但这条路走不远:每次对话都要发送全部内容,大量 token 花在用不到的知识上;更根本的是,context window 有硬上限,知识越多越快撞墙。</p><p>v5 的思路:把专项能力写成独立的 <code>SKILL.md</code> 文件,<strong>让 LLM 自己决定什么时候加载什么技能</strong>,而不是由我们预判塞满。</p><h4 id="SKILL-md-格式"><a href="#SKILL-md-格式" class="headerlink" title="SKILL.md 格式"></a>SKILL.md 格式</h4><p>每个技能是一个目录下的 <code>SKILL.md</code>,YAML frontmatter 存元数据,分隔符后是完整内容:</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: code-review</span><br><span class="line"><span class="section">description: Review code quality, style, and best practices</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># Code Review Skill</span></span><br><span class="line"></span><br><span class="line"><span class="section">### Capabilities</span></span><br><span class="line"><span class="bullet">1.</span> Analyze code for bugs and logic errors</span><br><span class="line"><span class="bullet">2.</span> Review naming conventions and readability</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>格式刻意简单:frontmatter 只需 <code>name</code> 和 <code>description</code> 两个字段,正文是普通 Markdown。</p><h4 id="第一步-启动时只扫描元数据"><a href="#第一步-启动时只扫描元数据" class="headerlink" title="第一步:启动时只扫描元数据"></a>第一步:启动时只扫描元数据</h4><p>Agent 启动时,<code>scanSkills()</code> 遍历 <code>skills/</code> 目录,对每个 <code>SKILL.md</code> 只读 frontmatter,<strong>不读正文</strong>:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="params">(sm *SkillManager)</span></span> scanSkills() &#123;</span><br><span class="line">entries, _ := os.ReadDir(sm.SkillsDir)</span><br><span class="line"><span class="keyword">for</span> _, entry := <span class="keyword">range</span> entries &#123;</span><br><span class="line"><span class="keyword">if</span> !entry.IsDir() &#123;</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line">skillPath := filepath.Join(sm.SkillsDir, entry.Name(), <span class="string">&quot;SKILL.md&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> _, err := os.Stat(skillPath); err == <span class="literal">nil</span> &#123;</span><br><span class="line">metadata, err := sm.parseSkillMetadata(skillPath)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line">sm.AvailableSkills[entry.Name()] = &amp;Skill&#123;Metadata: metadata, Loaded: <span class="literal">false</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"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(sm *SkillManager)</span></span> parseSkillMetadata(skillPath <span class="type">string</span>) (SkillMetadata, <span class="type">error</span>) &#123;</span><br><span class="line">content, _ := os.ReadFile(skillPath)</span><br><span class="line">text := <span class="type">string</span>(content)</span><br><span class="line"><span class="comment">// text[4:] 跳过开头的 &quot;---\n&quot;</span></span><br><span class="line">parts := strings.SplitN(text[<span class="number">4</span>:], <span class="string">&quot;\n---\n&quot;</span>, <span class="number">2</span>)</span><br><span class="line"><span class="comment">// parts[0] 是 YAML frontmatter,parts[1] 是正文(不读)</span></span><br><span class="line"><span class="comment">// 手动解析 name: / description: 两行</span></span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>parseSkillMetadata</code> 解析到 <code>\n---\n</code> 就停下,正文根本不进内存。<code>AvailableSkills</code> 里存的只是名字和一句描述。</p><h4 id="第二步-把元数据摘要注入-System-Prompt"><a href="#第二步-把元数据摘要注入-System-Prompt" class="headerlink" title="第二步:把元数据摘要注入 System Prompt"></a>第二步:把元数据摘要注入 System Prompt</h4><p>LLM 需要知道”有哪些技能可以用”,但不需要知道细节。<code>GetAvailableSkillsSummary()</code> 把元数据格式化成几行文字,拼进 system prompt:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">// main.go</span></span><br><span class="line">basePrompt := <span class="string">`你是一个助手,可以使用工具:...</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">`</span> + globalSkillManager.GetAvailableSkillsSummary() + <span class="string">`</span></span><br><span class="line"><span class="string">...`</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// GetAvailableSkillsSummary() 输出示例:</span></span><br><span class="line"><span class="comment">// 可用技能:</span></span><br><span class="line"><span class="comment">//   - pdf: Process and extract information from PDF files [未加载]</span></span><br><span class="line"><span class="comment">//   - code-review: Review code quality, style, and best practices [未加载]</span></span><br><span class="line"><span class="comment">// 使用 load_skill 工具加载完整技能内容</span></span><br></pre></td></tr></table></figure><p>每次对话多出的 token 消耗只有这几行摘要,而不是所有技能的完整文档。</p><h4 id="第三步-LLM-按需调用-load-skill"><a href="#第三步-LLM-按需调用-load-skill" class="headerlink" title="第三步:LLM 按需调用 load_skill"></a>第三步:LLM 按需调用 load_skill</h4><p>当 LLM 判断需要某个技能时,调用 <code>load_skill</code> 工具。<code>LoadSkill()</code> 这时才读完整文件,分割出正文,返回内容:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="params">(sm *SkillManager)</span></span> LoadSkill(skillName <span class="type">string</span>) (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line">skill, exists := sm.AvailableSkills[skillName]</span><br><span class="line"><span class="keyword">if</span> !exists &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span>, fmt.Errorf(<span class="string">&quot;技能不存在: %s&quot;</span>, skillName)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> skill.Loaded &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;技能 &#x27;%s&#x27; 已经加载&quot;</span>, skillName), <span class="literal">nil</span>  <span class="comment">// 幂等</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">content, _ := os.ReadFile(filepath.Join(sm.SkillsDir, skillName, <span class="string">&quot;SKILL.md&quot;</span>))</span><br><span class="line">text := <span class="type">string</span>(content)</span><br><span class="line">parts := strings.SplitN(text[<span class="number">4</span>:], <span class="string">&quot;\n---\n&quot;</span>, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">skill.Content = parts[<span class="number">1</span>]  <span class="comment">// 只取正文,去掉 frontmatter</span></span><br><span class="line">skill.Loaded = <span class="literal">true</span></span><br><span class="line">sm.LoadedSkills[skillName] = skill</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;✅ 技能 &#x27;%s&#x27; 已加载\n\n%s&quot;</span>, skillName, skill.Content), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>返回值直接作为工具结果追加进 <code>messages</code>,技能内容就此进入对话上下文——LLM 下一轮就能用上。注意幂等检查:重复加载直接返回,不会重复注入。</p><h4 id="Agent-Loop-完全透明"><a href="#Agent-Loop-完全透明" class="headerlink" title="Agent Loop 完全透明"></a>Agent Loop 完全透明</h4><p>v5 对 Agent Loop 本身没有任何改动。<code>load_skill</code> 只是 <code>ExecuteTool</code> 里新增的一个 case:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><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">// tools.go - ExecuteTool 新增一个分支,仅此而已</span></span><br><span class="line"><span class="keyword">if</span> toolName == <span class="string">&quot;load_skill&quot;</span> &#123;</span><br><span class="line"><span class="keyword">return</span> loadSkillTool(arguments)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Loop 不感知”技能”的存在,只是照常把工具返回值追加进 messages。技能内容经由这条普通路径自然流入上下文,不需要任何特殊协议。新增技能也不需要改任何代码——放一个目录进去,重启后自动被扫描到。</p><hr><h3 id="v6-上下文压缩-1590行"><a href="#v6-上下文压缩-1590行" class="headerlink" title="v6: 上下文压缩 (1590行)"></a>v6: 上下文压缩 (1590行)</h3><h4 id="问题-对话越长-成本越高-直到崩溃"><a href="#问题-对话越长-成本越高-直到崩溃" class="headerlink" title="问题:对话越长,成本越高,直到崩溃"></a>问题:对话越长,成本越高,直到崩溃</h4><p>LLM 的 API 调用按 token 计费,而且每次调用都要发送<strong>完整的历史消息</strong>。随着对话轮次增加,token 消耗线性增长——一个长任务里 bash 命令的输出、文件内容的读取结果会迅速撑大上下文。更根本的问题是 context window 有硬上限,超出直接报错,对话无法继续。</p><p>v6 引入 <code>ContextManager</code> 统一管理所有消息的读写。Agent Loop 里所有 <code>*messages</code> 的直接操作,全部改成走 <code>globalContextManager.AddMessage()</code> 和 <code>globalContextManager.GetMessages()</code>,压缩逻辑在这个统一出入口里自然嵌入。</p><h4 id="策略一-CompressMicro-每轮自动、零成本"><a href="#策略一-CompressMicro-每轮自动、零成本" class="headerlink" title="策略一:CompressMicro(每轮自动、零成本)"></a>策略一:CompressMicro(每轮自动、零成本)</h4><p><code>GetMessages()</code> 是 messages 流向 LLM 的唯一出口,每次调用都先触发 <code>CompressMicro</code>。它扫描所有 role 为 <code>tool</code> 的消息,只保留最近 <code>KeepRecentRounds</code> 条,把更早的替换成占位符,零 API 调用静默执行:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="params">(cm *ContextManager)</span></span> GetMessages() []Message &#123;</span><br><span class="line">cm.CompressMicro()</span><br><span class="line"><span class="keyword">return</span> cm.Messages</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(cm *ContextManager)</span></span> CompressMicro() &#123;</span><br><span class="line">toolIndices := []<span class="type">int</span>&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> i := <span class="built_in">len</span>(cm.Messages) - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i-- &#123;</span><br><span class="line"><span class="keyword">if</span> cm.Messages[i].Role == <span class="string">&quot;tool&quot;</span> &#123;</span><br><span class="line">toolIndices = <span class="built_in">append</span>([]<span class="type">int</span>&#123;i&#125;, toolIndices...)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(toolIndices) &lt;= cm.KeepRecentRounds &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> _, idx := <span class="keyword">range</span> toolIndices[:<span class="built_in">len</span>(toolIndices)-cm.KeepRecentRounds] &#123;</span><br><span class="line">msg := &amp;cm.Messages[idx]</span><br><span class="line"><span class="keyword">if</span> !strings.HasPrefix(msg.Content, <span class="string">&quot;[已压缩的工具结果]&quot;</span>) &#123;</span><br><span class="line">msg.Content = <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>代价是彻底丢失历史工具输出的细节——对 LLM 而言,那些工具调用”发生过”但结果已不可见。Micro 只截断工具结果,整体消息数量不减少,对话仍会持续增长。</p><h4 id="策略二-CompressAuto-超限触发、保留语义"><a href="#策略二-CompressAuto-超限触发、保留语义" class="headerlink" title="策略二:CompressAuto(超限触发、保留语义)"></a>策略二:CompressAuto(超限触发、保留语义)</h4><p>当 <code>NeedsCompression()</code> 检测到 token 估算超过阈值(<code>MaxTokens × CompressionRatio</code>,默认 8192 × 0.9),Agent Loop 在每次 Chat 前自动触发;用户也可随时 <code>/compact</code> 手动触发。核心是把待压缩的历史段用一次独立的 LLM 调用浓缩成摘要,再替换原始消息:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="params">(cm *ContextManager)</span></span> CompressAuto(ctx context.Context, client *GLMClient) <span class="type">error</span> &#123;</span><br><span class="line">compressEndIdx := <span class="built_in">len</span>(cm.Messages) - cm.KeepRecentRounds*<span class="number">2</span></span><br><span class="line">toSummarize := cm.Messages[systemMsgIdx+<span class="number">1</span> : compressEndIdx]</span><br><span class="line"></span><br><span class="line">response, _ := client.Chat(ctx, []Message&#123;</span><br><span class="line">&#123;Role: <span class="string">&quot;user&quot;</span>, Content: buildSummaryPrompt(toSummarize)&#125;,</span><br><span class="line">&#125;, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line">cm.Messages = []Message&#123;</span><br><span class="line">cm.Messages[systemMsgIdx],                                       <span class="comment">// 保留 system prompt</span></span><br><span class="line">&#123;Role: <span class="string">&quot;assistant&quot;</span>, Content: <span class="string">&quot;📋 **对话摘要**\n\n&quot;</span> + response.Content&#125;, <span class="comment">// 摘要替换历史</span></span><br><span class="line">cm.Messages[compressEndIdx:]...,                                 <span class="comment">// 保留最近 N 轮</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>摘要的质量取决于提示词。<code>buildSummaryPrompt</code> 把历史消息序列化后,要求 LLM 输出结构化的两段:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">sb.WriteString(<span class="string">&quot;输出格式:\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;&lt;analysis&gt;\n对当前状态的深入分析(2-3 句)\n&lt;/analysis&gt;\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;&lt;summary&gt;\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;1. 初始用户请求和高层目标\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;2. 已完成的关键步骤\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;3. 当前工作状态\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;4. 待完成的任务(如有)\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;5. 重要的技术决策和原因\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;6. 相关文件路径\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;7. 遇到的错误和解决方案(如有)\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;8. 下一步行动建议\n&quot;</span>)</span><br><span class="line">sb.WriteString(<span class="string">&quot;&lt;/summary&gt;\n&quot;</span>)</span><br></pre></td></tr></table></figure><p>结构化输出让 LLM 在后续轮次能快速定位上下文,而不是从一段流水叙述里自己提取。这也是为什么摘要以 <code>assistant</code> 角色注入——对 LLM 而言,这就是它”自己之前写下的记录”,而不是外部注入的元数据。</p><hr><h3 id="v7-vN-还能走多远"><a href="#v7-vN-还能走多远" class="headerlink" title="v7~vN:还能走多远?"></a>v7~vN:还能走多远?</h3><p>我们用 ~1600 行 Go 代码,从零实现了一个具备工具调用、权限控制、任务规划、技能加载和上下文压缩的 Agent。但这只是起点。现实中的 Agent 系统还有很多值得继续演进的方向:</p><h4 id="待完善的能力"><a href="#待完善的能力" class="headerlink" title="待完善的能力"></a>待完善的能力</h4><table><thead><tr><th>方向</th><th>描述</th></tr></thead><tbody><tr><td><strong>子 Agent</strong></td><td>主 Agent 把子任务委托给独立 Agent 并发执行,收摘要后继续</td></tr><tr><td><strong>多 Session</strong></td><td>同时维护多个独立对话上下文,支持并行任务或多用户场景</td></tr><tr><td><strong>长期 Memory</strong></td><td>跨 Session 持久化关键知识,下次对话不从零开始</td></tr><tr><td><strong>流式输出</strong></td><td>Streaming API 逐 token 返回,用户体验从”等待”变成”看着它思考”</td></tr></tbody></table><h4 id="但核心思想其实很简单"><a href="#但核心思想其实很简单" class="headerlink" title="但核心思想其实很简单"></a>但核心思想其实很简单</h4><p>回头看整个演进,Agent 的本质始终是同一个 for 循环:<strong>调用 LLM → 执行工具 → 把结果塞回上下文 → 重复</strong>。所有复杂性都是在这个循环的外围加约束、加状态、加策略。</p><p>以子 Agent 为例——听起来很复杂,实现思路其实直接:把”启动子 Agent”做成一个普通工具,主 Agent 通过 Function Call 调用它;工具实现里用 Go 的 goroutine 跑一个完整的 Agent Loop,完成后把执行摘要作为 tool result 返回;主 Agent 拿到摘要继续规划下一步。整个机制复用了我们已有的工具系统、上下文压缩和 todo 状态管理,没有引入任何新的架构概念。</p><p>多 Session、Memory 也是同理:Session 是上下文的隔离单元,Memory 是跨 Session 的持久化 Skill,它们都能用已有的抽象自然延伸出来。</p><h4 id="真正的难点-Harness"><a href="#真正的难点-Harness" class="headerlink" title="真正的难点:Harness"></a>真正的难点:Harness</h4><p>把 Agent Loop 写出来只需要几十行。让 Agent 真正<strong>把工作做好</strong>,才是难的部分——这就是 <strong>Harness</strong> 的概念:围绕 Agent 构建的一整套”脚手架”,决定了 LLM 能看到什么、能做什么、做错了怎么纠正。</p><p>我们的实现其实已经包含了 Harness 的雏形:</p><table><thead><tr><th>我们实现的</th><th>对应的 Harness 思想</th></tr></thead><tbody><tr><td>动态 System Prompt + Plan Mode</td><td>行为约束:告诉 LLM 在当前上下文里应该怎么行动</td></tr><tr><td>Todo 工具 + GetReminder 注入</td><td>状态跟踪:把任务进度持续喂给 LLM,防止它”忘事”</td></tr><tr><td>Skill 按需加载</td><td>知识路由:只在需要时注入相关上下文,避免噪音</td></tr><tr><td>权限系统 + ask_on_write</td><td>人机协作:在关键决策点把控制权交还给人</td></tr><tr><td>ContextManager + 压缩策略</td><td>上下文管理:控制 LLM 每次”看到”的信息密度</td></tr></tbody></table><p>但生产级的 Harness 远不止这些——评估与回滚(LLM 做错了怎么恢复)、可观测性(每一步的 token 消耗、工具调用链路)、沙箱隔离(bash 执行的安全边界)、重试与降级……每一项都是独立的工程课题。</p><p>从这个角度看,Agent 本身的代码是最简单的部分。<strong>Harness 才是让 Agent 真正可用的工程核心。</strong></p><p>这也是为什么理解 Agent 的最好方式,是自己从零写一遍——当你亲手把 for 循环、工具路由、权限检查、提示词注入这些拼在一起,”Agent”就从一个模糊的概念变成了一组具体的设计决策。</p>]]></content>
    
    
    <summary type="html">&lt;h3 id=&quot;为什么要从零写一个-Agent？&quot;&gt;&lt;a href=&quot;#为什么要从零写一个-Agent？&quot; class=&quot;headerlink&quot; title=&quot;为什么要从零写一个 Agent？&quot;&gt;&lt;/a&gt;为什么要从零写一个 Agent？&lt;/h3&gt;&lt;p&gt;AI 时代信息爆炸——MCP、RAG、Multi-Agent、Agentic Workflow……新概念一个接一个,每隔几周就有新的框架冒出来。很容易陷进去,感觉 Agent 是一个高深莫测的东西,离自己很远。&lt;/p&gt;
&lt;p&gt;但如果你亲手写过一遍(在 AI 帮助下,只需要一个上午),就会发现:&lt;strong&gt;Agent 本身的代码比大佬们写的控制器和转发面简单多了。&lt;/strong&gt; 核心逻辑就是一个 for 循环,加上几个工具调用。&lt;/p&gt;
&lt;p&gt;这个 Workshop 的目的就是&lt;strong&gt;祛魅&lt;/strong&gt;——把地基翻出来看清楚。从一个 223 行的最小 Agent 出发,一步步演进到具备权限控制、任务规划、技能加载、上下文压缩的完整系统。每一版只解决一个问题,每一行代码都有来处。&lt;/p&gt;
&lt;p&gt;看完之后,那些概念还会存在,但它们背后的地基你已经摸清楚了。雾里看花,变成近在眼前。&lt;/p&gt;</summary>
    
    
    
    
    <category term="AI" scheme="https://www.iloft.xyz/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>我的家庭 NAS 方案（当前进化至 EPYC 7D12 + TrueNAS）</title>
    <link href="https://www.iloft.xyz/archives/new-nas.html"/>
    <id>https://www.iloft.xyz/archives/new-nas.html</id>
    <published>2025-06-24T11:00:00.000Z</published>
    <updated>2025-10-30T11:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>PS：2025 年 10 月 30 日，问题已定位，BMC 会在主板的所有网口周期发送数据包（探测包？），如果 2.5Gbps 网口（非管理口）未接线，会导致其内核持续内存泄露，直至 OOM。解决办法是在 BMC 里禁用该网口。</strong></p></blockquote><blockquote><p><strong>PS：截止 2025 年 10 月 8 日，H12D BMC 模块存在内存泄露的 BUG，导致每 14～15 天就会挂死（IPMI 相关进程都被杀死了），管理界面、IPMI、风扇控制全部失灵，只能拔电源冷重启，暂不建议购买。</strong></p></blockquote><p>用了很久 NAS 了，简单记录一下 NAS 方案演进。</p><span id="more"></span><h2 id="初探-NAS（小而美）"><a href="#初探-NAS（小而美）" class="headerlink" title="初探 NAS（小而美）"></a>初探 NAS（小而美）</h2><p>&emsp;&emsp;刚玩 NAS 的时候，对 NAS 的理解就是稳定 7 x 24 开机的网络存储平台。硬件方面采用 DQ77KB + i5 3570S + 蜗牛星际方案（<a href="https://www.iloft.xyz/archives/dq77kb-nas.html">DQ77KB + 矿渣机箱翻车记</a>）。软件平台直接使用 Windows Server 开启 SMB，同时用 QBittorrent RSS 自动下载，配合 Jellyfin 进行刮削。</p><p><img src="/images/NAS-Case.jpg" alt="NAS机箱"></p><p>&emsp;&emsp;整体使用起来还是很舒服的，可以称作小而美。DQ77KB 自带 AMT（IPMI）， 配置完成后可以扔在角落吃灰，也奠定了后续演进一定要有带外管理便于远程摸鱼救砖的方针。</p><h2 id="转向服务器平台（换汤不换药）"><a href="#转向服务器平台（换汤不换药）" class="headerlink" title="转向服务器平台（换汤不换药）"></a>转向服务器平台（换汤不换药）</h2><p>&emsp;&emsp;随着库存数据越来越多，i5 3570S 性能瓶颈逐渐出现，最主要的是编解码性能，使用 Jellyfin 观看高清视频时拖拽会卡顿。同时因为矿盘逐渐增加，盘位不够，因此换成了 R730XD 12 盘位（E5 2680v4 x 2）。</p><p><img src="/images/r730xd.jpg" alt="r730xd"></p><p>&emsp;&emsp;服务器配齐了前后 12 + 2 盘，H730 阵列卡，服务器内部看起来非常舒心。</p><p><img src="/images/r730xd_1.jpg" alt="r730xd内部"></p><p>&emsp;&emsp;为什么说是换汤不换药呢，是因为这个阶段软件并没有发生变化，只是单纯的将系统迁移到新平台，解决了 Jellyfin 观看视频卡顿痛点。尴尬的是后面买了 AppleTV 使用 infuse 播放直接把转码问题给消灭了。总得来说，最初我对 R730XD 还是很满意的。其他用户反馈的噪音大的问题，在 4~6 盘情况下，通过更换静音风扇与配合 IPMI 调速到 20%，温度和噪音都是可接受的，隔一扇门基本无感。</p><h2 id="失败的硬阵列尝试（适合自己的才是最好的）"><a href="#失败的硬阵列尝试（适合自己的才是最好的）" class="headerlink" title="失败的硬阵列尝试（适合自己的才是最好的）"></a>失败的硬阵列尝试（适合自己的才是最好的）</h2><p>&emsp;&emsp;就这样度过了美好的 3 年，在 R730XD 逐步加盘的过程，问题出现了，每次迁移数据比较麻烦，需要硬盘一次次的拷贝，并且需要对盘符和路径做复杂处理才能让各种软件不感知变化。我决定一次性购入 8 块 16T MG08 通过 H730 组建硬阵列，毕其功于一役。</p><p><img src="/images/8mg08.jpg" alt="MG08"></p><p>&emsp;&emsp;此时的我还是很信任企业级的方案，选择硬阵列也是考虑到硬阵列可以屏蔽底层细节，向上层 Windows 直接虚拟出硬盘，不需要操作系统和软件层面做任何改动，80T 左右的可用空间也足够我较长时间使用。而如果使用在软阵列里性能相对较好比较靠谱的 TrueNAS 方案，就需要将物理机上的 Windows 虚拟化，作为一个懒人自然不予考虑。</p><p>&emsp;&emsp;理想是美好的，现实是骨感的。当插上所有硬盘，开启阵列的创建之后，痛苦开始了。由于硬阵列是基于块和条带的，因此创建阵列完毕一定需要初始化（填 0 和计算奇偶校验）。此时硬盘会持续写入，简单计算可知 16T &#x2F; 200MB&#x2F;s &#x3D; 22 小时。由于机架服务器的设计，进风几乎都来自前置硬盘位，因此插满硬盘后，会严重影响进风。这高速写入的 22 小时，风扇需要 50%+ 才能压制住机器，即使如此硬盘也保持在了 50℃+，吸尾气的 CPU 分分钟突破 90℃，此时噪音和热量根本无法忍受！！！</p><p>&emsp;&emsp;在组阵列，睡前关机，睡醒开机继续组阵列后，以及多次修改参数，尝试提升速度重新开始后，我终于崩溃了。将服务器挂到闲鱼上以 1200 的价格卖了出去（购入价 2000）。这里用我的血泪告诫大家，没有机柜和独立的设备间还是不要玩机架服务器了~当然我相信，机架服务器永远是 NAS 的最终的归宿，希望有一天能够把公司的星星海搬回家。</p><h2 id="取其精华去其糟粕"><a href="#取其精华去其糟粕" class="headerlink" title="取其精华去其糟粕"></a>取其精华去其糟粕</h2><p>&emsp;&emsp;痛定思痛之后，明白了我的需求从来没有变过，那就是“在角落吃灰”，它在这个物理世界需要足够安静和稳定。基于这个目的，就开始了硬件平台的选择。正巧最近 EPYC 7D12 大船到岸只需要 240，32 核 64 线程（1.10GHz ~ 3.00GHz），TDP 80W 简直就是 NAS 佬的梦中情 U。ZEN2 架构性能毋庸置疑，同时功耗发热足够低，无需像使用 E5 双路时修改功耗策略。</p><p>&emsp;&emsp;确定了 CPU 就要配套主板了，板 U 守恒定律名不虚传，二手的超微 H12SSL 以及华硕技嘉的主板价格都突破了 3000， 相较年初翻了一翻，说实话不是很好的选择了，因此就选择了寨板大厂华南金牌 H12D-8D，带有 BMC 模组可以满足基本的使用，同时有 3 个 8643 接口以及 4 个 SATA，满足了 16 盘的需求，不需要额外购买阵列卡。</p><p>&emsp;&emsp;剩下的最重要的就是选择机箱了，在乔思伯 N5 和梵隆 12 盘位中纠结了一段时间，考虑到无论是全高还是半高的梵隆散热器都限高，暴力风扇的阴影还笼罩在心头，因此还是选择了可以安装 12cm&#x2F;14cm 风扇的乔思伯。需要注意的是两点，一是乔思伯的背板是 SATA 的，需要购买 8643 转 SATA。二是乔思伯自带的三把风扇是 3Pin 非 PWM 调速，因此噪音会比较大，建议换 PWM 调速风扇。</p><p>&emsp;&emsp;购买&#x2F;装机清单如下：</p><ul><li>CPU：EPYC 7D12 ¥258.00</li><li>主板：华南金牌 H12D-8D BMC ¥2307.99</li><li>内存：三星 DDR4 64Gx4 ¥1467.15</li><li>散热器：利民 TA120EX TR4 SP3 ¥208.57</li><li>机箱风扇：利民 TL-G12Bx3 ¥68.36</li><li>电源：长城 N8 850W 金牌 ¥565.03</li><li>机箱：乔思伯 N5 ¥1140.40</li><li>固态 1（SLOG）：Intel 傲腾 900P 280G ¥534.00</li><li>固态 2（PVE）：Samsung PM981a 512GB ¥0.00 (存货)</li><li>固态 3 (缓存)：Intel S3520 800GB ¥0.00 (存货)</li><li><strong>总计：¥6549.50</strong></li></ul><p>&emsp;&emsp;安装过程是枯燥的，也没有太多值得说到的，箱子做工对于这个价位中规中矩，建议从下往上安装，把硬盘背板先装好，我的这个长城 N8 电源的 SATA 供电在接完背板之后就无法送到上层安装 2.5 寸 SSD 了，因此 S3520 扔在了下层。同时由于 7D12 内存缺通道（4 通道）所以需要多尝试尝试，最终我的这块 U 需要将内存安装在最外侧的插槽才能识别全部 256GB 内存。</p><p><img src="/images/board.jpg" alt="board"></p><p>&emsp;&emsp;这里可以发现，由于主板 CPU 插槽设计的朝向，CPU 风扇（侧吹）和机箱风扇（后吹）风向不是一致的，可能会影响风道，如果用服务器 2U 暴力风扇应该是一致的。个人因为比较懒，且 7D12 功耗不高，就不愿意再将机箱风扇调整到右侧了。同时需要提醒的是华南 H12D 的主板风扇只有两个风扇接口支持温控调速（我图上 CPU 和上层风扇所接的右上角接口）。用起来会发觉风扇很吵，调了很久主板 PWM 调速不见效果，仔细研究后才发现下层的风扇接的主板接口是 ipmi 手动调速的，并不支持温控（服务器好的不学，净学些坏的）。不过手动调整转速到 1000 RPM后，即使机箱放在床边也几乎无感了。</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"># 调速到 66%（0x42%），利民风扇全速 1500RPM</span></span><br><span class="line">ipmitool -I lanplus -H 172.17.0.199 -U admin -P admin raw 0x0e 0x65 0 0x42</span><br></pre></td></tr></table></figure><p>&emsp;&emsp;这是安装完成的效果，机箱很大可能图片上看不出来，橡胶把手凑合能用，真想拔出硬盘的时候会很担心拔断，比硬盘托架肯定差远了，不过确实是 12 盘位里比较好看的了：</p><p><img src="/images/n5-case1.jpg" alt="n5-case1"></p><p><img src="/images/n5-case2.jpg" alt="n5-case2"></p><p>&emsp;&emsp;忙完了硬件就是忙软件层面了，因为放弃了灵活性较差的硬阵列，所以这一代软件方案底层采用 PVE 虚拟化平台， 将之前物理硬盘安装的 Windows Server 进行虚拟化，同时运行 TrueNAS。并将全部 SATA 控制器以及 PCIE 的傲腾 900P 直通给 TrueNAS。</p><p><img src="/images/pve.png" alt="pve"></p><p>&emsp;&emsp;然后就是繁琐的软阵列创建和数据迁移工作，这里需要注意 TrueNAS 在 Scale 版本移除了 WEB 界面的数据导入以及内核里的 NTFS 支持，因此我是在 Core 版本将原始的 NTFS 硬盘数据完成迁移后再升级到 Scale 的。</p><p>&emsp;&emsp;总结一下，当前存储方案是，8 块 MG08 组成 RADIZ2，傲腾 900P 分出 32GB 分区作为阵列的 SLOG。RAIDZ2 使用 SMB 共享给 Window Server，实测 Jellyfin（不能运行在服务模式）和 QBittorrent 等软件都能正常运行。同时 S3520 创建 ZVOL 分区 iSCSI 挂载给 Windows Server 做下载缓存盘，减少阵列读写（夜间持续咯哒咯哒还是有点小烦）。</p><p><img src="/images/truenas.png" alt="truenas"></p><p><img src="/images/windows.png" alt="windows"></p><p>&emsp;&emsp;至此我的第三代 NAS 也完成了软硬件升级（PS：买的 Mellanox CX4 25Gbps 网卡还在路上，不过应该对 NAS 整体架构没有太大影响）~ </p><h2 id="小更新"><a href="#小更新" class="headerlink" title="小更新"></a>小更新</h2><p>&emsp;&emsp;CX4 网卡到手了，SR-IOV 直通 TrueNAS 和 Windows 相较 VirtIO 性能在网络传输性能上确实有大幅度提升，贴一下 Windows 远程读写 TrueNAS SMB 性能吧，性能够用。<br><img src="/images/cdm-nas-smb.png" alt="truenas"></p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;PS：2025 年 10 月 30 日，问题已定位，BMC 会在主板的所有网口周期发送数据包（探测包？），如果 2.5Gbps 网口（非管理口）未接线，会导致其内核持续内存泄露，直至 OOM。解决办法是在 BMC 里禁用该网口。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;PS：截止 2025 年 10 月 8 日，H12D BMC 模块存在内存泄露的 BUG，导致每 14～15 天就会挂死（IPMI 相关进程都被杀死了），管理界面、IPMI、风扇控制全部失灵，只能拔电源冷重启，暂不建议购买。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;用了很久 NAS 了，简单记录一下 NAS 方案演进。&lt;/p&gt;</summary>
    
    
    
    
    <category term="NAS" scheme="https://www.iloft.xyz/tags/NAS/"/>
    
  </entry>
  
  <entry>
    <title>VLAN 交换机单线复用</title>
    <link href="https://www.iloft.xyz/archives/my-network-vlan.html"/>
    <id>https://www.iloft.xyz/archives/my-network-vlan.html</id>
    <published>2024-12-21T15:00:00.000Z</published>
    <updated>2024-12-21T15:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>&emsp;&emsp;家里的入户的光猫在书房，书房到客厅的只有一条网线。因此诞生了单线复用的需求，拓扑如下：</p><span id="more"></span><figure class="highlight lasso"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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><br><span class="line">│         │      │         │               </span><br><span class="line">│  Modem  │      │   NAS   │               </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">     │     Switch     │                    </span><br><span class="line">┌────┴───┌────────┌───┴────┌────────┐      </span><br><span class="line">│  Port1 │  Port2 │  Port3 │  Port4 │<span class="params">...</span><span class="params">...</span></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">             │                             </span><br><span class="line">         ┌───┴────┐      ┌────────┐        </span><br><span class="line">         │        │      │        │        </span><br><span class="line">         │ Router ├──────┤   AP   │        </span><br><span class="line">         │        │      │        │        </span><br><span class="line">         └────────┘      └────────┘        </span><br></pre></td></tr></table></figure><p>&emsp;&emsp;交换机 VLAN 配置如图所示：</p><figure class="highlight subunit"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">VLAN    VLAN描述    成员端口    Tagged端口    Untagged端口</span><br><span class="line">1       LAN        2<span class="string">-6</span>        2<span class="string">-6</span></span><br><span class="line">2       WAN        1<span class="string">-2</span>        2            1</span><br><span class="line"></span><br><span class="line">端口     PVID</span><br><span class="line">端口1    2</span><br><span class="line">端口2    1</span><br><span class="line">端口3    1</span><br><span class="line">端口4    1</span><br><span class="line">端口5    1</span><br><span class="line">端口6    1</span><br></pre></td></tr></table></figure><p>&emsp;&emsp;关于 tagged untagged PVID 的解释，交换机的帮助说的比网上的介绍清楚很多，这里粘贴一下：</p><ol><li>Untagged 将此端口上的流量的出口规则设置为 untagged。在发送数据包之前，交换机会丢弃 tag 头。</li><li>Tagged 将此端口上的流量的出口规则设置为 tagged。在发送数据包之前，交换机会添加 tag 头。</li><li>PVID(端口 VLAN ID)是端口缺省 VID。当端口接收到一个 untagged 包，它将给这个包添加一个带有端口 PVID 的 VLAN tag 并转发该数据包。</li></ol><p>&emsp;&emsp;软路由的 Openwrt 配置很简单， LUCI 界面直接新增一个接口 eth0.2 虚拟口作为 WAN 口，原来的物理口 eth0 加入到 LAN。</p><p>&emsp;&emsp;简单的来说通过配置端口 1 和端口 2 被加入了 VLAN2，光猫的untagged包进入端口 1 后会被打上 VLAN2 标签（PVID2），被转发到端口 2 送给软路由 WAN 口 eth0.2 用于拨号上网。软路由的 WAN 口（eth0.2） 回程 VLAN2 数据包会在从端口 1 发往光猫时去除 VLAN 标签。这样就通过 VLAN2 完成了拨号功能。</p><p>&emsp;&emsp;对于软路由 LAN（eth0） 无标签数据包进入端口 2 后会被打上 VLAN1 标签（PVID1），转发给同在 VLAN1 的端口 3～6。数据包在从端口 3～6 发送给设备（NAS），也会去除 VLAN 标签，反之同理，这样就完成交换机下的 LAN 设备与软路由下其它 LAN 设备互通。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&amp;emsp;&amp;emsp;家里的入户的光猫在书房，书房到客厅的只有一条网线。因此诞生了单线复用的需求，拓扑如下：&lt;/p&gt;</summary>
    
    
    
    
    <category term="Network" scheme="https://www.iloft.xyz/tags/Network/"/>
    
  </entry>
  
  <entry>
    <title>TC Pedit 实现无状态 NAT</title>
    <link href="https://www.iloft.xyz/archives/tc-pedit-stateless-nat.html"/>
    <id>https://www.iloft.xyz/archives/tc-pedit-stateless-nat.html</id>
    <published>2024-09-20T10:36:51.000Z</published>
    <updated>2026-04-10T10:50:27.643Z</updated>
    
    <content type="html"><![CDATA[<p>　　最近有一个需求，修改 TCP 回程数据包的 SRC 和 DST IP 使与去程走不一样的路由。第一反应是使用 iptables 做 SNAT 和 DNAT，但是实测发现 iptables 是基于 Conntrack 实现的，对回程数据包并不生效。因为曾经用 XDP 实现过 LB，准备再次使用 XDP 写个小程序实现。</p><p>　　随后突然想起 ebpf 的 XDP Hook 是对 ingress 生效的，如果想做 egress 则得使用 TC Hook。既然如此， Linux 的 TC 工具是不是已经原生支持了我的小需求呢，之前对 TC 工具的使用局限在 netem 和 tbf 模块，也就是弱网模拟和流控。查阅文档发现 TC 的 Pedit 模块可以很轻松的提供我需要的无状态（Stateless）NAT 能力。</p><span id="more"></span> <p>　　在我的场景下需要将 wg0 发出的数据包的 SRC 和 DST 修改成特定的 IP，这里就以匹配 SRC 3.3.3.3 和 DST 4.4.4.4 的数据包并将其修改为 SRC 1.1.1.1 和 DST 2.2.2.2 为例了。</p><h2 id="创建队列"><a href="#创建队列" class="headerlink" title="创建队列"></a>创建队列</h2><p>　　首先就是创建一个 clsact 队列，这个队列有 ingress 和 egress 两个钩子。</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">$ tc qdisc add dev wg0 clsact</span><br></pre></td></tr></table></figure><h2 id="修改数据包"><a href="#修改数据包" class="headerlink" title="修改数据包"></a>修改数据包</h2><p>　　随后的命令很直观，指定网卡 wg0 的 egress 方向。flower（flow based traffic control filter）根据 src_ip dst_ip 匹配 ip 数据包，首先通过 pedit action 修改 ip，再使用 csum action 为数据包重新计算 Checksum。</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></pre></td><td class="code"><pre><span class="line">$ tc filter add dev wg0 egress protocol ip \</span><br><span class="line">    flower \</span><br><span class="line">    src_ip 3.3.3.3 \</span><br><span class="line">    dst_ip 4.4.4.4 \</span><br><span class="line">    action pedit ex \</span><br><span class="line">        munge ip src <span class="built_in">set</span> 1.1.1.1 \</span><br><span class="line">        munge ip dst <span class="built_in">set</span> 2.2.2.2 \</span><br><span class="line">    pipe \</span><br><span class="line">    action csum ip tcp udp</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　最近有一个需求，修改 TCP 回程数据包的 SRC 和 DST IP 使与去程走不一样的路由。第一反应是使用 iptables 做 SNAT 和 DNAT，但是实测发现 iptables 是基于 Conntrack 实现的，对回程数据包并不生效。因为曾经用 XDP 实现过 LB，准备再次使用 XDP 写个小程序实现。&lt;/p&gt;
&lt;p&gt;　　随后突然想起 ebpf 的 XDP Hook 是对 ingress 生效的，如果想做 egress 则得使用 TC Hook。既然如此， Linux 的 TC 工具是不是已经原生支持了我的小需求呢，之前对 TC 工具的使用局限在 netem 和 tbf 模块，也就是弱网模拟和流控。查阅文档发现 TC 的 Pedit 模块可以很轻松的提供我需要的无状态（Stateless）NAT 能力。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Network" scheme="https://www.iloft.xyz/tags/Network/"/>
    
  </entry>
  
  <entry>
    <title>十年</title>
    <link href="https://www.iloft.xyz/archives/talk.html"/>
    <id>https://www.iloft.xyz/archives/talk.html</id>
    <published>2023-04-19T16:00:00.000Z</published>
    <updated>2023-04-19T16:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>初闻不知曲中意，再闻已是曲中人。</p><span id="more"></span> <p>10 年前一个傻乎乎的高中生，对一切都充满了好奇，写下了第一篇现在看起来是黑历史的文章。<br>10 年后一个疲倦的大厂社畜，在异乡漂泊，扎不下根，空隙时突然想记下当下，此刻耳机响着的是 《Chasin’ You》。<br>愿下 10 年还能相见。</p><audio controls>  <source src="/uploads/chasinyou.mp4" type="audio/mpeg"></audio>]]></content>
    
    
    <summary type="html">&lt;p&gt;初闻不知曲中意，再闻已是曲中人。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Diary" scheme="https://www.iloft.xyz/tags/Diary/"/>
    
  </entry>
  
  <entry>
    <title>再见 二〇二一</title>
    <link href="https://www.iloft.xyz/archives/bye2021.html"/>
    <id>https://www.iloft.xyz/archives/bye2021.html</id>
    <published>2021-12-31T11:43:34.000Z</published>
    <updated>2026-04-10T10:50:27.641Z</updated>
    
    <content type="html"><![CDATA[<p>　　二〇二一。忙着恋爱，忙着毕业，忙着工作。</p><span id="more"></span>  <p>　　二〇二二。勿失勿忘。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　二〇二一。忙着恋爱，忙着毕业，忙着工作。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Diary" scheme="https://www.iloft.xyz/tags/Diary/"/>
    
  </entry>
  
  <entry>
    <title>OpenWrt ZeroTier 跨地区组网</title>
    <link href="https://www.iloft.xyz/archives/openwrt-zerotier.html"/>
    <id>https://www.iloft.xyz/archives/openwrt-zerotier.html</id>
    <published>2020-11-23T12:00:00.000Z</published>
    <updated>2020-11-23T12:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　之前一直在用 Ocserv 或者 Softether 进行组网，但是这两者不能比较好的去中心化，且需要公网 IP，总的来说不适合跨地区的家庭组网。这次考虑使用 ZeroTier 进行组网，参考了相关的教程，发现很多都选择采用 iptables 对网络进行 NAT 的方式，不仅配置繁琐，还会损失性能。这里直接使用静态路由的方式，无需多余配置，性能也比较好。</p><span id="more"></span> <p>　　以一个三节点的网络为例：两台路由器 Openwrt 版本分别为 18.06 与 19.04，一台服务器部署了 Ocserv 用于不能直接使用 ZeroTier 设备通过 VPN 接入，网段如下。</p><figure class="highlight dns"><table><tr><td class="gutter"><pre><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">ZeroTier：<span class="number">10.244.0.0</span>/<span class="number">24</span></span><br><span class="line">路由器 <span class="keyword">A</span>（Openwrt）：<span class="number">172.16.0.1</span>/<span class="number">16</span></span><br><span class="line">路由器 B（Openwrt）：<span class="number">172.17.0.1</span>/<span class="number">16</span></span><br><span class="line">服务器 C（Ocserv）：<span class="number">192.168.30.0</span>/<span class="number">24</span></span><br></pre></td></tr></table></figure><p>　　关于 ZeroTier 注册和 Ocserv 相关的设置就不赘述了，主要关注 Openwrt 的相关的配置。</p><h2 id="安装-ZeroTier"><a href="#安装-ZeroTier" class="headerlink" title="安装 ZeroTier"></a>安装 ZeroTier</h2><p>　　安装 ZeroTier 非常简单，使用 ssh 登录路由器，使用包管理器安装即可，使用 LUCI 图形化安装也行，不过下面的配置仍需要在命令行中进行。</p><figure class="highlight sh"><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">$ opkg update</span><br><span class="line">$ opkg install zerotier</span><br></pre></td></tr></table></figure><h2 id="配置-ZeroTier"><a href="#配置-ZeroTier" class="headerlink" title="配置 ZeroTier"></a>配置 ZeroTier</h2><p>　　安装完成后，编辑配置文件 <code>vi /etc/config/zerotier</code>，初始配置文件如下：</p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">config zerotier sample_config</span><br><span class="line">        <span class="keyword">option</span> enabled <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        # persistent <span class="keyword">configuration</span> folder (<span class="keyword">for</span> ZT controller mode)</span><br><span class="line">        #<span class="keyword">option</span> config_path <span class="string">&#x27;/etc/zerotier&#x27;</span></span><br><span class="line"></span><br><span class="line">        #<span class="keyword">option</span> port <span class="string">&#x27;9993&#x27;</span></span><br><span class="line"></span><br><span class="line">        # Generate secret <span class="keyword">on</span> first <span class="keyword">start</span></span><br><span class="line">        <span class="keyword">option</span> secret <span class="string">&#x27;generate&#x27;</span></span><br><span class="line"></span><br><span class="line">        # <span class="keyword">Join</span> a <span class="built_in">public</span> network <span class="keyword">called</span> Earth</span><br><span class="line">        list <span class="keyword">join</span> <span class="string">&#x27;8056c2e21c000001&#x27;</span></span><br></pre></td></tr></table></figure><p>　　需要修改的地方只有两处：</p><ol><li><code>option enabled 0</code> 将 <code>0</code> 修改为 <code>1</code>。</li><li><code>list join &#39;8056c2e21c000001&#39;</code> 将 Earth 网络 id 修改自己的私有网络 id。</li></ol><p>　　完成后执行命令启动即可。</p><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">$ /etc/init.d/zerotier start</span><br></pre></td></tr></table></figure><p>　　可以通过命令查看连接到的网络，建议不用着急去 ZeroTier 网页配置 IP，等全部配置完成后再进行统一规划，避免敲错网段导致断网（逃。</p><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">$ zerotier-cli listnetworks</span><br></pre></td></tr></table></figure><h2 id="创建接口与防火墙"><a href="#创建接口与防火墙" class="headerlink" title="创建接口与防火墙"></a>创建接口与防火墙</h2><p>　　为了便于配置，我们需要为 ZeroTier 创建的网卡创建一个接口和独立的防火墙，如图所示：<br>　　创建一个叫 ZeroTier 的接口，绑定 ZeroTier 创建的虚拟网卡。<br><img src="/images/zerotier-create-interface.png" alt="创建接口"><br>　　保存后，为 ZeroTier 创建一个专属的防火墙区域，也叫 ZeroTier。<br><img src="/images/zerotier-create-firewall.png" alt="创建防火墙"></p><h2 id="配置防火墙"><a href="#配置防火墙" class="headerlink" title="配置防火墙"></a>配置防火墙</h2><p>　　创建完成后，配置 ZeroTier 的防火墙，如图所示：</p><ol><li>Input、Output、Forward 全部允许。</li><li>允许 ZeroTier 转发至 lan。</li><li>允许 ZeroTier 转发自 lan。<br><img src="/images/zerotier-firewall.png" alt="配置防火墙"></li></ol><h2 id="Zerotier-网页配置"><a href="#Zerotier-网页配置" class="headerlink" title="Zerotier 网页配置"></a>Zerotier 网页配置</h2><p>　　接着登录 ZeroTier 网站，为我们的设备分配 IP。</p><figure class="highlight dns"><table><tr><td class="gutter"><pre><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">ZeroTier：<span class="number">10.244.0.0</span>/<span class="number">24</span></span><br><span class="line">路由器 <span class="keyword">A</span>（Openwrt）：<span class="number">172.16.0.1</span>/<span class="number">16</span>      <span class="number">10.244.16.1</span></span><br><span class="line">路由器 B（Openwrt）：<span class="number">172.17.0.1</span>/<span class="number">16</span>      <span class="number">10.244.17.1</span></span><br><span class="line">服务器 C（Ocserv）：<span class="number">192.168.30.0</span>/<span class="number">24</span>     <span class="number">10.244.30.1</span></span><br></pre></td></tr></table></figure><p>　　接着在网页添加静态路由，如图所示：<br><img src="/images/zerotier-route.png" alt="静态路由"></p><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>　　完成上面的配置后，网络就打通了，无需使用 iptables 进行 NAT。<br>　　在路由器 B 上执行 <code>ip route</code> 可以看到 ZeroTier 下发的路由表。</p><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></pre></td><td class="code"><pre><span class="line">$ ip route</span><br><span class="line">10.244.0.0/16 dev ztyouvscim proto kernel scope <span class="built_in">link</span> src 10.244.17.1</span><br><span class="line">172.16.0.0/16 via 10.244.16.1 dev ztyouvscim</span><br><span class="line">172.17.0.0/16 dev br-lan proto kernel scope <span class="built_in">link</span> src 172.17.0.1</span><br><span class="line">192.168.30.0/24 via 10.244.30.1 dev ztyouvscim</span><br></pre></td></tr></table></figure><p>　　在路由器 A 上也可以 traceroute B 路由器下的设备，说明我们完成了组网的工作。<br><img src="/images/zerotier-traceroute.png" alt="路由追踪"></p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><p>　　如果对配置看得不是很懂，可以参考下面这篇文章，配图更加详细，防火墙的配置按照本文来即可。</p><ol><li><a href="https://engrzhou.github.io/2018/07/Openwrt%E8%B7%AF%E7%94%B1%E9%80%9A%E8%BF%87Zerotier%E7%BB%84%E7%BD%91%E5%AE%9E%E7%8E%B0%E5%BC%82%E5%9C%B0%E5%86%85%E7%BD%91%E4%BA%92%E8%AE%BF/">Openwrt路由通过Zerotier组网实现异地内网互访</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　之前一直在用 Ocserv 或者 Softether 进行组网，但是这两者不能比较好的去中心化，且需要公网 IP，总的来说不适合跨地区的家庭组网。这次考虑使用 ZeroTier 进行组网，参考了相关的教程，发现很多都选择采用 iptables 对网络进行 NAT 的方式，不仅配置繁琐，还会损失性能。这里直接使用静态路由的方式，无需多余配置，性能也比较好。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Network" scheme="https://www.iloft.xyz/tags/Network/"/>
    
  </entry>
  
  <entry>
    <title>reference 与 let&#39;s encrypt 证书吊销</title>
    <link href="https://www.iloft.xyz/archives/lets-encrypt-boulder-reference.html"/>
    <id>https://www.iloft.xyz/archives/lets-encrypt-boulder-reference.html</id>
    <published>2020-03-17T14:50:51.000Z</published>
    <updated>2026-04-10T10:50:27.641Z</updated>
    
    <content type="html"><![CDATA[<p>　　最近 <code>let&#39;s encrypt</code> 宣布吊销 300 万个证书，因为没被波及所以没收到邮件。今天才看到了这条新闻，根据描述是因为 Boulder 的 bug 导致不能正确的验证 <code>CAA</code>。</p><span id="more"></span><h2 id="CAA"><a href="#CAA" class="headerlink" title="CAA"></a>CAA</h2><p>　　要搞明白这个 bug 得先了解 <code>CAA</code> 是干嘛用的。简单的来说 CA 在签发证书的时候需要检查一下域名的 <code>CAA</code> 记录，如果有自己的话就签发，如果没有就拒绝签发。通过设置 <code>CAA</code> 就可以防止某些人利用其它 CA 的一些漏洞签发证书。比如这次的 <code>let&#39;s encrypt</code>，<code>CAA</code> 需要设置成 <code>letsencrypt.org</code>。如果什么都不设，任何 CA 都可以签发证书。</p><h2 id="漏洞发现"><a href="#漏洞发现" class="headerlink" title="漏洞发现"></a>漏洞发现</h2><p>　　了解了 <code>CAA</code> 就可以来看一看 <code>let&#39;s encrypt</code> 论坛上的这个<a href="https://community.letsencrypt.org/t/rechecking-%60CAA%60-fails-with-99-identical-subproblems/113517">求助</a>。正确情况下检测多个域名 <code>CAA</code> 如果一个域名检测失败，报一次错；多个域名检测失败，报多个不同的错。但实际上 <code>admin.mrhs.hwrsd.org</code> 这个域名报错 99 次。并且 <code>let&#39;s encrypt</code> 也的确重复检查了 99 次。</p><p>　　也就是说实际上只检测了 <code>admin.mrhs.hwrsd.org</code> 一个域名，其它域名并没有被检测。这样就可以使用一个可以通过检测的域名来让那些那些没有正确设置 <code>CAA</code> 的域名逃过检测。</p><h2 id="漏洞代码"><a href="#漏洞代码" class="headerlink" title="漏洞代码"></a>漏洞代码</h2><p>　　那问题出在了哪呢，看到这个直白的 pull request 标题就明白了 <a href="https://github.com/letsencrypt/boulder/pull/4690/">Pass authzModel by value, not reference</a>。<br>　　看一下那段处理多个域名的代码。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="title">authzModelMapToPB</span><span class="params">(m <span class="keyword">map</span>[<span class="type">string</span>]authzModel)</span></span> (*sapb.Authorizations, <span class="type">error</span>) &#123;</span><br><span class="line">resp := &amp;sapb.Authorizations&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> k, v := <span class="keyword">range</span> m &#123;</span><br><span class="line"><span class="comment">// Make a copy of k because it will be reassigned with each loop.</span></span><br><span class="line">kCopy := k</span><br><span class="line">authzPB, err := modelToAuthzPB(&amp;v)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line">resp.Authz = <span class="built_in">append</span>(resp.Authz, &amp;sapb.Authorizations_MapElement&#123;Domain: &amp;kCopy, Authz: authzPB&#125;)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> resp, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>　　乍一看这段代码看起来都没有什么问题。循环调用 <code>modelToAuthzPB</code> 方法，传入引用 <code>v</code> 获得 <code>authPB</code>。为循环中会被重新分配的 <code>k</code> 创建拷贝。最终将 <code>k</code> 与 <code>authPB</code> 返回走。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="title">modelToAuthzPB</span><span class="params">(am *authzModel)</span></span> (*corepb.Authorization, <span class="type">error</span>) &#123;</span><br><span class="line">expires := am.Expires.UTC().UnixNano()</span><br><span class="line">id := fmt.Sprintf(<span class="string">&quot;%d&quot;</span>, am.ID)</span><br><span class="line">status := uintToStatus[am.Status]</span><br><span class="line">pb := &amp;corepb.Authorization&#123;</span><br><span class="line">Id:             &amp;id,</span><br><span class="line">Status:         &amp;status,</span><br><span class="line">Identifier:     &amp;am.IdentifierValue,</span><br><span class="line">RegistrationID: &amp;am.RegistrationID,</span><br><span class="line">Expires:        &amp;expires,</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">return</span> pb, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>　　但问题在于所返回的结构体 <code>pb</code> 内部的元素也是引用，比如这个 <code>&amp;am.IdentifierValue</code>。每次传入的 <code>v</code> 是引用，结构体中的元素也是引用。这就导致 <code>authPB</code> 看起来不是个引用，实际上它的内部还是个引用。循环结束后，<code>authPB</code> 的值都来自于最后一次的 <code>v</code> 的结果。也就是说如果最后一个域名通过了检测，那么之前的域名都会通过检测。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>　　知道了原因，那解决起来就简单了，第一种方法直接将 <code>modelToAuthzPB</code> 方法从传引用改成传值。第二种方法像 <code>k</code> 一样，为 <code>v</code> 也搞一个 <code>vCopy</code>。而 <code>let&#39;s encrypt</code> 选择了第一种方法。 </p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="title">authzModelMapToPB</span><span class="params">(m <span class="keyword">map</span>[<span class="type">string</span>]authzModel)</span></span> (*sapb.Authorizations, <span class="type">error</span>) &#123;</span><br><span class="line">resp := &amp;sapb.Authorizations&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> k, v := <span class="keyword">range</span> m &#123;</span><br><span class="line"><span class="comment">// Make a copy of k because it will be reassigned with each loop.</span></span><br><span class="line">kCopy := k</span><br><span class="line">authzPB, err := modelToAuthzPB(v)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line">resp.Authz = <span class="built_in">append</span>(resp.Authz, &amp;sapb.Authorizations_MapElement&#123;Domain: &amp;kCopy, Authz: authzPB&#125;)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> resp, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>　　说实话在我自己的项目里各种 reference 乱飞，多半某天也会像这样吃个亏。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　最近 &lt;code&gt;let&amp;#39;s encrypt&lt;/code&gt; 宣布吊销 300 万个证书，因为没被波及所以没收到邮件。今天才看到了这条新闻，根据描述是因为 Boulder 的 bug 导致不能正确的验证 &lt;code&gt;CAA&lt;/code&gt;。&lt;/p&gt;</summary>
    
    
    
    
    <category term="golang" scheme="https://www.iloft.xyz/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>golang 实现 token 认证</title>
    <link href="https://www.iloft.xyz/archives/gin-token.html"/>
    <id>https://www.iloft.xyz/archives/gin-token.html</id>
    <published>2020-03-06T18:36:43.000Z</published>
    <updated>2020-03-06T18:36:43.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　之前做项目的时候为了省事都是直接采用第三方的 Auth 包。这次使用 Gin 实现时决定参考去年写的 <a href="/archives/sars2019-5.html">JWT</a> 文章实现一个简单的 token 认证，如果存在安全漏洞或者不周到的地方烦请指出。</p><span id="more"></span><p>　　思路很简单，服务器验证账号和密码后，将拼接的用户名与过期时间作为 <code>Payload</code>,使用 HMACSHA256 对拼接后的 <code>Payload</code> 进行签名。再与 Base64 编码后的 <code>Payload</code> 进行拼接。token 结构为 <code>SignaturePayload</code>.</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">// 设置签名 key 与超时时间</span></span><br><span class="line"><span class="keyword">const</span> secret = <span class="string">&quot;Secretkey&quot;</span></span><br><span class="line"><span class="keyword">const</span> exp = <span class="number">31536000</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 生成 Token</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">EncodeToken</span><span class="params">(username <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="comment">// 过期时间</span></span><br><span class="line">    expireTime := time.Now().Unix() + exp</span><br><span class="line">    <span class="comment">// 拼接 payload</span></span><br><span class="line">    data := username + <span class="string">&quot;.&quot;</span> + strconv.FormatInt(expireTime, <span class="number">10</span>)</span><br><span class="line">    <span class="comment">// base64 编码</span></span><br><span class="line">    base64Data := base64.StdEncoding.EncodeToString([]<span class="type">byte</span>(data))</span><br><span class="line">    <span class="comment">// 生成签名</span></span><br><span class="line">mac := hmac.New(sha256.New, []<span class="type">byte</span>(secret))</span><br><span class="line">mac.Write([]<span class="type">byte</span>(data))</span><br><span class="line">    expectedMAC := hex.EncodeToString(mac.Sum(<span class="literal">nil</span>))</span><br><span class="line">    <span class="comment">// 生成 token</span></span><br><span class="line">token := expectedMAC + base64Data</span><br><span class="line"><span class="keyword">return</span> token</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>　　用户之后的访问携带该 <code>token</code>，服务器将 Base64 编码的 <code>Payload</code> 解码，使用 HMACSHA256 签名并与 <code>Signature</code> 部分验证。如果通过验证，判断用户名是否存在和 <code>token</code> 是否过期。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="title">VerifyToken</span><span class="params">(token <span class="type">string</span>, username *<span class="type">string</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="comment">// 提取签名与编码后的 payload</span></span><br><span class="line">messageMAC := token[:<span class="number">64</span>]</span><br><span class="line">    base64data := token[<span class="number">64</span>:]</span><br><span class="line">    <span class="comment">// base64 解码</span></span><br><span class="line">data, err := base64.StdEncoding.DecodeString(base64data)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</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 class="comment">// 验证签名</span></span><br><span class="line">mac := hmac.New(sha256.New, []<span class="type">byte</span>(secret))</span><br><span class="line">mac.Write(data)</span><br><span class="line"><span class="keyword">if</span> messageMAC == hex.EncodeToString(mac.Sum(<span class="literal">nil</span>)) &#123;</span><br><span class="line">        <span class="comment">// 验证用户名与超时时间</span></span><br><span class="line">*username = strings.Split(<span class="type">string</span>(data), <span class="string">&quot;.&quot;</span>)[<span class="number">0</span>]</span><br><span class="line">expireTime, _ := strconv.ParseInt(strings.Split(<span class="type">string</span>(data), <span class="string">&quot;.&quot;</span>)[<span class="number">1</span>], <span class="number">10</span>, <span class="number">64</span>)</span><br><span class="line"><span class="keyword">if</span> controller.HavingUser(*username) &amp;&amp; expireTime &gt; time.Now().Unix() &#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 class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　之前做项目的时候为了省事都是直接采用第三方的 Auth 包。这次使用 Gin 实现时决定参考去年写的 &lt;a href=&quot;/archives/sars2019-5.html&quot;&gt;JWT&lt;/a&gt; 文章实现一个简单的 token 认证，如果存在安全漏洞或者不周到的地方烦请指出。&lt;/p&gt;</summary>
    
    
    
    
    <category term="golang" scheme="https://www.iloft.xyz/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>使用 docker 搭建 Hadoop 集群</title>
    <link href="https://www.iloft.xyz/archives/rpi-docker-hadoop-cluster.html"/>
    <id>https://www.iloft.xyz/archives/rpi-docker-hadoop-cluster.html</id>
    <published>2020-03-04T10:44:56.000Z</published>
    <updated>2020-03-04T10:44:56.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　由于疫情原因手上只有一块树莓派，不能使用虚拟机搭建 Hadoop 集群，docker 上已有的镜像也不支持 ARM64 架构。因此就用 docker 手动配置了 Hadoop 集群，记录一下。由于对 docker 和 hadoop 了解还不够深入，手法可能很丑。</p><span id="more"></span><h3 id="安装-docker"><a href="#安装-docker" class="headerlink" title="安装 docker"></a>安装 docker</h3><p>　　树莓派上使用的是 arch 系的 manjaro，其它系统类似。</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></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> pacman -S install docker docker-compose docker-machine</span><br><span class="line"><span class="built_in">sudo</span> usermod -a -G docker <span class="variable">$user</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl start docker</span><br></pre></td></tr></table></figure><h3 id="创建容器"><a href="#创建容器" class="headerlink" title="创建容器"></a>创建容器</h3><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">docker pull alpine</span><br><span class="line">docker pull alpine</span><br><span class="line">docker network create --driver=bridge hadoop</span><br><span class="line">docker run -itd --name=master --net=hadoop alpine</span><br></pre></td></tr></table></figure><h3 id="安装-hadoop"><a href="#安装-hadoop" class="headerlink" title="安装 hadoop"></a>安装 hadoop</h3><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></pre></td><td class="code"><pre><span class="line">docker attach master</span><br><span class="line">apk update</span><br><span class="line">apk add openjdk8 openssh bash ncurses shadow openrc</span><br><span class="line"><span class="built_in">cd</span> /var</span><br><span class="line">wget http://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-3.2.1/hadoop-3.2.1.tar.gz</span><br><span class="line">tar xzvf hadoop-3.2.1.tar.gz</span><br><span class="line"><span class="built_in">mv</span> hadoop-3.2.1 hadoop</span><br><span class="line"><span class="built_in">rm</span> hadoop-3.2.1.tar.gz</span><br></pre></td></tr></table></figure><h3 id="配置环境变量"><a href="#配置环境变量" class="headerlink" title="配置环境变量"></a>配置环境变量</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 修改 /etc/profile</span></span><br><span class="line">vi /etc/profile</span><br><span class="line"><span class="comment"># 添加环境变量</span></span><br><span class="line"><span class="built_in">export</span> PATH=/var/hadoop/bin</span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk</span><br><span class="line"><span class="built_in">export</span> HADOOP_HOME=/var/hadoop/</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$HADOOP_HOME</span>/bin</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$HADOOP_HOME</span>/sbin</span><br><span class="line"><span class="built_in">export</span> HADOOP_MAPRED_HOME=<span class="variable">$HADOOP_HOME</span></span><br><span class="line"><span class="built_in">export</span> HADOOP_COMMON_HOME=<span class="variable">$HADOOP_HOME</span></span><br><span class="line"><span class="built_in">export</span> HADOOP_HDFS_HOME=<span class="variable">$HADOOP_HOME</span></span><br><span class="line"><span class="built_in">export</span> YARN_HOME=<span class="variable">$HADOOP_HOME</span></span><br><span class="line"><span class="built_in">export</span> HADOOP_COMMON_LIB_NATIVE_DIR=<span class="variable">$HADOOP_HOME</span>/lib/native</span><br><span class="line"><span class="built_in">export</span> HADOOP_OPTS=<span class="string">&quot;-DJava.library.path=<span class="variable">$HADOOP_HOME</span>/lib&quot;</span></span><br><span class="line"><span class="built_in">export</span> JAVA_LIBRARY_PATH=<span class="variable">$HADOOP_HOME</span>/lib/native:<span class="variable">$JAVA_LIBRARY_PATH</span></span><br></pre></td></tr></table></figure><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="comment"># 生效环境变量</span></span><br><span class="line"><span class="built_in">source</span> /etc/profile</span><br><span class="line"><span class="comment"># 启动 sshd</span></span><br><span class="line">rc-update add sshd</span><br><span class="line"><span class="built_in">touch</span> /run/openrc/softlevel</span><br><span class="line">/etc/init.d/sshd start</span><br></pre></td></tr></table></figure><h3 id="配置用户组与密钥"><a href="#配置用户组与密钥" class="headerlink" title="配置用户组与密钥"></a>配置用户组与密钥</h3><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></pre></td><td class="code"><pre><span class="line">adduser hadoop</span><br><span class="line">usermod -aG hadoop hadoop</span><br><span class="line"><span class="built_in">chown</span> hadoop:root -R /var/hadoop</span><br><span class="line"><span class="built_in">chmod</span> g+rwx -R /var/hadoop</span><br><span class="line">su - hadoop</span><br><span class="line">ssh-keygen -t rsa -P <span class="string">&#x27;&#x27;</span> -f ~/.ssh/id_rsa</span><br><span class="line"><span class="built_in">cat</span> ~/.ssh/id_dsa.pub &gt;&gt; ~/.ssh/authorized_keys</span><br></pre></td></tr></table></figure><h3 id="配置-hadoop"><a href="#配置-hadoop" class="headerlink" title="配置 hadoop"></a>配置 hadoop</h3><h4 id="配置-hadoop-env-sh"><a href="#配置-hadoop-env-sh" class="headerlink" title="配置 hadoop-env.sh"></a>配置 hadoop-env.sh</h4><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"><span class="comment"># 修改 hadoop-env.sh</span></span><br><span class="line">vi /var/hadoop/etc/hadoop/hadoop-env.sh </span><br><span class="line"><span class="comment"># 修改 JAVA_HOME</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk</span><br></pre></td></tr></table></figure><h4 id="配置-core-site-xml"><a href="#配置-core-site-xml" class="headerlink" title="配置 core-site.xml"></a>配置 core-site.xml</h4><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">vi /var/hadoop/etc/hadoop/core-site.xml</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- &lt;conﬁguration&gt; 标签内添加 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>fs.default.name<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>hdfs://localhost:9000<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="配置-yarn-site-xml"><a href="#配置-yarn-site-xml" class="headerlink" title="配置 yarn-site.xml"></a>配置 yarn-site.xml</h4><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">vi /var/hadoop/etc/hadoop/yarn-site.xml</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- &lt;conﬁguration&gt; 标签内添加 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.nodemanager.aux-services<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>mapreduce_shuffle<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.nodemanager.aux-services.mapreduce.shuffle.class<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>org.apache.hadoop.mapred.ShuffleHandler<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="配置-mapred-site-xml"><a href="#配置-mapred-site-xml" class="headerlink" title="配置 mapred-site.xml"></a>配置 mapred-site.xml</h4><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="built_in">cp</span> /var/hadoop/etc/hadoop/mapred-site.xml.template /var/hadoop/etc/hadoop/mapred-site.xml</span><br><span class="line">vi /var/hadoop/etc/hadoop/mapred-site.xml</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- &lt;conﬁguration&gt; 标签内添加 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>mapreduce.framework.name<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>yarn<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="配置-hdfs-site-xml"><a href="#配置-hdfs-site-xml" class="headerlink" title="配置 hdfs-site.xml"></a>配置 hdfs-site.xml</h4><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">vi /var/hadoop/etc/hadoop/hdfs-site.xml</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- &lt;conﬁguration&gt; 标签内添加 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>dfs.replication<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>3<span class="tag">&lt;/<span class="name">value</span>&gt;</span> </span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>dfs.namenode.name.dir<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span> file:/var/hadoop/hadoop_data/hdfs/namenode<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>dfs.datanode.data.dir<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span> file:/var/hadoop/hadoop_data/hdfs/datanode<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="创建-DataNode"><a href="#创建-DataNode" class="headerlink" title="创建 DataNode"></a>创建 DataNode</h3><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="built_in">mkdir</span> -p /var/hadoop/hadoop_data/hdfs/datanode</span><br><span class="line">CRTL P+Q</span><br></pre></td></tr></table></figure><h3 id="创建-hadoop-模板镜像"><a href="#创建-hadoop-模板镜像" class="headerlink" title="创建 hadoop 模板镜像"></a>创建 hadoop 模板镜像</h3><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">docker ps -a</span><br><span class="line">docker commit master hadoop-raw</span><br></pre></td></tr></table></figure><h3 id="创建配置-data1-容器"><a href="#创建配置-data1-容器" class="headerlink" title="创建配置 data1 容器"></a>创建配置 data1 容器</h3><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">docker run -itd --name=data1 --net=hadoop hadoop-raw</span><br><span class="line">docker attach data1</span><br></pre></td></tr></table></figure><h4 id="修改-core-site-xml"><a href="#修改-core-site-xml" class="headerlink" title="修改 core-site.xml"></a>修改 core-site.xml</h4><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">vi /var/hadoop/etc/hadoop/core-site.xml</span><br></pre></td></tr></table></figure><figure class="highlight xml"><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">&lt;!-- 修改原值 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">value</span>&gt;</span>hdfs://master:9000<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="修改-yarn-site-xml"><a href="#修改-yarn-site-xml" class="headerlink" title="修改 yarn-site.xml"></a>修改 yarn-site.xml</h4><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">vi /var/hadoop/etc/hadoop/yarn-site.xml</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- &lt;conﬁguration&gt; 标签内添加 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.resource-tracker.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8025<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.scheduler.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8030<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">name</span>&gt;</span>yarn.resourcemanager.address<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:8050<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="修改-mapred-site-xml"><a href="#修改-mapred-site-xml" class="headerlink" title="修改 mapred-site.xml"></a>修改 mapred-site.xml</h4><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">vi /var/hadoop/etc/hadoop/mapred-site.xml</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- &lt;conﬁguration&gt; 标签内添加 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">property</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>mapred.job.tracker<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">value</span>&gt;</span>master:54311<span class="tag">&lt;/<span class="name">value</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">property</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="修改-hdfs-site-xml"><a href="#修改-hdfs-site-xml" class="headerlink" title="修改 hdfs-site.xml"></a>修改 hdfs-site.xml</h4><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">vi /var/hadoop/etc/hadoop/hdfs-site.xml</span><br><span class="line"><span class="comment"># 删除namenode所在的&lt;property&gt;标签</span></span><br></pre></td></tr></table></figure><h3 id="更新-hadoop-模板镜像，创建-data2-3-容器"><a href="#更新-hadoop-模板镜像，创建-data2-3-容器" class="headerlink" title="更新 hadoop 模板镜像，创建 data2 3 容器"></a>更新 hadoop 模板镜像，创建 data2 3 容器</h3><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">docker commit data hadoop-raw</span><br><span class="line">docker run -itd --name=data2 --net=hadoop hadoop-raw</span><br><span class="line">docker run -itd --name=data3 --net=hadoop hadoop-raw</span><br></pre></td></tr></table></figure><h3 id="创建-NameNode"><a href="#创建-NameNode" class="headerlink" title="创建 NameNode"></a>创建 NameNode</h3><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">docker attach master</span><br><span class="line"><span class="built_in">mkdir</span> -p /var/hadoop/hadoop_data/hdfs/namenode</span><br><span class="line">hadoop namenode -format</span><br></pre></td></tr></table></figure><h3 id="启动-hadoop-集群"><a href="#启动-hadoop-集群" class="headerlink" title="启动 hadoop 集群"></a>启动 hadoop 集群</h3><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">start-all.sh</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　由于疫情原因手上只有一块树莓派，不能使用虚拟机搭建 Hadoop 集群，docker 上已有的镜像也不支持 ARM64 架构。因此就用 docker 手动配置了 Hadoop 集群，记录一下。由于对 docker 和 hadoop 了解还不够深入，手法可能很丑。&lt;/p&gt;</summary>
    
    
    
    
    <category term="hadoop" scheme="https://www.iloft.xyz/tags/hadoop/"/>
    
    <category term="pi" scheme="https://www.iloft.xyz/tags/pi/"/>
    
    <category term="docker" scheme="https://www.iloft.xyz/tags/docker/"/>
    
  </entry>
  
  <entry>
    <title>内核 Stdio 原理</title>
    <link href="https://www.iloft.xyz/archives/Kernel-Stdio-Theory.html"/>
    <id>https://www.iloft.xyz/archives/Kernel-Stdio-Theory.html</id>
    <published>2020-03-02T10:44:39.000Z</published>
    <updated>2020-03-02T10:44:39.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　在学完中断、系统调用后，群里有同学提出了一个问题，大家也进行了讨论。</p><blockquote><p>A：刚刚开了一个脑洞，比如 C 里面写了个 scanf 然后开始运行，这个应该算是一个程序运行带来的异常还是 IO 中断。  </p></blockquote><span id="more"></span><blockquote><p>B：我这么想，C 的 scanf 先系统调用进入内核，然后内核在这个状态等待一个 IO 中断，之后完成系统调用再返回用户态。  </p></blockquote><blockquote><p>C：先是陷入异常 后面可能会有 DMA 传输之类的东西？所以可能还会结合中断处理。  </p></blockquote><blockquote><p>D：感觉中断应该是外部事件，是正在运行的程序所不期望的，但是这个例子里的输入就是正在运行的这个程序所期望的，而且是这个程序内部的函数需要的，应该不算中断？  </p></blockquote><p>　　我个人第一反应是键盘产生中断，操作系统处理后供 scanf 读取，scanf 本身没有触发中断和异常机制。于是查询资料的时候找到了 <a href="https://wiki.osdev.org/Kernel_Stdio_Theory">Kernel Stdio Theory</a> 这篇文章，在理解还没到位的情况下，瞎翻译一下。</p><h3 id="标准输入-输出原理"><a href="#标准输入-输出原理" class="headerlink" title="标准输入&#x2F;输出原理"></a>标准输入&#x2F;输出原理</h3><h4 id="什么是标准输入-输出？"><a href="#什么是标准输入-输出？" class="headerlink" title="什么是标准输入&#x2F;输出？"></a>什么是标准输入&#x2F;输出？</h4><p>　　标准输入输出以及标准错误都是 C 标准库流实现的一部分。流是访问文件、硬件资源、其它进程的读写接口。</p><p>　　引用 stdio.h 后，三个流会自动创建并与环境的标准输入输出以及错误流连接。在大部分情况下，进程的标准输出和错误会绑定到打开它的终端上。在 Stdin 没有被重定向情况下，默认的标准输入源是键盘。</p><p>　　这些效果由 C 库处理，与底层操作系统交互提供流资源的访问。流资源包含以下属性：</p><ul><li>读&#x2F;写</li><li>文本&#x2F;二进制</li><li>缓冲&#x2F;无缓冲</li></ul><p>　　大部分情况，StdOut 默认是有缓冲，StdErr 无缓冲。这样可以让用户立即看到输出到 StdErr 中的数据。缓冲可以是行缓存或完全缓冲。C 标准库的复杂性不是本文的关注点。</p><p>　　内核必须提供底层设备的 API 并将它们提供给正在运行的程序。所有程序，为了输入默认将键盘绑定为 StdIn，控制台绑定为 StdOut。</p><p>　　由于 C 用于开发 Unix 内核，且 Unix 提供将设备抽象成一般文件的方法。进程可以简单的将设备当作资源流打开，将 StdIO 连接到设备上进行读写。</p><p>　　终端是一个经典的例子，来了解 Unix 的 StdIO 如何工作的基本知识。注意，提供这种抽象不一定要通过文件。Windows API 通过函数调用提供这种抽象。实际上程序只有运行在终端下时才会使用 StdIn、StdOut 和 StdErr。对于 GUI 程序 StdIn、StdOut、StdErr 会由开发人员替换成相关 API，如弹窗用于错误和警告。</p><p>　　在 Unix 内核中默认的基本输出连接到了用户的终端设备（如 &#x2F;dev&#x2F;tty0）。在你的 Unix&#x2F;Linux 命令提示符中输入 <code>who am i</code>。代表当前终端的标准输出的设备。</p><p>　　在微型计算机中没有终端连接到大型主机，因此现代 Linux&#x2F;Unix 微型计算机只是创建了一个终端设备，并将其连接到内核的标准输出。</p><p>　　执行：<code>echo Hello from std Output! &gt;file</code></p><p>　　通过重定向程序的基本输出，将字符串 “Hello from std Output” 打印到名为 file 的文件中。程序获得了 file 的文件句柄作为它的 stdout 设备，并写入文件。</p><p>　　返回到之前运行 <code>who am i</code> 或简化成 <code>tty</code> 的终端。将 file 替换成 tty 的输出。我的内核报告的终端是 <code>dev/pts/1</code>。</p><p>　　执行：<code>echo Hello from std Output! &gt;/dev/pts/1</code></p><p>　　这次会在终端看到 echo 的输出。这是由于我们把 Stdout 的基本输出重定向回了我们的 Stdout 上。也就是例子中的 <code>dev/pts/1</code>，实际上它与当前用户的标准输出同义。</p><p>　　有意思的是，这个文件也充当着 Stdin 设备：读取键盘输入的字符并报告给读取它的程序。</p><p>　　Unix <code>cat</code> 命令会读取并输出文件中的内容，键盘输入会被送到程序正在读取的 Stdin。</p><p>　　执行：<code>cat /dev/pts/1 </code></p><p>　　输入单词和字符并按下回车。输入的字符会被送到 <code>cat</code> 正在读取的 Stdin。按下 <code>enter</code> 表示到达当前输入的行尾，内核停止读取键盘输入，并将这行输入送入当前被 <code>cat</code> 读取的标准输入。<code>cat</code> 认为它正在读取一个普通的文件，并完成工作：将文件内容输出到终端。</p><p>　　输入完行后按下 <code>Ctl + d</code>。*Unix 中表示“没有更多输入&#x2F;输入结尾”，处理文件时，是“文件结束”的含义。内核收到没有更多输入的指示，并转发到 Stdin，<code>cat</code> 识别到“文件结束”信号，认为到达当前读取文件的末尾。与所有 Unices 一样，对待，将一切当作普通文件，停止从内核读取。</p><p>　　实际上动态性质的终端 StdIO 文件并不是硬盘上的普通文件，而是由内核 API 管理的 Stdin 和 Stdout 链接。</p><h3 id="如何在内核中实现-StdIO？"><a href="#如何在内核中实现-StdIO？" class="headerlink" title="如何在内核中实现 StdIO？"></a>如何在内核中实现 StdIO？</h3><h4 id="简短论述设计考虑（待续）"><a href="#简短论述设计考虑（待续）" class="headerlink" title="简短论述设计考虑（待续）"></a>简短论述设计考虑（待续）</h4><p>　　大多数早期开发者并没有考虑他们的内核需要为程序实现某种形式的标准输入输出。操作系统开发者最早开发的控制台驱动和键盘驱动应当成为标准输入输出的驱动程序的一部分。</p><p>　　Stdin 和 Stdout 不一定必须和键盘设备、控制台屏幕或者其它屏幕输出。一个程序可能打开一个文件句柄，将其作为标准输出流写入。</p><p>　　目前绝大部分内核包含作者的，在引导时实现任何形式的 Stdout 是没有意义的：除了内核没有任何程序可以使用。除此之外内核需要支持将 Stdout 绑定到多个流。</p><p>　　尽管屏幕不是唯一可以绑定 StdOut 的设备或资源，但必须注意的是，从所有迹象来看，所有的 OS 开发者都会编写一个在引导时直接引用 VGA 文本模式帧缓冲区的驱动程序。这不是错误或者有害的，但当内核能够运行程序时，必须存在一个资源支持将控制台屏幕作为标准输出资源。当然，由于缺乏适当的设计，在引导后内核图形部分必须起作用时，很多人仍然没有编写 Stdout 接口。</p><p>　　通常可以认为 Stdout 是程序可以访问的读写流。</p><p>　　让我们做一个简单的实验来了解 *Nix 中 StdIO 终端文件会发生什么：在 *Nix 中打开两个终端窗口。一个终端输入：<code>cat /dev/pts/1</code>（你实际的当前终端设备）并回车确认。会发现 Cat 开始从终端中读取。</p><p>　　在第二个终端输入 <code>echo &lt;输入随机单词&gt;</code>。注意当 shell 从键盘读取键盘输入时，允许通过转义来输入不可打印字符。<br>当输入：</p><p>　　echo hello &lt;enter&gt; Jane&lt;enter&gt; I&#39;m glad to meet you<enter> </p><p>　　</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　在学完中断、系统调用后，群里有同学提出了一个问题，大家也进行了讨论。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A：刚刚开了一个脑洞，比如 C 里面写了个 scanf 然后开始运行，这个应该算是一个程序运行带来的异常还是 IO 中断。  &lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    
    <category term="OS" scheme="https://www.iloft.xyz/tags/OS/"/>
    
  </entry>
  
  <entry>
    <title>WD Elements 10T 开箱与拆解</title>
    <link href="https://www.iloft.xyz/archives/wd-elements-unboxing.html"/>
    <id>https://www.iloft.xyz/archives/wd-elements-unboxing.html</id>
    <published>2019-12-06T22:06:42.000Z</published>
    <updated>2019-12-06T22:06:42.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　最近 NAS 硬盘空间告急，就在黑五以日常折扣价入手了 WD Elements 10TB。6 号中午就到了。<br>　　这盘的开箱已经被开烂了，着实没啥好看的。</p><span id="more"></span> <p><img src="/images/wd-elements-box.jpg" alt="包装"><br><img src="/images/wd-elements.jpg" alt="全家福"><br>　　后入两张卡，划开外壳。<br><img src="/images/unbox.jpg" alt="开盖"><br>　　没有开出红色传说，依然是熟悉的白色降速盘。<br><img src="/images/helium-hdd.jpg" alt="开盖"><br>　　完全拆出送入 NAS，希望它能和前辈一样勤恳工作。<br><img src="/images/elements-part.jpg" alt="完全拆解"><br><img src="/images/inject-nas.jpg" alt="插入 NAS"><br>　　熟悉的型号，熟悉的通电次数。<br><img src="/images/diskinfo.jpg" alt="diskinfo"><br>　　随便测个速。<br><img src="/images/hdtune.jpg" alt="hdtune"><br>　　索然无味的开箱，就这样了~~</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　最近 NAS 硬盘空间告急，就在黑五以日常折扣价入手了 WD Elements 10TB。6 号中午就到了。&lt;br&gt;　　这盘的开箱已经被开烂了，着实没啥好看的。&lt;/p&gt;</summary>
    
    
    
    
    <category term="NAS" scheme="https://www.iloft.xyz/tags/NAS/"/>
    
  </entry>
  
  <entry>
    <title>Github Action + Hexo + COS 配置</title>
    <link href="https://www.iloft.xyz/archives/github-action-hexo-cos.html"/>
    <id>https://www.iloft.xyz/archives/github-action-hexo-cos.html</id>
    <published>2019-09-14T19:00:00.000Z</published>
    <updated>2019-09-14T19:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　Github Action 是 Github 推出的 CI&#x2F;CD 工具，之前一直使用 Travis CI 进行部署，这次就尝试迁移到 Github Action 上，基本上属于换汤不换药。<span id="more"></span></p><h3 id="安装-hexo-deployer-cos"><a href="#安装-hexo-deployer-cos" class="headerlink" title="安装 hexo-deployer-cos"></a>安装 hexo-deployer-cos</h3><p>　　在 package.json 下的 dependencies 中添加：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;hexo-deployer-cos&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^1.0.0&quot;</span></span><br></pre></td></tr></table></figure><h3 id="配置-hexo-deployer-cos"><a href="#配置-hexo-deployer-cos" class="headerlink" title="配置 hexo-deployer-cos"></a>配置 hexo-deployer-cos</h3><p>　　在_config.yml 中填写配置信息。由于配置文件会上传到 github 中，为了安全文件不保存 SecretID 和 SecretKey。</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="attr">deploy:</span> </span><br><span class="line">  <span class="attr">type:</span> <span class="string">cos</span></span><br><span class="line">  <span class="attr">appId:</span> <span class="number">1234567890</span></span><br><span class="line">  <span class="attr">secretId:</span> <span class="string">SecretId</span></span><br><span class="line">  <span class="attr">secretKey:</span> <span class="string">SecretKey</span></span><br><span class="line">  <span class="attr">bucket:</span> <span class="string">iloft-1234567890</span></span><br><span class="line">  <span class="attr">region:</span> <span class="string">ap-shanghai</span></span><br></pre></td></tr></table></figure><h3 id="配置-Travis-CI"><a href="#配置-Travis-CI" class="headerlink" title="配置 Travis-CI"></a>配置 Travis-CI</h3><h4 id="配置环境变量"><a href="#配置环境变量" class="headerlink" title="配置环境变量"></a>配置环境变量</h4><p>　　与使用 Travis CI 一样，我们首先在 <code>Repo/Setting/Secrets</code> 里配置需要环境变量。<br><img src="/images/github-secrets.png" alt="环境变量"></p><h4 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h4><p>　　创建并编辑 <code>.github/workflows/deploy.yml</code> ，与 Travis CI 格式大同小异，仍然需要使用 sed 命令替换隐私信息。由于Github Action支持私有仓库，如果是私有仓库可以直接在配置文件里填写敏感信息。</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Deploy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">braches:</span> </span><br><span class="line">      <span class="bullet">-</span> <span class="string">master</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-18.04</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v1</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v1</span></span><br><span class="line">      <span class="attr">with:</span></span><br><span class="line">        <span class="attr">node-version:</span> <span class="string">&#x27;10.x&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">Hexo</span> <span class="string">&amp;</span> <span class="string">COS</span> <span class="string">env</span></span><br><span class="line">      <span class="attr">env:</span></span><br><span class="line">        <span class="attr">SecretId:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.SecretId</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">SecretKey:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.SecretKey</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        npm install hexo-cli -g</span></span><br><span class="line"><span class="string">        npm install</span></span><br><span class="line"><span class="string">        sed -i &quot;s/SecretId/$&#123;SecretId&#125;/&quot; _config.yml</span></span><br><span class="line"><span class="string">        sed -i &quot;s/SecretKey/$&#123;SecretKey&#125;/&quot; _config.yml</span></span><br><span class="line"><span class="string"></span>    </span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">to</span> <span class="string">COS</span></span><br><span class="line">      <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        hexo clean &amp;&amp; hexo g &amp;&amp; hexo d</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">to</span> <span class="string">Github</span> <span class="string">Pages</span></span><br><span class="line">      <span class="attr">env:</span></span><br><span class="line">        <span class="attr">GH_REF:</span> <span class="string">github.com/myloft/blog</span></span><br><span class="line">        <span class="attr">CI_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.CI_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        </span><br><span class="line">      <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        git clone https://$&#123;GH_REF&#125; .deploy_git</span></span><br><span class="line"><span class="string">        cd .deploy_git</span></span><br><span class="line"><span class="string">        git checkout master</span></span><br><span class="line"><span class="string">        cd ../</span></span><br><span class="line"><span class="string">        mv .deploy_git/.git/ ./public/</span></span><br><span class="line"><span class="string">        cd ./public</span></span><br><span class="line"><span class="string">        git init</span></span><br><span class="line"><span class="string">        git config user.name &quot;Yu&quot;</span></span><br><span class="line"><span class="string">        git config user.email &quot;admin@iloft.xyz&quot;</span></span><br><span class="line"><span class="string">        git add .</span></span><br><span class="line"><span class="string">        git commit -m &quot;:memo:\ Update blog by Actions&quot;</span></span><br><span class="line"><span class="string">        git push --force --quiet &quot;https://$&#123;CI_TOKEN&#125;@$&#123;GH_REF&#125;&quot; master:gh-pages</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　Github Action 是 Github 推出的 CI&amp;#x2F;CD 工具，之前一直使用 Travis CI 进行部署，这次就尝试迁移到 Github Action 上，基本上属于换汤不换药。</summary>
    
    
    
    
    <category term="hexo" scheme="https://www.iloft.xyz/tags/hexo/"/>
    
  </entry>
  
  <entry>
    <title>SARS 2019 第五次打卡（JWT）</title>
    <link href="https://www.iloft.xyz/archives/sars2019-5.html"/>
    <id>https://www.iloft.xyz/archives/sars2019-5.html</id>
    <published>2019-09-12T23:00:00.000Z</published>
    <updated>2019-09-12T23:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　由于同学们报名的热情太高涨，打卡任务被张老师改成了两天一打，说实话压力还是蛮大的。本想写道算法题完成打卡，点开 GitHub 第三方登录时，对其中的原理产生了兴趣，对其中的常见概念进行了简单的学习与记录，由于时间紧促难免存在理解不到位。<span id="more"></span></p><h3 id="Cookie-Session"><a href="#Cookie-Session" class="headerlink" title="Cookie &amp; Session"></a>Cookie &amp; Session</h3><p>　　由于 Http 具有无状态性，因此一般使用 Cookie &amp; Session 保存用户的登录状态。</p><h4 id="验证流程"><a href="#验证流程" class="headerlink" title="验证流程"></a>验证流程</h4><ol><li>用户向服务器发送用户名和密码。</li><li>服务器验证用户提交的用户名和密码，将当前用户的信息和状态保存到 Session 生成对应的 Session id，保存到浏览器 Cookie 中。</li><li>用户此后的请求中携带着包含 Session id 的 Cookie。</li><li>服务器根据 Session id 读取相应的 Session。</li></ol><p>　　使用 Cookie &amp; Session 虽然解决了用户状态问题但存在着一些弊端。</p><h4 id="弊端"><a href="#弊端" class="headerlink" title="弊端"></a>弊端</h4><ol><li>需要在服务器端保存大量数据，不利于服务器的水平扩展。</li><li>易遭受跨域攻击。</li></ol><h3 id="Token"><a href="#Token" class="headerlink" title="Token"></a>Token</h3><p>　　由于 Session 的弊端，Token 将用户状态保存在客户端中，认证流程与使用 Cookie &amp; Session 方式相似。</p><h4 id="验证流程-1"><a href="#验证流程-1" class="headerlink" title="验证流程"></a>验证流程</h4><ol><li>用户向服务器发送用户名和密码。</li><li>服务器验证用户提交的用户名和密码，将用户信息加密生成 Token 返回给用户，用户将其存储 localstorage 等容器中。</li><li>用户下次请求时将 Token 放入 Header、Body 、URL 中的一种。</li><li>服务器将用户传来的 Token 解密，就可以确定用户的身份了。</li></ol><h4 id="优势"><a href="#优势" class="headerlink" title="优势"></a>优势</h4><ol><li>可以用于 Restful API 中。</li><li>以时间（Token 解密）换取空间（Session），解决了水平扩展的问题。</li><li>可以抵御跨域攻击。</li></ol><h3 id="JWT-JSON-Web-Token"><a href="#JWT-JSON-Web-Token" class="headerlink" title="JWT(JSON Web Token)"></a>JWT(JSON Web Token)</h3><p>　　JWT 是一种 Token 的实现标准（RFC 7519），JWT 由三部分组成。</p><h4 id="Header（头部）"><a href="#Header（头部）" class="headerlink" title="Header（头部）"></a>Header（头部）</h4><figure class="highlight json"><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="punctuation">&#123;</span><span class="attr">&quot;typ&quot;</span><span class="punctuation">:</span> <span class="string">&quot;JWT&quot;</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">&quot;alg&quot;</span><span class="punctuation">:</span> <span class="string">&quot;HS256&quot;</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ol><li><code>typ</code>:Token 的类型（type）。</li><li><code>alg</code>: 签名算法（algorithm），默认使用 HMAC SHA256，缩写为 HS256。</li></ol><h4 id="Payload（负载）"><a href="#Payload（负载）" class="headerlink" title="Payload（负载）"></a>Payload（负载）</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;iss&quot;</span><span class="punctuation">:</span><span class="string">&quot;joe&quot;</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">&quot;exp&quot;</span><span class="punctuation">:</span><span class="number">1300819380</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">&quot;http://example.com/is_root&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">true</span></span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>　　RFC 7519 规定了七种保留字段以供选用，除此以外可以自行定义。</p><ol><li><code>iss</code>: 签发人（Issuer）。</li><li><code>sub</code>: 主题（Subject）。</li><li><code>aud</code>: 受众（Audience）。</li><li><code>exp</code>: 过期时间（Expiration Time）。</li><li><code>nbf</code>: 生效时间（Not Before）。</li><li><code>iat</code>: 签发时间（Issued At）。</li><li><code>jti</code>: 唯一编号（JWT ID）。</li></ol><h4 id="Signature（签名）"><a href="#Signature（签名）" class="headerlink" title="Signature（签名）"></a>Signature（签名）</h4><p>　　首先对 Header 和 Payload 进行 Base64Url 编码，指定一个密钥（secret），使用 Header 中的加密算法产生密钥。</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="title">HMACSHA256</span><span class="params">(base64UrlEncode(header)</span></span> + <span class="string">&quot;.&quot;</span> + <span class="built_in">base64UrlEncode</span>(payload), secret)</span><br></pre></td></tr></table></figure><p>　　算出 Signature 后以<code>Header.Payload.Signature</code>格式组成 JWT。</p><h4 id="弊端-1"><a href="#弊端-1" class="headerlink" title="弊端"></a>弊端</h4><ol><li>JWT 一旦泄露，所有人都能获取到其全部权限。</li><li>服务端无法注销 Token，只能等待过期。</li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　由于同学们报名的热情太高涨，打卡任务被张老师改成了两天一打，说实话压力还是蛮大的。本想写道算法题完成打卡，点开 GitHub 第三方登录时，对其中的原理产生了兴趣，对其中的常见概念进行了简单的学习与记录，由于时间紧促难免存在理解不到位。</summary>
    
    
    
    
    <category term="SARS" scheme="https://www.iloft.xyz/tags/SARS/"/>
    
  </entry>
  
  <entry>
    <title>SARS 2019 第四周打卡（SecureJSON）</title>
    <link href="https://www.iloft.xyz/archives/sars2019-4.html"/>
    <id>https://www.iloft.xyz/archives/sars2019-4.html</id>
    <published>2019-09-08T11:16:00.000Z</published>
    <updated>2019-09-08T11:16:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　本周是开学周，整个工作日都在开会，学习时间并不多。周四晚上参加字节跳动的宣讲会，简单的介绍了目前字节跳动的技术栈。其中字节跳动的 http 框架使用的是 Gin，因此周六周日就捣鼓起来了，其中就遇到了一个奇怪的功能——SecureJSON。<span id="more"></span></p><p>　　根据文档描述 SecureJSON 可以防止 json 劫持，如果给定的结构是数组值，则默认预置 <code>&quot;while(1);&quot;</code> 到响应体。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="title">securejson</span><span class="params">()</span></span> &#123;</span><br><span class="line">r := gin.Default()</span><br><span class="line">names := []<span class="type">string</span>&#123;<span class="string">&quot;lena&quot;</span>, <span class="string">&quot;austin&quot;</span>, <span class="string">&quot;foo&quot;</span>&#125;</span><br><span class="line">r.GET(<span class="string">&quot;securejson&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> &#123;</span><br><span class="line">c.SecureJSON(http.StatusOK, names)</span><br><span class="line">&#125;)</span><br><span class="line">r.Run()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>　　运行示例代码，响应体被插入了<code>&quot;while(1);&quot;</code>。</p><figure class="highlight shell"><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">λ curl http://127.0.0.1:8080/securejson</span><br><span class="line">while(1);[&quot;lena&quot;,&quot;austin&quot;,&quot;foo&quot;]</span><br></pre></td></tr></table></figure><p>　　根据文档的描述将 Array 修改成 Map。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="function"><span class="keyword">func</span> <span class="title">securejson</span><span class="params">()</span></span> &#123;</span><br><span class="line">r := gin.Default()</span><br><span class="line">r.GET(<span class="string">&quot;securejson1&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> &#123;</span><br><span class="line">c.SecureJSON(http.StatusOK, gin.H&#123;</span><br><span class="line"><span class="string">&quot;names&quot;</span>: <span class="string">&quot;Yu&quot;</span>,</span><br><span class="line">&#125;)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">r.Run()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>　　此时响应体前并未添加<code>&quot;while(1);&quot;</code>。</p><figure class="highlight shell"><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">λ curl http://127.0.0.1:8080/securejson1</span><br><span class="line">&#123;&quot;names&quot;:&quot;Yu&quot;&#125;</span><br></pre></td></tr></table></figure><p>　　难道<code>[]</code>是造成的所谓 JSON 劫持的原因吗，JSON 劫持到底是什么，通过查阅资料找到了一段示例代码。<br>　　某银行可以通过某个 Get 方法的 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></pre></td><td class="code"><pre><span class="line">GET https<span class="punctuation">:</span><span class="comment">//mybank.com/users/balance</span></span><br><span class="line"><span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;userName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;jayden&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;balance&quot;</span><span class="punctuation">:</span> <span class="number">1200</span>    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span>         <span class="attr">&quot;userName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;oscar&quot;</span><span class="punctuation">,</span></span><br><span class="line">         <span class="attr">&quot;balance&quot;</span><span class="punctuation">:</span> <span class="number">1200000</span>        <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br><span class="line">Cookie<span class="punctuation">:</span> XXXXXOOOXXXXX</span><br></pre></td></tr></table></figure><p>　　正常情况下 Cookie 和同源策略可以阻止 JS 跨域获取数据，那攻击者该如何获取到数据呢。在浏览器控制台实验发现以<code>[]</code>开头的 Javascript 代码可以直接执行。</p><figure class="highlight javascript"><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="built_in">eval</span>(<span class="string">&#x27;[1,2,3]&#x27;</span>);</span><br><span class="line"><span class="title class_">Array</span>(<span class="number">3</span>) [ <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span> ]</span><br></pre></td></tr></table></figure><p>　　也就是说如果将<code>[]</code>格式的数据放入<code>&lt;script&gt;</code>标签下可以直接被执行，即便如此又该如何获取到数据呢？这时候 Javascript 覆盖和重写特性就被利用了－如果有多个重名的函数与方法只有最后一个定义的有效。攻击者通过重写默认的<code>Array()</code>函数，并将返回<code>[]</code>格式的 API 放入<code>&lt;script&gt;</code>标签下，一旦运行到<code>[</code>就会调用覆写的<code>Array()</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">&lt;script&gt;<span class="keyword">function</span> <span class="title function_">Array</span>(<span class="params"></span>)&#123;</span><br><span class="line">    <span class="keyword">var</span> that = <span class="variable language_">this</span>;</span><br><span class="line">    <span class="keyword">var</span> index = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">var</span> valueExtractor = <span class="keyword">function</span>(<span class="params">value</span>) &#123;</span><br><span class="line">      <span class="comment">// Alert the value</span></span><br><span class="line">      <span class="title function_">alert</span>(value);</span><br><span class="line">      <span class="comment">// Set the next index to use this method as well</span></span><br><span class="line">      that.<span class="title function_">__defineSetter__</span>(index.<span class="title function_">toString</span>(),valueExtractor );</span><br><span class="line">      index++;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="comment">// Set the setter for item 0</span></span><br><span class="line">    that.<span class="title function_">__defineSetter__</span>(index.<span class="title function_">toString</span>(),valueExtractor );</span><br><span class="line">    index++;&#125;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://mybank.com/users/balance&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>　　主流浏览器通过删除<code>__defineSetter__</code>阻止覆写修复了该漏洞。但随着 ES6 Proxy 的发布，<code>Array()</code>又可以被覆写利用了。尽管浏览器很快修复了这些漏洞，Google、Facebook 等厂商通过在自己的 API 前添死循环阻止执行到<code>[</code>，也是为了避免今后出现其它利用方法。客户端则需要先将附加的安全头去除再进行处理。<br>　　这也是为什么 Gin 的 SecureJSON 不对<code>&#123;&#125;</code>格式的 JSON 进行处理，JS 会首先检查语法，导致<code>&#123;&#125;</code>格式的 API 无法被执行。</p><figure class="highlight javascript"><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="built_in">eval</span>(<span class="string">&#x27;&#123;&quot;key&quot;:&quot;value&quot;&#125;&#x27;</span>);</span><br><span class="line"><span class="title class_">SyntaxError</span>: unexpected <span class="attr">token</span>: <span class="string">&#x27;:&#x27;</span></span><br></pre></td></tr></table></figure><p>　　虽然只是一个小问题，但深入研究探究其中的缘由还是很有趣的。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　本周是开学周，整个工作日都在开会，学习时间并不多。周四晚上参加字节跳动的宣讲会，简单的介绍了目前字节跳动的技术栈。其中字节跳动的 http 框架使用的是 Gin，因此周六周日就捣鼓起来了，其中就遇到了一个奇怪的功能——SecureJSON。</summary>
    
    
    
    
    <category term="SARS" scheme="https://www.iloft.xyz/tags/SARS/"/>
    
  </entry>
  
  <entry>
    <title>SARS 2019 第二周打卡（验证码生成）</title>
    <link href="https://www.iloft.xyz/archives/sars2019-2.html"/>
    <id>https://www.iloft.xyz/archives/sars2019-2.html</id>
    <published>2019-08-25T20:40:14.000Z</published>
    <updated>2019-08-25T20:40:14.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　SARS 学习计划的第二周即将结束了。参加的数字识别和新生入学小助手小组的开发都在平稳的推进中，由于在两组中分别选择了微信小程序和前端开发。因此我主要还是以学习前端和小程序开发为主，暂时还没有具体的开发任务，只是进行了一些打杂工作。数字识别小组的任务是识别软微综合信息服务平台的验证码。没有真实的验证码作为训练数据，就不能完成这个任务。由于组里的各位大佬都忙于算实现，因此就由前端组我来生成这些验证码。<span id="more"></span></p><p>　　首先通过观察服务器的响应头中的 X-Powered-By 字段可以发现服务端采用的是 ThinkPHP。随意构造一个链接发现未关闭 Debug 模式，使用的 ThinkPHP 版本为 3.2.2。<br><img src="/images/thinkphp-debug.png" alt="ThinkPHP3.2.2"> <center>图 1 原始图像</center><br>　　知道了框架的版本问题就很简单了，去 ThinkPHP 官网下载框架并在电脑上部署。<br><img src="/images/thinkphp.png" alt="ThinkPHP"> <center>图 2 部署 ThinkPHP</center><br>　　通过阅读 PHP 语法和 ThinkPHP 的文档，尝试修改 IndexController.class.php 调用 ThinkPHP 提供的 Think\Verify 类。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="meta">&lt;?php</span></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">Home</span>\<span class="title class_">Controller</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Think</span>\<span class="title">Controller</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Think</span>\<span class="title">Verify</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IndexController</span> <span class="keyword">extends</span> <span class="title">Controller</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">index</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable">$verify</span> = <span class="keyword">new</span> <span class="title class_">Verify</span>(<span class="variable">$config</span>);</span><br><span class="line">        <span class="variable">$verify</span>-&gt;<span class="title function_ invoke__">entry</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>　　访问首页，成功显示验证码图像。<br><img src="/images/captcha-1.png" alt="生成验证码图像"> <center>图 3 生成验证码图像</center><br>　　修改验证码参数使得验证码风格与综合服务平台验证码一致。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="class"><span class="keyword">class</span> <span class="title">IndexController</span> <span class="keyword">extends</span> <span class="title">Controller</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">index</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable">$config</span> = <span class="keyword">array</span>(</span><br><span class="line">            <span class="string">&#x27;useImgBg&#x27;</span> =&gt; <span class="literal">false</span>,           <span class="comment">// 使用背景图片</span></span><br><span class="line">            <span class="string">&#x27;fontSize&#x27;</span> =&gt; <span class="number">16</span>,              <span class="comment">// 验证码字体大小(px)</span></span><br><span class="line">            <span class="string">&#x27;useCurve&#x27;</span> =&gt; <span class="literal">true</span>,            <span class="comment">// 是否画混淆曲线</span></span><br><span class="line">            <span class="string">&#x27;useNoise&#x27;</span> =&gt; <span class="literal">false</span>,           <span class="comment">// 是否添加杂点</span></span><br><span class="line">            <span class="string">&#x27;imageH&#x27;</span>   =&gt; <span class="number">37</span>,              <span class="comment">// 验证码图片高度</span></span><br><span class="line">            <span class="string">&#x27;imageW&#x27;</span>   =&gt; <span class="number">120</span>,             <span class="comment">// 验证码图片宽度</span></span><br><span class="line">            <span class="string">&#x27;length&#x27;</span>   =&gt; <span class="number">4</span>,               <span class="comment">// 验证码位数</span></span><br><span class="line">            <span class="string">&#x27;codeSet&#x27;</span>  =&gt; <span class="string">&#x27;0123456789&#x27;</span>,    <span class="comment">// 验证码字符集合</span></span><br><span class="line">        );</span><br><span class="line">        <span class="keyword">for</span>(<span class="variable">$num</span> = <span class="number">0</span>; <span class="variable">$num</span> &lt; <span class="number">10</span>; <span class="variable">$num</span>++) &#123;</span><br><span class="line">            <span class="variable">$verify</span> = <span class="keyword">new</span> <span class="title class_">Verify</span>(<span class="variable">$config</span>);</span><br><span class="line">            <span class="variable">$verify</span>-&gt;<span class="title function_ invoke__">entry</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><img src="/images/captcha-2.png" alt="修改参数后的验证码图像"> <center>图 4 修改参数后的验证码图像</center><br>　　尽管实现了验证码生成，但此时并不知道生成的验证码的明文，而且验证码是展示在网页上的。为了实现一个能够批量生成验证码到本地的工具，尝试对 Think\Verify 进行修改。通过阅读代码，首先找到输出验证码的代码块。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="variable">$key</span>                   = <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">authcode</span>(<span class="variable">$this</span>-&gt;seKey);</span><br><span class="line"><span class="variable">$code</span>                  = <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">authcode</span>(<span class="title function_ invoke__">strtoupper</span>(<span class="title function_ invoke__">implode</span>(<span class="string">&#x27;&#x27;</span>, <span class="variable">$code</span>)));</span><br><span class="line"><span class="variable">$secode</span>                = <span class="keyword">array</span>();</span><br><span class="line"><span class="variable">$secode</span>[<span class="string">&#x27;verify_code&#x27;</span>] = <span class="variable">$code</span>; <span class="comment">// 把校验码保存到session</span></span><br><span class="line"><span class="variable">$secode</span>[<span class="string">&#x27;verify_time&#x27;</span>] = NOW_TIME; <span class="comment">// 验证码创建时间</span></span><br><span class="line"><span class="title function_ invoke__">session</span>(<span class="variable">$key</span> . <span class="variable">$id</span>, <span class="variable">$secode</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">&#x27;Cache-Control: private, max-age=0, no-store, no-cache, must-revalidate&#x27;</span>);</span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">&#x27;Cache-Control: post-check=0, pre-check=0&#x27;</span>, <span class="literal">false</span>);</span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">&#x27;Pragma: no-cache&#x27;</span>);</span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">&quot;content-type: image/png&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出图像</span></span><br><span class="line"><span class="title function_ invoke__">imagepng</span>(<span class="variable">$this</span>-&gt;_image);</span><br><span class="line"><span class="title function_ invoke__">imagedestroy</span>(<span class="variable">$this</span>-&gt;_image);</span><br></pre></td></tr></table></figure><p>　　阅读 PHP 文档了解 imagepng() 的使用方法，修改代码实现保存。</p><figure class="highlight php"><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="title function_ invoke__">imagepng</span>(<span class="variable">$this</span>-&gt;_image, <span class="variable">$code</span>.<span class="string">&#x27;.png&#x27;</span>);</span><br><span class="line"><span class="title function_ invoke__">imagedestroy</span>(<span class="variable">$this</span>-&gt;_image);</span><br></pre></td></tr></table></figure><p>　　访问网页根目录下出现了一张验证码图像。<br><img src="/images/captcha-3.png" alt="验证码保存到本地"> <center>图 5 验证码保存到本地</center><br>　　尽管出现了一张验证码图像，但是文件名却不是我们想要的，继续阅读代码发现ThinkPHP为了安全会将生成好的验证码字符串加密后存储到session中，用户传来的验证码字符串也需要先进行加密，再与session中的字符串进行对比来判断是否输入正确。但是这些步骤以及服务器响应头对于生成训练数据来说并没有什么用，全部删除仅保留两行，并将每个字符拼接成字符串。</p><figure class="highlight php"><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="title function_ invoke__">imagepng</span>(<span class="variable">$this</span>-&gt;_image, <span class="variable">$code</span>[<span class="number">0</span>].<span class="variable">$code</span>[<span class="number">1</span>].<span class="variable">$code</span>[<span class="number">2</span>].<span class="variable">$code</span>[<span class="number">3</span>].<span class="string">&#x27;.png&#x27;</span>);</span><br><span class="line"><span class="title function_ invoke__">imagedestroy</span>(<span class="variable">$this</span>-&gt;_image);</span><br></pre></td></tr></table></figure><p>　　访问网页，此时出现了一张正确命名的验证码图像。<br><img src="/images/captcha-4.png" alt="验证码保存到本地"> <center>图 6 正确命名的验证码</center><br>　　完成了这些工作后只需要增加一个循环就可以批量生成验证码图像了。尝试生成 10 个验证码图像。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">public</span> <span class="function"><span class="keyword">function</span> <span class="title">index</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="variable">$config</span> = <span class="keyword">array</span>(</span><br><span class="line">        <span class="string">&#x27;useImgBg&#x27;</span> =&gt; <span class="literal">false</span>,           <span class="comment">// 使用背景图片</span></span><br><span class="line">        <span class="string">&#x27;fontSize&#x27;</span> =&gt; <span class="number">16</span>,              <span class="comment">// 验证码字体大小(px)</span></span><br><span class="line">        <span class="string">&#x27;useCurve&#x27;</span> =&gt; <span class="literal">true</span>,            <span class="comment">// 是否画混淆曲线</span></span><br><span class="line">        <span class="string">&#x27;useNoise&#x27;</span> =&gt; <span class="literal">false</span>,           <span class="comment">// 是否添加杂点</span></span><br><span class="line">        <span class="string">&#x27;imageH&#x27;</span>   =&gt; <span class="number">37</span>,              <span class="comment">// 验证码图片高度</span></span><br><span class="line">        <span class="string">&#x27;imageW&#x27;</span>   =&gt; <span class="number">120</span>,             <span class="comment">// 验证码图片宽度</span></span><br><span class="line">        <span class="string">&#x27;length&#x27;</span>   =&gt; <span class="number">4</span>,               <span class="comment">// 验证码位数</span></span><br><span class="line">        <span class="string">&#x27;codeSet&#x27;</span>  =&gt; <span class="string">&#x27;0123456789&#x27;</span>,    <span class="comment">// 验证码字符集合</span></span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">for</span>(<span class="variable">$num</span> = <span class="number">0</span>; <span class="variable">$num</span> &lt; <span class="number">10</span>; <span class="variable">$num</span>++) &#123;</span><br><span class="line">        <span class="variable">$verify</span> = <span class="keyword">new</span> <span class="title class_">Verify</span>(<span class="variable">$config</span>);</span><br><span class="line">        <span class="variable">$verify</span>-&gt;<span class="title function_ invoke__">entry</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="/images/captcha-5.png" alt="验证码保存到本地"> <center>图 7 生成的 10 张验证码图像</center><br>　　尽管面对的是一门不了解的语言和框架，由于有充足的文档，因此生成工作还是比较简单的。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　SARS 学习计划的第二周即将结束了。参加的数字识别和新生入学小助手小组的开发都在平稳的推进中，由于在两组中分别选择了微信小程序和前端开发。因此我主要还是以学习前端和小程序开发为主，暂时还没有具体的开发任务，只是进行了一些打杂工作。数字识别小组的任务是识别软微综合信息服务平台的验证码。没有真实的验证码作为训练数据，就不能完成这个任务。由于组里的各位大佬都忙于算实现，因此就由前端组我来生成这些验证码。</summary>
    
    
    
    
    <category term="SARS" scheme="https://www.iloft.xyz/tags/SARS/"/>
    
  </entry>
  
  <entry>
    <title>SARS 2019 第一周打卡</title>
    <link href="https://www.iloft.xyz/archives/sars2019-1.html"/>
    <id>https://www.iloft.xyz/archives/sars2019-1.html</id>
    <published>2019-08-18T18:37:39.000Z</published>
    <updated>2019-08-18T18:37:39.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　本周是 SARS 学习计划的第一周，说来惭愧这一整周我都在外领略祖国大好河山，直到周六晚才回到家中。不仅如此本周也是我们项目立项准备开始的第一周，根据兴趣我最终选择参与了数字识别小组和新生入学小助手小组。尽管不在家手头里也没有趁手的设备，但由于我对两个组的项目抱有很大兴趣，因此抽空也查阅了一些资料。<span id="more"></span> </p><p>　　数字识别小组最终确认的项目是识别软微综合信息服务平台的验证码。由于之前本科毕设做的是车牌识别，接触过一些验证码识别项目。识别验证码其实已经有很多人在做，也有很多种方法。单从字符算法上来看就可以分为 SVM、随机森林等传统的机器学习算法以及最近几年非常火热的深度学习算法两种。目前主流实现思路依然是先采用一些图像学算法对验证码图像进行预处理，将其中的干扰点和干扰线进行清除后分割字符对每个字符进行识别。</p><p>　　以一张验证码为例，首先对其灰度化，通过调整阈值将大部分噪点清除。<br><img src="/images/origin-captcha.png" alt="原始图像"> <center>图 1 原始图像</center><br><img src="/images/gray-captcha.png" alt="灰度图像"> <center>图 2 灰度图像</center><br><img src="/images/threashold-captcha.png" alt="调整阈值后的灰度图像"> <center>图 3 调整阈值后的灰度图像</center><br>　　通过灰度化和阈值分析后大部分噪点得以清除，但仍然存在少量的孤立噪点。此时计算图中每个点相邻 9 宫格的黑色点数，设置合适的阈值将其清除。<br><img src="/images/spotless-captcha.png" alt="清除噪点后的图像"> <center>图 4 清除噪点后的图像</center><br>　　这些噪点清除后就可以对图像中段的字符进行分割，其中的方法有很多，比如边缘算子，连通域分析。<br><img src="/images/captcha-slice1.png" alt="分割后的字符"><br><img src="/images/captcha-slice2.png" alt="分割后的字符"><br><img src="/images/captcha-slice3.png" alt="分割后的字符"><br><img src="/images/captcha-slice4.png" alt="分割后的字符"><center>图 5 分割后的字符</center><br>　　这时就可以使用机器学习或者深度学习对单个字符进行训练和识别。</p><p>　　这种方法看起来还是很简单的，但是毕设做车牌识别时的字符分割就没有那么简单了，首先车牌图像相对于图像的比例非常小且图像的质量受设备和天气的影响很大，一旦字符出现粘连，很大概率无法对车牌的字符进行分割。</p><p>　　除此以外，在图像尺寸较大时，ANN 深度无法过深，否则根本无法训练。因此，训练神经网络必须对图片进行切割，但是 CNN 的出现让我们可以考虑实现不分割字符一次性识别验证码或者车牌。</p><p>　　针对这种顺序书写的字符比如车牌以及验证码，使用RNN以及CTC可以在只知道序列顺序而不知道每个字符具体位置时让模型收敛。该模型最早由华中科技大学的白翔教授提出。这种模型不仅可以应用在图像中的文字识中，也可以实现对不定长的语音的识别。<br><img src="/images/rcnn-model.png" alt="RCNN 模型"><center>图 6 RCNN 模型</center><br>　　有同组的同学拿到使用 RCNN 模型的源码后迫不及待的使用其自带的验证码生成器进行训练。对学校综合信息服务平台的验证码进行测试，发现识别效果很差，由此得出该模型泛化差的结论，决定换模型，尝试字符分割算法。<br><img src="/images/rcnn-recognition.png" alt="识别效果"><center>图 7 识别效果</center><br>　　在之前做毕设的过程中，遇到最难的问题绝不是模型调优，而是数据的收集，一份高质量的车牌数据远比模型的选择更重要，这也是为什么很多老牌的车牌识别系统尽管使用传统的字符分割算法，但是识别效率远超那些用了各种深度学习算法的车牌识别系统。</p><p>　　除此以外按照新生入学小助手组的工作安排，周日抽出了点时间完成了一个 Restful 风格的 Python demo，无需赘述。</p><h4 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h4><ul><li><a href="https://blog.csdn.net/Neleuska/article/details/80040304">python 基于机器学习识别验证码</a></li><li><a href="https://ypw.io/captcha/">使用 Keras 来破解 captcha 验证码</a></li><li><a href="https://arxiv.org/pdf/1507.05717v1.pdf">An End-to-End Trainable Neural Network for Image-based SequenceRecognition and Its Application to Scene Text Recognition</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　本周是 SARS 学习计划的第一周，说来惭愧这一整周我都在外领略祖国大好河山，直到周六晚才回到家中。不仅如此本周也是我们项目立项准备开始的第一周，根据兴趣我最终选择参与了数字识别小组和新生入学小助手小组。尽管不在家手头里也没有趁手的设备，但由于我对两个组的项目抱有很大兴趣，因此抽空也查阅了一些资料。</summary>
    
    
    
    
    <category term="SARS" scheme="https://www.iloft.xyz/tags/SARS/"/>
    
  </entry>
  
  <entry>
    <title>软微面试记录</title>
    <link href="https://www.iloft.xyz/archives/interview-record.html"/>
    <id>https://www.iloft.xyz/archives/interview-record.html</id>
    <published>2019-07-22T10:20:45.000Z</published>
    <updated>2019-07-27T08:59:45.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　时间已经过了很久，记忆可能出现了偏差，可能存在遗忘和脑补的情况。<del>希望能给明年面试的学弟学妹们提供一些小小帮助</del>。</p><span id="more"></span> <div class="tip">第二十五条 自本办法实施之日起，学位授予单位不再招收第二学士学位生。————《学士学位授权与授予管理办法》</div><p>　　反复横跳…</p><div class="tip">一、各地各高校要认真贯彻落实党中央、国务院关于做好“六稳”工作的决策部署，充分发挥高等教育资源优势，根据学校发展规划和办学条件合理确定第二学士学位教育规模，加快培养社会紧缺人才，为稳定就业、增强学生就业能力提供有力支持。————《教育部办公厅关于进一步做好第二学士学位教育有关工作的通知》</div><h2 id="笔试"><a href="#笔试" class="headerlink" title="笔试"></a>笔试</h2><p>　　我是计算机科班的，报的大数据转录，没有达到要求，参加了笔试。面试前一天参加笔试。笔试分为英语和逻辑两部分，一份试卷 A4 纸单面打印，答题时间三个小时。</p><h4 id="笔试英语"><a href="#笔试英语" class="headerlink" title="笔试英语"></a>笔试英语</h4><p>　　英语题型有点类似江苏高考，选择 + 完形填空 + 阅读理解 + 作文。选择题分两部分考察，一部分考察语法如时态、虚拟语气等，应该是第一年出现这种题型，所以全靠语感来蒙；另一部分考察词汇也就是词汇辨析，认识自然就能得分了。完形填空和阅读理解比较简单，肯定是没有英语二难的，有没有四级难度都难说。作文一篇，题目大概是设想手机消失会发生什么，写的时候考研政治上头，经济、政治、文化、社会、生态一顿瞎写。</p><h4 id="笔试逻辑"><a href="#笔试逻辑" class="headerlink" title="笔试逻辑"></a>笔试逻辑</h4><p>　　逻辑题型参见 MBA 与 GCT 逻辑。一个题干可以有多个问题，四选一。考前做了几套卷子了解了一下题型，了解了常见的错误，除了上面所说的逻辑题还有一些小学数学题，比如「粗细蜡烛」、「圆形花圃间隔栽花」。</p><h4 id="笔试总结"><a href="#笔试总结" class="headerlink" title="笔试总结"></a>笔试总结</h4><p>　　总体来说笔试不难，英语和逻辑都有拿到 90 分以上的大佬，如果转录分数不高比如 25 分以下建议参加笔试。</p><h2 id="面试"><a href="#面试" class="headerlink" title="面试"></a>面试</h2><p>　　面试前一天准备了一晚上英语自我介绍，最终没有派上用场。到了面试地点，点完名要求现场做张卷子再面试，三选二。分别是「内切圆半径的简单计算」、「矩阵的简单计算」、「抽次品（条件概率）」。时间非常充沛，题目非常简单，然而考完研的我就把这些东西都忘了，最终只做出了一道半。</p><h4 id="英语面试"><a href="#英语面试" class="headerlink" title="英语面试"></a>英语面试</h4><p>　　做完之后就是排队等待面试。面试全程录像，进去之后将手上的准考证递给我右前方的老师，中间的老师和左边的老师先扫了眼个人材料，发现我是科班的，三个老师互相交流评价了一下「学校还不错嘛」，然后问了一下「你们的校长还是 XX 吗？」，由于没有印象被调侃「上了四年学连校长是谁都不知道？」、「你这个题目怎么就写对了一道。」随后开始面试，先由中间的郁莲教授面试英语，由于她的研究方向是软件理论，我本科学过《软件工程》。所以先提问「软件工程的英文怎么说？」答完这个问题之后噩梦开始，开始纯英文问答，由于我大四的《专业英语》被砍加上《软件工程》这门课已经遗忘干净，之后的提问如「Software engineering workflow」，只能非常尴尬的中英混答，表示我知道是什么然而用英语答不上来。</p><h4 id="技术面试"><a href="#技术面试" class="headerlink" title="技术面试"></a>技术面试</h4><p>　　熬完英语问答之后，左前方的老师瞄了一眼郁莲教授手中的成绩单，说「你数据结构学得不错，Prim 算法是贪心算法吗？」，我有些迟疑「应该是贪心算法吧。」然后让我分别解释了「Prim 算法」和「贪心算法」的概念，我用自己的话解释了一遍，其间老师一直在边听记录，还不忘安慰我「别紧张，能听出来你都懂。」</p><p>　　由于我在解释的时候类比了梯度下降。于是老师提问「局部最优点和全局最优点是什么关系？」「梯度下降可能存在一个点既是最高点又是最低点吗？」「鞍点是什么点？」由于紧张这几个问题我都答得不好，甚至答错了，回头查资料的时候发现老师在我答错的时候一直在引导我。问完了技术问题，老师边低头记录边问「有没有做过大数据分析的相关的项目」，我就坦白的回答「之前在学校里学过 Hadoop，做过 WordCount 实验，毕设的时候同组同学做了抗药性预测和推荐系统，答辩的时候我去听了。」「可以了，差不多了」拿着我的准考证的老师递过准考证和名单并让我签字，示意我可以离开了。离开时听到技术面的老师和其他老师交流「基础还不错。」</p><p>　　由于当时比较紧张，提前准备的关于深度学习的毕设的介绍也没能提起，实际上老师最后问有没有相关项目时可以提一下。不过最终还是被录取了，也就没有留下遗憾了。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　时间已经过了很久，记忆可能出现了偏差，可能存在遗忘和脑补的情况。&lt;del&gt;希望能给明年面试的学弟学妹们提供一些小小帮助&lt;/del&gt;。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Diary" scheme="https://www.iloft.xyz/tags/Diary/"/>
    
  </entry>
  
  <entry>
    <title>OpenWrt同网段桥接配置</title>
    <link href="https://www.iloft.xyz/archives/openwrt-bridge-wifi.html"/>
    <id>https://www.iloft.xyz/archives/openwrt-bridge-wifi.html</id>
    <published>2019-06-30T12:00:00.000Z</published>
    <updated>2019-06-30T12:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　最近组了个ITX主机，丐版主板不带无线网卡，坐的地方也没网口。因此尝试主机用网线连接从路由器，路由器之间通过WIFI桥接。</p><span id="more"></span> <p>　　主路由器为K3，从路由器为K2P同配置的友华1200js。家里的网段为172.16.0.0&#x2F;16，主路由器网关为172.16.0.1。尝试了两种方法来实现两路由器下设备在同一网段。</p><h2 id="优雅的方法（LAN-Bridge）"><a href="#优雅的方法（LAN-Bridge）" class="headerlink" title="优雅的方法（LAN Bridge）"></a>优雅的方法（LAN Bridge）</h2><p>　　比较优雅的方法是将从路由设置的IP设置在同一网段，如172.16.0.2，并关闭DHCP。按正常方式扫描WIFI并加入网络，再将接口从WWAN修改为LAN，最后删除自动创建的WWAN接口即可完成配置。<br><img src="/images/wwan-2-lan.jpg" alt="修改Network"><br>　　这种直接桥接的方式最为优雅，主路由器、从路由器、各路由器下的设备都在同一网段，无需路由器转发通讯。然而实际配置时出现了无线无法启动的问题。<br><img src="/images/wireless-not-associated.jpg" alt="无线无法启动"><br>　　经过搜索发现MT7615的驱动并不支持Wireless与Lan桥接模式。但在另一台路由器EA6350上成功实现。<br><img src="/images/bridge-not-allowed.jpg" alt="系统日志"> </p><h2 id="通用方法（Relay）"><a href="#通用方法（Relay）" class="headerlink" title="通用方法（Relay）"></a>通用方法（Relay）</h2><p>　　通用的方法是对LAN与WWAN进行转发，效率没有直接桥接高。</p><h4 id="加入网络"><a href="#加入网络" class="headerlink" title="加入网络"></a>加入网络</h4><p>　　这种方式从路由器与主路由器不在同一网段，如192.168.1.1，关闭DHCP。按正常方式扫描WIFI并加入网络。此时可以通过设置PC的网关为192.168.1.1正常上网。<br><img src="/images/join-wireless.jpg" alt="加入网络"></p><h4 id="安装软件包"><a href="#安装软件包" class="headerlink" title="安装软件包"></a>安装软件包</h4><p>　　为了实现转发，需要安装relayd和luci-proto-relay两个包，如果是一些定制固件可能已经包含了。</p><figure class="highlight cmake"><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">opkg update</span><br><span class="line">opkg <span class="keyword">install</span> luci-proto-relay</span><br></pre></td></tr></table></figure><h4 id="创建转发"><a href="#创建转发" class="headerlink" title="创建转发"></a>创建转发</h4><p>　　在网络接口点击创建新的网络接口，如命名为RELAY，选择接口类型为Relay bridge。<br><img src="/images/create-interface.jpg" alt="创建网络接口"><br>　　配置上一步创建的RELAY接口，将lan和wwan加入转发。<br><img src="/images/relay-configuration.jpg" alt="配置网络接口"></p><h4 id="配置防火墙"><a href="#配置防火墙" class="headerlink" title="配置防火墙"></a>配置防火墙</h4><p>　　完成转发配置之后需要配置防火墙，将Forward修改为为accept。<br><img src="/images/firewall-configuration.jpg" alt="配置防火墙"></p><h4 id="测试网络"><a href="#测试网络" class="headerlink" title="测试网络"></a>测试网络</h4><p>　　此时从路由器下的设备就可以通过DHCP获得正确的IP地址和网关了，可以正常联网并与局域网内的设备进行通信了。由于采用了Relay方式，路由追踪略微有些美中不足。<br><img src="/images/test-network.jpg" alt="测试网络"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　最近组了个ITX主机，丐版主板不带无线网卡，坐的地方也没网口。因此尝试主机用网线连接从路由器，路由器之间通过WIFI桥接。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Network" scheme="https://www.iloft.xyz/tags/Network/"/>
    
  </entry>
  
  <entry>
    <title>Travis CI + Hexo + COS 配置</title>
    <link href="https://www.iloft.xyz/archives/travisci-hexo-cos.html"/>
    <id>https://www.iloft.xyz/archives/travisci-hexo-cos.html</id>
    <published>2019-06-20T16:30:00.000Z</published>
    <updated>2019-06-20T16:30:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>　　之前一直使用Travis CI + Github Pages + Hexo + Cloudflare的方案托管博客，省心速度还将就。最近一段时间CF的443端口出现劣化现象，于是将博客重新迁移回腾讯云对象存储上了。并记录一下配置。</p><span id="more"></span> <h2 id="安装hexo-deployer-cos"><a href="#安装hexo-deployer-cos" class="headerlink" title="安装hexo-deployer-cos"></a>安装hexo-deployer-cos</h2><p>　　在package.json下的dependencies中添加：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;hexo-deployer-cos&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^1.0.0&quot;</span></span><br></pre></td></tr></table></figure><h2 id="配置hexo-deployer-cos"><a href="#配置hexo-deployer-cos" class="headerlink" title="配置hexo-deployer-cos"></a>配置hexo-deployer-cos</h2><h2 id="在-config-yml中填写配置信息。由于配置文件会上传到github中，为了安全文件不保存SecretID和SecretKey。配置Travis-CI"><a href="#在-config-yml中填写配置信息。由于配置文件会上传到github中，为了安全文件不保存SecretID和SecretKey。配置Travis-CI" class="headerlink" title="　　在_config.yml中填写配置信息。由于配置文件会上传到github中，为了安全文件不保存SecretID和SecretKey。配置Travis-CI"></a>　　在_config.yml中填写配置信息。由于配置文件会上传到github中，为了安全文件不保存SecretID和SecretKey。<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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="attr">deploy:</span> </span><br><span class="line">  <span class="attr">type:</span> <span class="string">cos</span></span><br><span class="line">  <span class="attr">appId:</span> <span class="number">1234567890</span></span><br><span class="line">  <span class="attr">secretId:</span> <span class="string">SecretId</span></span><br><span class="line">  <span class="attr">secretKey:</span> <span class="string">SecretKey</span></span><br><span class="line">  <span class="attr">bucket:</span> <span class="string">iloft-1234567890</span></span><br><span class="line">  <span class="attr">region:</span> <span class="string">ap-shanghai</span></span><br></pre></td></tr></table></figure><br>配置Travis-CI</h2><p>　　在Travis-CI项目设置界面中配置环境变量。<br><img src="/images/travis-environment-variables.jpg" alt="配置环境变量"><br>　　修改.travis.yml，与只部署到Github Pages相比，增加了将SecretId与SecretKey替换成为预设的环境变量的sed命令和hexo d命令。完整的部署到COS和Github Pages配置文件：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="attr">language:</span> <span class="string">node_js</span></span><br><span class="line"><span class="attr">node_js:</span> <span class="string">stable</span></span><br><span class="line"><span class="attr">cache:</span></span><br><span class="line">  <span class="attr">directories:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">node_modules</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># S: Build Lifecycle</span></span><br><span class="line"><span class="attr">install:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#before_script:</span></span><br><span class="line"> <span class="comment"># - npm install -g gulp</span></span><br><span class="line"></span><br><span class="line"><span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">sed</span> <span class="string">-i</span> <span class="string">&quot;s/SecretId/$&#123;SecretId&#125;/&quot;</span> <span class="string">_config.yml</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">sed</span> <span class="string">-i</span> <span class="string">&quot;s/SecretKey/$&#123;SecretKey&#125;/&quot;</span> <span class="string">_config.yml</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">hexo</span> <span class="string">clean</span> <span class="string">&amp;&amp;</span> <span class="string">hexo</span> <span class="string">g</span> <span class="string">&amp;&amp;</span> <span class="string">hexo</span> <span class="string">d</span></span><br><span class="line"></span><br><span class="line"><span class="attr">after_script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">clone</span> <span class="string">https://$&#123;GH_REF&#125;</span> <span class="string">.deploy_git</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">cd</span> <span class="string">.deploy_git</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">checkout</span> <span class="string">master</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">cd</span> <span class="string">../</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">mv</span> <span class="string">.deploy_git/.git/</span> <span class="string">./public/</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">cd</span> <span class="string">./public</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">init</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">config</span> <span class="string">user.name</span> <span class="string">&quot;Yu&quot;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">config</span> <span class="string">user.email</span> <span class="string">&quot;admin@iloft.xyz&quot;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">add</span> <span class="string">.</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">commit</span> <span class="string">-m</span> <span class="string">&quot;:memo:\ Update blog by CI&quot;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">git</span> <span class="string">push</span> <span class="string">--force</span> <span class="string">--quiet</span> <span class="string">&quot;https://$&#123;CI_TOKEN&#125;@$&#123;GH_REF&#125;&quot;</span> <span class="string">master:gh-pages</span></span><br><span class="line"><span class="comment"># E: Build LifeCycle</span></span><br><span class="line"></span><br><span class="line"><span class="attr">branches:</span></span><br><span class="line">  <span class="attr">only:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">master</span></span><br><span class="line"><span class="attr">env:</span></span><br><span class="line"> <span class="attr">global:</span></span><br><span class="line">   <span class="bullet">-</span> <span class="attr">GH_REF:</span> <span class="string">github.com/myloft/blog</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;　　之前一直使用Travis CI + Github Pages + Hexo + Cloudflare的方案托管博客，省心速度还将就。最近一段时间CF的443端口出现劣化现象，于是将博客重新迁移回腾讯云对象存储上了。并记录一下配置。&lt;/p&gt;</summary>
    
    
    
    
    <category term="hexo" scheme="https://www.iloft.xyz/tags/hexo/"/>
    
  </entry>
  
</feed>
