<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://foxlesbiao.github.io/</id>
  <title type="text">Illya·Lily·Iris·Alice</title>
  <subtitle type="text">修车、折腾、写代码</subtitle>
  <updated>2026-10-28T00:00:00.000Z</updated>
  <author><name>Illya-Lily-Iris·Alice</name></author>
  <link rel="alternate" href="https://foxlesbiao.github.io/"/>
  <link rel="self" href="https://foxlesbiao.github.io/atom.xml"/>
  <generator uri="https://github.com/foxlesbiao/blog">Firefly v6.16.8</generator>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/hermes/2026-10-28-oppo-health-cloud-mcp-implementation/</id>
      <title type="text">OPPO 健康云 MCP：从登录到本地 AI 分析的完整实现</title>
      <published>2026-10-28T00:00:00.000Z</published>
      <updated>2026-10-28T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/hermes/2026-10-28-oppo-health-cloud-mcp-implementation/"/>
      <summary type="text">把 OPPO 健康云数据接入 MCP：实验性账号登录、随机设备 ID、本地 SQLite 缓存、67 个健康指标的只读同步与趋势分析。</summary>
      <content type="html"><![CDATA[<section><h1>OPPO 健康云 MCP：从登录到本地 AI 分析的完整实现<a href="#oppo-健康云-mcp从登录到本地-ai-分析的完整实现"><span>#</span></a></h1><section><h2>这一篇写什么<a href="#这一篇写什么"><span>#</span></a></h2><p>之前写过<a href="/posts/hermes/2026-09-18-oppo-health-cloud-reversing-whitebox-signature">《OPPO 健康云同步逆向：从抓包到白盒签名的死局（已破）》</a>。那篇的重点是逆向过程：为什么一开始被白盒签名困住，后来才发现健康云数据接口走的是更浅的静态 HMAC 签名路径。</p><p>这篇文章不再重复逆向推演，而是把破局后的结果落成可用项目：<a href="https://github.com/foxlesbiao/oppo-health-cloud-mcp" target="_blank">oppo-health-cloud-mcp</a>。它是一个本机运行的 OPPO 健康（欢太健康）云端只读 MCP server，可以同步本人账号的数据到本地 SQLite，再让支持 MCP 的 AI 客户端查询和做周期分析。</p><p>一句话概括：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>欢太账号登录 / 已登录 App 凭据</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>健康云只读 API</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>本地 SQLite 缓存</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>MCP stdio server</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>9</div></div><div><span>AI 客户端查询 / 分析</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>先说边界<a href="#先说边界"><span>#</span></a></h2><p>这个项目有几个必须先讲清楚的边界：</p><ul>
<li>它不是 OPPO 官方 API，也没有得到 OPPO 官方授权。</li>
<li>它只做云端数据的读取和本地缓存，不提供写入、删除、修改健康数据的接口。</li>
<li>它只应该访问本人账号。健康数据非常敏感，不要拿它做多人共享服务，更不要尝试访问他人数据。</li>
<li>它服务端结构可能随 OPPO 健康 App 版本变化，项目适合作为学习和个人研究工具，而不是稳定商业服务。</li>
</ul><p>还有两个常见误解：</p><ol>
<li><strong>Web 端的欢太云备份不是 OPPO 健康云同步。</strong> 前者更多是云备份页面，后者是健康 App 的系统化云端数据通道，协议和认证并不相同。</li>
<li><strong>OPPO 健康不一定要靠系统 App 固定身份才能用。</strong> 登录器走的是欢太账号验证码登录，后续再换取健康包级 token；这和从已登录 App 私有文件里提取凭据是两条不同路线。</li>
</ol></section><section><h2>项目能力<a href="#项目能力"><span>#</span></a></h2><p>当前 MCP server 暴露四个工具：</p>

<table><thead><tr><th>工具</th><th>说明</th></tr></thead><tbody><tr><td><code>cloud_sync(metric, days=7)</code></td><td>拉取指定指标的云端数据，并写入本地 SQLite。</td></tr><tr><td><code>cloud_query(metric, date=None)</code></td><td>查询本地缓存；不指定日期时返回最近 100 条。</td></tr><tr><td><code>cloud_analyze(days=7)</code></td><td>汇总最近周期与上一周期，输出心率、血氧、步数、睡眠等趋势。</td></tr><tr><td><code>cloud_status</code></td><td>检查认证文件、缓存数据库和各指标的缓存数量。</td></tr></tbody></table><p>项目目前覆盖 67 个 MCP 指标：65 个独立指标，加上 <code>stress</code> 和 <code>physical_mental</code> 两个组合指标。常见类别包括：</p>
<strong>查看指标分类</strong>

<table><thead><tr><th>类别</th><th>示例指标</th></tr></thead><tbody><tr><td>心率</td><td><code>heart_rate</code>, <code>heart_rate_detail</code>, <code>heart_rate_stat</code>, <code>heart_rate_warning</code></td></tr><tr><td>血氧</td><td><code>blood_oxygen</code>, <code>blood_oxygen_detail</code>, <code>blood_oxygen_stat</code>, <code>blood_oxygen_warning</code>, <code>blood_oxygen_origin</code></td></tr><tr><td>血压</td><td><code>blood_pressure</code>, <code>blood_pressure_stat</code></td></tr><tr><td>血糖</td><td><code>blood_sugar</code>, <code>blood_sugar_stat</code>, <code>blood_sugar_warning</code></td></tr><tr><td>步数 / 运动</td><td><code>steps</code>, <code>steps_detail</code>, <code>steps_stat</code>, <code>sport_stat</code>, <code>sport_record</code>, <code>sport_goal</code></td></tr><tr><td>睡眠</td><td><code>sleep</code>, <code>sleep_stat</code>, <code>sleep_status</code>, <code>sleep_index</code>, <code>sleep_rr</code>, <code>sleep_advice</code></td></tr><tr><td>HRV / 压力</td><td><code>hrv</code>, <code>stress_detail</code>, <code>stress_stat</code>, <code>stress</code></td></tr><tr><td>身心健康</td><td><code>physical_mental_detail</code>, <code>physical_mental_stat</code>, <code>physical_mental</code>, <code>physical_mental_achievement</code></td></tr><tr><td>呼吸 / 心血管</td><td><code>breath_rate</code>, <code>atrial_fibril</code>, <code>cardiovascular</code></td></tr><tr><td>腕温 / ECG</td><td><code>wrist_temperature</code>, <code>wrist_temperature_detail</code>, <code>ecg</code></td></tr><tr><td>打鼾 / OSA</td><td><code>snore_osa_detail</code>, <code>snore_osa_sum</code>, <code>snore_env_noise</code>, <code>snore_feature</code>, <code>sensor_osa</code></td></tr><tr><td>体重</td><td><code>weight</code>, <code>weight_target</code></td></tr><tr><td>其他记录</td><td><code>tumble</code>, <code>sedentary</code>, <code>hearing</code>, <code>cervical_stat</code>, <code>sunshine</code>, <code>vitamin</code></td></tr><tr><td>女性健康</td><td><code>physiological_cycle</code>, <code>physiological_symptoms</code>, <code>ovulation</code></td></tr><tr><td>应用使用</td><td><code>app_usage</code>, <code>app_usage_detail</code></td></tr><tr><td>健康洞察</td><td><code>insight</code></td></tr></tbody></table><p>接口存在不代表账号里一定有数据。比如某项健康功能没有开启、设备不支持，或者过去没有记录，<code>cloud_sync</code> 成功但返回空集都是正常情况。另外，<code>wrist_temperature</code> 是相对个人基线的统计，不是体温计意义上的绝对体温；<code>sport_file</code> 还需要 App 运行时生成的密钥，当前项目未支持。</p></section><section><h2>登录与认证<a href="#登录与认证"><span>#</span></a></h2><section><h3>推荐路线：实验性登录器<a href="#推荐路线实验性登录器"><span>#</span></a></h3><p>仓库里的 <code>oppo_login.py</code> 实现了一个实验性登录器：</p><ol>
<li>用手机号接收验证码并完成欢太账号登录。</li>
<li>如果服务端触发二次认证，继续要求身份证后 6 位。</li>
<li>换取健康包级 token，并完成健康云账号登录。</li>
<li>把 <code>token</code>、<code>token_auth_id</code>、<code>ssoid</code> 等最小认证信息写入 <code>auth.json</code>。</li>
</ol><p>这条路线不需要 root，也不需要 ADB。服务端没有触发二次认证时，就不需要输入身份证信息。</p><p>登录器做了几层脱敏：</p><ul>
<li>手机号、验证码、身份证后 6 位和 token 不回显。</li>
<li>账号刷新 token 不写入磁盘。</li>
<li><code>auth.json</code> 使用 <code>0600</code> 权限保存。</li>
</ul></section><section><h3>备用路线：从本人已登录 App 提取凭据<a href="#备用路线从本人已登录-app-提取凭据"><span>#</span></a></h3><p>如果不想跑实验性登录器，也可以从本人设备上已登录的 OPPO 健康 App 私有账号文件读取凭据，再通过只读接口获取 <code>ssoid</code>。这条路需要 root/ADB，适合已经拥有本机 root 权限的用户。仓库里的认证手册写了具体步骤。</p><p>最小 <code>auth.json</code> 结构大致是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"token"</span><span>: </span><span>"..."</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"token_auth_id"</span><span>: </span><span>"..."</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"ssoid"</span><span>: </span><span>"..."</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个字段必须来自同一个账号。<code>auth.json</code> 等同于敏感凭据，不要提交、截图、粘贴，也不要放进公共目录。</p></section></section><section><h2>设备 ID 和多账号隔离<a href="#设备-id-和多账号隔离"><span>#</span></a></h2><p>早期方案里有一个隐患：如果多个用户复用同一组设备身份，服务端很容易把它们视为异常设备行为。现在已经改成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>每次登录默认生成随机 OpenSDK device ID</span></div></div><div><div><div>2</div></div><div><span>同一登录内的 DUID source ID 默认与其一致</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也就是说，普通用户不需要手工配置设备 ID。只有确实需要固定身份时，才显式传入参数或环境变量：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>OPPO_HEALTH_DEVICE_ID</span><span>=</span><span>"&lt;32 位 hex&gt;"</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>OPPO_HEALTH_SDK_DEVICE_ID</span><span>=</span><span>"&lt;32 位 hex&gt;"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>多账号本机隔离时，应该同时隔离认证文件和缓存数据库：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>OPPO_HEALTH_AUTH_FILE</span><span><span>=</span><span>/</span></span><span>secure</span><span>/</span><span>path</span><span>/</span><span>account-a-auth</span><span>.</span><span>json</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>OPPO_HEALTH_CACHE_DB</span><span><span>=</span><span>/</span></span><span>secure</span><span>/</span><span>path</span><span>/</span><span>account-a-cache</span><span>.</span><span>db</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不等于鼓励把项目部署成多人共享服务。当前设计仍然假设“一台机器、一个用户、一套本地缓存”。</p></section><section><h2>接入 MCP 客户端<a href="#接入-mcp-客户端"><span>#</span></a></h2><p>安装依赖：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>python3</span><span> </span><span>-m</span><span> </span><span>pip</span><span> </span><span>install</span><span> </span><span>"mcp[cli]"</span><span> </span><span>requests</span><span> </span><span>pycryptodome</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 MCP 客户端里注册 stdio server：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"mcpServers"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"oppo-health-cloud"</span><span>: {</span></div></div><div><div><div>4</div></div><div><span>      </span><span>"command"</span><span>: </span><span>"python3"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"args"</span><span>: [</span><span>"/absolute/path/to/oppo-health-cloud-mcp/cloud_mcp.py"</span><span>]</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>首次验证可以这样跑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cloud_status</span></div></div><div><div><div>2</div></div><div><span>cloud_sync(metric="steps", days=1)</span></div></div><div><div><div>3</div></div><div><span>cloud_query(metric="steps", date="2026-10-28")</span></div></div><div><div><div>4</div></div><div><span>cloud_analyze(days=7)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cloud_status</code> 只检查本地状态；云端可用性还是要跑一次只读同步接口确认。</p></section><section><h2>本地缓存模型<a href="#本地缓存模型"><span>#</span></a></h2><p>缓存文件默认是仓库目录下的 <code>cloud_cache.db</code>，主表结构很直接：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>CREATE</span><span> </span><span>TABLE</span><span> </span><span>IF</span><span> </span><span>NOT</span><span> </span><span>EXISTS</span><span> cloud_data(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>metric </span><span>TEXT</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>date</span><span> </span><span>TEXT</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>payload </span><span>TEXT</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>fetched_at </span><span>INTEGER</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>PRIMARY KEY</span><span>(metric, </span><span>date</span><span>)</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里做了一个取舍：</p><ul>
<li><strong>原始 payload 保留 JSON 字符串。</strong> 不同健康指标的返回结构差异很大，强行拍平会导致每个指标一套表结构，维护成本高。</li>
<li><strong>查询时再解析 JSON。</strong> 对个人数据量来说，这个代价可以接受。</li>
<li><strong>同一 <code>(metric, date)</code> 覆盖写入。</strong> 重复同步不会无限膨胀。</li>
</ul><p><code>cloud_analyze</code> 基于本地缓存计算两个周期：</p><ul>
<li>最近 N 天。</li>
<li>再往前一个 N 天。</li>
</ul><p>它目前会汇总心率、血氧、步数、千卡、睡眠时长、深睡、REM 和最新体重，并给出简单异常标记。口径也会明确写进报告，例如：</p><ul>
<li>步数和千卡按全天各模式累加。</li>
<li>睡眠在同一日有多条记录时取总时长最长的一条。</li>
<li>心率使用指定类型下的单点均值，不等价于静息心率。</li>
</ul></section><section><h2>请求与签名设计<a href="#请求与签名设计"><span>#</span></a></h2><p>健康云请求的核心不是“能拼出 URL”，而是保持请求形态足够接近客户端：</p><ol>
<li>构造规范化签名字符串。</li>
<li>用静态应用密钥计算 HMAC 签名。</li>
<li>保留精确的请求头集合。</li>
<li>通过 <code>curl</code> 子进程发送请求。</li>
</ol><p>这里有几个实测结论：</p>

<table><thead><tr><th>问题</th><th>原因</th><th>处理</th></tr></thead><tbody><tr><td>Python <code>urllib/requests</code> 被网关拦截</td><td>TLS 指纹不符合客户端形态</td><td>核心请求使用 <code>curl</code> 子进程</td></tr><tr><td>多加请求头后签名失败</td><td>请求头参与签名或网关校验</td><td>不随意增删请求头</td></tr><tr><td>明细接口报时间范围错误</td><td>单次请求范围有限制</td><td>按天或版本游标分页</td></tr><tr><td>部分指标直接拉不到</td><td>服务端要求先查版本列表</td><td>先查版本，再按版本读详情</td></tr></tbody></table><p>所以 <code>cloud_sync</code> 内部不是单个 HTTP 请求，而是一层适配器：按指标选择端点、展开组合指标、处理分页、归一化日期、缓存结果，再返回给 MCP 客户端。</p></section><section><h2>为什么不用 Web 或系统备份接口<a href="#为什么不用-web-或系统备份接口"><span>#</span></a></h2><p>我最初的目标不是做一个“云备份爬虫”，而是让 AI 能读取本人健康数据的明细。两者需求不同：</p><ul>
<li>云备份更偏设备资料恢复和备份管理。</li>
<li>健康云同步保存的是心率、血氧、睡眠、运动等明细，适合后续趋势分析。</li>
<li>系统 App 的本地数据库权限和私有账号文件依赖系统环境，不适合作为长期 PC 端 MCP 的基础。</li>
</ul><p>因此项目最终选择健康 App 的云端只读通道，而不是 Web 备份页面或本地数据库 hook。</p></section><section><h2>数据安全和实际风险<a href="#数据安全和实际风险"><span>#</span></a></h2><p>健康数据的敏感等级比一般网页 token 高得多。使用这个项目时，应该把两类文件都当作私密数据：</p>

<table><thead><tr><th>文件</th><th>内容</th><th>风险</th></tr></thead><tbody><tr><td><code>auth.json</code></td><td>账号认证凭据</td><td>可能读取本人健康云数据</td></tr><tr><td><code>cloud_cache.db</code></td><td>本地健康数据缓存</td><td>可能包含心率、睡眠、位置相关运动记录、应用使用等明细</td></tr></tbody></table><p>建议：</p><ul>
<li>只在本机可信目录保存认证和缓存文件。</li>
<li>不要把 MCP 输出直接粘贴到公开群组或网页。</li>
<li>如果 AI 客户端会把上下文写入日志，确认你愿意让这些日志接触健康数据。</li>
<li>如果凭据泄露，尽快在欢太账号或 OPPO 健康里退出登录、修改密码，并删除本地 <code>auth.json</code> 和缓存数据库。</li>
</ul><p>这也是为什么项目不适合部署成多人共享服务。它不是“一个服务端支持所有用户”的形态，而是“一个用户在本机控制自己的数据”。</p></section><section><h2>适合的使用方式<a href="#适合的使用方式"><span>#</span></a></h2><p>这类工具比较适合几个场景：</p><ol>
<li><strong>周期复盘</strong>：让 AI 读取最近 7 天、30 天的数据，找趋势和异常点。</li>
<li><strong>本地数据整理</strong>：把云端 JSON 收敛成个人可搜索、可分析的 SQLite 数据。</li>
<li><strong>跨 App 自动化</strong>：在支持 MCP 的客户端里把健康数据和其他日记、工作、训练记录放在一起分析。</li>
<li><strong>设备切换后的历史分析</strong>：只要云端仍保留数据，就不再依赖某台手机的本地数据库。</li>
</ol><p>不适合的使用方式也很明确：</p><ul>
<li>不要给陌生人代跑。</li>
<li>不要把 token 打包进镜像、容器或笔记仓库。</li>
<li>不要让 MCP server 暴露到公网。</li>
<li>不要把工具当成医疗诊断器。异常标记只是阈值提示，不能替代专业医疗判断。</li>
</ul></section><section><h2>后续方向<a href="#后续方向"><span>#</span></a></h2><p>基础同步、查询、分析和登录已经跑通，后面我会优先做两件小事：</p><ol>
<li>把 <code>cloud_analyze</code> 的报告格式再收敛，让 AI 更稳定地输出“周期变化、异常点和下一步问题”，而不是把大段原始数据丢给模型。</li>
<li>继续完善错误提示，把 token 过期、TLS 指纹拦截、时间范围超限这类常见问题解释得更清楚。</li>
</ol><p>项目地址：</p><p><a href="https://github.com/foxlesbiao/oppo-health-cloud-mcp" target="_blank">https://github.com/foxlesbiao/oppo-health-cloud-mcp</a></p><p>如果你想看签名为什么一开始被误判成“死局”，可以先读上一篇文章；如果你只想让 AI 读取自己的 OPPO 健康数据，直接从这个仓库的 README 开始就可以。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/hermes/2026-10-23-xiaoheihe-mcp-signing-implementation/</id>
      <title type="text">小黑盒 MCP：从抓包到 Native 签名的完整逆向</title>
      <published>2026-10-23T00:00:00.000Z</published>
      <updated>2026-10-23T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/hermes/2026-10-23-xiaoheihe-mcp-signing-implementation/"/>
      <summary type="text">用 Unidbg 复现小黑盒 Android App 的 hkey/_rnd 签名，从抓包分析到 Unidbg JNI 环境搭建的完整过程。</summary>
      <content type="html"><![CDATA[<section><h1>小黑盒 MCP：从抓包到 Native 签名的完整逆向<a href="#小黑盒-mcp从抓包到-native-签名的完整逆向"><span>#</span></a></h1><section><h2>背景<a href="#背景"><span>#</span></a></h2><p><a href="https://github.com/foxlesbiao/xiaoheihe-mcp" target="_blank">codex-mcp</a> 是我给小黑盒社区做的只读 MCP Server，支持搜索、话题、游戏、帖子浏览和登录态管理。仓库本身不含签名算法源码——签名器以外部命令方式提供，用户需要自行实现。这篇文章完整记录签名算法的逆向过程和实现细节，给同样想走这条路的人参考。</p></section><section><h2>API 请求签名概览<a href="#api-请求签名概览"><span>#</span></a></h2><p>小黑盒 API 请求需要两类签名参数：</p>

<table><thead><tr><th>签名类型</th><th>关键参数</th><th>使用场景</th></tr></thead><tbody><tr><td>Web 签名</td><td><code>hkey</code>、<code>_rnd</code>、<code>_time</code>、<code>nonce</code></td><td>公开 Web API 浏览</td></tr><tr><td>Native 签名</td><td><code>hkey</code>、<code>_rnd</code>（由 SO 计算）</td><td>登录、已登录功能</td></tr></tbody></table><p>两者输出格式相似但计算路径完全不同。Web 签名是纯算法，Native 签名由 Android SO 中的 JNI 方法计算。</p></section><section><h2>抓包分析<a href="#抓包分析"><span>#</span></a></h2><p>用 Charles 做 HTTPS 中间人抓包，每个请求都带：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>_time=1700000000</span></div></div><div><div><div>2</div></div><div><span>nonce=aBcD...32位随机字符</span></div></div><div><div><div>3</div></div><div><span>hkey=XXXX0000</span></div></div><div><div><div>4</div></div><div><span>_rnd=15:hex_string</span></div></div><div><div><div>5</div></div><div><span>version=999.0.4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 Frida hook <code>okhttp3.Request</code>，定位到签名在 <code>com.graphice.shaderar.ShaderManager</code>（JNI 对应 <code>libglesv3_1.so</code>）和 <code>com.max.xiaoheihe.utils.NDKTools</code>（JNI 对应 <code>libnative-lib.so</code>）中生成。</p></section><section><h2>Web 签名算法<a href="#web-签名算法"><span>#</span></a></h2><p>Web 签名用于非登录状态的公开 API，纯算法可复现，不依赖 SO。</p><section><h3>hkey 计算<a href="#hkey-计算"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>R_CHARSET</span><span> </span><span>=</span><span> </span></span><span>"AB45STUVWZEFGJ6CH01D237IXYPQRKLMN89"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span># 1. timestamp+1 经字符表映射（取前 2 个字符的 ord % 2）</span></div></div><div><div><div>4</div></div><div><span><span>part_time </span><span>=</span><span> </span><span>tw</span><span>(</span></span><span>str</span><span><span>(timestamp </span><span>+</span><span> </span></span><span>1</span><span><span>), </span><span>R_CHARSET</span><span>, </span></span><span>2</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span># 2. 路径规范化：/api/endpoint/ → 每字符 ord % len(R_CHARSET) 映射</span></div></div><div><div><div>7</div></div><div><span><span>part_path </span><span>=</span><span> </span><span>rw</span><span>(normalized_path, </span><span>R_CHARSET</span><span>)</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># 3. nonce 同样映射</span></div></div><div><div><div>10</div></div><div><span><span>part_nonce </span><span>=</span><span> </span><span>rw</span><span>(nonce, </span><span>R_CHARSET</span><span>)</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span># 4. 三段交错拼接取前 20 字符 → MD5</span></div></div><div><div><div>13</div></div><div><span><span>combined </span><span>=</span><span> </span><span>interleave</span><span>([part_time, part_path, part_nonce])[:</span></span><span>20</span><span>]</span></div></div><div><div><div>14</div></div><div><span><span>digest </span><span>=</span><span> </span><span>md5</span><span>(combined.</span><span>encode</span><span>()).</span><span>hexdigest</span><span>()</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span># 5. digest 后 6 字符做自定义列混淆（位运算 + XOR）</span></div></div><div><div><div>17</div></div><div><span><span>transformed </span><span>=</span><span> </span><span>mix_columns</span><span>([</span></span><span>ord</span><span>(c) </span><span>for</span><span> c </span><span>in</span><span><span> digest[</span><span>-</span></span><span>6</span><span>:]])</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span># 6. hkey = 映射(digest[:5]) + pad(sum(transformed) % 100, 2)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>其中 <code>mix_columns</code> 用到一组位运算变换链：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>ty</span><span>(</span><span>v</span><span>): </span><span>return</span><span><span> ((v </span><span>&lt;&lt;</span><span> </span></span><span>1</span><span><span>) </span><span>^</span><span> </span></span><span>27</span><span><span>) </span><span>&amp;</span><span> </span></span><span>255</span><span> </span><span>if</span><span><span> v </span><span>&amp;</span><span> </span></span><span>128</span><span> </span><span>else</span><span><span> v </span><span>&lt;&lt;</span><span> </span></span><span>1</span></div></div><div><div><div>2</div></div><div><span>def</span><span> </span><span>da</span><span>(</span><span>v</span><span>): </span><span>return</span><span><span> </span><span>ty</span><span>(v) </span><span>^</span><span> v</span></span></div></div><div><div><div>3</div></div><div><span>def</span><span> </span><span>vs</span><span>(</span><span>v</span><span>): </span><span>return</span><span><span> </span><span>da</span><span>(</span><span>ty</span><span>(v))</span></span></div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>dol_c</span><span>(</span><span>v</span><span>): </span><span>return</span><span><span> </span><span>vs</span><span>(</span><span>da</span><span>(</span><span>ty</span><span>(v)))</span></span></div></div><div><div><div>5</div></div><div><span>def</span><span> </span><span>dl</span><span>(</span><span>v</span><span>): </span><span>return</span><span><span> </span><span>dol_c</span><span>(v) </span><span>^</span><span> </span><span>vs</span><span>(v) </span><span>^</span><span> </span><span>da</span><span>(v)</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 4x4 列混淆</span></div></div><div><div><div>8</div></div><div><span><span>result[i] </span><span>=</span><span> </span><span>dl</span><span>(a) </span><span>^</span><span> </span><span>dol_c</span><span>(b) </span><span>^</span><span> </span><span>vs</span><span>(c) </span><span>^</span><span> </span><span>da</span><span>(d)  </span></span><span># 按不同顺序组合</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>_rnd 计算<a href="#_rnd-计算"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>RND_KEY</span><span> </span><span>=</span><span> </span></span><span>"Z7mFG4tQp9Ws2LxB8H"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>message </span><span>=</span><span> (</span><span>RND_KEY</span><span> </span><span>+</span><span> nonce </span><span>+</span><span> </span></span><span>f</span><span>"</span><span>{</span><span>timestamp</span><span>}</span><span>:</span><span>{</span><span>nonce</span><span>}</span><span>"</span><span><span>).</span><span>encode</span><span>()</span></span></div></div><div><div><div>4</div></div><div><span><span>_rnd </span><span>=</span><span> </span></span><span>"15:"</span><span><span> </span><span>+</span><span> hmac.</span><span>new</span><span>(</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>RND_KEY</span><span>.</span><span>encode</span><span>(), message, hashlib.sha256</span></div></div><div><div><div>6</div></div><div><span><span>).</span><span>hexdigest</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>Native App 签名（Unidbg）<a href="#native-app-签名unidbg"><span>#</span></a></h2><p>Native 签名由 SO 库计算，无法用纯 Python 复现。方案是 <a href="https://github.com/zhkl0228/unidbg" target="_blank">Unidbg</a> 模拟执行。</p><section><h3>签名调用链<a href="#签名调用链"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1. 初始化 SO：</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>ShaderManager.setParseDepth("/tmp/xhh-cache", false)</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>ShaderManager.setSecLevel()</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>ShaderManager.setBlendMode(BLEND_SEED, timestamp)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>2. 获取 chunk flag：</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>ShaderManager.getChunkFlag(context, CHUNK_SEED) → chunk_flag</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>3. 生成 nonce：</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>ShaderManager.getIdxOffset(context, chunk_flag, timestamp, identity) → nonce</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>4. 设置签名上下文：</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>ShaderManager.setViewport(timestamp, nonce)</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>ShaderManager.setGramLen(path, nonce)</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>ShaderManager.setBuf(timestamp, nonce)</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>ShaderManager.setDepRel(device_model, nonce)</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>ShaderManager.setDLen(os_version, nonce)</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>ShaderManager.setPtrOffset(app_version, nonce)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>5. 编码：</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>NDKTools.encode(context, path, timestamp, nonce)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>6. 获取签名：</span></div></div><div><div><div>24</div></div><div><span><span>   </span></span><span>ShaderManager.getObjType(context, nonce, false) → hkey</span></div></div><div><div><div>25</div></div><div><span><span>   </span></span><span>ShaderManager.getObjType(context, nonce, true)  → rnd_suffix</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>7. 输出：</span></div></div><div><div><div>28</div></div><div><span><span>   </span></span><span>_rnd = security_version_or_os_version + ":" + rnd_suffix</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>种子常量<a href="#种子常量"><span>#</span></a></h3><p>从 SO 中提取的两个关键种子：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>CHUNK_SEED = "PAENEHAMGACOBHIEMIHIJLKJPMMHJMMQABCNGBPPENCENP"</span></div></div><div><div><div>2</div></div><div><span>BLEND_SEED = "MFANEHAMGACOBHIEMIHIJLKJPMMHJMMLABCNGBPPENCENPOM"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>JNI 环境模拟<a href="#jni-环境模拟"><span>#</span></a></h3><p>Unidbg 需要模拟 Android 环境中 SO 期望的所有 JNI 调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>getPackageName</span><span>()     → </span><span>"com.max.xiaoheihe"</span></div></div><div><div><div>2</div></div><div><span>getFilesDir</span><span>()        → </span><span>"/data/user/0/com.max.xiaoheihe/files"</span></div></div><div><div><div>3</div></div><div><span>getCacheDir</span><span>()        → </span><span>"/data/user/0/com.max.xiaoheihe/cache"</span></div></div><div><div><div>4</div></div><div><span>Build</span><span>.</span><span>MODEL</span><span>          → 设备型号</span></div></div><div><div><div>5</div></div><div><span>Build</span><span>.</span><span>BRAND</span><span>          → </span><span>"vivo"</span></div></div><div><div><div>6</div></div><div><span>Build</span><span>.</span><span>VERSION</span><span>.</span><span>RELEASE</span><span> → Android 版本</span></div></div><div><div><div>7</div></div><div><span>PackageInfo</span><span>.</span><span>signatures</span><span> → 从 APK 解析证书</span></div></div><div><div><div>8</div></div><div><span>getSharedPreferences</span><span><span>(</span><span>...</span><span>) → 空实现</span></span></div></div><div><div><div>9</div></div><div><span>getString</span><span>(</span><span>"DebugInfo"</span><span><span>) → imei </span><span>+</span><span> identity</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>安全上下文注入<a href="#安全上下文注入"><span>#</span></a></h3><p>从真机抓包的 <code>/account/getui/fix</code> 响应中获取 <code>security_code</code> 和 <code>security_version</code>，在签名前注入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ShaderManager</span><span>.</span><span>setHdrField</span><span>(</span><span>Base64</span><span>.</span><span>decode</span><span>(security_code))</span></div></div><div><div><div>2</div></div><div><span>ShaderManager</span><span>.</span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>true</span><span>              </span><span>// 静态 boolean</span></div></div><div><div><div>3</div></div><div><span>ShaderManager</span><span>.</span><span>c</span><span><span> </span><span>=</span><span> security_version  </span></span><span>// 静态 int</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这会影响 <code>_rnd</code> 前缀：用 <code>security_version</code> 替代 <code>os_version</code>。</p></section><section><h3>Unidbg 环境搭建<a href="#unidbg-环境搭建"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>AndroidEmulator</span><span> emulator </span><span><span>=</span><span> </span></span><span>AndroidEmulatorBuilder</span><span>.</span><span>for64Bit</span><span>()</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>.</span><span>setProcessName</span><span>(</span><span>"com.max.xiaoheihe"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>.</span><span>addBackendFactory</span><span>(</span><span>new</span><span> </span><span>Unicorn2Factory</span><span>(</span><span>true</span><span>))</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>.</span><span>build</span><span>();</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>emulator</span><span>.</span><span>getSyscallHandler</span><span>().</span><span>addIOResolver</span><span>(</span><span>this</span><span>);</span></div></div><div><div><div>7</div></div><div><span>Memory</span><span> memory </span><span><span>=</span><span> </span></span><span>emulator</span><span>.</span><span>getMemory</span><span>();</span></div></div><div><div><div>8</div></div><div><span>memory</span><span>.</span><span>setLibraryResolver</span><span>(</span><span>new</span><span> </span><span>AndroidResolver</span><span>(</span><span>23</span><span>));</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>VM</span><span> vm </span><span><span>=</span><span> </span></span><span>emulator</span><span>.</span><span>createDalvikVM</span><span>(</span><span>new</span><span> </span><span>File</span><span>(resources, </span><span>"minimal-app.apk"</span><span>));</span></div></div><div><div><div>11</div></div><div><span>vm</span><span>.</span><span>setVerbose</span><span>(</span><span>false</span><span>);</span></div></div><div><div><div>12</div></div><div><span>vm</span><span>.</span><span>setJni</span><span>(</span><span>this</span><span>);</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>DalvikModule</span><span> shaderModule </span><span><span>=</span><span> </span></span><span>vm</span><span>.</span><span>loadLibrary</span><span>(</span></div></div><div><div><div>15</div></div><div><span>    </span><span>new</span><span> </span><span>File</span><span>(resources, </span><span>"libglesv3_1.so"</span><span>), </span><span>true</span><span>);</span></div></div><div><div><div>16</div></div><div><span>shaderModule</span><span>.</span><span>callJNI_OnLoad</span><span>(emulator);</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>DalvikModule</span><span> nativeModule </span><span><span>=</span><span> </span></span><span>vm</span><span>.</span><span>loadLibrary</span><span>(</span></div></div><div><div><div>19</div></div><div><span>    </span><span>new</span><span> </span><span>File</span><span>(resources, </span><span>"libnative-lib.so"</span><span>), </span><span>true</span><span>);</span></div></div><div><div><div>20</div></div><div><span>nativeModule</span><span>.</span><span>callJNI_OnLoad</span><span>(emulator);</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>DvmClass</span><span> shaderManager </span><span><span>=</span><span> </span></span><span>vm</span><span>.</span><span>resolveClass</span><span>(</span><span>"com/graphice/shaderar/ShaderManager"</span><span>);</span></div></div><div><div><div>23</div></div><div><span>DvmClass</span><span> ndkTools </span><span><span>=</span><span> </span></span><span>vm</span><span>.</span><span>resolveClass</span><span>(</span><span>"com/max/xiaoheihe/utils/NDKTools"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section><section><h2>SMS 登录加密<a href="#sms-登录加密"><span>#</span></a></h2><p>App 使用 RSA PKCS#1 v1.5 加密手机号（1024 位，公钥从 APK 提取）。公钥本身不是秘密，加密流程就是标准 RSA：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 从 DER 提取 modulus 和 exponent</span></div></div><div><div><div>2</div></div><div><span><span>modulus </span><span>=</span><span> </span><span>...</span><span> </span></span><span># 1024 bit</span></div></div><div><div><div>3</div></div><div><span><span>exponent </span><span>=</span><span> </span></span><span>65537</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># PKCS#1 v1.5 填充</span></div></div><div><div><div>6</div></div><div><span><span>block </span><span>=</span><span> </span></span><span>b</span><span>"</span><span>\x00\x02</span><span>"</span><span><span> </span><span>+</span><span> random_nonzero_padding </span><span>+</span><span> </span></span><span>b</span><span>"</span><span>\x00</span><span>"</span><span><span> </span><span>+</span><span> phone_bytes</span></span></div></div><div><div><div>7</div></div><div><span><span>cipher </span><span>=</span><span> </span></span><span>pow</span><span>(</span><span>int</span><span><span>.</span><span>from_bytes</span><span>(block, </span></span><span>"big"</span><span>), exponent, modulus)</span></div></div><div><div><div>8</div></div><div><span><span>result </span><span>=</span><span> base64.</span><span>b64encode</span><span>(cipher.</span><span>to_bytes</span><span>(</span></span><span>128</span><span>, </span><span>"big"</span><span>))</span></div></div><div><div><div>9</div></div><div><span># 按 76 字符换行</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>登录请求还需要一组设备指纹查询参数（<code>imei</code>、<code>device_info</code>、<code>os_version</code> 等），与签名一起发送。</p></section><section><h2>外部签名器接口<a href="#外部签名器接口"><span>#</span></a></h2><p>仓库通过 <code>XIAOHEIHE_SIGNER_COMMAND</code> 环境变量或 <code>config.json</code> 的 <code>signer_command</code> 字段指定外部签名器路径。签名器接受命令行参数并输出 JSON：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>signer &lt;path&gt; &lt;timestamp&gt; &lt;identity&gt; &lt;device&gt; &lt;os-version&gt; &lt;app-version&gt; &lt;imei&gt; &lt;security-code&gt; &lt;security-version&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"path"</span><span>: </span><span>"/api/endpoint/"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"_time"</span><span>: </span><span>"1700000000"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"nonce"</span><span>: </span><span>"随机32字符"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"hkey"</span><span>: </span><span>"8位签名"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>"_rnd"</span><span>: </span><span>"前缀:签名值"</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>合规<a href="#合规"><span>#</span></a></h2><p>本文仅供个人学习、研究和兼容性分析。逆向工程可能违反平台服务条款和当地法律。请勿用于商业化、批量采集或恶意用途。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/hermes/2026-09-18-oppo-health-cloud-reversing-whitebox-signature/</id>
      <title type="text">OPPO 健康云同步逆向：从抓包到白盒签名的死局（已破）</title>
      <published>2026-09-18T00:00:00.000Z</published>
      <updated>2026-09-18T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/hermes/2026-09-18-oppo-health-cloud-reversing-whitebox-signature/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<section><h1>OPPO 健康云同步逆向：从抓包到白盒签名的死局<a href="#oppo-健康云同步逆向从抓包到白盒签名的死局"><span>#</span></a></h1><section><h2>背景<a href="#背景"><span>#</span></a></h2><p>我有一个长期项目：把 OPPO 健康（欢太健康）App 里的心率/HRV/血氧/睡眠等数据自动导出到本地做分析。已有方案是 root + LSPosed hook 本地数据库，稳定跑着。</p><p>新想法很自然：<strong>云同步里既然有全部数据，能不能直接抓云端的接口，做一个纯 PC 的 MCP？</strong> 这样使用者只需要抓一次 cookie，不用 root、不用 LSPosed、App 升级也不用改 hook。</p><p>结论先说：<strong>思路验证成立，但被白盒签名封死在最后一步。</strong> 下面是完整的调查过程，给同样想走这条路的人省时间。</p></section><section><h2>第一步：静态分析 APK —— 云端存的是明细<a href="#第一步静态分析-apk--云端存的是明细"><span>#</span></a></h2><p>反编译健康 App（脱敏版本号 6.9.x），找到云同步的 API 网关（域名脱敏为 <code>***.heytapmobi.com/sporthealth/</code>），共 667 个 <code>v1-v5/c2s/*</code> 端点。关键事实：</p><p><strong>云端存的是明细数据，不是聚合值。</strong> 每类指标都有独立同步接口：</p>

<table><thead><tr><th>指标</th><th>接口模式</th><th>响应数据</th></tr></thead><tbody><tr><td>血氧</td><td>pull 明细</td><td>值+时间戳+设备ID 数组</td></tr><tr><td>步数</td><td>pull 明细</td><td>分段 steps/distance/calories/sportMode</td></tr><tr><td>心率</td><td>syncStat 日聚合</td><td>max/min/avg/rest/sleepBase</td></tr><tr><td>睡眠</td><td>版本增量</td><td>睡眠指数（含 spo2/呼吸/心率/入睡时间）</td></tr><tr><td>血糖/压力/腕温/久坐/日晒/跌倒</td><td>pull 明细</td><td>全通</td></tr></tbody></table><p>增量协议也确认了：<code>queryXxxVersion(水位) → modifiedTimestampList → 按版本拉数据</code>，标准的双向增量同步（push 上传 + pull 下载），App 卸载重装恢复数据靠的就是它。</p><p>鉴权比预想的轻：请求头一个 <code>virtual-ssoid</code> 就够，账号 token 走 accountsdk。没有多层签名链——<strong>当时我以为这条路能通。</strong></p></section><section><h2>第二步：抓包 —— 认证体系完整还原<a href="#第二步抓包--认证体系完整还原"><span>#</span></a></h2><p>真机 Reqable 抓包，1296 条请求（1084 条健康 API），303 个不同端点。认证头全部识别：</p>

<table><thead><tr><th>头</th><th>来源</th></tr></thead><tbody><tr><td>token（<code>ACCESS_</code> 开头，753 次复用同一值）</td><td>accountsdk</td></tr><tr><td>token-auth-id</td><td>= usercenter.db 的 deviceId 列</td></tr><tr><td>appid / app-key-version</td><td>固定值</td></tr><tr><td>nonce + timestamp + <strong>signature</strong></td><td>每请求唯一 ← 问题所在</td></tr></tbody></table><p>前四个都是静态的，抓一次包就有。唯一的变数是 signature——每条请求都不一样。</p></section><section><h2>第三步：smali 逆向 —— 签名协议 100% 还原<a href="#第三步smali-逆向--签名协议-100-还原"><span>#</span></a></h2><p>顺 <code>SignInterceptor</code> 挖下去，签名链完整还原：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>HttpSignData{method, url, body, 10个签名头}</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>→ V2Signer.getCommonSignHeaders()</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>→ SecKitClient.macSign(context, bytes)   ← libwbkit-seckit3.so 白盒</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>→ signature header</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>签名构造算法本身是可复刻的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>canonical = method\n + path\n + query\n + 排序headers\n + signedHeaders\n + sha256(body)</span></div></div><div><div><div>2</div></div><div><span>sig = HmacSHA256(mk, canonical) → CMSSignedData protobuf → base64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>问题在密钥 <code>mk</code> 的来源：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>服务端 KMS 协商下发</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>getkmssystemtime → getkmscertificate → getserviceticket</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>→ ServiceSessionInfo{Dek, Mk, Ticket}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>死锁</strong>：初始 <code>getserviceticket</code> 协商请求本身需要 <code>accessKey + WbkitAndr.signature</code>（白盒 native 签名）。即：</p><blockquote><p>要拿到会话密钥 mk，得先通过白盒签名认证；要白盒签名，得在真机上跑那个 so。</p></blockquote></section><section><h2>第四步：实验 —— 实测封死纯 PC 路线<a href="#第四步实验--实测封死纯-pc-路线"><span>#</span></a></h2><p>理论推完还不算完，直接做实验验证（之前的 403 结论用错了 token 类型，不算数，这次重测）：</p>

<table><thead><tr><th>实验</th><th>结果</th></tr></thead><tbody><tr><td>HAR 原始请求<strong>原样重放</strong>（同 token/sig/nonce/body，间隔几分钟）</td><td>403 <code>10103</code></td></tr><tr><td>去掉 signature</td><td>403 同上</td></tr><tr><td>伪造 signature + 新 nonce</td><td>403 同上</td></tr><tr><td>按 smali 还原的 canonical 格式，用所有静态头组合当 HMAC 密钥穷举</td><td>全不中</td></tr></tbody></table><p>两组实验锁死事实链：</p><ol>
<li>原样重放都被拒 → nonce 有时效/一次性，网关验的不只是静态凭证</li>
<li>HMAC 密钥用客户端一切静态值都凑不出 → 密钥就是协商下发的 <code>mk</code></li>
<li>协商要白盒 native 签名 → 死循环闭合</li>
</ol><p><strong>本质是信息的物理分布问题</strong>：签名密钥那 32 字节此刻只存在于两个地方——服务端 KMS 数据库里，和真机内存里（用完即弃的会话态）。它不在 APK 里、不在抓包文件里、不在任何静态文件里。这不是”AI 不够聪明”，是白盒密码的设计目标就是防提取。</p></section><section><h2>行业方案盘点<a href="#行业方案盘点"><span>#</span></a></h2><p>白盒签名怎么破，业界工具箱其实很固定：</p>

<table><thead><tr><th>方案</th><th>原理</th><th>本案可行性</th></tr></thead><tbody><tr><td>unidbg 离线模拟</td><td>Java/unicorn 模拟 so，黑盒调 macSign</td><td>企业级 SDK 带 Context/KeyStore 依赖，成功率 &lt;30%，工作量 1-2 周</td></tr><tr><td>Frida RPC 签名代理</td><td>手机 hook macSign 暴露 RPC，PC 发请求前过手机签名</td><td><strong>可行</strong>，半天工作量，但要手机在线</td></tr><tr><td>Xposed 常驻签名服务</td><td>模块内起 localhost HTTP 包 macSign</td><td>可行且比 Frida 稳，但要维护模块</td></tr><tr><td>静态还原白盒算法</td><td>IDA/Ghidra 啃 so</td><td>不现实，白盒设计目标就是防这个</td></tr></tbody></table><p>淘宝 x-sign 这类能被 unidbg 破的前提是”纯算法 so”；一旦签名密钥走服务端协商（本案），静态路线全灭。<strong>行业共识：动态调用是标准答案。</strong></p></section><section><h2>总结<a href="#总结"><span>#</span></a></h2><ol>
<li><strong>云同步数据方案没有死，但”抓 cookie 就能用”死</strong>。最短可行路径是 Frida RPC 签名桥：PC 发请求 → 手机（连 Wi-Fi 充电即可）出签名 → 云端拉数据。依赖等级 = 手机在线，和原 hook 方案持平。</li>
<li><strong>原 hook 方案仍是依赖最少的形态</strong>。数据管道已经全自动跑着，云抓取的真正价值只有”换机/重装免配置”，收益配不上新增的签名服务复杂度——所以这个方向收档了。</li>
<li><strong>方法论收获</strong>：403 结论下早了会误导方向（第一次测试 token 类型就是错的）；重放实验+密钥穷举这种”否定性实验”比正向尝试更有信息量——两组实验直接闭合了死循环的证明链。密码学死锁面前，逆向工程的终点是确认”这条路的门是焊死的”，然后换路。</li>
</ol><p>签名对拍样本已留存（740 条），哪天重启这个方向，5 分钟捡起来。</p></section><section><h2>后记（2026-09-22）：死局已破，上面有一处关键误判<a href="#后记2026-09-22死局已破上面有一处关键误判"><span>#</span></a></h2><p>四天后重启这个方向，5 分钟真的捡起来了——而且发现”死锁”的证明链有一环是错的。</p><p><strong>误判在哪</strong>：第三步里 <code>SecKitClient.macSign → libwbkit-seckit3.so</code> 那条调用链确实是 App 里的真实代码路径，但它只服务于 KMS 协商那类接口。健康数据 API（<code>v5/c2s/*</code>）走的<strong>根本不是白盒 native 签名</strong>，而是一个纯 Java 层的 <code>HmacSHA256(canonical, httpSecret)</code>——而 <code>httpSecret</code> 是 APK <code>BuildConfig</code> 里的<strong>静态常量</strong>，XXTEA 加密存放，两个 UUID 逐字符交错当 XXTEA 密钥解出来即可。</p><p><strong>为什么当时没发现</strong>：smali 顺藤摸瓜时先撞见了 SecKitClient/白盒 so（KMS 体系），就顺着走到底推出了”协商需要白盒签名”的完整死锁。实际上健康 API 的签名在另一条更浅的路径上（pk9/HttpSignatureUtil），抓包对拍 50/50 通过——之前第四步”穷举静态值凑不出 HMAC 密钥”失败，是因为穷举列表里没有解出来的 <code>httpSecret</code>，而不是密钥真的动态。</p><p><strong>复盘教训</strong>：逆向时”先撞见的正确答案”会遮住”更简单的正确答案”。签名这种东西应该在 smali 里全局搜 <code>HmacSHA256</code> 的所有调用点做穷举对拍，而不是顺着一条最显眼的链走到底。白盒和静态密钥经常共存，选择走哪条链决定了你看到的是死局还是一行 Python。</p><p><strong>破局后的完整实现已开源</strong>：<a href="https://github.com/foxlesbiao/oppo-health-cloud-mcp" target="_blank">foxlesbiao/oppo-health-cloud-mcp</a> —— 无 root、无 LSPosed、无手机依赖，PC 直接拉云端明细（心率/血氧/步数/睡眠/体重/App使用等），MCP 接入任意 agent。签名静态复刻，50/50 对拍通过。附带实测踩坑：python TLS 指纹被网关拦（必须 curl 子进程）、多余请求头参与验签（<code>virtual-ssoid</code> 头会直接 403）。</p><p>原始结论”抓 cookie 就能用”依然不成立——还需要做一次静态逆向（或者直接用上面的开源实现）；但”需要真机签名代理”这半句错了，真正的依赖只有：抓包拿一次 token，之后全程 PC 独立运行。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/hermes/2026-08-19-oppo-health-export-mcp/</id>
      <title type="text">OPPO 健康数据导出：LSPosed 逆向 + MCP 规范化接入 AI</title>
      <published>2026-08-19T00:00:00.000Z</published>
      <updated>2026-08-19T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/hermes/2026-08-19-oppo-health-export-mcp/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<section><h1>OPPO 健康数据导出：LSPosed 逆向 + MCP 规范化接入 AI<a href="#oppo-健康数据导出lsposed-逆向--mcp-规范化接入-ai"><span>#</span></a></h1><section><h2>问题<a href="#问题"><span>#</span></a></h2><p>想把自己 OPPO 健康（心率 / 睡眠 / 血氧 / 呼吸率等）的<strong>全部明细数据</strong>交给本地 AI 深度分析，而不是只看 App 里的聚合图表。但 OPPO 健康的本地数据库是 <strong>SQLCipher 加密</strong>的，直接打开是乱码，拿不到数据。</p><p>同时，导出的数据要喂给 AI agent 分析，接收端接口如果写得不够规范，别人（其它 agent）没法按标准接入。所以最后把服务端做成了 <strong>MCP (Model Context Protocol)</strong> 规范。</p><p>完整代码已开源：github.com/foxlesbiao/oppo-health-export</p></section><section><h2>三层方案<a href="#三层方案"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>手机(LSPosed hook) ──增量导出──▶ 服务端 Flask(手机专通道) ──▶ sink库 ──▶ MCP server</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>提取SQLCipher密钥                                   (行哈希幂等)      │</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>流式分片 + 分块上传                                                     ▼</span></div></div><div><div><div>4</div></div><div><span><span>                                                                 </span></span><span>任意 AI agent</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>第 1 层：手机端 LSPosed 逆向拿密钥<a href="#第-1-层手机端-lsposed-逆向拿密钥"><span>#</span></a></h3><ul>
<li>OPPO 健康的库用 SQLCipher 加密，密钥链最终落在 Android Keystore。</li>
<li>写 LSPosed 模块 hook 它的加解密类，拿到解密密钥就能打开整个库。</li>
<li><strong>用 libxposed 新 API</strong>（<code>io.github.libxposed.api.XposedModule</code>），旧 <code>de.robv.android.xposed</code> 已停更，千万别用。</li>
<li>hook 要点：
<ul>
<li><code>AesGcmAndroidKeyStore.b()</code> 抓解密后的密钥</li>
<li><code>bj4.a(Context)</code> 拿单例密钥</li>
<li><strong>不要在 onPackageLoaded 里直接反射调目标方法</strong>（静态初始化没就绪会崩 App），要 hook <code>Activity.onCreate</code> 后延迟几秒再“虚拟打开”数据库。</li>
</ul>
</li>
<li>密钥格式：<code>32位十六进制 + "db_key"</code> 后缀。</li>
</ul></section><section><h3>第 2 层：增量导出（行业标准做法）<a href="#第-2-层增量导出行业标准做法"><span>#</span></a></h3><p>一开始 <code>SELECT * FROM 全表</code> 直接全量读进内存，在手机上直接 <strong>OOM</strong>（一张运动明细表单次分配 400+MB）。按数据工程的标准做法改成：</p><ul>
<li><strong>流式分片</strong>：每表 <code>WHERE 时间戳 &gt; 水位 ORDER BY 时间戳 LIMIT 2000</code> 循环分批写，绝不整表进内存。</li>
<li><strong>水位（cursor）</strong>：服务端存各表上次同步到的最大时间戳，手机每次只导新增。首次设<strong>初始水位 90 天</strong>建基线，不回溯全历史。</li>
<li><strong>分块多次上传</strong>：每导满 2 万行就 POST 一个分块，单包几 MB、失败只重试该块、手机内存恒定。</li>
<li><strong>幂等</strong>：服务端给每行算 <code>__row_hash</code>（SHA-256）做 UNIQUE，<code>INSERT OR IGNORE</code>，重复上传自动去重。</li>
</ul></section><section><h3>第 3 层：服务端做成 MCP 规范<a href="#第-3-层服务端做成-mcp-规范"><span>#</span></a></h3><p>自定义 Flask API 不规范，对外开放给别的 agent 时按 <strong>MCP</strong> 暴露，用 <code>FastMCP</code> 写成一个独立 server（纯 mcp + sqlite3，不依赖 Flask），暴露 6 个标准工具：</p>

<table><thead><tr><th>工具</th><th>功能</th></tr></thead><tbody><tr><td><code>health_ingest</code></td><td>接收增量分节数据（幂等入库 + 更新水位）</td></tr><tr><td><code>health_watermark</code></td><td>查询各表水位（增量游标）</td></tr><tr><td><code>health_list_tables</code> / <code>health_stats</code></td><td>列出表 / 统计概览</td></tr><tr><td><code>health_query</code></td><td>按时间 / 列查询已入库数据</td></tr><tr><td><code>health_schema</code></td><td>表结构</td></tr></tbody></table><p>任何 AI agent / MCP 客户端注册这个 server 就能拿到 <code>mcp__oppo_health__health_query</code> 等工具，直接查健康数据。</p><p>数据分节格式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>===TABLE:DBHeartRate|TIMECOL:data_created_timestamp===</span></div></div><div><div><div>2</div></div><div><span>ssoid,device_type,data_created_timestamp,value,...</span></div></div><div><div><div>3</div></div><div><span>1249250120,,1779393420000,85,...</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>踩过的坑<a href="#踩过的坑"><span>#</span></a></h2>

<table><thead><tr><th>坑</th><th>修复</th></tr></thead><tbody><tr><td>全量 SELECT 进内存 OOM</td><td>流式分片（LIMIT 2000/批）</td></tr><tr><td>hook 多入口多线程重复导出，CPU 500%+</td><td><code>AtomicBoolean</code> 单实例互斥</td></tr><tr><td>首次全历史几百万行极慢</td><td>初始水位取近 90 天</td></tr><tr><td>SQL 注入（表名 / 列名拼进 SQL）</td><td>标识符白名单校验 + 全参数化</td></tr><tr><td>数据库跨连接写锁</td><td>SQLite WAL + busy_timeout</td></tr><tr><td>软重启 <code>stop;start</code> 停死 system_server</td><td>统一用完整 <code>adb reboot</code></td></tr></tbody></table></section><section><h2>有了数据 ≠ 能解读：还需要医学知识库<a href="#有了数据--能解读还需要医学知识库"><span>#</span></a></h2><p>导出的只是<strong>原始指标</strong>（心率、血氧、睡眠时长、呼吸率……）——它们本身是无医学含义的数字。要<strong>准确</strong>分析、而不是让模型凭内置的泛泛知识臆断，还必须给 agent 接入靠谱的<strong>医学知识库</strong>：</p><ul>
<li>把权威资料（医学教材、临床指南、中医典籍）入库（RAG / 向量检索）</li>
<li>分析时<strong>先检索、再下结论</strong>，并标注「这个判断依据哪份资料」</li>
<li>缺少对应资料时<strong>明确说不知道</strong>，而不是编一个看似合理的解读</li>
</ul><p>这才是工程上负责任的健康分析：数据链路负责「绝对真实的你」，知识库负责「尽量正确的解读」，两者缺一不可。否则再精确的数据，也可能被一句想当然的解读带偏。</p><section><h3>而且：可穿戴数据只是「参考级」<a href="#而且可穿戴数据只是参考级"><span>#</span></a></h3><p>别忘了，输入给 AI 的只是<strong>消费级可穿戴设备（手环/手表）的估算值</strong>——心率、血氧、睡眠算法都有误差，<strong>只能作为参考</strong>，不能替代或作为临床诊断依据。真正权威的健康判断要靠<strong>日常体检/医院检查</strong>（生化、影像等）来决定。这套数据智能的价值在于「帮你发现趋势、提醒你何时该去医院」，而不是「替你下诊断」。</p></section><section><h3>别被单位坑了（分析必知）<a href="#别被单位坑了分析必知"><span>#</span></a></h3><p>OPPO 健康库里部分字段<strong>不是直觉单位</strong>，分析时最容易算错：</p>

<table><thead><tr><th>字段</th><th>原始单位</th><th>换算</th></tr></thead><tbody><tr><td>体重 weight</td><td>克</td><td>÷1000 = kg</td></tr><tr><td>睡眠 total_sleep_time</td><td>分钟</td><td>分钟</td></tr><tr><td>卡路里</td><td>卡(cal)</td><td>÷1000 = kcal</td></tr><tr><td>呼吸率 value</td><td>0.1 次/分</td><td>÷10 ≈ 12 次/分</td></tr><tr><td>时间戳</td><td>毫秒</td><td>÷1000 = 秒</td></tr></tbody></table></section><section><h3>隐私与安全<a href="#隐私与安全"><span>#</span></a></h3><p>这套链路的数据<strong>从头到尾只在你自己手里</strong>：</p><ul>
<li>手机 → 自家服务，内网直连；出门走 <strong>Tailscale</strong>（零暴露面，不把 80/443 暴露公网）</li>
<li>sink 库、AI 分析（本地知识库 OpenViking）、生成报告全部在本地</li>
<li><strong>不经过任何第三方云 / 服务器</strong></li>
</ul></section><section><h3>自动化闭环<a href="#自动化闭环"><span>#</span></a></h3><p>新数据上传即自动分析，不用手动跑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>手机上传(/api/upload) → HMAC 签名的本地 webhook → agent 读新数据生成周报告</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>→ 写入本地知识库(OpenViking) → 推送到 IM</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>数据一进来就被消费，形成「导出一分析一归档」的闭环。</p></section><section><h3>分析示例：这些是跑出来的真实数据<a href="#分析示例这些是跑出来的真实数据"><span>#</span></a></h3><p>聚合自 sink 库（近 90 天增量，去敏后仅保留统计值）：</p>

<table><thead><tr><th>指标</th><th>真实结果</th></tr></thead><tbody><tr><td>睡眠时长（平均）</td><td><strong>7.5 小时 / 晚</strong>（80 晚有效记录）</td></tr><tr><td>中位入睡 / 醒来</td><td><strong>02&lt;07&gt; / 07&lt;28&gt;</strong>（近 30 天，与 App 一致）</td></tr><tr><td>深睡</td><td>1.1 小时</td></tr><tr><td>REM（快速眼动）</td><td>1.9 小时</td></tr><tr><td>每日光照（平均）</td><td><strong>44 分钟</strong>（健康目标 20 分钟）</td></tr><tr><td>光照达标率</td><td>44%（54 天中 24 天达标）</td></tr></tbody></table><blockquote><p>光照/户外是 OPPO 健康里很有价值的维度（对应维生素 D、昼夜节律）；达标题直接映射设备设定的「每日光照目标」。</p></blockquote></section><section><h3>实测结论：为什么不做「免 root」分发<a href="#实测结论为什么不做免-root分发"><span>#</span></a></h3><p>常有朋友问：能不能把模块<strong>内置进 APK、免 root</strong>给普通人用？我实测了 <strong>LSPatch</strong> 方案，结论是<strong>此路不通</strong>，记录如下省得后人重复踩坑：</p><ul>
<li><strong>静态 OK</strong>：LSPatch 能顺利把 dbkeyhook 模块内置进 OPPO 健康 APK 并重打包（该 App <strong>未加固</strong>）。</li>
<li><strong>真机安装 ❌</strong>：报错 <code>INSTALL_FAILED_SHARED_USER_INCOMPATIBLE: oplus named app is not match signature</code>。</li>
<li><strong>根因</strong>：OPPO 健康是 <strong>ColorOS 平台签名共享 uid（<code>android:sharedUserId="oplus"</code>）</strong> 的系统应用，只接受带平台签名的同 uid 安装；LSPatch 重签名后 uid 签名不匹配，系统直接拒绝。</li>
<li><strong>因此免 root 绕不开</strong>：要 hook 它，只能是 <strong>KernelSU/Magisk + LSPosed</strong> 这条真 root 路径（即本仓库模块的默认用法）。</li>
</ul><blockquote><p>给想「免 root 分发 hook 模块」的人一条排查捷径：先看目标 App 的 manifest 有没有 <code>android:sharedUserId</code>——只要命中了厂商系统 uid（如 <code>oplus</code>、<code>miui</code>、<code>huawei</code> 之类），LSPatch 重签名立刻报废，别浪费时间。</p></blockquote><blockquote><p><strong>测试环境说明</strong>：以上 hook 模块仅在 <strong>一加 13T（OPPO/ColorOS ROM）及 OPPO 设备</strong>上验证过；其他品牌 / 型号 / ROM 未实测，不能保证兼容，自行安装风险自负。</p></blockquote></section></section><section><h2>近期更新（v5.3.7 → v5.3.9b）<a href="#近期更新v537--v539b"><span>#</span></a></h2><p>版本迭代中又踩了一轮新坑，补充记录：</p>

<table><thead><tr><th>坑</th><th>修复</th></tr></thead><tbody><tr><td>模块 APK 里没有 libsqlcipher.so，无法直接加载</td><td>不自带 so，反射宿主的 ClassLoader 拿 zetetic SQLCipher 类</td></tr><tr><td>主进程加载 SQLCipher native 失败（clns-11 namespace 拒绝）</td><td>只在 <code>:SportDaemonService</code> 进程放行导出，其余进程直接跳过</td></tr><tr><td>宿主 native 未就绪时 hook 触发过早</td><td>openDatabase 失败重试（6 次 × 5s），等宿主加载完成</td></tr><tr><td>6.7.19 版健康 App 懒加载 + 保活，App 活着时 hook 不会重新触发</td><td>手动导出 = 先 su 强杀健康 App 再拉起</td></tr><tr><td>健康 App UID 无权在 /data/local/tmp 建文件，密钥落盘失败</td><td>兜底写 cacheDir，UI 端用 su 读取</td></tr><tr><td>上传时手机在外网（出门）链路不通</td><td>Tailscale 自动开关：导出前探测外网地址，不可达则广播 <code>CONNECT_VPN</code> 拉起（最长等 12s），导出结束自动关闭；任意 HTTP 响应（含 401）即视为链路通，不依赖 Tailscale API</td></tr></tbody></table><p>工程细节：上传顺序为 <strong>内网 → 外网 → Tailscale</strong> 三路依次尝试；配置项（上传地址 / token / Tailscale 地址）全部由 UI 运行时输入，写入 SharedPreferences + 共享 XML 给 hook 进程读取，代码里零硬编码——这也是它能直接开源的前提。</p><p>构建侧：debug 签名 keystore 不再硬编码绝对路径，默认 <code>~/.android/debug.keystore</code>，可用 <code>DEBUG_KEYSTORE</code> 环境变量覆盖（开源仓库的基本修养）。</p></section><section><h2>总结<a href="#总结"><span>#</span></a></h2><ul>
<li><strong>逆向层的核心</strong>：libxposed 新 API + 钩对加解密类拿密钥 + 延迟虚拟打开，避免早期反射崩 App。</li>
<li><strong>导出层的核心</strong>：不追求一次回溯全历史，设初始水位 → 首次导近期 → 之后增量；分片保证内存峰值恒定。</li>
<li><strong>接收层的核心</strong>：做标准 MCP server 而非自造 API，别人能按规范接入。</li>
</ul><p>这样你的健康数据就能持续、增量、幂等地流进本地 AI，且对外是标准 MCP 接口。</p><p>代码：github.com/foxlesbiao/oppo-health-export</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/shellcrash-singbox-ruleset-fix/</id>
      <title type="text">ShellCrash sing-box 规则集不生效？模板规则被丢弃的修复记录</title>
      <published>2026-08-11T00:00:00.000Z</published>
      <updated>2026-08-11T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/shellcrash-singbox-ruleset-fix/"/>
      <summary type="text">ShellCrash sing-box 内核规则模板的分流规则完全不生效——规则集下载了但没被引用。完整诊断过程 + 持久化修复方案。</summary>
      <content type="html"><![CDATA[<section><h1>ShellCrash sing-box 规则集不生效？模板规则被丢弃的修复记录<a href="#shellcrash-sing-box-规则集不生效模板规则被丢弃的修复记录"><span>#</span></a></h1><section><h2>问题<a href="#问题"><span>#</span></a></h2><p>在路由器（OpenWrt）上跑 ShellCrash（sing-box 内核），在 TUI 里选了 <strong>“全分组规则+去广告”</strong> 规则模板，规则集文件也都下载好了（<code>ruleset/*.srs</code> 一堆），但实际体验是：<strong>国内网站也绕代理，广告不拦截，流媒体不分流</strong>——规则好像完全没起作用。</p></section><section><h2>诊断：规则集下载了，但 route 没引用<a href="#诊断规则集下载了但-route-没引用"><span>#</span></a></h2><p>ShellCrash sing-box 版（1.9.5beta1+，内核 singboxr）的配置生成机制是<strong>多 JSON 模块拼接</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/tmp/ShellCrash/jsons/*.json  （启动时 singbox_modify.sh 生成）</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├─ route.json        基础路由</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├─ add_rule_set.json 规则集定义</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>├─ dns.json          DNS 规则</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>├─ outbounds.json    策略组+节点</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>└─ cust_*.json       持久化自定义（从 $CRASHDIR/jsons/ 软链）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>启动命令是 <code>CrashCore run -D $CRASHDIR -C /tmp/ShellCrash/jsons</code>（目录模式深度合并）。</p><p>排查后发现：</p><ul>
<li><code>add_rule_set.json</code> 里<strong>只有 <code>cn</code> 一个规则集</strong>定义</li>
<li><code>route.json</code> 只有 6 条基础规则（sniff → dns 劫持 → 内网直连 → 拒 QUIC/BT → <strong>全部走代理</strong>）</li>
<li>模板（含完整的分流 rules + 22 个 rule_set 引用）被丢弃，<strong>完全没被合并</strong></li>
</ul><p><strong>根因</strong>：<code>starts/singbox_modify.sh</code> 的 <code>gen_dns()</code> 硬编码只生成 <code>cn</code> 规则集；启动时重新生成的 <code>route.json</code> 只有基础规则。模板的完整 route（rules + rule_set）<strong>被覆盖/丢弃</strong>——这就是”规则集下载了但分流不生效”的原因。</p></section><section><h2>修复：持久化 route.json 补上模板该做的事<a href="#修复持久化-routejson-补上模板该做的事"><span>#</span></a></h2><p>不修改 ShellCrash 的脚本（改模板 JSON 会被覆盖，改脚本下次更新又还原），而是利用 ShellCrash <strong>官方设计的持久化机制</strong>：</p><p><code>$CRASHDIR/jsons/route.json</code> 会在启动时被软链为 <code>cust_route.json</code> 参与合并。所以直接在这个持久化文件里写完整的 route：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"route"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"rules"</span><span>: [</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>{ </span><span>"action"</span><span>: </span><span>"sniff"</span><span> },</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>{ </span><span>"protocol"</span><span>: </span><span>"dns"</span><span>, </span><span>"action"</span><span>: </span><span>"hijack-dns"</span><span> },</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>{ </span><span>"ip_is_private"</span><span>: </span><span>true</span><span>, </span><span>"outbound"</span><span>: </span><span>"DIRECT"</span><span> },</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>{ </span><span>"protocol"</span><span>: </span><span>"quic"</span><span>, </span><span>"action"</span><span>: </span><span>"reject"</span><span>, </span><span>"no_drop"</span><span>: </span><span>true</span><span> },</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>{ </span><span>"protocol"</span><span>: </span><span>"bittorrent"</span><span>, </span><span>"action"</span><span>: </span><span>"reject"</span><span> },</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>{ </span><span>"rule_set"</span><span>: [</span><span>"custom-direct"</span><span>], </span><span>"outbound"</span><span>: </span><span>"DIRECT"</span><span> },</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>{ </span><span>"rule_set"</span><span>: [</span><span>"custom-proxy"</span><span>], </span><span>"outbound"</span><span>: </span><span>"节点选择"</span><span> },</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>{ </span><span>"rule_set"</span><span>: [</span><span>"ads"</span><span>], </span><span>"outbound"</span><span>: </span><span>"REJECT"</span><span> },</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>{ </span><span>"rule_set"</span><span>: [</span><span>"cn"</span><span>, </span><span>"cnip"</span><span>], </span><span>"outbound"</span><span>: </span><span>"DIRECT"</span><span> },</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>{ </span><span>"outbound"</span><span>: </span><span>"节点选择"</span><span> }</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>],</span></div></div><div><div><div>15</div></div><div><span>    </span><span>"rule_set"</span><span>: [</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>{ </span><span>"tag"</span><span>: </span><span>"custom-direct"</span><span>, </span><span>"type"</span><span>: </span><span>"local"</span><span>, </span><span>"format"</span><span>: </span><span>"binary"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>        </span><span>"path"</span><span>: </span><span>"/mnt/.../ShellCrash/ruleset/Custom_Direct.srs"</span><span> }</span></div></div><div><div><div>18</div></div><div><span>      </span><span>// ... 其他规则集定义</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>关键点：</p><ol>
<li><strong>规则集定义用 <code>local</code> + 绝对路径</strong>（<code>path</code> 指向 <code>ruleset/*.srs</code>）。相对路径 <code>./ruleset/</code> 在 merge 时工作目录解析有问题，绝对路径最稳。</li>
<li><strong>route 里必须含 <code>cn</code> 定义</strong>——<code>singbox_modify.sh</code> 检测到持久化目录已有 cn tag 就会<strong>跳过 add_rule_set.json 的生成</strong>，避免重复定义冲突。</li>
<li><strong>规则集文件要先编译好</strong>，用 sing-box 自带的编译命令：</li>
</ol><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 把 Clash .list 转成 sing-box source JSON</span></div></div><div><div><div>2</div></div><div><span># {"version":1,"rules":[{"domain_suffix":"x"},...]}   ← 必须带 version 字段</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 编译成 .srs</span></div></div><div><div><div>5</div></div><div><span>/tmp/ShellCrash/CrashCore</span><span> </span><span>rule-set</span><span> </span><span>compile</span><span> </span><span>/tmp/x.json</span><span> </span><span>-o</span><span> </span><span>/mnt/.../ruleset/X.srs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li>重启用 <code>./start.sh stop &amp;&amp; ./start.sh start</code>（比 TUI 交互更可靠）。</li>
</ol></section><section><h2>自定义规则（Aethersailor 等 OpenClash 规则）接入<a href="#自定义规则aethersailor-等-openclash-规则接入"><span>#</span></a></h2><p>用户想要的 Aethersailor/Custom_OpenClash_Rules 是 <strong>OpenClash（Mihomo）格式</strong>（.mrs/.yaml），sing-box 不能直接用。转换流程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.list（Clash 格式）→ source JSON（{"version":1,"rules":[...]}）→ rule-set compile → .srs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把转换好的 <code>.srs</code> 放进 <code>ruleset/</code>，在 route.json 里加引用即可。自动更新用 crontab：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 每周一 4:30 更新自定义规则集</span></div></div><div><div><div>2</div></div><div><span>30</span><span> </span><span>4</span><span> </span><span>*</span><span> </span><span>*</span><span> </span><span>1</span><span> </span><span>sh</span><span> </span><span>/mnt/.../ShellCrash/tools/update_asailor_rules.sh</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>验证<a href="#验证"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 看已加载的规则集</span></div></div><div><div><div>2</div></div><div><span>curl</span><span> </span><span>-s</span><span> </span><span>http://127.0.0.1:9999/rules</span><span> | </span><span>grep</span><span> </span><span>-o</span><span> </span><span>'rule_set=[^"}]*'</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 分流测试</span></div></div><div><div><div>5</div></div><div><span>curl</span><span> </span><span>-s</span><span> </span><span>-x</span><span> </span><span>http://127.0.0.1:7890</span><span> </span><span>https://www.google.com/generate_204</span><span> </span><span>-o</span><span> </span><span>/dev/null</span><span> </span><span>-w</span><span> </span><span>'%{http_code}'</span></div></div><div><div><div>6</div></div><div><span># → 204（走代理）</span></div></div><div><div><div>7</div></div><div><span>curl</span><span> </span><span>-s</span><span> </span><span>https://www.baidu.com</span><span> </span><span>-o</span><span> </span><span>/dev/null</span><span> </span><span>-w</span><span> </span><span>'%{http_code}'</span></div></div><div><div><div>8</div></div><div><span># → 200（直连）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>修复后：Google 走代理 204，百度直连 200，广告规则集、流媒体分流全部生效，重启后配置持久保留。</p></section><section><h2>总结<a href="#总结"><span>#</span></a></h2><ul>
<li>ShellCrash sing-box 模式的<strong>模板 route 被丢弃</strong>是规则”不生效”的根因（已提 issue：<a href="https://github.com/juewuy/ShellCrash/issues/1326" target="_blank">#1326</a>）</li>
<li>修复方式是<strong>利用官方持久化机制</strong>（<code>$CRASHDIR/jsons/route.json</code> → <code>cust_route.json</code> 合并）补上模板该做的事</li>
<li>自定义 OpenClash 规则需要先转成 sing-box .srs 格式才能用</li>
<li>规则集定义用 <strong>local + 绝对路径</strong>，route 里含 cn 可避免 add_rule_set 重复生成</li>
</ul></section><section><h2>追加：实战中踩到的两个大坑（2026-08-12）<a href="#追加实战中踩到的两个大坑2026-08-12"><span>#</span></a></h2><section><h3>坑 1：全量规则集导致路由器 OOM 崩溃<a href="#坑-1全量规则集导致路由器-oom-崩溃"><span>#</span></a></h3><p>尝试把 senshinya/singbox_ruleset（blackmatrix7 全集，679 个分类）合并成一个规则集，结果是 <strong>137 万条规则</strong>。路由器（804MB 内存）<strong>直接 OOM 崩溃</strong>，整个路由器失去响应需要手动重启。</p><p><strong>教训</strong>：小内存路由器（&lt;1GB）不要全量合并规则集。正确做法：</p><ul>
<li>用 DustinWin 的 30 个 remote .srs（启动按需下载，不占内存）</li>
<li>或 senshinya 只选 5-10 个高价值分类（Netflix/YouTube/Steam 等，remote URL 方式）</li>
<li>想要全量规则只能上 x86 软路由（内存 ≥2GB）</li>
</ul></section><section><h3>坑 2：官方流程重新生成配置后代理不通<a href="#坑-2官方流程重新生成配置后代理不通"><span>#</span></a></h3><p>用 TUI 官方流程（6 → b → 1 生成配置）重新生成后，<strong>代理节点完全加载不出来</strong>，所有策略组显示 <code>Compatible</code> 空占位。</p><p><strong>根因</strong>：ShellCrash 的 provider 缓存文件 <code>providers/kq.yaml</code> 内容是 <strong>sing-box JSON 格式</strong>（<code>{"outbounds":[...]}</code>），但 sing-box 1.14 的 provider <strong>默认按 yaml 解析</strong> → 解析失败 → 节点全空。</p><p><strong>最简恢复</strong>：直接用<strong>旧的节点内联版 config.json</strong>（ShellCrash 早期生成的，节点直接内联在 outbounds 里，不依赖 provider 机制）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cp</span><span> </span><span>$CRASHDIR</span><span>/jsons/config.json.bak</span><span> </span><span>$CRASHDIR</span><span>/jsons/config.json</span></div></div><div><div><div>2</div></div><div><span># 然后 TUI 启动：mm → 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个版本的节点是内联的，代理正常（Google 204）。<strong>这是”原来的规则+策略组”最稳定的状态</strong>——不要轻易用 TUI 重新生成配置，否则会触发 provider 格式 bug。</p></section><section><h3>附：USB 挂载点变化<a href="#附usb-挂载点变化"><span>#</span></a></h3><p>路由器重启后 USB 挂载点可能变化（<code>/mnt/usb-XXXX</code> 的 XXXX 是 UUID），导致 ShellCrash 找不到。恢复方法：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 找到 ShellCrash 实际位置</span></div></div><div><div><div>2</div></div><div><span>find</span><span> </span><span>/mnt</span><span> </span><span>-maxdepth</span><span> </span><span>2</span><span> </span><span>-name</span><span> </span><span>"ShellCrash"</span><span> 2&gt;</span><span>/dev/null</span></div></div><div><div><div>3</div></div><div><span># 启动（指定 CRASHDIR）</span></div></div><div><div><div>4</div></div><div><span>export</span><span> </span><span>CRASHDIR</span><span><span>=</span><span>/</span></span><span>mnt</span><span>/</span><span>usb-XXXX</span><span>/</span><span>ShellCrash</span><span> &amp;&amp; </span><span>sh</span><span> </span><span>/mnt/usb-XXXX/ShellCrash/menu.sh</span></div></div><div><div><div>5</div></div><div><span># 菜单里选 1 启动服务</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/memory-system-evolution/</id>
      <title type="text">Hermes Memory System：从 MEMORY.md 到认知记忆治理层的 10 版演化</title>
      <published>2026-08-01T00:00:00.000Z</published>
      <updated>2026-08-01T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/memory-system-evolution/"/>
      <summary type="text">Hermes Memory System：从 MEMORY.md 到认知记忆治理层的 10 版演化</summary>
      <content type="html"><![CDATA[<section><h1>Hermes Memory System：从 MEMORY.md 到认知记忆治理层的 10 版演化<a href="#hermes-memory-system从-memorymd-到认知记忆治理层的-10-版演化"><span>#</span></a></h1><section><h2>前言<a href="#前言"><span>#</span></a></h2><p>这是 Hermes Agent 系列的第四篇文章。</p><ul>
<li><a href="https://foxlesbiao.github.io/blog/2026/07/31/building-personal-ai-engineering-knowledge-os.html">第一篇：构建个人 AI 工程知识操作系统</a> — 记忆分层架构</li>
<li><a href="https://foxlesbiao.github.io/blog/2026/07/31/hermes-multi-model-routing-architecture.html">第二篇：多模型路由架构演化</a> — v1→v3 路由</li>
<li><a href="https://foxlesbiao.github.io/blog/2026/07/31/observable-ai-system.html">第三篇：可观测 AI 系统</a> — task-memory + trace + 经验闭环</li>
<li><strong>第四篇（本文）：Memory System v1.0→v1.41 演化</strong> — 冲突解决 + 权限治理 + 实体图谱 + 检索引擎</li>
</ul><p>本文记录 Hermes Agent 的记忆系统从简单的 MEMORY.md 文件，经过 10 个版本迭代，演化为一个带权限治理、来源链审计、时间版本、实体图谱和意图感知检索的认知记忆层的过程。</p><hr /></section><section><h2>起点：为什么需要 Memory System<a href="#起点为什么需要-memory-system"><span>#</span></a></h2><p>Hermes Agent 原来使用 MEMORY.md 存储用户事实，存在五个核心问题：</p><ol>
<li>无时间戳 — 无法判断过期</li>
<li>无可信度标记 — 事实和推测混在一起</li>
<li>无冲突解决 — 多来源矛盾时无裁决依据</li>
<li>无过期机制 — 老旧条目永久占空间</li>
<li>无冷冻机制 — 关键条目可能被意外覆盖</li>
</ol><hr /></section><section><h2>演化路线<a href="#演化路线"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>v1.0  能记东西</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>v1.1  会冲突检测</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>v1.2  会生命周期管理（持久化 + decay）</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>v1.3  会排序（Memory Ranking Engine）</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>9</div></div><div><span>v1.35 会权限和实体（authority + entity/field）</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>11</div></div><div><span>v1.36 会不可变保护（IMMUTABLE_TYPES）</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>13</div></div><div><span>v1.37 会混合评分（authority_gate × 加权和）</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>15</div></div><div><span>v1.38 会来源审计（provenance + scope 隔离）</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>17</div></div><div><span>v1.39 会时间记忆（valid_from/until + memory_layer）</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>19</div></div><div><span>v1.40 会检索（Query Planner + GC + EntityGraph）</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>21</div></div><div><span>v1.41 会意图感知 + 预算管理（Intent Detection + Budget）</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section><section><h2>核心架构（v1.41 最终状态）<a href="#核心架构v141-最终状态"><span>#</span></a></h2><section><h3>MemoryEntry 数据模型<a href="#memoryentry-数据模型"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>MemoryEntry</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>value              </span><span># 内容</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>memory_type        </span><span># IDENTITY / FACT / PREFERENCE / SKILL / PROJECT / TEMP</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>memory_layer       </span><span># EPISODIC / SEMANTIC / PROCEDURAL / IDENTITY / TEMPORARY</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>authority          </span><span># user=1.0 / tool=0.6 / agent=0.3</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>source             </span><span># 来源（与 authority 分离）</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>confidence         </span><span># 0.0-1.0</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>explicitness       </span><span># 0.0-1.0</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>freshness          </span><span># 基于 decay_days 自动衰减</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>retention          </span><span># freshness×0.5 + usage×0.3 + confidence×0.2</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>provenance[]       </span><span># 来源链审计（可追溯用户原始陈述）</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>valid_from         </span><span># 记忆生效时间</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>valid_until        </span><span># 记忆失效时间（supersede 时自动关闭）</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>entity             </span><span># 实体名（如 NAS、GPU、OS）</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>field              </span><span># 属性名（如 ip、model、system）</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>scope              </span><span># 范围隔离（如 main_pc、laptop）</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>relation           </span><span># 实体关系（如 located_at）</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>target             </span><span># 关系目标（如 home_network）</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>relation_confidence</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>identity_status    </span><span># ACTIVE / CONFIRMED / CHANGED / ARCHIVED</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>usage_count        </span><span># 被引用次数（影响 retention）</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>change_reason      </span><span># 变更原因</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>supersedes         </span><span># 前任 memory_id</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>superseded_by      </span><span># 继任者</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>conflict_with      </span><span># 冲突条目</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>评分公式<a href="#评分公式"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>score = authority_gate × (</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>0.4 × confidence</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>+ 0.2 × freshness</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>+ 0.2 × explicitness</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>+ 0.1 × retention</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>+ 0.1 × usage_frequency</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>authority_gate 是一个门控函数：</p>

<table><thead><tr><th>权限级别</th><th>权重</th><th>gate</th></tr></thead><tbody><tr><td>user</td><td>1.0</td><td>1.0</td></tr><tr><td>tool</td><td>0.6</td><td>0.7</td></tr><tr><td>agent</td><td>0.3</td><td>0.4</td></tr><tr><td>unknown</td><td>0.0</td><td>0.1</td></tr></tbody></table><p>这确保了：外部信息（网页、模型推理）永远无法覆盖用户确认的事实，即使外部信息的 confidence/freshness 全部为 1.0。</p></section><section><h3>冲突解决管道<a href="#冲突解决管道"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>用户输入</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>Assertion Parser（解析断言类型）</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>├── CONFIRMED    → 明确事实</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>├── HYPOTHETICAL → 假设，不写入</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>├── UNCERTAIN    → 可能/应该/觉得</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>├── RECALL       → 我记得</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>├── QUERY        → 提问，不产生 memory</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>└── REFERENCE    → 保持，不改变</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>11</div></div><div><span>Intent Detection（是否为更新意图）</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>13</div></div><div><span>Authority Check（权限检查）</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>├── IDENTITY 保护：非 user 权限不可修改</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>└── authority_gate 评分门控</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>17</div></div><div><span>Conflict Detection（冲突检测）</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>├── scope 隔离：main_pc vs laptop 不冲突</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>├── score 比较：新分数 &gt; 旧分数才允许覆盖</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>└── 权限门控：agent 永远无法超越 user</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>22</div></div><div><span>Decision（决策）</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>├── supersede        → 新值替代旧值，保留历史链</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>├── supersede_frozen → 用户确认覆盖 FROZEN</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>├── flag_conflict    → 保留双方，请求用户确认</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>├── reject           → 拒绝写入</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>└── no_op            → 无操作</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>29</div></div><div><span>Memory Write（写入）</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>├── provenance 自动传播（继承旧链 + 追加新条目）</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>├── memory_layer/scope/entity 继承</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>└── valid_until 自动关闭旧版本</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>记忆生命周期<a href="#记忆生命周期"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ACTIVE → STALE → COMPRESSED → ARCHIVED</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>↑                        ↓</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>└── TEMP 高频使用 → EPISODIC（自动升级）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关键设计：从不删除。TEMPORARY 如果被高频引用，自动升级为 EPISODIC。</p></section><section><h3>意图感知检索<a href="#意图感知检索"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>MemoryQueryPlanner</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># 检测查询意图</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>intents </span><span>=</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>"WHOAMI"</span><span>:      </span><span>"我是/我的"</span><span><span>       → </span><span>IDENTITY</span><span> 层权重提升</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>"HOWTO"</span><span>:       </span><span>"怎么/如何/安装"</span><span><span>   → </span><span>PROCEDURAL</span><span> 层权重提升</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>"FACT_LOOKUP"</span><span>: </span><span>"是什么/是多少"</span><span><span>    → </span><span>SEMANTIC</span><span> 层权重提升</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>"DIAGNOSE"</span><span>:    </span><span>"为什么/报错/失败"</span><span><span> → </span><span>EPISODIC</span><span> 层权重提升</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>"TEMPORAL"</span><span>:    </span><span>"什么时候/历史/之前"</span><span><span> → </span><span>EPISODIC</span><span> 层权重提升</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span># 检索评分</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>retrieval_score </span><span>=</span><span> (</span></div></div><div><div><div>13</div></div><div><span>        </span><span>0.35</span><span> × keyword_match</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>+</span><span> </span><span>0.25</span><span> × authority</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>+</span><span> </span><span>0.15</span><span> × freshness</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>+</span><span> </span><span>0.15</span><span> × scope_match</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>+</span><span> </span><span>0.10</span><span> × layer_priority</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># Token 预算控制</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>budget </span><span>=</span><span> {</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>IDENTITY</span><span>: </span><span>1000</span><span><span>, </span><span>SEMANTIC</span><span>: </span></span><span>3000</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>PROCEDURAL</span><span>: </span><span>2000</span><span><span>, </span><span>EPISODIC</span><span>: </span></span><span>1000</span><span><span>, </span><span>TEMPORARY</span><span>: </span></span><span>500</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>    </span><span># 按意图动态调整</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>关键设计决策<a href="#关键设计决策"><span>#</span></a></h2><section><h3>1. 来源与权限分离<a href="#1-来源与权限分离"><span>#</span></a></h3><p>信息来源（source）和修改权限（authority）是两个独立维度：</p>

<table><thead><tr><th>source</th><th>authority</th></tr></thead><tbody><tr><td>用户输入</td><td>user</td></tr><tr><td>FTS5 检索</td><td>tool</td></tr><tr><td>OpenViking</td><td>tool</td></tr><tr><td>传感器</td><td>tool</td></tr><tr><td>网页搜索</td><td>agent</td></tr><tr><td>模型推理</td><td>agent</td></tr></tbody></table><p>这样设计的好处：用户提供的网址（source=web）仍然是 user 权限，不会被降级。</p></section><section><h3>2. IDENTITY 不可变保护<a href="#2-identity-不可变保护"><span>#</span></a></h3><p>IDENTITY 类型的记忆（如”用户使用 Debian”）不能被网页或模型推理覆盖。只有 user 权限的明确修改意图才能 supersede。</p><p>这防止了 Agent 最容易犯的错误：自己推理出来的信息污染用户画像。</p></section><section><h3>3. Provenance 自动传播<a href="#3-provenance-自动传播"><span>#</span></a></h3><p>每次 supersede 时，新记忆自动继承旧记忆的 provenance 链，并追加当前变更记录。调用者不传 provenance 也不会断链。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Arch (ACTIVE)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├── provenance[0]: user said "I use Debian" (2026-07-01)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>└── provenance[1]: user changed OS to Arch (2026-07-31)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这使 Agent 可以回答：“为什么认为你使用 Arch？” → “因为你在 2026-07-31 明确说 OS 改成 Arch”。</p></section><section><h3>4. Scope 隔离<a href="#4-scope-隔离"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>GPU.model = 7650GRE  (scope=main_pc)</span></div></div><div><div><div>2</div></div><div><span>GPU.model = RTX3060  (scope=laptop)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不再冲突。<code>find_active(entity, field, scope)</code> 支持范围过滤。</p></section><section><h3>5. 时间版本<a href="#5-时间版本"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Debian  valid_from=2025-01  valid_until=2026-07-31</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>↓ supersede</span></div></div><div><div><div>3</div></div><div><span>Arch    valid_from=2026-07-31  valid_until=NULL</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以回答：“我什么时候开始用 Arch？” → “2026-07-31”。</p></section><section><h3>6. Memory Layer 分类<a href="#6-memory-layer-分类"><span>#</span></a></h3>

<table><thead><tr><th>Layer</th><th>用途</th><th>例子</th></tr></thead><tbody><tr><td>IDENTITY</td><td>长期身份</td><td>我使用 Debian 13</td></tr><tr><td>SEMANTIC</td><td>事实知识</td><td>7650GRE 参数</td></tr><tr><td>PROCEDURAL</td><td>技能流程</td><td>Docker 安装步骤</td></tr><tr><td>EPISODIC</td><td>事件</td><td>昨天修复 grub</td></tr><tr><td>TEMPORARY</td><td>短期状态</td><td>正在测试 OTA</td></tr></tbody></table><hr /></section></section><section><h2>测试验证<a href="#测试验证"><span>#</span></a></h2><p>15 个测试案例，覆盖：</p>

<table><thead><tr><th>批次</th><th>案例</th><th>场景</th></tr></thead><tbody><tr><td>第一批 A-E</td><td>用户确认/假设/FROZEN/冲突/FTS5覆盖</td><td>基础冲突解决</td></tr><tr><td>第二批 F-J</td><td>不确定/提问/确认/废弃/保持</td><td>语义压力测试</td></tr><tr><td>第三批 K-N</td><td>重启恢复/示例误写入/重复确认/过期decay</td><td>生命周期测试</td></tr><tr><td>第四批 O</td><td>CONFIRMED 无 update_intent 不覆盖 FROZEN</td><td>权限安全</td></tr></tbody></table><p>全部 15/15 PASS。</p><hr /></section><section><h2>与普通 RAG 的区别<a href="#与普通-rag-的区别"><span>#</span></a></h2><p>普通 RAG：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>文档 → Embedding → Search → Answer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Hermes Memory System：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>观察</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>→ Assertion Parser（断言分类）</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>→ Authority Check（权限检查）</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>→ Conflict Resolver（冲突解决）</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>→ Ranking（评分排序）</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>→ Consolidation（记忆合并）</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>→ Entity Graph（实体图谱）</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>→ Intent-aware Retrieval（意图感知检索）</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>→ Token Budget（预算控制）</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>→ Context Injection（上下文注入）</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>→ Reasoning（推理）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h2>下一阶段：v1.42 Hybrid Retrieval<a href="#下一阶段v142-hybrid-retrieval"><span>#</span></a></h2><p>v1.41 已冻结为基线。下一步不是继续堆字段，而是：</p><ol>
<li><strong>Entity Normalization</strong>（最高优先级）— “显卡” = “GPU” = “7650GRE”</li>
<li><strong>Hybrid Retrieval</strong> — keyword + vector + entity graph</li>
<li><strong>多标签 Intent</strong> — 一个查询可以同时是 FACT_LOOKUP + WHOAMI</li>
<li><strong>value_per_token</strong> — relevance × authority × freshness ÷ token_cost</li>
<li><strong>模块化拆分</strong> — 1020 行单文件拆为 memory/ 包</li>
</ol><p>策略：先让 Memory System 在真实任务中运行，收集 retrieval_success / retrieval_failure 数据，用真实失败样本驱动 v1.42 迭代。</p><hr /></section><section><h2>文件清单<a href="#文件清单"><span>#</span></a></h2>

<table><thead><tr><th>文件</th><th>说明</th></tr></thead><tbody><tr><td><code>~/.hermes/lib/memory_conflict_resolver.py</code></td><td>核心引擎（~1020 行）</td></tr><tr><td><code>~/.hermes/scripts/memory_lifecycle_v1.2_test.py</code></td><td>15 个测试案例</td></tr><tr><td><code>~/.hermes/plugins/experience-recorder/</code></td><td>Hook 自动记录插件</td></tr><tr><td><code>~/.hermes/skills/meta/experience-recorder/</code></td><td>分类规则 Skill</td></tr><tr><td><code>~/.hermes/skills/meta/memory-lifecycle-rules/</code></td><td>生命周期规则 Skill</td></tr><tr><td><code>~/.hermes/task-memory/raw/*.jsonl</code></td><td>任务记忆原始日志</td></tr><tr><td><code>~/.hermes/failure-memory.md</code></td><td>失败记录</td></tr><tr><td><code>~/.hermes/traces/</code></td><td>完整 trace 文件</td></tr></tbody></table><hr /></section><section><h2>总结<a href="#总结"><span>#</span></a></h2><p>从 v1.0 到 v1.41，Hermes Memory System 经历了：</p><ul>
<li><strong>v1.0</strong>：能记东西</li>
<li><strong>v1.2</strong>：会生命周期管理</li>
<li><strong>v1.3</strong>：会排序</li>
<li><strong>v1.37</strong>：会混合评分（authority gate）</li>
<li><strong>v1.39</strong>：会时间记忆和来源审计</li>
<li><strong>v1.41</strong>：会意图感知检索和预算控制</li>
</ul><p>已经从一个简单的 MEMORY.md 文件，演化为一个接近认知科学记忆模型的长期记忆治理层。</p><p>下一步的重点不是继续增加规则，而是让系统在真实任务中产生数据，用失败案例驱动迭代。这比理论优化更重要。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/personal-ai-engineering-system-v1/</id>
      <title type="text">Hermes 个人 AI 工程系统 v1.0：从零搭建到可观测认知架构的完整记录</title>
      <published>2026-08-01T00:00:00.000Z</published>
      <updated>2026-08-01T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/personal-ai-engineering-system-v1/"/>
      <summary type="text">Hermes 个人 AI 工程系统 v1.0：从零搭建到可观测认知架构的完整记录</summary>
      <content type="html"><![CDATA[<section><h1>Hermes 个人 AI 工程系统 v1.0<a href="#hermes-个人-ai-工程系统-v10"><span>#</span></a></h1><blockquote><p>这不是一篇配置教程，而是一个关于<strong>如何让 AI 记住你、理解你、并越用越聪明</strong>的工程记录。</p></blockquote><section><h2>前言<a href="#前言"><span>#</span></a></h2><p>我日常使用 Hermes Agent 处理系统架构设计、NAS 运维、家庭能源系统规划、代码编写等任务。但几个月下来，暴露了一个核心问题：<strong>AI 没有长期记忆</strong>。每次问同一个问题，它都从零开始推理。不会记住我的设备参数，不会记住之前的决策，不会从历史经验中学习。</p><p>解决这个问题需要一套完整的<strong>工程系统</strong>，而不是单一工具。经过连续多日的迭代，这套系统从”配置多个模型”正式进入 v1.0 阶段。</p><p>本文是对四篇系列文章的完整整合，一次性呈现这套系统的全貌。</p><hr /></section><section><h2>系统总览<a href="#系统总览"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>┌─────────────────────────────────────────────────────────────┐</span></div></div><div><div><div>2</div></div><div><span>│                    Hermes 个人 AI 工程系统 v1.0               │</span></div></div><div><div><div>3</div></div><div><span>├─────────────────────────────────────────────────────────────┤</span></div></div><div><div><div>4</div></div><div><span>│                                                             │</span></div></div><div><div><div>5</div></div><div><span>│  ┌─────────────────────────────────────────────────────┐   │</span></div></div><div><div><div>6</div></div><div><span>│  │  Layer 1: 行为准则 (SOUL.md)                         │   │</span></div></div><div><div><div>7</div></div><div><span>│  │  实事求是、调查研究、矛盾分析、系统思维、控制论        │   │</span></div></div><div><div><div>8</div></div><div><span>│  │  反馈闭环、假设验证、工程优先                         │   │</span></div></div><div><div><div>9</div></div><div><span>│  └─────────────────────────────────────────────────────┘   │</span></div></div><div><div><div>10</div></div><div><span>│                            │                                 │</span></div></div><div><div><div>11</div></div><div><span>│  ┌─────────────────────────────────────────────────────┐   │</span></div></div><div><div><div>12</div></div><div><span>│  │  Layer 2: 专业技能 (Skills)                          │   │</span></div></div><div><div><div>13</div></div><div><span>│  │  knowledge-retrieval-evidence-policy                 │   │</span></div></div><div><div><div>14</div></div><div><span>│  │  decision-memory / model-router v3                   │   │</span></div></div><div><div><div>15</div></div><div><span>│  │  memory-lifecycle-rules / experience-recorder        │   │</span></div></div><div><div><div>16</div></div><div><span>│  └─────────────────────────────────────────────────────┘   │</span></div></div><div><div><div>17</div></div><div><span>│                            │                                 │</span></div></div><div><div><div>18</div></div><div><span>│  ┌─────────────────────────────────────────────────────┐   │</span></div></div><div><div><div>19</div></div><div><span>│  │  Layer 3: 持久事实 (MEMORY.md + USER.md)             │   │</span></div></div><div><div><div>20</div></div><div><span>│  │  用户偏好、设备参数、环境信息、已验证的长期经验        │   │</span></div></div><div><div><div>21</div></div><div><span>│  └─────────────────────────────────────────────────────┘   │</span></div></div><div><div><div>22</div></div><div><span>│                            │                                 │</span></div></div><div><div><div>23</div></div><div><span>│  ┌─────────────────────────────────────────────────────┐   │</span></div></div><div><div><div>24</div></div><div><span>│  │  Layer 4: 知识库 (OpenViking)                        │   │</span></div></div><div><div><div>25</div></div><div><span>│  │  resources: 工程控制论/系统科学/软件工程/方法论       │   │</span></div></div><div><div><div>26</div></div><div><span>│  │  memories: 决策记录/历史案例/架构模式                │   │</span></div></div><div><div><div>27</div></div><div><span>│  └─────────────────────────────────────────────────────┘   │</span></div></div><div><div><div>28</div></div><div><span>│                            │                                 │</span></div></div><div><div><div>29</div></div><div><span>│  ┌─────────────────────────────────────────────────────┐   │</span></div></div><div><div><div>30</div></div><div><span>│  │  Layer 5: 多模型路由 (Model Router)                  │   │</span></div></div><div><div><div>31</div></div><div><span>│  │  Flash(主控制器) + Pro(审查) + GLM(专家) + Doubao(视觉)│   │</span></div></div><div><div><div>32</div></div><div><span>│  └─────────────────────────────────────────────────────┘   │</span></div></div><div><div><div>33</div></div><div><span>│                            │                                 │</span></div></div><div><div><div>34</div></div><div><span>│  ┌─────────────────────────────────────────────────────┐   │</span></div></div><div><div><div>35</div></div><div><span>│  │  Layer 6: 可观测性 (Observability)                   │   │</span></div></div><div><div><div>36</div></div><div><span>│  │  task-memory / failure-memory / trace / batch test  │   │</span></div></div><div><div><div>37</div></div><div><span>│  └─────────────────────────────────────────────────────┘   │</span></div></div><div><div><div>38</div></div><div><span>│                                                             │</span></div></div><div><div><div>39</div></div><div><span>└─────────────────────────────────────────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section><section><h2>第一章：为什么需要记忆系统<a href="#第一章为什么需要记忆系统"><span>#</span></a></h2><section><h3>起点<a href="#起点"><span>#</span></a></h3><p>Hermes Agent 原本只靠 MEMORY.md 存储用户事实，存在五个核心问题：</p>

<table><thead><tr><th>问题</th><th>后果</th></tr></thead><tbody><tr><td>无时间戳</td><td>无法判断过期</td></tr><tr><td>无可信度标记</td><td>事实和推测混在一起</td></tr><tr><td>无冲突解决</td><td>多来源矛盾时无裁决依据</td></tr><tr><td>无过期机制</td><td>老旧条目永久占空间</td></tr><tr><td>无冷冻机制</td><td>关键条目可能被意外覆盖</td></tr></tbody></table></section><section><h3>关键问题：AI 没有长期记忆<a href="#关键问题ai-没有长期记忆"><span>#</span></a></h3><p>举一个典型例子：测试中 AI 把”三电压逆变器”理解成”48V DC + 220V AC + 电网”，但实际是<strong>电池侧兼容 48V/60V/72V 三种电压</strong>。原因是模型用通用知识推测，没有优先尊重用户 memory 中已记录的设备事实。</p><p>这说明：<strong>再强的模型，如果不尊重用户已有事实，输出也是错的。</strong></p><hr /></section></section><section><h2>第二章：知识库搭建（OpenViking）<a href="#第二章知识库搭建openviking"><span>#</span></a></h2><section><h3>选型对比<a href="#选型对比"><span>#</span></a></h3>

<table><thead><tr><th>方案</th><th>优势</th><th>劣势</th></tr></thead><tbody><tr><td>Supermemory</td><td>自带 OCR、Gemini 提取</td><td>太重（PyTorch ~2GB），ARM64 慢</td></tr><tr><td>knowledge-base-mcp</td><td>MCP 协议，Claude 兼容</td><td>同样太重，ARM64 1 chunk/s</td></tr><tr><td><strong>OpenViking 0.4.11</strong></td><td>轻量 Go 二进制，FTS5 全文搜索，ARM64 飞快</td><td>无 OCR，需手动组织知识</td></tr></tbody></table><p>最终选择 OpenViking。纯 Go 编译，无 Python 依赖，在 ARM64 上毫秒级响应。</p></section><section><h3>知识分类<a href="#知识分类"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>viking://resources/rag/</span></div></div><div><div><div>2</div></div><div><span>├── engineering_control/    — 《工程控制论》上下册</span></div></div><div><div><div>3</div></div><div><span>├── history_politics/       — 毛选等（方法论层）</span></div></div><div><div><div>4</div></div><div><span>├── software_engineering/   — 《人月神话》等</span></div></div><div><div><div>5</div></div><div><span>└── systems_thinking/       — 《一般系统论》《系统科学方法概论》</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>知识检索策略<a href="#知识检索策略"><span>#</span></a></h3><p>创建 <code>knowledge-retrieval-evidence-policy</code> skill，强制规则：</p><ol>
<li><strong>事实优先于推测</strong> — 用户 memory 中的设备参数 &gt; 模型通用知识</li>
<li><strong>未知参数必须标记</strong> — 不允许自行补全，列出条件分支</li>
<li><strong>检索在推理之前</strong> — 复杂问题先查知识库，再推理</li>
</ol><p>优先级链：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>当前用户明确提供的事实 &gt; 用户已验证的长期事实 &gt; 当前环境实际检测结果</span></div></div><div><div><div>2</div></div><div><span>&gt; 官方文档与可靠资料 &gt; 知识库专业知识 &gt; 一般经验 &gt; 推测与假设</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section></section><section><h2>第三章：记忆系统从 v1.0 到 v1.41 的演化<a href="#第三章记忆系统从-v10-到-v141-的演化"><span>#</span></a></h2><section><h3>演化路线<a href="#演化路线"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>v1.0  能记东西</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>v1.1  会冲突检测</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>v1.2  会生命周期管理（持久化 + decay）</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>v1.3  会排序（Memory Ranking Engine）</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>9</div></div><div><span>v1.35 会权限和实体（authority + entity/field）</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>11</div></div><div><span>v1.36 会不可变保护（IMMUTABLE_TYPES）</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>13</div></div><div><span>v1.37 会混合评分（authority_gate × 加权和）</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>15</div></div><div><span>v1.38 会来源审计（provenance + scope 隔离）</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>17</div></div><div><span>v1.39 会时间记忆（valid_from/until + memory_layer）</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>19</div></div><div><span>v1.40 会检索（Query Planner + GC + EntityGraph）</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>21</div></div><div><span>v1.41 会意图感知 + 预算管理（Intent Detection + Budget）</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>v1.41 核心架构<a href="#v141-核心架构"><span>#</span></a></h3><p><strong>MemoryEntry 数据模型</strong> — 20+ 维度，包括：value、entity、field、scope、authority、confidence、provenance、valid_from/until、memory_layer、identity_status 等。</p><p><strong>评分公式</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>score = authority_gate × (</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>0.4 × confidence</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>+ 0.2 × freshness</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>+ 0.2 × explicitness</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>+ 0.1 × retention</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>+ 0.1 × usage_frequency</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>权限门控</strong>：</p>

<table><thead><tr><th>权限级别</th><th>权重</th><th>gate</th></tr></thead><tbody><tr><td>user</td><td>1.0</td><td>1.0</td></tr><tr><td>tool</td><td>0.6</td><td>0.7</td></tr><tr><td>agent</td><td>0.3</td><td>0.4</td></tr><tr><td>unknown</td><td>0.0</td><td>0.1</td></tr></tbody></table><p>外部信息（网页、模型推理）永远无法覆盖用户确认的事实。</p><p><strong>冲突解决管道</strong>：用户输入 → Assertion Parser（CONFIRMED/HYPOTHETICAL/UNCERTAIN/RECALL/QUERY/REFERENCE）→ Intent Detection → Authority Check → Conflict Detection → Decision（supersede/flag_conflict/reject）→ Memory Write</p><p><strong>记忆生命周期</strong>：ACTIVE → STALE → COMPRESSED → ARCHIVED。从不删除，TEMPORARY 高频使用自动升级为 EPISODIC。</p><p><strong>意图感知检索</strong>：根据查询意图（WHOAMI/HOWTO/FACT_LOOKUP/DIAGNOSE/TEMPORAL）动态调整各记忆层权重，以匹配度 × 权限 × 新鲜度 × 范围匹配 × 层优先级 综合评分。</p></section><section><h3>关键设计决策<a href="#关键设计决策"><span>#</span></a></h3><ol>
<li><strong>来源与权限分离</strong> — 信息来源（source）和修改权限（authority）是两个独立维度。用户提供的网址（source=web）仍然是 user 权限，不会被降级。</li>
<li><strong>IDENTITY 不可变保护</strong> — 用户身份/偏好不能被网页或模型推理覆盖。</li>
<li><strong>Provenance 自动传播</strong> — 每次 supersede 自动继承旧链并追加新记录。</li>
<li><strong>Scope 隔离</strong> — main_pc 和 laptop 的同属性不同值不冲突。</li>
<li><strong>时间版本</strong> — 每次变更保留 valid_from/valid_until 历史链。</li>
</ol></section><section><h3>测试验证<a href="#测试验证"><span>#</span></a></h3><p>15 个测试案例，覆盖基础冲突解决、语义压力测试、生命周期测试、权限安全测试，全部 15/15 PASS。</p><hr /></section></section><section><h2>第四章：多模型路由架构<a href="#第四章多模型路由架构"><span>#</span></a></h2><section><h3>问题<a href="#问题"><span>#</span></a></h3><p>Hermes 只有一个主模型，但日常问答、复杂架构设计、图片识别对模型能力要求不同。</p></section><section><h3>可用模型池<a href="#可用模型池"><span>#</span></a></h3>

<table><thead><tr><th>模型</th><th>角色</th><th>资源等级</th></tr></thead><tbody><tr><td>DeepSeek V4 Flash</td><td>主模型 + Router + 整合者</td><td>快速</td></tr><tr><td>DeepSeek V4 Pro</td><td>审查 + 增强</td><td>增强</td></tr><tr><td>GLM-5.2</td><td>深度推理专家</td><td>深度</td></tr><tr><td>Doubao Seed 2.0 Pro</td><td>视觉</td><td>专用</td></tr></tbody></table></section><section><h3>架构演化（v1 → v2 → v3）<a href="#架构演化v1--v2--v3"><span>#</span></a></h3><p><strong>v1（关键词触发）</strong>：看到”架构”→ delegate，看到”状态机”→ delegate。问题：误触发，过拟合。</p><p><strong>v2（三级任务分级）</strong>：Level 0（Flash）→ Level 1（Flash+Pro）→ Level 2（GLM）。改进：从关键词升级为按复杂度分级。遗留：GLM 直接输出给用户，无用户覆盖。</p><p><strong>v3（三级路由 + 专家回流 + 用户覆盖）</strong> — 当前冻结版本：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                </span></span><span>User Request</span></div></div><div><div><div>2</div></div><div><span><span>                     </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>                     </span></span><span>▼</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>DeepSeek V4 Flash</span></div></div><div><div><div>5</div></div><div><span><span>          </span></span><span>Router + Orchestrator</span></div></div><div><div><div>6</div></div><div><span><span>                     </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>┌────────────┼────────────┐</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>│            │            │</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>▼            ▼            ▼</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>Level 0      Level 1      Level 2</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>Flash       Flash+Pro      GLM-5.2</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>│            │            │</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>│            │            │</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>▼            ▼            ▼</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>简单任务      审查增强      专家设计</span></div></div><div><div><div>16</div></div><div><span><span>                     </span></span><span>│</span></div></div><div><div><div>17</div></div><div><span><span>                     </span></span><span>▼</span></div></div><div><div><div>18</div></div><div><span><span>              </span></span><span>Flash 整合输出</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>核心规则</strong>：</p><ol>
<li>GLM 是专家，不直接面对用户，输出必须回流 Flash 验证+整合</li>
<li>用户明确指定模型时覆盖自动路由</li>
<li>先查 OpenViking 知识库，再决定是否 delegate</li>
<li>预估 &lt;1000 tokens 且无架构/安全需求时不 delegate</li>
<li>资源等级替代成本等级（避免 API Plan 价格变化导致错误）</li>
</ol></section><section><h3>完整决策树<a href="#完整决策树"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Step 1: 用户指定了模型？ → 尊重用户选择</span></div></div><div><div><div>2</div></div><div><span>Step 2: 包含图片？ → Doubao Vision（自动）</span></div></div><div><div><div>3</div></div><div><span>Step 3: 有 OpenViking 知识？ → 先检索</span></div></div><div><div><div>4</div></div><div><span>Step 4: 复杂度评估</span></div></div><div><div><div>5</div></div><div><span>Step 5: 路由（Level 0/1/2）</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>配置实现<a href="#配置实现"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>default</span><span>: </span><span>deepseek-v4-flash</span></div></div><div><div><div>3</div></div><div><span>  </span><span>provider</span><span>: </span><span>custom</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>delegation</span><span>:</span></div></div><div><div><div>6</div></div><div><span>  </span><span>model</span><span>: </span><span>glm-5.2</span></div></div><div><div><div>7</div></div><div><span>  </span><span>provider</span><span>: </span><span>custom</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>auxiliary</span><span>:</span></div></div><div><div><div>10</div></div><div><span>  </span><span>vision</span><span>:</span></div></div><div><div><div>11</div></div><div><span>    </span><span>model</span><span>: </span><span>doubao-seed-2-0-pro-260215</span></div></div><div><div><div>12</div></div><div><span>    </span><span>base_url</span><span>: </span><span>"https://ark.cn-beijing.volces.com/api/coding/v3"</span></div></div><div><div><div>13</div></div><div><span>    </span><span>timeout</span><span>: </span><span>120</span></div></div><div><div><div>14</div></div><div><span>  </span><span>compression</span><span>:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div><div><div><div>16</div></div><div><span>  </span><span>background_review</span><span>:</span></div></div><div><div><div>17</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div><div><div><div>18</div></div><div><span>  </span><span>curator</span><span>:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section></section><section><h2>第五章：可观测 AI 系统<a href="#第五章可观测-ai-系统"><span>#</span></a></h2><section><h3>三个阶段<a href="#三个阶段"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>阶段1: 工具调用 Agent → 能调用工具，但每次独立决策，无记忆</span></div></div><div><div><div>2</div></div><div><span>阶段2: 多模型 Agent → 多模型分工，规则路由，有记忆分层</span></div></div><div><div><div>3</div></div><div><span>阶段3: 可观测 AI 系统 → 有日志，有版本，有实验，有回滚，有失败分析</span></div></div><div><div><div>4</div></div><div><span>阶段4: 经验驱动 AI 系统（未来）→ 基于积累数据自动优化路由</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>内存生命周期规则<a href="#内存生命周期规则"><span>#</span></a></h3><p>7 类可信度标记：</p>

<table><thead><tr><th>标记</th><th>含义</th><th>TTL</th></tr></thead><tbody><tr><td>CONFIRMED ✅</td><td>用户确认的事实</td><td>30-90 天</td></tr><tr><td>OBSERVED 📋</td><td>自动探测的信息</td><td>30 天</td></tr><tr><td>DERIVED 🔄</td><td>推理得出的结论</td><td>14 天</td></tr><tr><td>SPECULATIVE ❓</td><td>猜测，未验证</td><td>1 天</td></tr><tr><td>STALE ⌛</td><td>已过期</td><td>保留 7 天后移除</td></tr><tr><td>CONFLICTED ⚠️</td><td>多个来源不一致</td><td>待裁决</td></tr><tr><td>FROZEN 🧊</td><td>用户锁定，永不覆盖</td><td>永久</td></tr></tbody></table><p>冲突优先级：<strong>来源可信度 &gt; 存储位置</strong>。用户今天说的 CONFIRMED 事实，优先级高于旧 MEMORY.md 记录。</p></section><section><h3>三层记忆职责边界<a href="#三层记忆职责边界"><span>#</span></a></h3>

<table><thead><tr><th>层</th><th>存储</th><th>职责</th></tr></thead><tbody><tr><td>MEMORY.md / USER.md</td><td>高频事实</td><td>系统 prompt 自动注入，零延迟</td></tr><tr><td>OpenViking</td><td>语义知识</td><td>长期知识、决策、经验、架构模式</td></tr><tr><td>FTS5 (session_search)</td><td>原始对话</td><td>全文检索历史对话，兜底</td></tr></tbody></table><p>交叉检索策略：先查 MEMORY → 再查 OpenViking → 最后查 FTS5。</p></section><section><h3>Task-Memory 和 Failure-Memory<a href="#task-memory-和-failure-memory"><span>#</span></a></h3><p><strong>Task-Memory</strong> 记录每次路由决策的完整信息：任务类型、复杂度评分、路由判断、使用模型、耗时、token、结果摘要。</p><p><strong>Failure-Memory</strong> 记录每次失败的根本原因和修复方案：</p>

<table><thead><tr><th>ID</th><th>类型</th><th>原因</th><th>修复</th></tr></thead><tbody><tr><td>F001</td><td>CONFIG</td><td>delegation.provider 指向不存在的 provider</td><td>修复为 volcengine-coding-plan</td></tr><tr><td>F002</td><td>TOOL</td><td>GLM 子任务执行过多环境探测，600s 超时</td><td>Flash 在 delegate 前完成检索</td></tr><tr><td>F003</td><td>TOOL</td><td>web_search 依赖缺失，子任务花时间安装</td><td>delegate context 预装依赖</td></tr><tr><td>F004</td><td>CONFIG</td><td>auxiliary.provider 同样指向不存在的 provider</td><td>统一切换 provider</td></tr></tbody></table></section><section><h3>Batch Test v1<a href="#batch-test-v1"><span>#</span></a></h3>

<table><thead><tr><th>任务</th><th>等级</th><th>模型</th><th>耗时</th><th>结果</th></tr></thead><tbody><tr><td>什么是 MQTT broker</td><td>Level 0</td><td>Flash</td><td>0.5s</td><td>正确路由</td></tr><tr><td>查 NAS IP</td><td>Level 0</td><td>Flash</td><td>0.5s</td><td>正确路由</td></tr><tr><td>写光伏效率函数</td><td>Level 1</td><td>Flash</td><td>0.5s</td><td>正确路由</td></tr><tr><td>审查 HA MQTT 配置</td><td>Level 1</td><td>Pro</td><td>52s</td><td>正确路由，发现 9 项风险</td></tr><tr><td>设计 15 状态机</td><td>Level 2</td><td>GLM</td><td>419s</td><td>正确路由，36KB 设计文档</td></tr><tr><td>对比 MQTT vs Modbus</td><td>Level 2</td><td>GLM</td><td>102s</td><td>正确路由，18K 字符深度分析</td></tr></tbody></table><p>路由正确率：6/6。样本量小，需扩充到 20-50。</p></section><section><h3>关键发现<a href="#关键发现"><span>#</span></a></h3><ol>
<li><strong>配置错误导致静默失败</strong> — delegation.provider 未配置 API key，所有 delegate 任务静默失败</li>
<li><strong>GLM 子任务执行过剩环境探测</strong> — 600 秒中大部分时间花在 session_search、execute_code 等环境探测上</li>
<li><strong>1000-token 阈值无数据支撑</strong> — 拍脑袋的，需要更多样本</li>
<li><strong>Flash 同时作为路由器和审查者是循环依赖</strong> — v4 目标：引入 Pro 作为独立审查者</li>
</ol><hr /></section></section><section><h2>第六章：经验教训<a href="#第六章经验教训"><span>#</span></a></h2><section><h3>架构原则<a href="#架构原则"><span>#</span></a></h3><ol>
<li><strong>分层比堆叠重要</strong> — 把行为准则、知识、事实、技能分开，比往一个文件里塞更多内容有效</li>
<li><strong>事实约束比模型能力重要</strong> — 再强的模型，如果不尊重用户已有事实，输出也是错的</li>
<li><strong>轻量方案优先</strong> — ARM64 上跑 PyTorch 是不归路，FTS5 全文搜索足够快</li>
<li><strong>AI Agent 需要反馈闭环</strong> — 一次回答不够，需要设计验证、修正、迭代的机制</li>
</ol></section><section><h3>路由设计教训<a href="#路由设计教训"><span>#</span></a></h3><ol>
<li><strong>不要用关键词触发路由</strong> — 容易误触发，趋向过拟合</li>
<li><strong>Flash 是大脑，不是最低级模型</strong> — 它负责判断和整合，不是替代品</li>
<li><strong>GLM 不直接面对用户</strong> — 专家输出必须经过主模型验证和整合</li>
<li><strong>成本等级不要硬编码</strong> — 不同 API Plan 价格不同，用资源等级替代</li>
<li><strong>先查知识库再 delegate</strong> — 充分利用已有知识，避免不必要的专家调用</li>
<li><strong>规则路由是起点，经验路由是终点</strong> — 先跑数据，再做自适应</li>
</ol><hr /></section></section><section><h2>第七章：当前状态与下一阶段<a href="#第七章当前状态与下一阶段"><span>#</span></a></h2><section><h3>v1.0 冻结清单<a href="#v10-冻结清单"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model-router v3 (frozen-baseline)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├── 三级路由：Level 0 (Flash) / Level 1 (Flash/Pro) / Level 2 (GLM)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├── 固定分工：视觉 → Doubao，审查 → Pro，压缩 → Pro</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>└── 用户覆盖：用户指定模型时跳过自动路由</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>memory-lifecycle-rules v1</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>├── 7 类可信度标记</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>├── 5 类生命周期规则</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>├── 三层记忆职责边界</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>└── 冲突优先级：来源可信度 &gt; 存储位置</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>memory_conflict_resolver v1.41</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>├── ~1020 行 Python 核心引擎</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>├── 15/15 测试通过</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>├── 意图感知检索 + Token 预算控制</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>└── 冻结，等待真实数据驱动迭代</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>task-memory / failure-memory (observation only)</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>└── 不自动影响路由，仅收集数据</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>experience-recorder (Python plugin)</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>└── hook 自动触发，不依赖模型遵守规则</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>4 个观察指标<a href="#4-个观察指标"><span>#</span></a></h3>

<table><thead><tr><th>指标</th><th>目标</th><th>说明</th></tr></thead><tbody><tr><td>路由准确率</td><td>逐步提升</td><td>任务→路由→结果质量评分</td></tr><tr><td>GLM 调用比例</td><td>5-15%</td><td>超过 30% 说明 Router 太激进</td></tr><tr><td>升级失败案例</td><td>重点收集</td><td>Flash 不足→用户追问→GLM 解决的案例</td></tr><tr><td>决策稳定性</td><td>同类任务一致</td><td>避免同类型任务反复切换模型</td></tr></tbody></table></section><section><h3>理想模型分配<a href="#理想模型分配"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Flash:  70-85%</span></div></div><div><div><div>2</div></div><div><span>Pro:    10-20%</span></div></div><div><div><div>3</div></div><div><span>GLM:     5-15%</span></div></div><div><div><div>4</div></div><div><span>Doubao: 按需</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>下一阶段方向<a href="#下一阶段方向"><span>#</span></a></h3><p><strong>v1.42 Memory System</strong>（不急着写，先收集真实数据）：</p><ol>
<li>Entity Normalization（最高优先级）— “显卡” = “GPU” = “7650GRE”</li>
<li>Hybrid Retrieval — keyword + vector + entity graph</li>
<li>多标签 Intent</li>
<li>value_per_token budget</li>
<li>模块化拆分（1020 行单文件拆为 memory/ 包）</li>
</ol><p><strong>v4 路由</strong>（暂不实现）：</p><ul>
<li>Flash = Orchestrator</li>
<li>GLM = Design Expert</li>
<li>Pro = Independent Reviewer</li>
<li>Doubao = Vision</li>
<li>经验驱动路由替代规则驱动</li>
</ul><hr /></section><section><h3>文件清单<a href="#文件清单"><span>#</span></a></h3>

<table><thead><tr><th>文件</th><th>说明</th></tr></thead><tbody><tr><td><code>~/.hermes/lib/memory_conflict_resolver.py</code></td><td>核心引擎（~1020 行）</td></tr><tr><td><code>~/.hermes/scripts/memory_lifecycle_v1.2_test.py</code></td><td>15 个测试案例</td></tr><tr><td><code>~/.hermes/plugins/experience-recorder/</code></td><td>Hook 自动记录插件</td></tr><tr><td><code>~/.hermes/skills/meta/model-router/</code></td><td>三级路由规则</td></tr><tr><td><code>~/.hermes/skills/meta/experience-recorder/</code></td><td>分类规则 Skill</td></tr><tr><td><code>~/.hermes/skills/meta/memory-lifecycle-rules/</code></td><td>生命周期规则 Skill</td></tr><tr><td><code>~/.hermes/skills/meta/knowledge-retrieval-evidence-policy/</code></td><td>事实约束规则</td></tr><tr><td><code>~/.hermes/task-memory/raw/*.jsonl</code></td><td>任务记忆原始日志</td></tr><tr><td><code>~/.hermes/failure-memory.md</code></td><td>失败记录</td></tr><tr><td><code>~/.hermes/traces/</code></td><td>完整 trace 文件</td></tr></tbody></table><hr /></section></section><section><h2>结语<a href="#结语"><span>#</span></a></h2><p>这套系统现在已经从”配置多个模型”进入”个人 AI 工程系统”的阶段。</p><p>当前阶段的成功标准不是路由更复杂，而是：</p><ul>
<li><strong>每一次决策都有记录</strong></li>
<li><strong>每一次失败都有原因</strong></li>
<li><strong>每一次改进都有依据</strong></li>
</ul><p>下一步的重点不是继续增加规则，而是让系统在真实任务中产生数据，用失败案例驱动迭代。这比理论优化更重要。</p><p><strong>已从”配置 AI”进入”培养 AI”阶段。</strong></p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/building-personal-ai-engineering-knowledge-os/</id>
      <title type="text">构建个人 AI 工程知识操作系统：Hermes + OpenViking 多层记忆架构实践</title>
      <published>2026-07-31T00:00:00.000Z</published>
      <updated>2026-07-31T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/building-personal-ai-engineering-knowledge-os/"/>
      <summary type="text">构建个人 AI 工程知识操作系统：Hermes + OpenViking 多层记忆架构实践</summary>
      <content type="html"><![CDATA[<section><h1>构建个人 AI 工程知识操作系统：Hermes + OpenViking 多层记忆架构实践<a href="#构建个人-ai-工程知识操作系统hermes--openviking-多层记忆架构实践"><span>#</span></a></h1><section><h2>背景<a href="#背景"><span>#</span></a></h2><p>我日常使用 <strong>Hermes Agent</strong>（Nous Research 开源）作为 AI 助手，处理系统架构设计、NAS 运维、家庭能源系统规划、代码编写等任务。但几个月下来，暴露了一个核心问题：<strong>AI 没有长期记忆</strong>。</p><p>每次问同一个问题，它都从零开始推理。不会记住我的设备参数、不会记住之前的决策、不会从历史经验中学习。</p><p>解决这个问题需要一套完整的<strong>记忆架构</strong>，而不是单一工具。这篇博客记录了过去几天的优化过程。</p></section><section><h2>一、知识库搭建：OpenViking<a href="#一知识库搭建openviking"><span>#</span></a></h2><section><h3>选型<a href="#选型"><span>#</span></a></h3><p>对比了几个方案：</p>

<table><thead><tr><th>方案</th><th>优势</th><th>劣势</th></tr></thead><tbody><tr><td>Supermemory</td><td>自带 OCR、Gemini 提取</td><td>太重（PyTorch ~2GB），ARM64 慢，需自建服务</td></tr><tr><td>knowledge-base-mcp</td><td>MCP 协议，Claude 兼容</td><td>同样太重（PyTorch + BGE-M3 ~4GB），ARM64 1 chunk/s</td></tr><tr><td>OpenViking</td><td>轻量（Go 二进制），本地文件系统索引，FTS5 全文搜索</td><td>无 OCR，需要手动组织知识</td></tr></tbody></table><p>最终选择 <strong>OpenViking 0.4.11</strong>。原因：纯 Go 编译，无 Python 依赖，在 ARM64 上跑得飞快，FTS5 搜索毫秒级响应。</p></section><section><h3>知识分类<a href="#知识分类"><span>#</span></a></h3><p>将知识库按领域划分：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>viking://resources/rag/</span></div></div><div><div><div>2</div></div><div><span>├── engineering_control/    — 《工程控制论》上下册</span></div></div><div><div><div>3</div></div><div><span>├── history_politics/       — 毛选等（方法论层）</span></div></div><div><div><div>4</div></div><div><span>├── software_engineering/   — 《人月神话》等</span></div></div><div><div><div>5</div></div><div><span>└── systems_thinking/       — 《一般系统论》《系统科学方法概论》</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>核心是 <strong>工程控制论 + 系统科学</strong>，作为解决复杂工程问题时的知识来源。</p></section><section><h3>Hermes 集成<a href="#hermes-集成"><span>#</span></a></h3><p>配置 Hermes 的 memory provider 为 OpenViking，并启用 <code>OPENVIKING_RECALL_RESOURCES=true</code>，让 Hermes 在遇到复杂问题时自动检索知识库：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>memory</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>provider</span><span>: </span><span>openviking</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>二、多层记忆架构<a href="#二多层记忆架构"><span>#</span></a></h2><p>Hermes 的长期记忆系统设计为四层，各司其职：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Layer 1: SOUL.md（行为准则）</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├── 实事求是、调查研究、矛盾分析</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├── 系统思维、控制论、工程优先</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>└── 反馈闭环、假设验证</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Layer 2: Skills（怎么做）</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>├── knowledge-retrieval-evidence-policy</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>├── decision-memory</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>└── 各领域专业技能</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Layer 3: USER.md + MEMORY.md（事实）</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>├── 用户偏好、设备参数、环境信息</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>└── 已验证的长期经验</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>Layer 4: OpenViking（知识 + 经验）</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>├── 工程控制论、系统科学</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>├── 软件工程、项目管理</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>└── 决策记录、历史案例</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>关键原则</strong>：不要把知识往 SOUL.md 里堆。SOUL.md 是行为准则，不是知识库。知识归知识库，事实归 memory，做法归 skills。</p></section><section><h2>三、知识检索策略优化<a href="#三知识检索策略优化"><span>#</span></a></h2><section><h3>发现的问题<a href="#发现的问题"><span>#</span></a></h3><p>测试中暴露了一个典型错误：AI 把”三电压逆变器”理解成”48V DC + 220V AC + 电网”，但实际是<strong>电池侧兼容 48V/60V/72V 三种电压</strong>。</p><p>原因是：模型用通用知识推测，没有优先尊重用户 memory 中已记录的设备事实。</p></section><section><h3>解决方案<a href="#解决方案"><span>#</span></a></h3><p>创建 <code>knowledge-retrieval-evidence-policy</code> skill，强制以下规则：</p><ol>
<li><strong>事实优先于推测</strong> — 用户 memory 中的设备参数 &gt; 模型通用知识</li>
<li><strong>未知参数必须标记</strong> — 不允许自行补全，列出条件分支</li>
<li><strong>检索在推理之前</strong> — 复杂问题先查知识库，再推理</li>
</ol><p>结果：下次测试时，Hermes 正确识别了三电压逆变器的含义，并主动询问未知参数，而不是自行假设。</p></section></section><section><h2>四、家庭能源系统设计<a href="#四家庭能源系统设计"><span>#</span></a></h2><section><h3>跨领域测试<a href="#跨领域测试"><span>#</span></a></h3><p>作为知识库效果验证，设计了一套家庭能源管理系统。Hermes 在这个过程中主动调用了 OpenViking 中的工程控制论和系统科学知识，完成了跨领域综合。</p></section><section><h3>四层架构<a href="#四层架构"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>L4: 米家智能家居层 — 非关键负载控制</span></div></div><div><div><div>2</div></div><div><span>L3: HA 能源管理层 — 状态机、能量调度、SOC 管理</span></div></div><div><div><div>3</div></div><div><span>L2: 逆变器控制层 — MPPT、EPS 切换、电池检测</span></div></div><div><div><div>4</div></div><div><span>L1: 硬件保护层 — 断路器、SPD、熔断器（纯物理）</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>15 状态机<a href="#15-状态机"><span>#</span></a></h3><p>覆盖无电池、电池怠速、PV 充电、PV 自用、余电上网、电网充电、削峰放电、多级停电、换电池、故障、手动接管等场景。</p></section><section><h3>租赁电池的特殊性<a href="#租赁电池的特殊性"><span>#</span></a></h3><p>用户电池不是固定购买，而是临时租赁。这意味着系统必须支持：</p><ul>
<li><strong>无电池状态</strong>：PV 自发自用，余电上网</li>
<li><strong>电池接入检测</strong>：自动识别 48V/60V/72V，无需 BMS 通信</li>
<li><strong>换电池不停机</strong>：热插拔支持</li>
</ul></section><section><h3>停电备用<a href="#停电备用"><span>#</span></a></h3><p>电气切换由逆变器硬件 EPS 完成（&lt;20ms），HA 负责 30s 后管理非关键负载，不承担毫秒级安全切换。</p></section></section><section><h2>五、多模型路由配置<a href="#五多模型路由配置"><span>#</span></a></h2><section><h3>问题<a href="#问题"><span>#</span></a></h3><p>Hermes 只有一个主模型。但日常问答、复杂架构设计、图片识别对模型能力要求不同。全部用最强模型浪费 token，全部用最便宜模型质量不够。</p></section><section><h3>方案<a href="#方案"><span>#</span></a></h3><p>利用 Hermes 的 <code>auxiliary</code> 和 <code>delegation</code> 机制，实现多模型分工：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                    </span></span><span>Hermes</span></div></div><div><div><div>2</div></div><div><span><span>                       </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>           </span></span><span>DeepSeek V4 Flash（主模型）</span></div></div><div><div><div>4</div></div><div><span><span>           </span></span><span>日常对话、RAG、工具调用、记忆管理</span></div></div><div><div><div>5</div></div><div><span><span>                       </span></span><span>│</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>┌──────────────┼──────────────┐</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>│              │              │</span></div></div><div><div><div>8</div></div><div><span><span>          </span></span><span>▼              ▼              ▼</span></div></div><div><div><div>9</div></div><div><span><span>       </span></span><span>GLM-5.2      DeepSeek V4 Pro  Doubao Seed 2.0 Pro</span></div></div><div><div><div>10</div></div><div><span><span>       </span></span><span>─────────     ─────────────     ─────────────────</span></div></div><div><div><div>11</div></div><div><span><span>       </span></span><span>子任务         压缩             vision_analyze</span></div></div><div><div><div>12</div></div><div><span><span>       </span></span><span>delegate      后台审查          OCR/截图/电路图</span></div></div><div><div><div>13</div></div><div><span><span>                     </span></span><span>策展</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>配置<a href="#配置"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>default</span><span>: </span><span>deepseek-v4-flash</span></div></div><div><div><div>3</div></div><div><span>  </span><span>provider</span><span>: </span><span>custom</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>delegation</span><span>:</span></div></div><div><div><div>6</div></div><div><span>  </span><span>model</span><span>: </span><span>glm-5.2</span><span>          </span><span># 子任务自动走 GLM</span></div></div><div><div><div>7</div></div><div><span>  </span><span>provider</span><span>: </span><span>custom</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>auxiliary</span><span>:</span></div></div><div><div><div>10</div></div><div><span>  </span><span>vision</span><span>:</span></div></div><div><div><div>11</div></div><div><span>    </span><span>model</span><span>: </span><span>doubao-seed-2-0-pro-260215</span></div></div><div><div><div>12</div></div><div><span>    </span><span>base_url</span><span>: </span><span>"https://ark.cn-beijing.volces.com/api/coding/v3"</span></div></div><div><div><div>13</div></div><div><span>    </span><span>timeout</span><span>: </span><span>120</span></div></div><div><div><div>14</div></div><div><span>  </span><span>compression</span><span>:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span><span>   </span><span># 压缩质量优先，不丢关键约束</span></div></div><div><div><div>16</div></div><div><span>  </span><span>background_review</span><span>:</span></div></div><div><div><div>17</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div><div><div><div>18</div></div><div><span>  </span><span>curator</span><span>:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>限制</strong>：<code>delegation.model</code> 是”所有子任务固定走 GLM”，不是智能路由。Hermes 目前没有内置根据任务难度自动切换模型的能力。</p></section></section><section><h2>六、清理与取舍<a href="#六清理与取舍"><span>#</span></a></h2><section><h3>放弃的方案<a href="#放弃的方案"><span>#</span></a></h3><p>过去几个月尝试过的一些路线，最终都放弃了：</p>

<table><thead><tr><th>项目</th><th>放弃原因</th><th>替代方案</th></tr></thead><tbody><tr><td>knowledge-base-mcp</td><td>PyTorch ~2GB + BGE-M3 ~2.2GB，ARM64 极慢</td><td>OpenViking FTS5</td></tr><tr><td>Supermemory</td><td>同样太重，Gemini 配额限制每天 20 次</td><td>直接放弃，改为 OpenViking</td></tr><tr><td>lottery-agent（彩票分析）</td><td>不再需要</td><td>已删除</td></tr></tbody></table></section><section><h3>清理命令<a href="#清理命令"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>rm</span><span> </span><span>-rf</span><span> </span><span>~/knowledge-base-mcp</span><span> </span><span>~/kb-mcp-server</span></div></div><div><div><div>2</div></div><div><span>rm</span><span> </span><span>-rf</span><span> </span><span>~/lottery-agent</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>七、三层系统整合<a href="#七三层系统整合"><span>#</span></a></h2><p>NAS、AI Agent、家庭能源系统形成三层架构：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Layer A: AI Agent（智能决策层）</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├── HA API 读取能源状态</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├── SSH 管理 NAS 服务</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>├── cron 定时任务（日报/异常检测）</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>└── OpenViking 知识库</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>Layer B: NAS（基础设施层）</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>├── 数据存储（InfluxDB/SQLite）</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>├── Grafana 可视化</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>├── nginx 反向代理</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>└── Ghost 博客</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>Layer C: HA 能源管理层</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>├── 状态机执行</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>├── 逆变器 Modbus 通信</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>├── 米家设备集成</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>└── SOC 管理 / 负载调度</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h2>总结<a href="#总结"><span>#</span></a></h2><section><h3>当前状态<a href="#当前状态"><span>#</span></a></h3>

<table><thead><tr><th>组件</th><th>评分</th></tr></thead><tbody><tr><td>SOUL.md（行为准则）</td><td>8.5/10</td></tr><tr><td>OpenViking 知识库</td><td>8/10</td></tr><tr><td>知识分类</td><td>8/10</td></tr><tr><td>知识调用策略</td><td>7/10</td></tr><tr><td>多模型路由</td><td>已完成</td></tr></tbody></table></section><section><h3>经验教训<a href="#经验教训"><span>#</span></a></h3><ol>
<li><strong>分层比堆叠重要</strong> — 把行为准则、知识、事实、技能分开，比往一个文件里塞更多内容有效</li>
<li><strong>事实约束比模型能力重要</strong> — 再强的模型，如果不尊重用户已有事实，输出也是错的</li>
<li><strong>轻量方案优先</strong> — ARM64 上跑 PyTorch 是不归路，FTS5 全文搜索足够快</li>
<li><strong>AI Agent 需要反馈闭环</strong> — 一次回答不够，需要设计验证、修正、迭代的机制</li>
</ol></section><section><h3>下一步<a href="#下一步"><span>#</span></a></h3><p>做一个 <strong>delegate 触发规则 skill</strong>，让 Hermes 能自动判断什么时候需要走 GLM-5.2 子任务，接近真正的多模型智能路由。</p></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/multi-model-routing-architecture/</id>
      <title type="text">Hermes 多模型路由架构：从单模型到三级专家协作的演化之路</title>
      <published>2026-07-31T00:00:00.000Z</published>
      <updated>2026-07-31T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/multi-model-routing-architecture/"/>
      <summary type="text">Hermes 多模型路由架构：从单模型到三级专家协作的演化之路</summary>
      <content type="html"><![CDATA[<section><h1>Hermes 多模型路由架构：从单模型到三级专家协作的演化之路<a href="#hermes-多模型路由架构从单模型到三级专家协作的演化之路"><span>#</span></a></h1><section><h2>背景<a href="#背景"><span>#</span></a></h2><p>在上一篇《构建个人 AI 工程知识操作系统》中，我记录了 Hermes Agent 多层记忆架构的搭建过程。但解决了”记住什么”之后，下一个问题是——<strong>“用什么模型来思考”</strong>。</p><p>日常对话、RAG 检索、简单脚本用 Flash 就够，但系统架构设计、硬件安全分析、多方案权衡需要更强的推理能力。全部用最强模型浪费 token，全部用最便宜模型质量不够。</p><p>本文记录从单模型到多模型路由的完整演化过程。</p></section><section><h2>问题定义<a href="#问题定义"><span>#</span></a></h2><section><h3>可用模型池<a href="#可用模型池"><span>#</span></a></h3>

<table><thead><tr><th>模型</th><th>能力特征</th><th>资源消耗</th></tr></thead><tbody><tr><td>DeepSeek V4 Flash</td><td>快速响应，工具调用稳定</td><td>低</td></tr><tr><td>DeepSeek V4 Pro</td><td>代码能力中等，适合审查</td><td>中</td></tr><tr><td>GLM-5.2</td><td>深度推理，架构设计强</td><td>高</td></tr><tr><td>Doubao Seed 2.0 Pro</td><td>视觉理解，OCR</td><td>专用</td></tr></tbody></table></section><section><h3>核心矛盾<a href="#核心矛盾"><span>#</span></a></h3><ul>
<li><strong>单一模型无法同时兼顾</strong>速度、成本、复杂推理质量</li>
<li>Hermes 没有内置”根据任务难度自动切换模型”的能力</li>
<li>主模型只能有一个，但任务类型多种多样</li>
</ul></section><section><h3>设计目标<a href="#设计目标"><span>#</span></a></h3><ol>
<li>简单任务不浪费 GLM</li>
<li>复杂任务 Flash 能判断出来交给专家</li>
<li>视觉不走主模型</li>
<li>用户明确指定时覆盖自动路由</li>
<li>路由决策可追溯、可优化</li>
</ol></section></section><section><h2>架构演化<a href="#架构演化"><span>#</span></a></h2><section><h3>v1：关键词触发（跳过）<a href="#v1关键词触发跳过"><span>#</span></a></h3><p>第一版简单粗暴——用关键词匹配判断是否 delegate：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>看到"架构" → delegate</span></div></div><div><div><div>2</div></div><div><span>看到"状态机" → delegate</span></div></div><div><div><div>3</div></div><div><span>看到"电路图" → delegate</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>问题</strong>：</p><ul>
<li>用户问”什么是状态机？“也会误触发</li>
<li>简单问题被升级，浪费 token</li>
<li>规则越来越复杂，趋向过拟合</li>
</ul></section><section><h3>v2：三级任务分级<a href="#v2三级任务分级"><span>#</span></a></h3><p>将任务分为三个等级：</p>

<table><thead><tr><th>等级</th><th>描述</th><th>路由目标</th></tr></thead><tbody><tr><td>Level 0</td><td>单步问答、查询、配置</td><td>Flash 直接回答</td></tr><tr><td>Level 1</td><td>需要分析，不需要重新设计系统</td><td>Flash 或 Pro</td></tr><tr><td>Level 2</td><td>系统设计、架构、多方案权衡</td><td>delegate → GLM-5.2</td></tr></tbody></table><p><strong>改进</strong>：从”关键词触发”升级为”按复杂度分级”，误触发减少。</p><p><strong>遗留问题</strong>：</p><ul>
<li>GLM 直接输出给用户，没有经过 Flash 验证</li>
<li>没有用户覆盖机制</li>
<li>成本等级标注不准确（不同 API Plan 价格不同）</li>
</ul></section><section><h3>v3：三级路由 + 专家回流 + 用户覆盖（当前版本）<a href="#v3三级路由--专家回流--用户覆盖当前版本"><span>#</span></a></h3><p>最终架构：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                </span></span><span>User Request</span></div></div><div><div><div>2</div></div><div><span><span>                     </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>                     </span></span><span>▼</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>DeepSeek V4 Flash</span></div></div><div><div><div>5</div></div><div><span><span>          </span></span><span>Router + Orchestrator</span></div></div><div><div><div>6</div></div><div><span><span>                     </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>┌────────────┼────────────┐</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>│            │            │</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>▼            ▼            ▼</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>Level 0      Level 1      Level 2</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>Flash       Flash+Pro      GLM-5.2</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>│            │            │</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>│            │            │</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>▼            ▼            ▼</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>简单任务      审查增强      专家设计</span></div></div><div><div><div>16</div></div><div><span><span>                     </span></span><span>│</span></div></div><div><div><div>17</div></div><div><span><span>                     </span></span><span>▼</span></div></div><div><div><div>18</div></div><div><span><span>              </span></span><span>Flash 整合输出</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h4>核心改进 1：GLM 输出回流<a href="#核心改进-1glm-输出回流"><span>#</span></a></h4><p>GLM 是专家，不直接面对用户。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Flash</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>delegate_task → GLM-5.2（深度推理）</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>GLM 结果返回 Flash</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>Flash 职责：</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>├── 验证结果合理性</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>├── 合并 memory 上下文（用户偏好、设备参数）</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>├── 检查已知/未知约束</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>├── 补充 OpenViking 知识</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>└── 输出最终答案给用户</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>核心改进 2：用户覆盖<a href="#核心改进-2用户覆盖"><span>#</span></a></h4><p>如果用户明确指定模型，<strong>始终尊重</strong>，跳过路由判断。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"用 GLM-5.2 分析这个架构" → 走 GLM</span></div></div><div><div><div>2</div></div><div><span>"用 DeepSeek Pro 审查代码"  → 走 Pro</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>核心改进 3：资源等级替代成本等级<a href="#核心改进-3资源等级替代成本等级"><span>#</span></a></h4><p>避免不同 API Plan 下价格标注不准确：</p>

<table><thead><tr><th>模型</th><th>资源等级</th></tr></thead><tbody><tr><td>Flash</td><td>快速</td></tr><tr><td>Pro</td><td>增强</td></tr><tr><td>GLM</td><td>深度</td></tr></tbody></table></section><section><h4>核心改进 4：RAG 优先<a href="#核心改进-4rag-优先"><span>#</span></a></h4><p>先查 OpenViking 知识库，再决定是否 delegate。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>问题</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>有 OpenViking 知识可用？</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>↓ YES → 先检索，看能否直接回答</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>↓ NO  → 继续复杂度判断</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>核心改进 5：Token 预算判断<a href="#核心改进-5token-预算判断"><span>#</span></a></h4><p>delegate 前先预估：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>如果预计回答 &lt; 1000 tokens</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>AND 无架构设计</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>AND 无多方案比较</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>AND 无硬件安全</span></div></div><div><div><div>5</div></div><div><span>→ 不要 delegate，Flash 足够</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section></section><section><h2>完整路由决策树<a href="#完整路由决策树"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Step 1: 用户指定了模型？</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>YES → 尊重用户选择，跳过路由</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>Step 2: 包含图片？</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>YES → Doubao Vision（自动，无需操作）</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>Step 3: 有 OpenViking 知识可用？</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>YES → 先检索，看能否直接回答</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Step 4: 复杂度评估</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>预计回答长度、模块数、是否含方案比较、</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>是否涉及硬件安全、是否需要架构图</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>Step 5: 路由</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>Level 0 → Flash 直接回答</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>Level 1 → Flash 或 Pro（代码审查走 Pro）</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>Level 2 → delegate → GLM-5.2 → 回流 Flash 整合</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h2>辅助链（自动路由，无需操作）<a href="#辅助链自动路由无需操作"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>图片/截图/OCR</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>▼</span></div></div><div><div><div>4</div></div><div><span>Doubao Seed 2.0 Pro</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>后台任务（压缩、策展、background_review）</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>▼</span></div></div><div><div><div>9</div></div><div><span>DeepSeek V4 Pro</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>为什么 Flash 是”大脑”而不是”最低级模型”<a href="#为什么-flash-是大脑而不是最低级模型"><span>#</span></a></h2><p>很多多模型方案的结构是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>便宜模型 → 判断 → 贵模型</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这种方案的问题：便宜模型判断不准，路由错误率高。</p><p>我们的方案：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Flash → 理解上下文 → 决定是否调用专家 → 整合输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Flash 不是”便宜替代品”，而是<strong>主控制器</strong>。它负责：</p><ul>
<li>理解用户完整意图</li>
<li>调用 memory 和知识库</li>
<li>判断是否需要专家介入</li>
<li>整合专家输出为最终答案</li>
</ul></section><section><h2>配置实现<a href="#配置实现"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>default</span><span>: </span><span>deepseek-v4-flash</span></div></div><div><div><div>3</div></div><div><span>  </span><span>provider</span><span>: </span><span>custom</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>delegation</span><span>:</span></div></div><div><div><div>6</div></div><div><span>  </span><span>model</span><span>: </span><span>glm-5.2</span><span>          </span><span># 子任务自动走 GLM</span></div></div><div><div><div>7</div></div><div><span>  </span><span>provider</span><span>: </span><span>custom</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>auxiliary</span><span>:</span></div></div><div><div><div>10</div></div><div><span>  </span><span>vision</span><span>:</span></div></div><div><div><div>11</div></div><div><span>    </span><span>model</span><span>: </span><span>doubao-seed-2-0-pro-260215</span></div></div><div><div><div>12</div></div><div><span>    </span><span>base_url</span><span>: </span><span>"https://ark.cn-beijing.volces.com/api/coding/v3"</span></div></div><div><div><div>13</div></div><div><span>    </span><span>timeout</span><span>: </span><span>120</span></div></div><div><div><div>14</div></div><div><span>  </span><span>compression</span><span>:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div><div><div><div>16</div></div><div><span>  </span><span>background_review</span><span>:</span></div></div><div><div><div>17</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div><div><div><div>18</div></div><div><span>  </span><span>curator</span><span>:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>model</span><span>: </span><span>deepseek-v4-pro</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h2>当前状态：冻结 v3，进入数据收集<a href="#当前状态冻结-v3进入数据收集"><span>#</span></a></h2><p>现在不做更多规则调整，让真实使用数据跑出来。</p><section><h3>4 个观察指标<a href="#4-个观察指标"><span>#</span></a></h3>

<table><thead><tr><th>指标</th><th>目标</th><th>说明</th></tr></thead><tbody><tr><td>路由准确率</td><td>逐步提升</td><td>任务→路由→结果质量评分</td></tr><tr><td>GLM 调用比例</td><td>5-15%</td><td>超过 30% 说明 Router 太激进</td></tr><tr><td>升级失败案例</td><td>重点收集</td><td>Flash 不足→用户追问→GLM 解决的案例</td></tr><tr><td>决策稳定性</td><td>同类任务一致</td><td>避免同类型任务反复切换模型</td></tr></tbody></table></section><section><h3>理想分配<a href="#理想分配"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Flash:  70-85%</span></div></div><div><div><div>2</div></div><div><span>Pro:    10-20%</span></div></div><div><div><div>3</div></div><div><span>GLM:     5-15%</span></div></div><div><div><div>4</div></div><div><span>Doubao: 按需</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>未来方向<a href="#未来方向"><span>#</span></a></h2><section><h3>v4.1：task-memory（经验驱动路由）<a href="#v41task-memory经验驱动路由"><span>#</span></a></h3><p>记录每次路由决策的效果，积累经验数据：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>task-memory/</span></div></div><div><div><div>2</div></div><div><span>├── energy/EMS设计</span></div></div><div><div><div>3</div></div><div><span>│     ├── model: GLM-5.2</span></div></div><div><div><div>4</div></div><div><span>│     ├── success: true</span></div></div><div><div><div>5</div></div><div><span>│     └── pattern: state-machine</span></div></div><div><div><div>6</div></div><div><span>├── software/docker迁移</span></div></div><div><div><div>7</div></div><div><span>│     ├── model: Flash</span></div></div><div><div><div>8</div></div><div><span>│     └── success: true</span></div></div><div><div><div>9</div></div><div><span>└── hardware/BMS分析</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>├── model: GLM-5.2</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>└── success: true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>以后遇到类似任务，直接查询历史，自动选择最佳模型。</p></section><section><h3>v4.2：failure-memory（失败升级）<a href="#v42failure-memory失败升级"><span>#</span></a></h3><p>Flash 尝试解决 → 检测到多次工具失败/不确定性 → 自动升级到 GLM。</p></section><section><h3>最终形态<a href="#最终形态"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                  </span></span><span>Hermes</span></div></div><div><div><div>2</div></div><div><span><span>                    </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>             </span></span><span>Model Router</span></div></div><div><div><div>4</div></div><div><span><span>                    </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>┌───────────┼───────────┐</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>Rules       Memory       Feedback</span></div></div><div><div><div>7</div></div><div><span><span>       </span></span><span>│            │             │</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>当前规则     历史决策      成功率</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>└───────────┼───────────┘</span></div></div><div><div><div>10</div></div><div><span><span>                    </span></span><span>▼</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>自适应模型选择</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>架构模式总结<a href="#架构模式总结"><span>#</span></a></h2><p>当前 Hermes 已经形成 4 层知识体系：</p>

<table><thead><tr><th>层级</th><th>内容</th><th>回答的问题</th></tr></thead><tbody><tr><td>Resource Memory</td><td>专业知识库</td><td>“这个领域有什么知识？”</td></tr><tr><td>Event/Experience Memory</td><td>历史经验</td><td>“以前发生过什么？”</td></tr><tr><td>Decision Memory</td><td>设计决策</td><td>“为什么选择这个方案？”</td></tr><tr><td>Pattern Memory</td><td>架构模式</td><td>“以后类似问题怎么处理？”</td></tr></tbody></table><p>模型链形成 <strong>Planner → Specialist → Reviewer → Integrator</strong> 结构：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>创造能力 = GLM-5.2</span></div></div><div><div><div>2</div></div><div><span>执行能力 = DeepSeek V4 Flash</span></div></div><div><div><div>3</div></div><div><span>审查能力 = DeepSeek V4 Pro</span></div></div><div><div><div>4</div></div><div><span>经验能力 = OpenViking</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>经验教训<a href="#经验教训"><span>#</span></a></h2><ol>
<li><strong>不要用关键词触发路由</strong> — 容易误触发，趋向过拟合</li>
<li><strong>Flash 是大脑，不是最低级模型</strong> — 它负责判断和整合，不是替代品</li>
<li><strong>GLM 不直接面对用户</strong> — 专家输出必须经过主模型验证和整合</li>
<li><strong>成本等级不要硬编码</strong> — 不同 API Plan 价格不同，用资源等级替代</li>
<li><strong>先查知识库再 delegate</strong> — 充分利用已有知识，避免不必要的专家调用</li>
<li><strong>规则路由是起点，经验路由是终点</strong> — 先跑数据，再做自适应</li>
</ol></section><section><h2>关联阅读<a href="#关联阅读"><span>#</span></a></h2><ul>
<li><a href="/blog/2026/07/31/building-personal-ai-engineering-knowledge-os.html">构建个人 AI 工程知识操作系统：Hermes + OpenViking 多层记忆架构实践</a> — 记忆架构搭建</li>
<li>model-router skill：<code>~/.hermes/skills/meta/model-router/SKILL.md</code></li>
<li>决策记录：OpenViking <code>viking://user/memories/events/</code></li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/observable-ai-system/</id>
      <title type="text">Hermes 可观测 AI 系统：从规则驱动到数据驱动的工程闭环</title>
      <published>2026-07-31T00:00:00.000Z</published>
      <updated>2026-07-31T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/observable-ai-system/"/>
      <summary type="text">Hermes 可观测 AI 系统：从规则驱动到数据驱动的工程闭环</summary>
      <content type="html"><![CDATA[<section><h1>Hermes 可观测 AI 系统：从规则驱动到数据驱动的工程闭环<a href="#hermes-可观测-ai-系统从规则驱动到数据驱动的工程闭环"><span>#</span></a></h1><section><h2>前情提要<a href="#前情提要"><span>#</span></a></h2><p>前两篇记录了 Hermes Agent 的搭建过程：</p><ol>
<li><strong>《构建个人 AI 工程知识操作系统》</strong> —— 多层记忆架构（OpenViking + 分层记忆 + 知识检索）</li>
<li><strong>《Hermes 多模型路由架构》</strong> —— 三级专家协作路由（v1→v2→v3 演化）</li>
</ol><p>本篇文章是系列的第三篇，也是<strong>阶段总结篇</strong>。经过八小时连续的架构设计、规则编写、测试验证和故障修复，这套系统从”配置多个模型”正式进入”个人 AI 工程系统”的第三阶段——<strong>可观测 AI 系统</strong>。</p><hr /></section><section><h2>三个阶段<a href="#三个阶段"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>阶段1: 工具调用 Agent</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>→ 能调用工具，但每次独立决策，无记忆</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>阶段2: 多模型 Agent</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>→ 多模型分工，规则路由，有记忆分层</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>阶段3: 可观测 AI 系统  ← 现在这里</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>→ 有日志，有版本，有实验，有回滚，有失败分析</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>阶段4: 经验驱动 AI 系统（未来）</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>→ 基于积累数据自动优化路由</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h2>本次构建的核心成果<a href="#本次构建的核心成果"><span>#</span></a></h2><section><h3>1. 内存生命周期规则<a href="#1-内存生命周期规则"><span>#</span></a></h3><p>解决了”记忆污染”问题——Agent 不再把旧事实当新事实，也不再不分来源地混用记忆。</p><p><strong>7 类可信度标记</strong>：</p>

<table><thead><tr><th>标记</th><th>含义</th><th>TTL</th></tr></thead><tbody><tr><td>CONFIRMED ✅</td><td>用户确认的事实</td><td>30-90 天</td></tr><tr><td>OBSERVED 📋</td><td>自动探测的信息</td><td>30 天</td></tr><tr><td>DERIVED 🔄</td><td>推理得出的结论</td><td>14 天</td></tr><tr><td>SPECULATIVE ❓</td><td>猜测，未验证</td><td>1 天</td></tr><tr><td>STALE ⌛</td><td>已过期</td><td>保留 7 天后移除</td></tr><tr><td>CONFLICTED ⚠️</td><td>多个来源不一致</td><td>待裁决</td></tr><tr><td>FROZEN 🧊</td><td>用户锁定，永不覆盖</td><td>永久</td></tr></tbody></table><p><strong>5 类生命周期规则</strong>：写入规则、冲突解决、过期管理、回滚机制、冷冻保护。</p><p><strong>冲突优先级修正</strong>：核心原则改为”来源可信度 &gt; 存储位置”。用户今天说的 CONFIRMED 事实，优先级高于旧 MEMORY.md 记录。不再因为”存在 MEMORY 里”就自动压过新输入。</p></section><section><h3>2. 三层记忆职责边界<a href="#2-三层记忆职责边界"><span>#</span></a></h3>

<table><thead><tr><th>层</th><th>存储</th><th>职责</th></tr></thead><tbody><tr><td>MEMORY.md / USER.md</td><td>高频事实</td><td>系统 prompt 自动注入，零延迟</td></tr><tr><td>OpenViking</td><td>语义知识</td><td>长期知识、决策、经验、架构模式</td></tr><tr><td>FTS5 (session_search)</td><td>原始对话</td><td>全文检索历史对话，兜底</td></tr></tbody></table><p>三层的交叉检索策略：先查 MEMORY（高频事实）→ 再查 OpenViking（语义知识）→ 最后查 FTS5（原始对话）。</p></section><section><h3>3. 真实请求 Trace 系统<a href="#3-真实请求-trace-系统"><span>#</span></a></h3><p>建立完整链路追踪：<strong>用户输入 → 检索 → 路由 → delegate → 模型执行 → 整合结果</strong></p><p>每次请求记录：</p><ul>
<li>输入任务和复杂度评分</li>
<li>路由判断依据</li>
<li>检索来源（哪些知识库命中）</li>
<li>使用模型</li>
<li>输出结果摘要</li>
<li>用户反馈（待收集）</li>
</ul></section><section><h3>4. Task-Memory 和 Failure-Memory 观察数据库<a href="#4-task-memory-和-failure-memory-观察数据库"><span>#</span></a></h3><p><strong>Task-Memory</strong> 记录每次路由决策的完整信息：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>task_id</span><span>: </span><span>T6</span></div></div><div><div><div>2</div></div><div><span>task</span><span>: </span><span>对比 MQTT vs Modbus TCP</span></div></div><div><div><div>3</div></div><div><span>complexity</span><span>: { </span><span>modules</span><span>: </span><span>1</span><span>, </span><span>safety</span><span>: </span><span>true</span><span>, </span><span>arch</span><span>: </span><span>false</span><span>, </span><span>tradeoff</span><span>: </span><span>true</span><span>, </span><span>scale</span><span>: </span><span>large</span><span> }</span></div></div><div><div><div>4</div></div><div><span>routing</span><span>: { </span><span>decision</span><span>: </span><span>Level 2</span><span>, </span><span>model</span><span>: </span><span>GLM-5.2</span><span> }</span></div></div><div><div><div>5</div></div><div><span>evaluation</span><span>:</span></div></div><div><div><div>6</div></div><div><span>  </span><span>latency_seconds</span><span>: </span><span>102</span></div></div><div><div><div>7</div></div><div><span>  </span><span>input_tokens</span><span>: </span><span>19590</span></div></div><div><div><div>8</div></div><div><span>  </span><span>output_tokens</span><span>: </span><span>2414</span></div></div><div><div><div>9</div></div><div><span>  </span><span>human_rating</span><span>: </span><span>(待收集)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>Failure-Memory</strong> 记录每次失败的根本原因和修复方案：</p>

<table><thead><tr><th>ID</th><th>类型</th><th>原因</th><th>修复</th></tr></thead><tbody><tr><td>F001</td><td>CONFIG</td><td>delegation.provider 指向不存在的 provider</td><td>修复为 volcengine-coding-plan</td></tr><tr><td>F002</td><td>TOOL</td><td>GLM 子任务执行过多环境探测，600s 超时</td><td>Flash 在 delegate 前完成检索</td></tr><tr><td>F003</td><td>TOOL</td><td>web_search 依赖缺失，子任务花时间安装</td><td>delegate context 预装依赖</td></tr><tr><td>F004</td><td>CONFIG</td><td>auxiliary.provider 同样指向不存在的 provider</td><td>统一切换 provider</td></tr></tbody></table><p><strong>分类</strong>：CONFIG / ROUTING / RETRIEVAL / MODEL / TOOL / MEMORY</p></section><section><h3>5. Batch Test v1：6 个测试用例 × 3 个模型<a href="#5-batch-test-v16-个测试用例--3-个模型"><span>#</span></a></h3>

<table><thead><tr><th>任务</th><th>等级</th><th>模型</th><th>耗时</th><th>结果</th></tr></thead><tbody><tr><td>什么是 MQTT broker</td><td>Level 0</td><td>Flash</td><td>0.5s</td><td>正确路由</td></tr><tr><td>查 NAS IP</td><td>Level 0</td><td>Flash</td><td>0.5s</td><td>正确路由</td></tr><tr><td>写光伏效率函数</td><td>Level 1</td><td>Flash</td><td>0.5s</td><td>正确路由</td></tr><tr><td>审查 HA MQTT 配置</td><td>Level 1</td><td>Pro</td><td>52s</td><td>正确路由，发现 9 项风险</td></tr><tr><td>设计 15 状态机</td><td>Level 2</td><td>GLM</td><td>419s</td><td>正确路由，36KB 设计文档</td></tr><tr><td>对比 MQTT vs Modbus</td><td>Level 2</td><td>GLM</td><td>102s</td><td>正确路由，18K 字符深度分析</td></tr></tbody></table><p>路由正确率：6/6。但样本量小，需要扩充到 20-50 个。</p><hr /></section></section><section><h2>当前冻结的架构 v3<a href="#当前冻结的架构-v3"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model-router v3 (frozen-baseline)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├── 三级路由：Level 0 (Flash) / Level 1 (Flash/Pro) / Level 2 (GLM)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├── 固定分工：视觉 → Doubao，审查 → Pro，压缩 → Pro</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>├── 复杂度评分维度：模块数、安全风险、架构设计、方案权衡、输出规模</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>└── 用户覆盖：用户指定模型时跳过自动路由</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>memory-lifecycle-rules v1</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>├── 7 类可信度标记</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>├── 5 类生命周期规则</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>├── 三层记忆职责边界</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>└── 冲突优先级：来源可信度 &gt; 存储位置</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>task-memory / failure-memory (observation only)</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>└── 不自动影响路由，仅收集数据</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h2>关键发现<a href="#关键发现"><span>#</span></a></h2><section><h3>1. 配置错误导致静默失败<a href="#1-配置错误导致静默失败"><span>#</span></a></h3><p>delegation.provider 设为 “custom” 但未配置 API key，所有 delegate 任务静默失败。同样问题存在于 auxiliary.compression 和 auxiliary.curator。修复后管道畅通。</p><p><strong>教训</strong>：配置必须验证，不能假设”看起来对”。</p></section><section><h3>2. GLM 子任务执行过剩环境探测<a href="#2-glm-子任务执行过剩环境探测"><span>#</span></a></h3><p>GLM 在推理前做了 session_search、execute_code、skill_view 等 15 次探测操作，消耗了 600 秒中的大部分时间。这些应该在 Flash 阶段完成，只传递精简 context 给 GLM。</p><p><strong>教训</strong>：delegate 是推理增强，不是环境探测增强。环境探测是 orchestrator 的职责。</p></section><section><h3>3. 1000-token 阈值无数据支撑<a href="#3-1000-token-阈值无数据支撑"><span>#</span></a></h3><p>路由规则中的 1000-token 阈值是拍脑袋的。本次 batch test 第一次收集了实际数据（T6 实际输出 22,004 tokens）。需要更多样本才能确定合理阈值。</p><p><strong>教训</strong>：路由参数必须有数据支撑，不能凭感觉设。</p></section><section><h3>4. Flash 同时作为路由器和审查者是循环依赖<a href="#4-flash-同时作为路由器和审查者是循环依赖"><span>#</span></a></h3><p>当前架构中，Flash 负责判断”这是 Level 2”→ delegate 给 GLM → GLM 返回 → Flash 审查 GLM 输出。如果 Flash 无法识别 GLM 的错误，坏答案直接交付。</p><p><strong>v4 目标</strong>：引入 Pro 作为独立审查者，形成 Flash → GLM → Pro → Flash 管道。</p><hr /></section></section><section><h2>下一阶段策略<a href="#下一阶段策略"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>当前策略：</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>规则驱动 &gt; 经验驱动</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>经验库只观察，不控制</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>下一阶段目标：</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>不是让 Hermes 更聪明</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>而是让 Hermes 的每一次决策都有证据、有反馈、可优化</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>需要积累：</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>- 20-50 条真实任务 trace</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>- 不同难度边界案例</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>- 用户反馈质量标签</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>- 模型耗时/token/成功率</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>v4 设计目标（暂不实现）：</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>Flash = Orchestrator</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>GLM = Design Expert</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>Pro = Independent Reviewer</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>Doubao = Vision</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section><section><h2>成功标准<a href="#成功标准"><span>#</span></a></h2><p>当前阶段的成功标准不是路由更复杂，而是：</p><ul>
<li><strong>每一次决策都有记录</strong></li>
<li><strong>每一次失败都有原因</strong></li>
<li><strong>每一次改进都有依据</strong></li>
</ul><p>这套系统现在已经从”配置多个模型”进入”个人 AI 工程系统”的阶段。下一步是收集数据，而不是继续堆规则。</p><hr /><p><strong>相关文章</strong>：</p><ul>
<li><a href="https://foxlesbiao.github.io/blog/2026/07/31/building-personal-ai-engineering-knowledge-os.html">构建个人 AI 工程知识操作系统</a></li>
<li><a href="https://foxlesbiao.github.io/blog/2026/07/31/hermes-multi-model-routing-architecture.html">Hermes 多模型路由架构</a></li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/autobangumi-tmdb-wall-fix/</id>
      <title type="text">AutoBangumi TMDB 被墙？不用代理，一行 sed 搞定</title>
      <published>2026-06-12T00:00:00.000Z</published>
      <updated>2026-06-12T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/autobangumi-tmdb-wall-fix/"/>
      <summary type="text">AutoBangumi TMDB 被墙？不用代理，一行 sed 搞定</summary>
      <content type="html"><![CDATA[<section><h1>AutoBangumi TMDB 被墙？不用代理，一行 sed 搞定<a href="#autobangumi-tmdb-被墙不用代理一行-sed-搞定"><span>#</span></a></h1><section><h2>问题<a href="#问题"><span>#</span></a></h2><p>AutoBangumi（AB）用 TMDB 获取番剧元数据。默认请求的是 <code>api.themoviedb.org</code>，但这个域名在国内很多网络环境下丢包严重甚至完全不通，导致元数据查询持续超时。</p><p>AB 虽然支持 HTTP 代理（<code>proxy.enable</code>），但不是所有人都能搭代理。有没有更简单的办法？</p></section><section><h2>答案<a href="#答案"><span>#</span></a></h2><p><code>api.themoviedb.org</code> 的短域名 <code>api.tmdb.org</code> 在很多国内网络下是通的。只需把容器里的硬编码 URL 换掉就行。</p></section><section><h2>方法<a href="#方法"><span>#</span></a></h2><section><h3>第一步：找到要改的文件<a href="#第一步找到要改的文件"><span>#</span></a></h3><p>AB 容器里硬编码 TMDB URL 的文件：</p>

<table><thead><tr><th>文件</th><th>硬编码 URL</th></tr></thead><tbody><tr><td><code>/app/module/parser/analyser/tmdb_parser.py</code></td><td><code>https://api.themoviedb.org</code></td></tr><tr><td><code>/app/module/parser/analyser/tmdb_parser.py</code></td><td><code>https://image.tmdb.org/t/p/w780</code></td></tr><tr><td><code>/app/module/bgmi_calendar.py</code></td><td><code>https://api.bgm.tv/calendar</code></td></tr></tbody></table><p>一般只需要改第一个。<code>image.tmdb.org</code> 和 <code>api.bgm.tv</code> 通常能通。</p></section><section><h3>第二步：进容器改<a href="#第二步进容器改"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>exec</span><span> </span><span>-it</span><span> </span><span>AutoBangumi</span><span> </span><span>bash</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span># 替换 TMDB API 域名</span></div></div><div><div><div>4</div></div><div><span>sed</span><span> </span><span>-i</span><span> </span><span>'s|https://api.themoviedb.org|https://api.tmdb.org|g'</span><span> </span><span>/app/module/parser/analyser/tmdb_parser.py</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span># 退出容器后重启</span></div></div><div><div><div>7</div></div><div><span>exit</span></div></div><div><div><div>8</div></div><div><span>docker</span><span> </span><span>restart</span><span> </span><span>AutoBangumi</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>改完立刻生效，AB 会用 <code>api.tmdb.org</code> 请求 TMDB。</p></section><section><h3>第三步：持久化（关键）<a href="#第三步持久化关键"><span>#</span></a></h3><p>容器重建（更新镜像、<code>docker-compose down &amp;&amp; up</code>）后改动会丢失。两种方式持久化：</p><section><h4>方式一：crontab 脚本（推荐）<a href="#方式一crontab-脚本推荐"><span>#</span></a></h4><p>写一个脚本，NAS 启动时自动 patch：</p><div><figure><figcaption><span>/vol1/1000/Mikan/autobangumi/patch_tmdb.sh</span></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/bin/bash</span></div></div><div><div><div>2</div></div><div><span>sleep</span><span> </span><span>10</span><span>  </span><span># 等容器启动</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>FILE</span><span>=</span><span>/app/module/parser/analyser/tmdb_parser.py</span></div></div><div><div><div>5</div></div><div><span>CONTAINER</span><span>=</span><span>AutoBangumi</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>if</span><span> </span><span>docker</span><span> </span><span>exec</span><span> </span><span>$CONTAINER</span><span> </span><span>grep</span><span> </span><span>-q</span><span> </span><span>'api.themoviedb.org'</span><span> </span><span>"</span><span>$FILE</span><span>"</span><span> 2&gt;</span><span>/dev/null</span><span>; </span><span>then</span></div></div><div><div><div>8</div></div><div><span>    </span><span>docker</span><span> </span><span>exec</span><span> </span><span>$CONTAINER</span><span> </span><span>sed</span><span> </span><span>-i</span><span> </span><span>'s|https://api.themoviedb.org|https://api.tmdb.org|g'</span><span> </span><span>"</span><span>$FILE</span><span>"</span></div></div><div><div><div>9</div></div><div><span>    </span><span>docker</span><span> </span><span>restart</span><span> </span><span>$CONTAINER</span></div></div><div><div><div>10</div></div><div><span>    </span><span>echo</span><span> </span><span>"$(</span><span>date</span><span>): TMDB patched"</span><span> &gt;&gt; </span><span>/vol1/1000/Mikan/autobangumi/patch.log</span></div></div><div><div><div>11</div></div><div><span>fi</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>加到 crontab：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>crontab</span><span> </span><span>-e</span></div></div><div><div><div>2</div></div><div><span># 加入：</span></div></div><div><div><div>3</div></div><div><span>@reboot</span><span> </span><span>/vol1/1000/Mikan/autobangumi/patch_tmdb.sh</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>脚本有判断逻辑：只有 URL 还没被改过才执行 patch，避免重复重启。</p></section><section><h4>方式二：Dockerfile 继承（更规范）<a href="#方式二dockerfile-继承更规范"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>FROM</span><span> ghcr.io/estrellaxd/auto_bangumi:latest</span></div></div><div><div><div>2</div></div><div><span>RUN</span><span> sed -i </span><span>'s|https://api.themoviedb.org|https://api.tmdb.org|g'</span><span> \</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>/app/module/parser/analyser/tmdb_parser.py</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后用这个自定义镜像代替官方镜像。缺点是每次 AB 更新要重新 build。</p></section></section></section><section><h2>验证<a href="#验证"><span>#</span></a></h2><p>改完后查看 AB 日志，TMDB 请求应该不再超时：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>logs</span><span> </span><span>AutoBangumi</span><span> </span><span>--tail</span><span> </span><span>50</span><span> | </span><span>grep</span><span> </span><span>-i</span><span> </span><span>tmdb</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果看到正常的元数据返回而不是 timeout，就成功了。</p></section><section><h2>总结<a href="#总结"><span>#</span></a></h2>

<table><thead><tr><th>方案</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td><code>sed</code> + crontab</td><td>简单，一行命令</td><td>容器重建要等脚本触发</td></tr><tr><td>Dockerfile 继承</td><td>更新时自动生效</td><td>每次 AB 更新要重新 build</td></tr><tr><td>HTTP 代理</td><td>AB 原生支持</td><td>需要搭代理服务器</td></tr></tbody></table><p>对于不想折腾代理的用户，<code>sed</code> 替换是最快的方案。</p><hr /><blockquote><p>我已向 AB 开发者提交了 <a href="https://github.com/EstrellaXD/Auto_Bangumi/issues/1042" target="_blank">Feature Request #1042</a>，建议在配置中增加自定义 API Base URL 的选项。如果被采纳，以后就不需要手动 patch 了。</p></blockquote></section></section>]]></content>
    </entry>
    <entry>
      <id>https://foxlesbiao.github.io/blog/posts/sqlite-fts5-cross-architecture-migration/</id>
      <title type="text">SQLite FTS5 跨架构迁移踩坑记：从 x86 到 ARM64</title>
      <published>2026-06-10T00:00:00.000Z</published>
      <updated>2026-06-10T00:00:00.000Z</updated>
      <author><name>Illya-Lily-Iris·Alice</name></author>
      <link rel="alternate" href="https://foxlesbiao.github.io/blog/posts/sqlite-fts5-cross-architecture-migration/"/>
      <summary type="text">SQLite FTS5 跨架构迁移踩坑记：从 x86 到 ARM64</summary>
      <content type="html"><![CDATA[<section><h1>SQLite FTS5 跨架构迁移踩坑记：从 x86 到 ARM64<a href="#sqlite-fts5-跨架构迁移踩坑记从-x86-到-arm64"><span>#</span></a></h1><section><h2>背景<a href="#背景"><span>#</span></a></h2><p>最近将 <strong>Hermes Agent</strong>（一个基于 SQLite 的 AI 聊天代理）从一台 x86_64 Arch Linux 服务器迁移到一台 ARM64 Armbian 开发板上。数据量不大：约 111MB 的 <code>state.db</code>，包含 9786 条消息、186 个会话。</p><p>迁移数据库嘛，听起来很简单对吧？然而这次踩了一个大坑。</p></section><section><h2>迁移方案的选择<a href="#迁移方案的选择"><span>#</span></a></h2><section><h3>方案一：<code>.dump</code> / <code>.reload</code>（我选的，后来证明是错的）<a href="#方案一dump--reload我选的后来证明是错的"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 在源机器上导出</span></div></div><div><div><div>2</div></div><div><span>sqlite3</span><span> </span><span>state.db</span><span> </span><span>".dump"</span><span> &gt; </span><span>dump.sql</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 在目标机器上导入</span></div></div><div><div><div>5</div></div><div><span>sqlite3</span><span> </span><span>state.db</span><span> &lt; </span><span>dump.sql</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>看起来很标准对吧？SQLite 官方文档也推荐这个方法来做版本升级或格式转换。但问题是——<strong>当数据库包含 FTS5 虚拟表时，这个方法有严重的隐患。</strong></p></section><section><h3>方案二：直接复制 <code>.db</code> 文件（正确方案）<a href="#方案二直接复制-db-文件正确方案"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 直接拷贝</span></div></div><div><div><div>2</div></div><div><span>scp</span><span> </span><span>user@source-server:/path/to/state.db</span><span> </span><span>./state.db</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没错，就这么简单。<strong>SQLite 的 <code>.db</code> 文件是二进制跨架构兼容的</strong>——x86 和 ARM64 使用相同的字节序（小端），SQLite 文件格式不依赖任何平台特定的结构。直接复制就行。</p></section></section><section><h2>问题现象<a href="#问题现象"><span>#</span></a></h2><p>使用 <code>.dump</code> / <code>.reload</code> 导入后，数据库看起来正常，表和数据都在。但当 Agent 尝试写入新消息时：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>RuntimeError: constraint failed</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>具体报错指向 FTS5 虚拟表的插入操作。进一步测试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 查询没问题</span></div></div><div><div><div>2</div></div><div><span>SELECT</span><span> </span><span>COUNT</span><span>(*) </span><span>FROM</span><span> messages;  </span><span>-- 9786，正常</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>-- 插入就报错</span></div></div><div><div><div>5</div></div><div><span>INSERT INTO</span><span> messages (session_id, </span><span>role</span><span>, content, </span><span>timestamp</span><span>)</span></div></div><div><div><div>6</div></div><div><span>VALUES</span><span> (</span><span>'test'</span><span>, </span><span>'user'</span><span>, </span><span>'hello'</span><span>, </span><span>datetime</span><span>(</span><span>'now'</span><span>));</span></div></div><div><div><div>7</div></div><div><span>-- Error: constraint failed</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>诊断过程<a href="#诊断过程"><span>#</span></a></h2><section><h3>1. 标准完整性检查——没用<a href="#1-标准完整性检查没用"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>PRAGMA integrity_check;</span></div></div><div><div><div>2</div></div><div><span>-- ok</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>PRAGMA integrity_check</code> 返回正常！但它<strong>不会检查 FTS5 shadow 表的完整性</strong>。这是一个容易被忽略的坑。</p></section><section><h3>2. FTS5 专用完整性检查——发现问题<a href="#2-fts5-专用完整性检查发现问题"><span>#</span></a></h3><p>FTS5 提供了内置的完整性检查命令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SELECT</span><span> * </span><span>FROM</span><span> messages_fts(</span><span>'integrity-check'</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>如果 FTS5 索引完好，这条语句返回<strong>空结果集</strong></li>
<li>如果索引损坏，会返回错误描述</li>
</ul><p>在我的案例中，它返回了不一致的错误信息，确认 FTS5 索引已损坏。</p></section><section><h3>3. 检查 shadow 表<a href="#3-检查-shadow-表"><span>#</span></a></h3><p>FTS5 虚拟表会在底层创建一组 “shadow 表”（以 <code>_data</code>、<code>_idx</code>、<code>_content</code>、<code>_docsize</code>、<code>_config</code> 为后缀）。你可以直接查看：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 查看有哪些 shadow 表</span></div></div><div><div><div>2</div></div><div><span>SELECT</span><span> </span><span>name</span><span> </span><span>FROM</span><span> sqlite_master</span></div></div><div><div><div>3</div></div><div><span>WHERE</span><span> </span><span>type</span><span>=</span><span>'table'</span><span> </span><span>AND</span><span> </span><span>name</span><span> </span><span>LIKE</span><span> </span><span>'messages_%'</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这些表的状态在 dump/reload 后可能不一致。</p></section></section><section><h2>根因分析<a href="#根因分析"><span>#</span></a></h2><p><strong>问题出在 <code>.dump</code> 导出的 SQL 语句顺序上。</strong></p><p><code>.dump</code> 会导出：</p><ol>
<li><code>CREATE VIRTUAL TABLE messages_fts USING fts5(...)</code> — 创建 FTS5 表</li>
<li>各种 <code>INSERT</code> 语句填充主表</li>
<li>可能还有触发器（用于自动同步主表和 FTS5 索引）</li>
</ol><p>但在 reload 时，触发器可能会在插入主表时<strong>同时触发 FTS5 索引更新</strong>，而这些更新与后续的 shadow 表数据发生冲突。在不同架构间，SQLite 内部的某些排序或序列化行为可能有微小差异，进一步加剧了这种不一致。</p><p>另外还发现 <code>sqlite_sequence</code> 表的值严重异常——autoincrement 计数器显示约 3 万亿，而实际只有不到 15000 行。这也是 dump/reload 过程中的副作用。</p></section><section><h2>修复方案<a href="#修复方案"><span>#</span></a></h2><section><h3>完整修复脚本<a href="#完整修复脚本"><span>#</span></a></h3><p>以下是一个通用的 FTS5 索引修复脚本：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env bash</span></div></div><div><div><div>2</div></div><div><span># repair_fts.sh - 修复 FTS5 索引</span></div></div><div><div><div>3</div></div><div><span># 用法: bash repair_fts.sh &lt;数据库路径&gt; &lt;FTS表名&gt; &lt;主表名&gt; &lt;内容列&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>set</span><span> </span><span>-euo</span><span> </span><span>pipefail</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>DB</span><span>=</span><span>"</span><span><span>${1</span><span>:?</span></span><span>用法</span><span>:</span><span> </span><span>bash</span><span> </span><span>repair_fts</span><span>.</span><span>sh</span><span> </span><span>&lt;db_path&gt;</span><span> </span><span>&lt;fts_table&gt;</span><span> </span><span>&lt;main_table&gt;</span><span> </span><span>&lt;content_columns&gt;</span><span>}</span><span>"</span></div></div><div><div><div>8</div></div><div><span>FTS_TABLE</span><span>=</span><span>"</span><span><span>${2</span><span>:?</span></span><span>请指定 </span><span>FTS5</span><span> 虚拟表名</span><span>}</span><span>"</span></div></div><div><div><div>9</div></div><div><span>MAIN_TABLE</span><span>=</span><span>"</span><span><span>${3</span><span>:?</span></span><span>请指定主表名</span><span>}</span><span>"</span></div></div><div><div><div>10</div></div><div><span>CONTENT_COLS</span><span>=</span><span>"</span><span><span>${4</span><span>:?</span></span><span>请指定内容列名，如 'role, content'</span><span>}</span><span>"</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>BACKUP</span><span>=</span><span>"</span><span><span>${</span><span>DB</span><span>}</span></span><span>.bak.$(</span><span>date</span><span> +%Y%m%d%H%M%S)"</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>echo</span><span> </span><span>"=== SQLite FTS5 索引修复工具 ==="</span></div></div><div><div><div>15</div></div><div><span>echo</span><span> </span><span>""</span></div></div><div><div><div>16</div></div><div><span>echo</span><span> </span><span>"数据库: </span><span>$DB</span><span>"</span></div></div><div><div><div>17</div></div><div><span>echo</span><span> </span><span>"FTS表:  </span><span>$FTS_TABLE</span><span>"</span></div></div><div><div><div>18</div></div><div><span>echo</span><span> </span><span>"主表:   </span><span>$MAIN_TABLE</span><span>"</span></div></div><div><div><div>19</div></div><div><span>echo</span><span> </span><span>"列:     </span><span>$CONTENT_COLS</span><span>"</span></div></div><div><div><div>20</div></div><div><span>echo</span><span> </span><span>""</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span># 1. 备份</span></div></div><div><div><div>23</div></div><div><span>echo</span><span> </span><span>"[1/5] 备份数据库..."</span></div></div><div><div><div>24</div></div><div><span>cp</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> </span><span>"</span><span>$BACKUP</span><span>"</span></div></div><div><div><div>25</div></div><div><span>echo</span><span> </span><span>"  备份已保存到: </span><span>$BACKUP</span><span>"</span></div></div><div><div><div>26</div></div><div><span>echo</span><span> </span><span>""</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span># 2. 诊断</span></div></div><div><div><div>29</div></div><div><span>echo</span><span> </span><span>"[2/5] 诊断当前状态..."</span></div></div><div><div><div>30</div></div><div><span>echo</span><span> </span><span>"  主表行数:"</span></div></div><div><div><div>31</div></div><div><span>sqlite3</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> </span><span>"SELECT COUNT(*) FROM </span><span>$MAIN_TABLE</span><span>;"</span></div></div><div><div><div>32</div></div><div><span>echo</span><span> </span><span>"  FTS5 完整性检查:"</span></div></div><div><div><div>33</div></div><div><span>RESULT</span><span><span>=</span><span>$(</span></span><span>sqlite3</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> </span><span>"SELECT * FROM </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>('integrity-check');"</span><span>)</span></div></div><div><div><div>34</div></div><div><span>if</span><span><span> [ </span><span>-z</span><span> </span></span><span>"</span><span>$RESULT</span><span>"</span><span> ]; </span><span>then</span></div></div><div><div><div>35</div></div><div><span>    </span><span>echo</span><span> </span><span>"  ✓ FTS5 索引完好"</span></div></div><div><div><div>36</div></div><div><span>    </span><span>read</span><span> </span><span>-p</span><span> </span><span>"  索引看起来正常，是否仍要继续？(y/N) "</span><span> </span><span>-n</span><span> </span><span>1</span><span> </span><span>-r</span></div></div><div><div><div>37</div></div><div><span>    </span><span>echo</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>[[ </span><span>$REPLY</span><span> </span><span>=~</span><span> ^[Yy]$ ]] || </span><span>exit</span><span> </span><span>0</span></div></div><div><div><div>39</div></div><div><span>else</span></div></div><div><div><div>40</div></div><div><span>    </span><span>echo</span><span> </span><span>"  ✗ 发现问题: </span><span>$RESULT</span><span>"</span></div></div><div><div><div>41</div></div><div><span>fi</span></div></div><div><div><div>42</div></div><div><span>echo</span><span> </span><span>""</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span># 3. 删除旧 FTS 表及其相关对象</span></div></div><div><div><div>45</div></div><div><span>echo</span><span> </span><span>"[3/5] 删除旧的 FTS 表..."</span></div></div><div><div><div>46</div></div><div><span>sqlite3</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> &lt;&lt;</span><span>SQL</span></div></div><div><div><div>47</div></div><div><span>-- 删除触发器（如果存在）</span></div></div><div><div><div>48</div></div><div><span>SELECT 'DROP TRIGGER IF EXISTS ' || name || ';'</span></div></div><div><div><div>49</div></div><div><span>FROM sqlite_master WHERE type='trigger' AND tbl_name='</span><span>$MAIN_TABLE</span><span>'</span></div></div><div><div><div>50</div></div><div><span>AND (name LIKE '%fts%' OR name LIKE '%ai%' OR name LIKE '%ad%' OR name LIKE '%au%');</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>-- 列出将被删除的相关表</span></div></div><div><div><div>53</div></div><div><span>SELECT '  将删除: ' || name FROM sqlite_master</span></div></div><div><div><div>54</div></div><div><span>WHERE type='table' AND name LIKE '</span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>%';</span></div></div><div><div><div>55</div></div><div><span>SQL</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>sqlite3</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> &lt;&lt;</span><span>SQL</span></div></div><div><div><div>58</div></div><div><span>DROP TABLE IF EXISTS </span><span>$FTS_TABLE</span><span>;</span></div></div><div><div><div>59</div></div><div><span>DROP TABLE IF EXISTS </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>_data;</span></div></div><div><div><div>60</div></div><div><span>DROP TABLE IF EXISTS </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>_idx;</span></div></div><div><div><div>61</div></div><div><span>DROP TABLE IF EXISTS </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>_content;</span></div></div><div><div><div>62</div></div><div><span>DROP TABLE IF EXISTS </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>_docsize;</span></div></div><div><div><div>63</div></div><div><span>DROP TABLE IF EXISTS </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>_config;</span></div></div><div><div><div>64</div></div><div><span>SQL</span></div></div><div><div><div>65</div></div><div><span>echo</span><span> </span><span>"  ✓ 已删除"</span></div></div><div><div><div>66</div></div><div><span>echo</span><span> </span><span>""</span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span># 4. 重建 FTS 表和索引</span></div></div><div><div><div>69</div></div><div><span>echo</span><span> </span><span>"[4/5] 重建 FTS 表..."</span></div></div><div><div><div>70</div></div><div><span>sqlite3</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> &lt;&lt;</span><span>SQL</span></div></div><div><div><div>71</div></div><div><span>CREATE VIRTUAL TABLE IF NOT EXISTS </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span> USING fts5(</span><span>$CONTENT_COLS</span><span>, content='</span><span>$MAIN_TABLE</span><span>', content_rowid='id');</span></div></div><div><div><div>72</div></div><div><span>INSERT INTO </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>(</span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>) VALUES('rebuild');</span></div></div><div><div><div>73</div></div><div><span>SQL</span></div></div><div><div><div>74</div></div><div><span>echo</span><span> </span><span>"  ✓ 已重建"</span></div></div><div><div><div>75</div></div><div><span>echo</span><span> </span><span>""</span></div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span># 5. 验证</span></div></div><div><div><div>78</div></div><div><span>echo</span><span> </span><span>"[5/5] 验证..."</span></div></div><div><div><div>79</div></div><div><span>RESULT</span><span><span>=</span><span>$(</span></span><span>sqlite3</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> </span><span>"SELECT * FROM </span><span><span>${</span><span>FTS_TABLE</span><span>}</span></span><span>('integrity-check');"</span><span>)</span></div></div><div><div><div>80</div></div><div><span>if</span><span><span> [ </span><span>-z</span><span> </span></span><span>"</span><span>$RESULT</span><span>"</span><span> ]; </span><span>then</span></div></div><div><div><div>81</div></div><div><span>    </span><span>echo</span><span> </span><span>"  ✓ FTS5 索引已修复！"</span></div></div><div><div><div>82</div></div><div><span>    </span><span>COUNT</span><span><span>=</span><span>$(</span></span><span>sqlite3</span><span> </span><span>"</span><span>$DB</span><span>"</span><span> </span><span>"SELECT COUNT(*) FROM </span><span>$MAIN_TABLE</span><span>;"</span><span>)</span></div></div><div><div><div>83</div></div><div><span>    </span><span>echo</span><span> </span><span>"  ✓ 索引覆盖 </span><span>$COUNT</span><span> 条记录"</span></div></div><div><div><div>84</div></div><div><span>else</span></div></div><div><div><div>85</div></div><div><span>    </span><span>echo</span><span> </span><span>"  ✗ 仍有问题: </span><span>$RESULT</span><span>"</span></div></div><div><div><div>86</div></div><div><span>    </span><span>echo</span><span> </span><span>"  请从备份恢复: cp </span><span>$BACKUP</span><span> </span><span>$DB</span><span>"</span></div></div><div><div><div>87</div></div><div><span>    </span><span>exit</span><span> </span><span>1</span></div></div><div><div><div>88</div></div><div><span>fi</span></div></div><div><div><div>89</div></div><div>
</div></div><div><div><div>90</div></div><div><span>echo</span><span> </span><span>""</span></div></div><div><div><div>91</div></div><div><span>echo</span><span> </span><span>"=== 修复完成 ==="</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>使用方法<a href="#使用方法"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 赋予执行权限</span></div></div><div><div><div>2</div></div><div><span>chmod</span><span> </span><span>+x</span><span> </span><span>repair_fts.sh</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 针对 messages 表修复</span></div></div><div><div><div>5</div></div><div><span>bash</span><span> </span><span>repair_fts.sh</span><span> </span><span>./state.db</span><span> </span><span>messages_fts</span><span> </span><span>messages</span><span> </span><span>"role, content"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>关键步骤解释<a href="#关键步骤解释"><span>#</span></a></h3>

<table><thead><tr><th>步骤</th><th>说明</th></tr></thead><tbody><tr><td>备份</td><td>修复前必须备份，万一出问题可以回退</td></tr><tr><td>诊断</td><td>用 FTS5 的 <code>integrity-check</code> 确认问题</td></tr><tr><td>删除</td><td>彻底清除损坏的 FTS5 表和 shadow 表</td></tr><tr><td>重建</td><td>用 <code>content=</code> 参数关联主表，<code>rebuild</code> 命令重建索引</td></tr><tr><td>验证</td><td>再次运行 integrity-check 确认修复成功</td></tr></tbody></table></section><section><h3>修复 <code>sqlite_sequence</code><a href="#修复-sqlite_sequence"><span>#</span></a></h3><p>如果 autoincrement 计数器也异常了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 查看当前值</span></div></div><div><div><div>2</div></div><div><span>SELECT</span><span> * </span><span>FROM</span><span> sqlite_sequence;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>-- 重置为实际最大值</span></div></div><div><div><div>5</div></div><div><span>UPDATE</span><span> sqlite_sequence </span><span>SET</span><span><span> seq </span><span>=</span><span> (</span></span><span>SELECT</span><span> </span><span>MAX</span><span>(id) </span><span>FROM</span><span> messages) </span><span>WHERE</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>'messages'</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>-- 或者直接删除，下次插入时会自动重新生成</span></div></div><div><div><div>8</div></div><div><span>DELETE</span><span> </span><span>FROM</span><span> sqlite_sequence </span><span>WHERE</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>'messages'</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>最终状态<a href="#最终状态"><span>#</span></a></h2><p>修复完成后，数据库恢复正常：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>数据库大小:  111.4 MB</span></div></div><div><div><div>2</div></div><div><span>消息总数:    9786</span></div></div><div><div><div>3</div></div><div><span>会话数:      186</span></div></div><div><div><div>4</div></div><div><span>FTS5 索引:   ✓ 健康</span></div></div><div><div><div>5</div></div><div><span>完整性检查:  ok</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>经验总结<a href="#经验总结"><span>#</span></a></h2><section><h3>核心教训<a href="#核心教训"><span>#</span></a></h3><blockquote><p><strong>SQLite + FTS5 跨架构迁移，永远不要用 <code>.dump</code> / <code>.reload</code>。直接复制 <code>.db</code> 文件。</strong></p></blockquote><p>SQLite 数据库文件是二进制兼容的（前提是字节序相同，x86 和 ARM64 都是小端）。直接 <code>scp</code> 或 <code>rsync</code> 就行，不需要转成 SQL 文本再导入。</p></section><section><h3>如果你必须用 <code>.dump</code><a href="#如果你必须用-dump"><span>#</span></a></h3><p>有时候你确实需要文本格式（比如要合并两个数据库、或者要跨字节序迁移），那么：</p><ol>
<li>导出时<strong>排除 FTS5 表</strong>：先 dump 除 FTS 相关表以外的所有内容</li>
<li>导入主表数据</li>
<li>在目标数据库上<strong>重新创建 FTS5 虚拟表</strong></li>
<li>运行 <code>INSERT INTO fts_table(fts_table) VALUES('rebuild');</code> 重建索引</li>
<li>用 <code>SELECT * FROM fts_table('integrity-check');</code> 验证</li>
</ol></section><section><h3>FTS5 完整性检查速查<a href="#fts5-完整性检查速查"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 常规检查（不检查 FTS5！）</span></div></div><div><div><div>2</div></div><div><span>PRAGMA integrity_check;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>-- FTS5 专用检查（这才是正确的）</span></div></div><div><div><div>5</div></div><div><span>SELECT</span><span> * </span><span>FROM</span><span> your_fts_table(</span><span>'integrity-check'</span><span>);</span></div></div><div><div><div>6</div></div><div><span>-- 返回空 = 正常</span></div></div><div><div><div>7</div></div><div><span>-- 返回内容 = 有问题</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>迁移前检查清单<a href="#迁移前检查清单"><span>#</span></a></h3><ul>
<li> 备份原始数据库</li>
<li> 确认迁移方式（推荐直接复制文件）</li>
<li> 迁移后运行 <code>PRAGMA integrity_check;</code></li>
<li> 如果有 FTS5，运行 <code>SELECT * FROM fts_table('integrity-check');</code></li>
<li> 测试读写操作</li>
<li> 检查 <code>sqlite_sequence</code> 值是否合理</li>
</ul><hr /><p><em>本文记录于 2026-06-11，基于 Hermes Agent 从 x86_64 Arch Linux 迁移到 ARM64 Armbian 的实际经历。希望对遇到类似问题的朋友有所帮助。</em></p></section></section></section>]]></content>
    </entry>
</feed>
