<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://blog.liao-ke.com/</id>
  <title type="text">LiaoKe的博客</title>
  <subtitle type="text">记录生活,分享技术</subtitle>
  <updated>2026-08-19T00:00:00.000Z</updated>
  <author><name>LiaoKe</name></author>
  <link rel="alternate" href="https://blog.liao-ke.com/"/>
  <link rel="self" href="https://blog.liao-ke.com/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://blog.liao-ke.com/posts/waline-bugfix/</id>
      <title type="text">随笔杂谈 | Waline用户列表加载失败 </title>
      <published>2026-08-19T00:00:00.000Z</published>
      <updated>2026-08-19T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/waline-bugfix/"/>
      <summary type="text">记录一次 Waline 评论系统用户列表接口返回 500 的排查与修复过程，问题最终定位在 avatar 服务的字段名不匹配。</summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><p>前几天登录博客的评论系统waline，发现用户列表加载不出来，打开网络调试器发现请求接口返回500，一直是以为我数据库出错了，连续换了几个类型的数据库（MongoDB,SQLite,PostgreSQL），后来又尝试换了部署平台从Vercel换到了Netlify，所有情况都试遍了，仍然加载不出用户列表。
<img alt="" loading="lazy" width="1304" height="403" src="/_astro/1.eFbZDqKQ_rfKfX.webp" /></p></section>
<section><h2>排查<a href="#排查"><span>#</span></a></h2><p>尝试了众多方法无果之后，我在想是不是waline server端的代码本身出了问题，于是就去翻看了一下他们官方的<a href="https://github.com/walinejs/waline/blob/main/packages/server/src/controller/user.js" target="_blank">源代码</a>，打开 <code>packages/server/src/controller/user.js</code>，定位到用户列表接口 <code>getAction</code> 里生成头像的这一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>user</span><span>.</span></span><span>avatar</span><span> </span><span>||=</span><span> </span><span>await</span><span><span> </span><span>think</span><span>.</span></span><span>service</span><span>(</span><span>'avatar'</span><span>).</span><span>stringify</span><span><span>(</span><span>user</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再看 <code>avatar</code> 服务内部的模板：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>%-</span><span><span> </span><span>elif</span><span> </span><span>qqMailExp</span><span>.</span></span><span>test</span><span><span>(</span><span>mail</span><span>) </span></span><span>-%</span><span>}</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>...</span><span><span>{{</span><span>mail</span></span><span>|</span><span>replace</span><span>(</span><span>'@qq.com'</span><span>, </span><span>''</span><span>)}}</span><span>...</span></div></div><div><div><div>3</div></div><div><span>{</span><span>%-</span><span> </span><span>else</span><span> </span><span>-%</span><span>}</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>https</span><span>:</span><span>//seccdn.libravatar.org/avatar/{{mail | trim | lower | md5}}</span></div></div><div><div><div>5</div></div><div><span>{</span><span>%-</span><span><span> </span><span>endif</span><span> </span></span><span>-%</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>模板取的是 <code>mail</code> 字段，但 <code>user</code> 表里邮箱字段叫 <code>email</code>，不叫 <code>mail</code>。直接把 <code>user</code> 对象传进去，模板里 <code>mail</code> 是 <code>undefined</code>，执行 <code>mail|replace(...)</code> 时报错：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>TypeError: Cannot read properties of undefined (reading 'replace')</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>跟控制台报错完全对上。跟数据库类型、部署平台没有任何关系。</p></section>
<section><h2>定位根因<a href="#定位根因"><span>#</span></a></h2><p>全局搜了一下项目里所有调用 <code>avatar</code> 服务的地方，发现 <code>token.js</code>（登录）和 <code>base.js</code>（评论列表）在处理同一张 <code>user</code> 表时，都做了字段映射：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>await</span><span><span> </span><span>think</span><span>.</span></span><span>service</span><span>(</span><span>'avatar'</span><span>).</span><span>stringify</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>mail</span><span>:</span><span><span> </span><span>user</span><span>.</span></span><span>email</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>nick</span><span>:</span><span><span> </span><span>user</span><span>.</span></span><span>display_name</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>link</span><span>:</span><span><span> </span><span>user</span><span>.</span></span><span>url</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>user.js</code> 这一处漏了映射，直接传了原始对象。是官方代码里孤立的一处遗漏。</p></section>
<section><h2>修复<a href="#修复"><span>#</span></a></h2><p>把 <code>user.js</code> 改成和另外两处一致的写法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>user</span><span>.</span></span><span>avatar</span><span> </span><span>||=</span><span> </span><span>await</span><span><span> </span><span>think</span><span>.</span></span><span>service</span><span>(</span><span>'avatar'</span><span>).</span><span>stringify</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>mail</span><span>:</span><span><span> </span><span>user</span><span>.</span></span><span>email</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>nick</span><span>:</span><span><span> </span><span>user</span><span>.</span></span><span>display_name</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>link</span><span>:</span><span><span> </span><span>user</span><span>.</span></span><span>url</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>重新部署waline服务，用户列表恢复正常。</p></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><p>最后改完之后也是把修复补丁重新<a href="https://github.com/walinejs/waline/pull/3816" target="_blank">pr</a>回上游了。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/autoglm-dev-diary/</id>
      <title type="text">开发日记 | AutoGlm：用 Chaquopy 把 AI 控制手机的 Python 项目搬进安卓 APP</title>
      <published>2026-07-30T00:00:00.000Z</published>
      <updated>2026-07-30T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/autoglm-dev-diary/"/>
      <summary type="text">记录将 Open-AutoGLM 这套基于 ADB 的手机自动化控制系统，通过 Chaquopy 嵌入式 Python 和自制 libadb.so 移植进安卓原生 APP 的开发过程与踩坑经历。</summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><p>几个月前，偶然间发现了一个项目<a href="https://github.com/iamr0s/Ruto-GLM" target="_blank"><strong>Ruto</strong></a>，简单来说就是可以用自然语言让AI操控手机进行一些操作，比如说点外卖，给谁发消息之类的。于是乎我突发奇想，想着能不能做一个类似的项目,但是让我从零手搓一套ai控制系统显然是有一点难度的。</p></section>
<section><h2>开发历程<a href="#开发历程"><span>#</span></a></h2><p>然后我又看到了一个项目<a href="https://github.com/zai-org/Open-AutoGLM" target="_blank"><strong>Open-AutoGLM</strong></a>,这个项目是在电脑上基于Python，通过有线或者无线ADB连接手机，然后让AI操作手机，是一个现成的操作控制系统，但是我要如何把这套系统移植到安卓手APP上来呢？</p><section><h3>用 Chaquopy 把 Python 塞进安卓<a href="#用-chaquopy-把-python-塞进安卓"><span>#</span></a></h3><p>这个项目是基于Python的，于是我便利用了<a href="https://chaquo.com/chaquopy" target="_blank"><strong>chaquopy</strong></a>这个嵌入式Python运行器(因为安卓上没有runtime这个概念,不像windows一样可以装Python运行环境，安卓应用只能使用嵌入式python来运行python代码,且<a href="https://chaquo.com/chaquopy" target="_blank"><strong>chaquopy</strong></a>他的Python依赖库不全，而且有些版本有点落后,出现了项目的语法兼容性问题。为了让<a href="https://chaquo.com/chaquopy" target="_blank"><strong>chaquopy</strong></a>兼容<a href="https://github.com/zai-org/Open-AutoGLM" target="_blank"><strong>Open-AutoGLM</strong></a>,当时调试依赖版本，就费了好长的时间)。在 <code>build.gradle.kts</code> 里接入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>plugins</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>alias</span><span>(libs.plugins.android.application)</span></div></div><div><div><div>3</div></div><div><span>    </span><span>alias</span><span>(libs.plugins.kotlin.compose)</span></div></div><div><div><div>4</div></div><div><span>    </span><span>id</span><span>(</span><span>"com.chaquo.python"</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>phone_agent</code> 这一整套 Python 包（<code>agent.py</code>、<code>adb/</code>、<code>actions/</code>、<code>config/</code> 等）就可以原封不动地放进 <code>app/src/main/python/</code> 目录，跟着 APK 一起打包，运行时由 Kotlin 侧直接调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>val</span><span><span> py </span><span>=</span><span> Python.</span></span><span>getInstance</span><span>()</span></div></div><div><div><div>2</div></div><div><span>val</span><span><span> module </span><span>=</span><span> py.</span></span><span>getModule</span><span>(</span><span>"main"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>val</span><span><span> result </span><span>=</span><span> module.</span></span><span>callAttr</span><span>(</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"main"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"--adb-path"</span><span>, adbPath,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"--base-url"</span><span>, baseUrl,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"--model"</span><span>, model,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"--apikey"</span><span>, apiKey,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"--device-type"</span><span>, </span><span>"adb"</span><span>,</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>task</span></div></div><div><div><div>12</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>adb 二进制变成 libadb.so<a href="#adb-二进制变成-libadbso"><span>#</span></a></h3><p>原项目里面用的是adb命令连接手机，于是我费了九牛二虎之力，终于在github上找到了能在安卓上成功运行起来的adb二进制文件,但是由于在新的安卓开发标准中不允许直接把现成的二进制文件打包进APK里了,必须要把adb命名为adb.so。为了让它落地到真实文件系统而不是留在压缩包里，还得在 <code>build.gradle.kts</code> 里强制用 legacy 打包方式：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>android</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>packaging</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>jniLibs</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>// 强制将 .so 文件提取到磁盘上，而不是留在 APK 中</span></div></div><div><div><div>5</div></div><div><span>            </span><span>// 这样 context.applicationInfo.nativeLibraryDir 目录下才会有真实文件</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>useLegacyPackaging </span><span>=</span><span> </span><span>true</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>defaultConfig</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>ndk</span><span> {</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>abiFilters </span><span>+=</span><span> </span><span>listOf</span><span>(</span><span>"arm64-v8a"</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></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>让 Python 侧的 adb 调用重定向到 libadb.so<a href="#让-python-侧的-adb-调用重定向到-libadbso"><span>#</span></a></h3><p>但是原项目又是直接调用终端环境中的adb,最后没办法，只能修改源代码，又增加了对adb执行命令的hook，让它自动重定向到我设置目录下的adb.so。</p><p><code>phone_agent/adb/connection.py</code>（以及 <code>screenshot.py</code>、<code>input.py</code>、<code>device.py</code>）里的每一处 adb 调用，默认都是直接执行名为 <code>adb</code> 的命令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>ADBConnection</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>adb_path</span></span><span>:</span><span> </span><span>str</span><span> </span><span>|</span><span><span> </span><span>None</span><span> </span></span><span>=</span><span><span> </span><span>None</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span><span>.adb_path </span><span>=</span><span> adb_path </span></span><span>or</span><span><span> os.</span><span>getenv</span><span>(</span></span><span>"PHONE_AGENT_ADB_PATH"</span><span>, </span><span>"adb"</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>def</span><span> </span><span>connect</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>address</span></span><span>:</span><span> </span><span>str</span><span>,</span><span><span> </span><span>timeout</span></span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>10</span></span><span>) -&gt; tuple[</span><span>bool</span><span>, </span><span>str</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>result </span><span>=</span><span> subprocess.</span><span>run</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>[</span><span>self</span><span>.adb_path, </span><span>"connect"</span><span>, address],</span></div></div><div><div><div>9</div></div><div><span>            </span><span>capture_output</span><span>=</span><span>True</span><span>,</span></div></div><div><div><div>10</div></div><div><span>            </span><span>text</span><span>=</span><span>True</span><span>,</span></div></div><div><div><div>11</div></div><div><span>            </span><span>timeout</span><span><span>=</span><span>timeout,</span></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><p>好在原作者已经预留了 <code>PHONE_AGENT_ADB_PATH</code> 这个环境变量作为覆盖入口，所以我不需要大动干戈改 Python 逻辑，只要在 Kotlin 侧启动 Python 之前，把这个环境变量／命令行参数指向我 APK 私有目录下解压出来的 <code>libadb.so</code> 即可：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>fun</span><span> </span><span>runPython</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>context: </span><span>Context</span><span>,</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>baseUrl: </span><span>String</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>model: </span><span>String</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>apiKey: </span><span>String</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>task: </span><span>String</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>onLog: (</span><span>String</span><span>) -&gt; </span><span>Unit</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>onFinish: () </span><span>-&gt;</span><span> Unit</span></div></div><div><div><div>9</div></div><div><span>) {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>thread</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>val</span><span><span> wakeLock </span><span>=</span><span> pm.</span></span><span>newWakeLock</span><span>(PowerManager.PARTIAL_WAKE_LOCK, </span><span>"Python:Run"</span><span>).</span><span>apply</span><span> { </span><span>acquire</span><span>(</span><span>15</span><span>*</span><span>60</span><span>*</span><span>1000L</span><span>) }</span></div></div><div><div><div>12</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>val</span><span><span> adbPath </span><span>=</span><span> </span></span><span>File</span><span>(context.applicationInfo.nativeLibraryDir, </span><span>"libadb.so"</span><span>).absolutePath</span></div></div><div><div><div>14</div></div><div><span>            </span><span>val</span><span><span> py </span><span>=</span><span> Python.</span></span><span>getInstance</span><span>()</span></div></div><div><div><div>15</div></div><div><span>            </span><span>val</span><span><span> module </span><span>=</span><span> py.</span></span><span>getModule</span><span>(</span><span>"main"</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>            </span><span>val</span><span><span> result </span><span>=</span><span> module.</span></span><span>callAttr</span><span>(</span></div></div><div><div><div>18</div></div><div><span>                </span><span>"main"</span><span>,</span></div></div><div><div><div>19</div></div><div><span>                </span><span>"--adb-path"</span><span>, adbPath,</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>                </span></span><span>task</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>23</div></div><div><span>            </span><span>onLog</span><span>(</span><span>"任务结束：</span><span>\n</span><span>$result</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>} </span><span>catch</span><span> (e: </span><span>Exception</span><span>) {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>onLog</span><span>(</span><span>"运行错误：</span><span>\n</span><span><span>${</span><span>e.message</span><span>}</span></span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>27</div></div><div><span>            </span><span>if</span><span> (wakeLock.isHeld) wakeLock.</span><span>release</span><span>()</span></div></div><div><div><div>28</div></div><div><span>            </span><span>onFinish</span><span>()</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</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>而 <code>main.py</code> 里接收到这个参数后，会写回同一个环境变量，<code>connection.py</code>、<code>screenshot.py</code>、<code>input.py</code>、<code>device.py</code> 里所有 <code>os.getenv("PHONE_AGENT_ADB_PATH", "adb")</code> 的调用就都会统一指向 <code>libadb.so</code>，而不是系统里根本不存在的 <code>adb</code> 命令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>os.environ[</span><span>"PHONE_AGENT_ADB_PATH"</span><span><span>] </span><span>=</span><span> args.adb_path </span></span><span>or</span><span> </span><span>"adb"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>另外我还单独封装了一个 <code>Adb.kt</code> 工具对象，用来处理无线连接、配对这类交互场景（比如输入配对码），逻辑上和 Python 侧是平行的两条调用链：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>object</span><span> </span><span>Adb</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fun</span><span> </span><span>exec</span><span>(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>ctx: </span><span>Context</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>args: </span><span>List</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>input: </span><span>String</span><span><span>? </span><span>=</span><span> </span></span><span>null</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>onLine: ((</span><span>String</span><span>) -&gt; </span><span>Unit</span><span><span>)? </span><span>=</span><span> </span></span><span>null</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>): </span><span>String</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>val</span><span><span> adbPath </span><span>=</span><span> </span></span><span>File</span><span>(ctx.applicationInfo.nativeLibraryDir, </span><span>"libadb.so"</span><span>).absolutePath</span></div></div><div><div><div>9</div></div><div><span>        </span><span>val</span><span><span> process </span><span>=</span><span> </span></span><span>ProcessBuilder</span><span>(</span><span>listOf</span><span><span>(adbPath) </span><span>+</span><span> args)</span></span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>.</span><span>redirectErrorStream</span><span>(</span><span>true</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>start</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>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>fun</span><span> </span><span>connect</span><span>(ctx: </span><span>Context</span><span>, addr: </span><span>String</span><span>, onLine: ((</span><span>String</span><span>) -&gt; </span><span>Unit</span><span><span>)? </span><span>=</span><span> </span></span><span>null</span><span><span>) </span><span>=</span></span></div></div><div><div><div>16</div></div><div><span>        </span><span>exec</span><span>(ctx, </span><span>listOf</span><span>(</span><span>"connect"</span><span><span>, addr), onLine </span><span>=</span><span> onLine)</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>fun</span><span> </span><span>pair</span><span>(ctx: </span><span>Context</span><span>, addr: </span><span>String</span><span>, code: </span><span>String</span><span>, onLine: ((</span><span>String</span><span>) -&gt; </span><span>Unit</span><span><span>)? </span><span>=</span><span> </span></span><span>null</span><span><span>) </span><span>=</span></span></div></div><div><div><div>19</div></div><div><span>        </span><span>exec</span><span>(ctx, </span><span>listOf</span><span>(</span><span>"pair"</span><span><span>, addr), input </span><span>=</span><span> code, onLine </span><span>=</span><span> onLine)</span></span></div></div><div><div><div>20</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>这样一来，“电脑上跑的 Python + 命令行 adb”这套控制系统，就被完整地搬进了一个安卓 APP 里：Chaquopy 用于跑 Python 逻辑，<code>libadb.so</code> 冒充原生库绕过打包限制，<code>PHONE_AGENT_ADB_PATH</code> 这个环境变量作为两边的”接口”，把 Python 里所有原本指向系统 <code>adb</code> 命令的调用，重定向到了 APK 自己私有目录下的这个二进制文件上。</p></section></section>
<section><h2>后记<a href="#后记"><span>#</span></a></h2><p>这种项目本质上是把安卓系统上的操作截图让ai理解，然后<strong>返回相应的指令模拟人为的触屏操作</strong>，这种方法过于低效，而且容易让ai陷入死循环，无法执行正确的决策。不过最新的安卓应用开发标准，已经支持应用对外暴露函数接口和方法让ai智能体直接调用，不需要模拟人为的触屏。相信不久的将来马上会出现更为高效的Ai控制手机的方案。</p></section>
<section><h2>项目地址<a href="#项目地址"><span>#</span></a></h2><p><a href="https://github.com/iliaoke/AutoGlm/tree/main" target="_blank">https://github.com/iliaoke/AutoGlm/tree/main</a></p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/github-oauth-callback-worker/</id>
      <title type="text">学习笔记 | 简单设计一个OAuth认证回调服务器</title>
      <published>2026-07-28T00:00:00.000Z</published>
      <updated>2026-07-28T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/github-oauth-callback-worker/"/>
      <summary type="text">使用 Cloudflare Worker 实现一个无状态的 GitHub OAuth 回调服务器，完成 code 换取 access_token，并通过自定义 App Scheme 返回移动端，分析这种方案的实现原理、优缺点及适用场景。</summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><p>暑假待在家里没得事做，想做一个移动端的博客写作客户端,由于我的博客内容是储存在github上的,所以在开发的过程中，我需要学习一下github OAuth认证流程。简单的了解了一下 github的OAuth认证流程大致是当用户认证成功之后，会下发一个code给认证地址,然后通过code和客户端id以及客户端密钥向github发起请求,便可以得到用户的access token。我一开始以为这个code到access token的流程必须要一个服务器才可以完成，但是突然想到这种流程化的事情，不需要保存状态的,像cloudflare worker这样的serverless平台,应该也可以办到,于是用cloudflare worker简单的做了一个认证回调服务器。</p></section>
<section><h2>代码<a href="#代码"><span>#</span></a></h2><p>先解析 URL 上的参数:<code>code</code>、<code>state</code>,还有可能出现的 <code>error</code>。这里有个容易漏掉的点——用户在授权页点”取消”的时候,GitHub 不会给 <code>code</code>,而是直接带 <code>error=access_denied</code> 跳回来,所以必须先判断 <code>errorParam</code>,不然后面拿着空 <code>code</code> 去请求 GitHub,只会得到一个莫名其妙的报错。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span><span> (</span><span>errorParam</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>return</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>error</span><span>:</span><span><span> </span><span>errorParam</span><span>,</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>error_description</span><span>:</span><span><span> </span><span>errorDescription</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>state</span><span>:</span><span><span> </span><span>state</span><span> </span></span><span>||</span><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></div></div></code></pre><div><div></div><div></div></div></figure></div><p>拿到正常的 <code>code</code> 之后,就是 POST 请求打到 <code>https://github.com/login/oauth/access_token</code>,body 里带上 <code>client_id</code>、<code>client_secret</code>、<code>code</code>、<code>redirect_uri</code>,这一步是服务端对服务端换 token,<code>client_secret</code> 只能放在这里,绝对不能出现在 App 里,不然反编译一下就全露了。这里踩了个小坑&lt;请求头一定要加&gt; <code>Accept: application/json</code>,不加的话 GitHub 默认返回的是 <code>key=value&amp;key2=value2</code> 这种查询字符串格式,还得自己手动解析,挺麻烦的。</p><p>换完之后,不管是换 token 失败、GitHub 返回业务错误（比如 <code>bad_verification_code</code>，也就是 code 过期或者已经被用过了）、还是成功拿到了 <code>access_token</code>,最后都统一走 <code>redirectToApp</code> 这一个函数 302 回 App:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>function</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, </span><span>params</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>const</span><span> </span><span>target</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>URL</span><span><span>(</span><span>env</span><span>.</span></span><span>APP_SCHEME</span><span>);</span></div></div><div><div><div>3</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> [</span><span>k</span><span>, </span><span>v</span><span>] </span><span>of</span><span><span> </span><span>Object</span><span>.</span></span><span>entries</span><span><span>(</span><span>params</span><span>)) {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span><span> (</span><span>v</span><span> </span></span><span>!==</span><span> </span><span>undefined</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>v</span><span> </span></span><span>!==</span><span> </span><span>null</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>v</span><span> </span></span><span>!==</span><span> </span><span>""</span><span>) {</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>target</span><span>.</span><span>searchParams</span><span>.</span><span>set</span><span><span>(</span><span>k</span><span>, </span><span>v</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>return</span><span><span> </span><span>Response</span><span>.</span></span><span>redirect</span><span><span>(</span><span>target</span><span>.</span></span><span>toString</span><span>(), </span><span>302</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样设计的好处是,App 那边只需要检查跳转回来的 URL 上有没有 <code>access_token</code>,就能判断整个流程走没走通,完全不用关心中间到底是哪个环节出的问题。</p></section>
<section><h2>后记<a href="#后记"><span>#</span></a></h2><p>弄完之后，才发现其实连服务器都不需要,浏览器是支持输入应用app scheme的,在浏览器通过这个跳转到应用内部，让应用接收code，然后在应用内部实现code转access token的过程。不过这样的话，就得把客户端id和客户端密钥写进应用内部，如果被人反编译出来，就会有密钥泄露的风险。而且如果是设计更复杂的应用，涉及到第三方账号的绑定，数据库的查询读写，这些操作都应该放到云端认证回调服务器上去执行，而不应该放在本地客户端里面。把认证回调服务器写成本地APP scheme这种方案只能临时用于没有服务器的情况，不能放在正式的生产环境。</p></section>
<section><h2>index.js<a href="#indexjs"><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>* GitHub OAuth 回调 Worker（简化版，仅用于拿到 access_token）</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><span><span> </span></span><span>* 1. App 用系统浏览器打开 GitHub 授权页</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>*    https://github.com/login/oauth/authorize?client_id=...&amp;redirect_uri=&lt;本Worker的/callback地址&gt;&amp;scope=repo&amp;state=xxx</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>* 2. 用户同意后，GitHub 302 重定向到本 Worker 的 /callback，带上 code、state</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>* 3. 本 Worker 用 code 向 GitHub 换取 access_token（服务端对服务端，带 client_secret）</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>* 4. 本 Worker 302 重定向到 App 的自定义 scheme，把 access_token 直接带回去</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>*    myapp://oauth/callback?access_token=xxx&amp;state=xxx</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>* 5. App 拦截该 scheme，取出 access_token，存到本地安全存储，之后用它直接调 GitHub API</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>* 需要在 wrangler 中配置以下变量：</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>* - GITHUB_CLIENT_ID      GitHub OAuth App 的 Client ID</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>* - GITHUB_CLIENT_SECRET  GitHub OAuth App 的 Client Secret（用 secret 存储）</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>* - APP_SCHEME            App 自定义 scheme，例如 "myapp://oauth/callback"</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>
</div></div><div><div><div>19</div></div><div><span>export</span><span> </span><span>default</span><span> {</span></div></div><div><div><div>20</div></div><div><span>  </span><span>async</span><span> </span><span>fetch</span><span><span>(</span><span>request</span><span>, </span><span>env</span><span>) {</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>const</span><span> </span><span>url</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>URL</span><span><span>(</span><span>request</span><span>.</span></span><span>url</span><span>);</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[REQUEST] </span><span>${</span><span>request</span><span>.</span><span>method</span><span>}</span><span> </span><span>${</span><span>url</span><span>.</span><span>pathname</span><span>}${</span><span>url</span><span>.</span><span>search</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span>if</span><span><span> (</span><span>url</span><span>.</span></span><span>pathname</span><span> </span><span>===</span><span> </span><span>"/callback"</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>request</span><span>.</span></span><span>method</span><span> </span><span>===</span><span> </span><span>"GET"</span><span>) {</span></div></div><div><div><div>26</div></div><div><span>      </span><span>return</span><span> </span><span>handleCallback</span><span>(</span><span>request</span><span>, </span><span>env</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[REQUEST] 未匹配任何路由，返回默认响应`</span><span>);</span></div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span> </span><span>new</span><span> </span><span>Response</span><span>(</span><span>"GitHub OAuth callback worker is running."</span><span>, {</span></div></div><div><div><div>31</div></div><div><span>      </span><span>status</span><span>:</span><span> </span><span>200</span><span>,</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>34</div></div><div><span>};</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>async</span><span> </span><span>function</span><span> </span><span>handleCallback</span><span><span>(</span><span>request</span><span>, </span><span>env</span><span>) {</span></span></div></div><div><div><div>37</div></div><div><span>  </span><span>const</span><span> </span><span>url</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>URL</span><span><span>(</span><span>request</span><span>.</span></span><span>url</span><span>);</span></div></div><div><div><div>38</div></div><div><span>  </span><span>const</span><span> </span><span>code</span><span> </span><span>=</span><span><span> </span><span>url</span><span>.</span></span><span>searchParams</span><span>.</span><span>get</span><span>(</span><span>"code"</span><span>);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>const</span><span> </span><span>state</span><span> </span><span>=</span><span><span> </span><span>url</span><span>.</span></span><span>searchParams</span><span>.</span><span>get</span><span>(</span><span>"state"</span><span>);</span></div></div><div><div><div>40</div></div><div><span>  </span><span>const</span><span> </span><span>errorParam</span><span> </span><span>=</span><span><span> </span><span>url</span><span>.</span></span><span>searchParams</span><span>.</span><span>get</span><span>(</span><span>"error"</span><span>);</span></div></div><div><div><div>41</div></div><div><span>  </span><span>const</span><span> </span><span>errorDescription</span><span> </span><span>=</span><span><span> </span><span>url</span><span>.</span></span><span>searchParams</span><span>.</span><span>get</span><span>(</span><span>"error_description"</span><span>);</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>console</span><span>.</span><span>log</span><span>(</span></div></div><div><div><div>44</div></div><div><span>    </span><span>`[CALLBACK] 收到回调参数: code=</span><span>${</span><span>code</span><span> </span><span>?</span><span> </span><span>code</span><span>.</span><span>slice</span><span>(</span><span>0</span><span>,</span><span> </span><span>8</span><span>)</span><span> </span><span>+</span><span> </span><span>"</span><span>...</span><span>"</span><span> </span><span>:</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>, `</span><span> </span><span>+</span></div></div><div><div><div>45</div></div><div><span>      </span><span>`state=</span><span>${</span><span>state</span><span> </span><span>||</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>, error=</span><span>${</span><span>errorParam</span><span> </span><span>||</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>, `</span><span> </span><span>+</span></div></div><div><div><div>46</div></div><div><span>      </span><span>`error_description=</span><span>${</span><span>errorDescription</span><span> </span><span>||</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>`</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>);</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>  </span><span>// 用户在 GitHub 授权页点击了"取消"</span></div></div><div><div><div>50</div></div><div><span>  </span><span>if</span><span><span> (</span><span>errorParam</span><span>) {</span></span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[CALLBACK] 用户拒绝授权或GitHub返回错误: </span><span>${</span><span>errorParam</span><span>}</span><span> - </span><span>${</span><span>errorDescription</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>52</div></div><div><span>    </span><span>return</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, {</span></span></div></div><div><div><div>53</div></div><div><span>      </span><span>error</span><span>:</span><span><span> </span><span>errorParam</span><span>,</span></span></div></div><div><div><div>54</div></div><div><span>      </span><span>error_description</span><span>:</span><span><span> </span><span>errorDescription</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>55</div></div><div><span>      </span><span>state</span><span>:</span><span><span> </span><span>state</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>code</span><span>) {</span></span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[CALLBACK] 缺少code参数，终止处理`</span><span>);</span></div></div><div><div><div>61</div></div><div><span>    </span><span>return</span><span> </span><span>new</span><span> </span><span>Response</span><span>(</span><span>"缺少 code 参数"</span><span>, { </span><span>status</span><span>:</span><span> </span><span>400</span><span> });</span></div></div><div><div><div>62</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span></div></div><div><div><div>66</div></div><div><span>      </span><span>`[TOKEN] 开始向GitHub换取access_token, client_id=</span><span>${</span><span>env</span><span>.</span><span>GITHUB_CLIENT_ID</span><span>}</span><span>, `</span><span> </span><span>+</span></div></div><div><div><div>67</div></div><div><span>        </span><span>`redirect_uri=</span><span>${</span><span>url</span><span>.</span><span>origin</span><span>}</span><span>/callback`</span></div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>69</div></div><div>
</div></div><div><div><div>70</div></div><div><span>    </span><span>const</span><span> </span><span>tokenRes</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span>(</span><span>"https://github.com/login/oauth/access_token"</span><span>, {</span></div></div><div><div><div>71</div></div><div><span>      </span><span>method</span><span>:</span><span> </span><span>"POST"</span><span>,</span></div></div><div><div><div>72</div></div><div><span>      </span><span>headers</span><span>:</span><span> {</span></div></div><div><div><div>73</div></div><div><span>        </span><span>"Content-Type"</span><span>:</span><span> </span><span>"application/json"</span><span>,</span></div></div><div><div><div>74</div></div><div><span>        </span><span>Accept</span><span>:</span><span> </span><span>"application/json"</span><span>,</span></div></div><div><div><div>75</div></div><div><span>        </span><span>"User-Agent"</span><span>:</span><span> </span><span>"cloudflare-worker-github-oauth"</span><span>,</span></div></div><div><div><div>76</div></div><div><span><span>      </span></span><span>},</span></div></div><div><div><div>77</div></div><div><span>      </span><span>body</span><span>:</span><span> </span><span>JSON</span><span>.</span><span>stringify</span><span>({</span></div></div><div><div><div>78</div></div><div><span>        </span><span>client_id</span><span>:</span><span><span> </span><span>env</span><span>.</span></span><span>GITHUB_CLIENT_ID</span><span>,</span></div></div><div><div><div>79</div></div><div><span>        </span><span>client_secret</span><span>:</span><span><span> </span><span>env</span><span>.</span></span><span>GITHUB_CLIENT_SECRET</span><span>,</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>code</span><span>,</span></div></div><div><div><div>81</div></div><div><span>        </span><span>redirect_uri</span><span>:</span><span> </span><span>`</span><span>${</span><span>url</span><span>.</span><span>origin</span><span>}</span><span>/callback`</span><span>,</span></div></div><div><div><div>82</div></div><div><span><span>      </span></span><span>}),</span></div></div><div><div><div>83</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>84</div></div><div>
</div></div><div><div><div>85</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[TOKEN] GitHub token接口HTTP状态码: </span><span>${</span><span>tokenRes</span><span>.</span><span>status</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>tokenRes</span><span>.</span></span><span>ok</span><span>) {</span></div></div><div><div><div>88</div></div><div><span>      </span><span>const</span><span> </span><span>bodyText</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>tokenRes</span><span>.</span></span><span>text</span><span>();</span></div></div><div><div><div>89</div></div><div><span><span>      </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[TOKEN] 请求失败，响应体: </span><span>${</span><span>bodyText</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>90</div></div><div><span>      </span><span>return</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, {</span></span></div></div><div><div><div>91</div></div><div><span>        </span><span>error</span><span>:</span><span> </span><span>"token_exchange_failed"</span><span>,</span></div></div><div><div><div>92</div></div><div><span>        </span><span>error_description</span><span>:</span><span> </span><span>`GitHub token endpoint returned </span><span>${</span><span>tokenRes</span><span>.</span><span>status</span><span>}</span><span>`</span><span>,</span></div></div><div><div><div>93</div></div><div><span>        </span><span>state</span><span>:</span><span><span> </span><span>state</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>94</div></div><div><span><span>      </span></span><span>});</span></div></div><div><div><div>95</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>96</div></div><div>
</div></div><div><div><div>97</div></div><div><span>    </span><span>const</span><span> </span><span>tokenData</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>tokenRes</span><span>.</span></span><span>json</span><span>();</span></div></div><div><div><div>98</div></div><div>
</div></div><div><div><div>99</div></div><div><span>    </span><span>// 注意：access_token 只打印前几位，避免完整token出现在日志里</span></div></div><div><div><div>100</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span></div></div><div><div><div>101</div></div><div><span>      </span><span>`[TOKEN] GitHub返回内容: `</span><span> </span><span>+</span></div></div><div><div><div>102</div></div><div><span>        </span><span>`access_token=</span><span>${</span><span>tokenData</span><span>.</span><span>access_token</span><span> </span><span>?</span><span> </span><span>tokenData</span><span>.</span><span>access_token</span><span>.</span><span>slice</span><span>(</span><span>0</span><span>,</span><span> </span><span>8</span><span>)</span><span> </span><span>+</span><span> </span><span>"</span><span>...</span><span>"</span><span> </span><span>:</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>, `</span><span> </span><span>+</span></div></div><div><div><div>103</div></div><div><span>        </span><span>`scope=</span><span>${</span><span>tokenData</span><span>.</span><span>scope</span><span> </span><span>||</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>, `</span><span> </span><span>+</span></div></div><div><div><div>104</div></div><div><span>        </span><span>`token_type=</span><span>${</span><span>tokenData</span><span>.</span><span>token_type</span><span> </span><span>||</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>, `</span><span> </span><span>+</span></div></div><div><div><div>105</div></div><div><span>        </span><span>`error=</span><span>${</span><span>tokenData</span><span>.</span><span>error</span><span> </span><span>||</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>, `</span><span> </span><span>+</span></div></div><div><div><div>106</div></div><div><span>        </span><span>`error_description=</span><span>${</span><span>tokenData</span><span>.</span><span>error_description</span><span> </span><span>||</span><span> </span><span>"</span><span>null</span><span><span>"</span><span>}</span></span><span>`</span></div></div><div><div><div>107</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>108</div></div><div>
</div></div><div><div><div>109</div></div><div><span>    </span><span>if</span><span><span> (</span><span>tokenData</span><span>.</span></span><span>error</span><span>) {</span></div></div><div><div><div>110</div></div><div><span>      </span><span>// 常见：bad_verification_code（code过期/已用过）、incorrect_client_credentials（client_id或secret错）</span></div></div><div><div><div>111</div></div><div><span><span>      </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[TOKEN] GitHub返回业务错误: </span><span>${</span><span>tokenData</span><span>.</span><span>error</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>112</div></div><div><span>      </span><span>return</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, {</span></span></div></div><div><div><div>113</div></div><div><span>        </span><span>error</span><span>:</span><span><span> </span><span>tokenData</span><span>.</span></span><span>error</span><span>,</span></div></div><div><div><div>114</div></div><div><span>        </span><span>error_description</span><span>:</span><span><span> </span><span>tokenData</span><span>.</span></span><span>error_description</span><span> </span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>115</div></div><div><span>        </span><span>state</span><span>:</span><span><span> </span><span>state</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>116</div></div><div><span><span>      </span></span><span>});</span></div></div><div><div><div>117</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>118</div></div><div>
</div></div><div><div><div>119</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>tokenData</span><span>.</span></span><span>access_token</span><span>) {</span></div></div><div><div><div>120</div></div><div><span><span>      </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[TOKEN] 异常：GitHub未返回error，但也没有access_token`</span><span>);</span></div></div><div><div><div>121</div></div><div><span>      </span><span>return</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, {</span></span></div></div><div><div><div>122</div></div><div><span>        </span><span>error</span><span>:</span><span> </span><span>"no_access_token"</span><span>,</span></div></div><div><div><div>123</div></div><div><span>        </span><span>error_description</span><span>:</span><span> </span><span>"GitHub未返回access_token"</span><span>,</span></div></div><div><div><div>124</div></div><div><span>        </span><span>state</span><span>:</span><span><span> </span><span>state</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>125</div></div><div><span><span>      </span></span><span>});</span></div></div><div><div><div>126</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>127</div></div><div>
</div></div><div><div><div>128</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[TOKEN] 换取成功，即将302重定向回App`</span><span>);</span></div></div><div><div><div>129</div></div><div>
</div></div><div><div><div>130</div></div><div><span>    </span><span>// 直接把 GitHub 的 access_token 带回 App</span></div></div><div><div><div>131</div></div><div><span>    </span><span>// 注意：GitHub 传统 OAuth App 签发的 token 默认长期有效，没有 refresh_token</span></div></div><div><div><div>132</div></div><div><span>    </span><span>return</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, {</span></span></div></div><div><div><div>133</div></div><div><span>      </span><span>access_token</span><span>:</span><span><span> </span><span>tokenData</span><span>.</span></span><span>access_token</span><span>,</span></div></div><div><div><div>134</div></div><div><span>      </span><span>scope</span><span>:</span><span><span> </span><span>tokenData</span><span>.</span></span><span>scope</span><span> </span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>135</div></div><div><span>      </span><span>state</span><span>:</span><span><span> </span><span>state</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>136</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>137</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span><span> (</span><span>err</span><span>) {</span></span></div></div><div><div><div>138</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[ERROR] 处理过程中抛出异常: </span><span>${</span><span>err</span><span> </span><span>&amp;&amp;</span><span> </span><span>err</span><span>.</span><span>stack</span><span> </span><span>?</span><span> </span><span>err</span><span>.</span><span>stack</span><span> </span><span>:</span><span> </span><span>err</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>139</div></div><div><span>    </span><span>return</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, {</span></span></div></div><div><div><div>140</div></div><div><span>      </span><span>error</span><span>:</span><span> </span><span>"internal_error"</span><span>,</span></div></div><div><div><div>141</div></div><div><span>      </span><span>error_description</span><span>:</span><span> </span><span>String</span><span><span>(</span><span>err</span><span> </span></span><span>&amp;&amp;</span><span><span> </span><span>err</span><span>.</span></span><span>message</span><span> </span><span>?</span><span><span> </span><span>err</span><span>.</span></span><span>message</span><span> </span><span>:</span><span><span> </span><span>err</span><span>),</span></span></div></div><div><div><div>142</div></div><div><span>      </span><span>state</span><span>:</span><span><span> </span><span>state</span><span> </span></span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>143</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>144</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>145</div></div><div><span>}</span></div></div><div><div><div>146</div></div><div>
</div></div><div><div><div>147</div></div><div><span>/**</span></div></div><div><div><div>148</div></div><div><span><span> </span></span><span>* 把结果通过 302 重定向带回 App 自定义 scheme</span></div></div><div><div><div>149</div></div><div><span><span> </span></span><span>* env.APP_SCHEME 形如："myapp://oauth/callback"</span></div></div><div><div><div>150</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>151</div></div><div><span>function</span><span> </span><span>redirectToApp</span><span><span>(</span><span>env</span><span>, </span><span>params</span><span>) {</span></span></div></div><div><div><div>152</div></div><div><span>  </span><span>const</span><span> </span><span>target</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>URL</span><span><span>(</span><span>env</span><span>.</span></span><span>APP_SCHEME</span><span>);</span></div></div><div><div><div>153</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> [</span><span>k</span><span>, </span><span>v</span><span>] </span><span>of</span><span><span> </span><span>Object</span><span>.</span></span><span>entries</span><span><span>(</span><span>params</span><span>)) {</span></span></div></div><div><div><div>154</div></div><div><span>    </span><span>if</span><span><span> (</span><span>v</span><span> </span></span><span>!==</span><span> </span><span>undefined</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>v</span><span> </span></span><span>!==</span><span> </span><span>null</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>v</span><span> </span></span><span>!==</span><span> </span><span>""</span><span>) {</span></div></div><div><div><div>155</div></div><div><span><span>      </span></span><span>target</span><span>.</span><span>searchParams</span><span>.</span><span>set</span><span><span>(</span><span>k</span><span>, </span><span>v</span><span>);</span></span></div></div><div><div><div>156</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>157</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>158</div></div><div>
</div></div><div><div><div>159</div></div><div><span>  </span><span>// access_token 是敏感信息，日志里只打印前几位，避免完整token写入日志</span></div></div><div><div><div>160</div></div><div><span>  </span><span>const</span><span> </span><span>loggedUrl</span><span> </span><span>=</span><span><span> </span><span>target</span><span>.</span></span><span>toString</span><span>().</span><span>replace</span><span>(</span></div></div><div><div><div>161</div></div><div><span>    </span><span>/access_token=(</span><span>[</span><span>^</span><span>&amp;]</span><span><span>+</span><span>)/</span></span><span>,</span></div></div><div><div><div>162</div></div><div><span><span>    </span></span><span>(</span><span>_</span><span>, </span><span>t</span><span>) </span><span>=&gt;</span><span> </span><span>`access_token=</span><span>${</span><span>t</span><span>.</span><span>slice</span><span>(</span><span>0</span><span>,</span><span> </span><span>8</span><span>)</span><span>}</span><span>...`</span></div></div><div><div><div>163</div></div><div><span><span>  </span></span><span>);</span></div></div><div><div><div>164</div></div><div><span><span>  </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`[REDIRECT] 302重定向回App: </span><span>${</span><span>loggedUrl</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>165</div></div><div>
</div></div><div><div><div>166</div></div><div><span>  </span><span>return</span><span><span> </span><span>Response</span><span>.</span></span><span>redirect</span><span><span>(</span><span>target</span><span>.</span></span><span>toString</span><span>(), </span><span>302</span><span>);</span></div></div><div><div><div>167</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></section>
<section><h2>wrangler.toml<a href="#wranglertoml"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>name</span><span> = </span><span>'oauth-woker'</span></div></div><div><div><div>2</div></div><div><span>main</span><span> = </span><span>"index.js"</span></div></div><div><div><div>3</div></div><div><span>compatibility_date</span><span> = </span><span>"2026-07-24"</span></div></div><div><div><div>4</div></div><div><span># 非敏感变量可以直接写在这里</span></div></div><div><div><div>5</div></div><div><span>[</span><span>vars</span><span>]</span></div></div><div><div><div>6</div></div><div><span>APP_SCHEME</span><span> = </span><span>"xxx://xx/xx"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 以下敏感信息不要写在 toml 里，改用 wrangler secret 命令设置：</span></div></div><div><div><div>9</div></div><div><span>#</span></div></div><div><div><div>10</div></div><div><span>#   wrangler secret put GITHUB_CLIENT_ID</span></div></div><div><div><div>11</div></div><div><span>#   wrangler secret put GITHUB_CLIENT_SECRET</span></div></div><div><div><div>12</div></div><div><span>#</span></div></div><div><div><div>13</div></div><div><span># 设置后 Cloudflare 会加密存储，不会出现在代码仓库或部署日志中</span></div></div><div><div><div>14</div></div><div><span>[</span><span>observability</span><span>]</span></div></div><div><div><div>15</div></div><div><span>enabled</span><span> = </span><span>true</span><span>          </span><span># 总开关必须是 true，下面的子配置才会生效</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>[</span><span>observability</span><span>.</span><span>logs</span><span>]</span></div></div><div><div><div>18</div></div><div><span>enabled</span><span> = </span><span>true</span></div></div><div><div><div>19</div></div><div><span>head_sampling_rate</span><span> = </span><span>1</span></div></div><div><div><div>20</div></div><div><span>invocation_logs</span><span> = </span><span>true</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>[</span><span>observability</span><span>.</span><span>traces</span><span>]</span></div></div><div><div><div>23</div></div><div><span>enabled</span><span> = </span><span>false</span><span>          </span><span># 你现在确实不需要traces，关掉没问题</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/educoder-anti-copy-paste-bypass/</id>
      <title type="text">学习笔记 | 头歌破解考试的防复制粘贴</title>
      <published>2026-07-23T00:00:00.000Z</published>
      <updated>2026-07-23T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/educoder-anti-copy-paste-bypass/"/>
      <summary type="text">从作业到考试，头歌（EduCoder）的防复制粘贴限制分别实现在配置层和事件层，记录一次逆向分析与油猴脚本破解过程。</summary>
      <content type="html"><![CDATA[本文已加密保护，请访问网站查看。]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/orchids-openai-proxy/</id>
      <title type="text">学习笔记 | 一次网页端逆向到 OpenAI 接口封装的完整过程</title>
      <published>2026-07-21T00:00:00.000Z</published>
      <updated>2026-07-21T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/orchids-openai-proxy/"/>
      <summary type="text">基于 Orchids 网页端会话链路做协议分析，复用既有鉴权与 SSE 数据流，封装出兼容 OpenAI API 的本地中转服务，并支持流式输出、思考模式与文件上传。</summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><p>最近看到 <a href="https://www.orchids.app" target="_blank">Orchids</a> 给了免费的 Claude 和 GPT 额度，但它只提供网页端，没有开放后端 API。<br />
我就做了一个小实验：不去改它服务端，只复用网页端已有调用链路，把能力封装成一个 <strong>OpenAI API 兼容网关</strong>，方便接入各类客户端和工具链。</p><p><img alt="" loading="lazy" width="210" height="327" src="/_astro/1.BKWxusSM_1aaSNI.webp" />
<img alt="" loading="lazy" width="293" height="109" src="/_astro/2.BCHg8sUG_1jK7Iv.webp" /></p><p>这个方案可以跑通，支持流式、思考模式和文件上传，但并不适合做高并发号池，额度和速率都受原平台会话机制限制。本文主要是技术复盘与工程总结。</p><hr /></section>
<section><h2>一、协议适配<a href="#一协议适配"><span>#</span></a></h2><p>这次的目标很明确：</p><ol>
<li>复用网页端已有会话与鉴权流程</li>
<li>把上游 SSE 数据流转换成标准 OpenAI 返回结构</li>
<li>对外暴露常见 OpenAI 接口，做到低改造接入</li>
</ol><p>最终本地服务提供了这些路由：</p><ul>
<li><code>GET /health</code></li>
<li><code>GET /v1/models</code></li>
<li><code>POST /v1/chat/completions</code></li>
<li><code>POST /v1/responses</code></li>
<li><code>POST /v1/files</code></li>
<li><code>GET /v1/files</code></li>
<li><code>GET /v1/files/{file_id}</code></li>
<li><code>GET /v1/files/{file_id}/content</code></li>
<li><code>GET /v1/files/{file_id}/url</code></li>
<li><code>DELETE /v1/files/{file_id}</code></li>
</ul><hr /></section>
<section><h2>二、链路分析：从网页请求里抽象最小参数集<a href="#二链路分析从网页请求里抽象最小参数集"><span>#</span></a></h2><p>在网页端实际请求里，可以定位到这几个关键参数：</p><ul>
<li><code>clerk_active_context</code></li>
<li><code>projectId</code></li>
<li><code>userId</code></li>
<li><code>__client</code></li>
</ul><p>其中：</p><ul>
<li><code>projectId</code> 来自对话创建后的项目上下文</li>
<li><code>clerk_active_context</code> 与 <code>__client</code> 用于会话鉴权</li>
<li><code>userId</code> 用于拼接上游请求体</li>
</ul><p>这四个参数就是整个中转服务的最小可用配置。程序启动时交互输入，避免硬编码到源码里。</p><p><img alt="" loading="lazy" width="1035" height="496" src="/_astro/3.BPPPOOQw_Z1hUB2b.webp" /></p><hr /></section>
<section><h2>三、程序架构<a href="#三程序架构"><span>#</span></a></h2><section><h3>1) 配置与服务启动<a href="#1-配置与服务启动"><span>#</span></a></h3><p>服务启动后读取配置并初始化两个 HTTP Client：</p><ul>
<li><code>jwtClient</code>：用于向 Clerk 换取 JWT</li>
<li><code>upstreamClient</code>：用于请求 Orchids 的 <code>stream-run</code> SSE 接口</li>
</ul><p>同时挂载 OpenAI 风格路由，作为统一入口。</p></section><section><h3>2) 鉴权流程<a href="#2-鉴权流程"><span>#</span></a></h3><p>核心流程是：</p><ol>
<li>调用 Clerk token 接口（带 <code>__client</code> Cookie）</li>
<li>获取临时 JWT</li>
<li>用 JWT 访问上游推理 SSE</li>
</ol><p>也就是说，本地网关没有伪造新鉴权体系，而是复用了网页端已有会话链路。</p></section><section><h3>3) 上游请求与容错<a href="#3-上游请求与容错"><span>#</span></a></h3><p>上游请求采用以下策略：</p><ul>
<li><code>Authorization: Bearer &lt;jwt&gt;</code></li>
<li><code>Accept: text/event-stream</code></li>
<li>网络可重试错误自动重试一次</li>
<li>失败时返回清晰错误（包含状态码与上游响应）</li>
</ul><p>这块是工程稳定性的关键，不然很容易因为临时网络抖动导致误判不可用。</p><hr /></section></section>
<section><h2>四、协议转换：把 Orchids SSE 映射成 OpenAI 语义<a href="#四协议转换把-orchids-sse-映射成-openai-语义"><span>#</span></a></h2><p>上游事件流里主要有三类信息：</p><ul>
<li>文本增量（<code>text_delta</code>）</li>
<li>思考增量（<code>thinking_delta</code>）</li>
<li>最终结果与 usage</li>
</ul><p>程序分两套解析逻辑：</p><ul>
<li><code>parseOrchidsSSEForFinalResult</code>：非流式聚合</li>
<li><code>parseOrchidsSSEStream</code>：流式逐段回传</li>
</ul><p>再映射为：</p><ul>
<li>Chat Completions：<code>chat.completion</code> / <code>chat.completion.chunk</code></li>
<li>Responses API：<code>response.output_text.delta</code> / <code>response.completed</code></li>
</ul><p>并且 reasoning 开关打开时，会把 thinking 内容透传到对应字段，保证“思考模式”可用。</p><hr /></section>
<section><h2>五、文件能力：兼容 OpenAI Files 接口<a href="#五文件能力兼容-openai-files-接口"><span>#</span></a></h2><p>除了聊天接口，程序还实现了文件链路：</p><ol>
<li><code>POST /v1/files</code> 接收 multipart 文件上传</li>
<li>本地落盘并生成 <code>file-xxxx</code> 形式的 file_id</li>
<li>在消息输入中提取 <code>input_file</code> 的 <code>file_id</code></li>
<li>映射为上游可访问的 <code>attachmentUrls</code></li>
</ol><p>这意味着可以兼容部分依赖 Files API 的客户端工作流，而不只是纯文本对话。</p><hr /></section>
<section><h2>六、支持模型<a href="#六支持模型"><span>#</span></a></h2><p>当前版本内置模型列表如下：</p><ul>
<li><code>claude-sonnet-4-6</code></li>
<li><code>gpt-5.4</code></li>
<li><code>gpt-5.3-codex</code></li>
<li><code>claude-opus-4.6</code></li>
<li><code>claude-opus-4.5</code></li>
<li><code>gpt-5.2-codex</code></li>
<li><code>gpt-5.2</code></li>
<li><code>gemini-3-flash</code></li>
<li><code>gemini-3-pro</code></li>
<li><code>gemini-3.1-pro</code></li>
</ul><p>可通过 <code>GET /v1/models</code> 查询。</p><hr /></section>
<section><h2>七、实际体验与边界<a href="#七实际体验与边界"><span>#</span></a></h2><p>这个方案的优点：</p><ul>
<li>对接现有 OpenAI 生态方便</li>
<li>协议统一后可被多数工具直接消费</li>
<li>流式和 reasoning 都可跑通</li>
</ul><p>但边界也很明显：</p><ul>
<li>依赖网页会话参数，稳定性受平台变更影响</li>
<li>额度和速度都不适合当号池</li>
<li>上游接口一旦改字段，网关要同步维护</li>
</ul><p>所以它更适合作为逆向与协议适配实践，而不是生产级高可用方案。</p><hr /></section>
<section><h2>总结<a href="#总结"><span>#</span></a></h2><p>其实本质上是：<br />
<strong>把“网页端调用流程”重构成“标准 API 服务”。</strong></p><p>从工程角度看，真正有价值的是：</p><ul>
<li>能否抽象出最小参数集</li>
<li>能否稳定解析 SSE 并保持语义一致</li>
<li>能否用统一接口服务不同客户端</li>
</ul><p>如果只是临时脚本，那是一次调用；<br />
完成了协议映射、错误模型和流式对齐，才算一个可复用的接口封装方案。</p><hr /><p>程序下载链接：<br />
<a href="https://www.123865.com/s/vxarVv-o7yHH" target="_blank">https://www.123865.com/s/vxarVv-o7yHH</a></p><p>仅用于学习逆向、协议理解和网关设计；<br />
如果想直接拿来做业务化池子，建议降低预期。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/pageagent/</id>
      <title type="text">开发日记 | Page-agent项目改进</title>
      <published>2026-06-14T00:00:00.000Z</published>
      <updated>2026-06-14T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/pageagent/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><p>有一次在<a href="https://linux.do" target="_blank">linux.do</a>上逛的时候,发现了一个有趣的项目(<a href="https://alibaba.github.io/page-agent/docs/introduction/quick-start" target="_blank">page-agent</a>),可以用自然语言让ai控制网页，不像传统的自动化网页方案，需要利用Python使用有头或者无头浏览器之类的。这个项目直接是作为js脚本，直接可以嵌入或hook在浏览器中,不需要依赖Python或者其他的东西,更简单易用，好上手。
<img alt="" loading="lazy" width="2556" height="1342" src="/_astro/1.Bhw3m5iz_1vj0Q8.webp" /></p></section>
<section><h2>思考<a href="#思考"><span>#</span></a></h2><p>不过很可惜，原项目是完全基于网页dom文本化的操作方案，网页上的图片信息并不会输出在给ai的文本信息里面，也就是说无法识别网页图片，并且在在网页的文字识别上也加了很多限制，有时候有一些网站的有效信息无法被正常识别。于是乎我想研究一下，看一下能不能在原项目的基础上，让它支持识别图片，并且可以识别更多的网页文字。在经过几天的努力，也是终于把这两个问题给搞定了。</p></section>
<section><h2>改进方法<a href="#改进方法"><span>#</span></a></h2><section><h3>一、让 AI 识别图片：将图片链接注入 DOM 树<a href="#一让-ai-识别图片将图片链接注入-dom-树"><span>#</span></a></h3><p>原项目在生成 DOM 树字符串时，默认的属性白名单 <code>r</code> 中 <strong>不包含 <code>src</code></strong>，因此 <code>&lt;img&gt;</code> 标签的链接不会传递给 AI。此外，图片元素通常不被标记为“可交互”，导致其属性在 <code>F()</code> 函数中根本不会被提取。</p><p><strong>修改步骤</strong>：</p><ol>
<li>
<p><strong>将 <code>src</code> 加入属性白名单</strong><br />
在 <code>flatTreeToString</code> 函数的开头，<code>r</code> 数组中添加 <code>'src'</code>，这样 <code>src</code> 属性就会在 <code>matchAttributes</code> 中被匹配并输出。</p>
</li>
<li>
<p><strong>在 <code>dom_tree_default</code> 的 <code>F()</code> 函数中手动提取 <code>&lt;img&gt;</code> 的属性</strong><br />
原代码只有满足 <code>k(e)</code>（交互元素）或 <code>iframe</code>/<code>body</code> 的元素才会提取属性。我们在 <code>F()</code> 中增加一个分支：当 <code>e.tagName.toLowerCase() === 'img'</code> 时，手动将其所有属性复制到 <code>r.attributes</code> 对象中。</p>
</li>
<li>
<p><strong>在 <code>flatTreeToString</code> 中为 <code>&lt;img&gt;</code> 特殊输出</strong><br />
在 <code>u()</code> 函数中，当遇到 <code>e.tagName === 'img'</code> 时，直接输出 <code>&lt;img src="..." alt="..." /&gt;</code> 格式，并附带 <code>[图片链接: ...]</code> 标记，便于 AI 识别。为了避免输出 <code>data:image</code> 等内嵌图片，用正则过滤掉 Base64 链接。</p>
</li>
</ol><p><strong>效果</strong>：<br />
AI 收到的 <code>&lt;browser_state&gt;</code> 中会出现如 <code>[图片链接: https://example.com/photo.jpg]</code> 的信息，AI 知道这是一张图片，可以调用后续的工具来识别。</p></section><section><h3>二、突破文字识别限制：移除文本节点的可见性过滤<a href="#二突破文字识别限制移除文本节点的可见性过滤"><span>#</span></a></h3><p>原项目对文本节点的输出设有多重限制：</p><ul>
<li><code>l(e)</code> 检查：若文本位于可交互元素内部则跳过。</li>
<li><code>isTopElement</code> 检查：只有父元素是“顶层元素”（未被遮挡、不在视口外）时才输出。</li>
<li><code>isVisible</code> 检查：父元素必须可见（<code>offsetWidth/Height &gt; 0</code> 且 CSS 未隐藏）。</li>
</ul><p>这些限制导致许多嵌套较深或位置特殊的文本（如 Monaco 编辑器内的代码、被 <code>overflow: hidden</code> 包裹的内容）无法被 AI 读取。</p><p><strong>修改步骤</strong>：</p><ol>
<li><strong>移除 <code>l(e)</code> 检查</strong>：在 <code>flatTreeToString</code> 中删除 <code>if (l(e)) return;</code> 这一行。</li>
<li><strong>移除 <code>isTopElement</code> 检查</strong>：将文本输出的条件从 <code>e.parent.isTopElement</code> 改为直接忽略，只保留 <code>e.parent.isVisible</code>（或完全去掉，保留所有可见文本）。</li>
<li><strong>放宽 <code>x(e)</code> 函数</strong>：在 <code>dom_tree_default</code> 中，将 <code>x(e)</code> 改为始终返回 <code>true</code>，让所有文本节点都被视为可见，不会被丢弃。</li>
<li><strong>可选：移除 <code>C(e)</code> 中的尺寸检查</strong>：如果希望进一步解除限制，可将 <code>C(e)</code> 中的 <code>offsetWidth &gt; 0 &amp;&amp; offsetHeight &gt; 0</code> 条件改为始终 <code>true</code>，但这会导致大量不可见元素被纳入，建议谨慎。</li>
</ol><p><strong>效果</strong>：<br />
现在几乎所有可见的文本节点都会被输出，包括代码编辑器内部的代码、被遮挡的说明文字、侧边栏小字等。AI 的上下文信息量显著增加，但同时 token 消耗也会上升，可根据实际场景调整。</p></section><section><h3>三、让 AI 自主调用图片识别工具<a href="#三让-ai-自主调用图片识别工具"><span>#</span></a></h3><p>仅让 AI “看到”图片链接还不够，还需要让它能够<strong>理解图片内容</strong>。我们增加一个自定义工具 <code>recognize_image</code>，AI 可以调用它来识别指定 URL 的图片，并将结果写回图片的 <code>alt</code> 属性。</p><p><strong>工具定义</strong>（使用 Zod 进行参数校验）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>recognize_image</span><span>: {</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>description</span><span>: </span><span>'识别指定 URL 的图片内容，将识别结果写入该图片的 alt 属性，并返回描述文本。'</span><span>,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>inputSchema</span><span>: </span><span>z</span><span>.</span><span>object</span><span>({</span></div></div><div><div><div>4</div></div><div><span>    </span><span>imageUrl</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>string</span><span>().</span><span>url</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>execute</span><span>: </span><span>async</span><span> </span><span>function</span><span><span> (</span><span>input</span><span>, { </span><span>signal</span><span> }) {</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>const</span><span> { </span><span>imageUrl</span><span> } </span><span>=</span><span><span> </span><span>input</span><span>;</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>// 优先尝试直接使用 URL，失败则转换为 Base64</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// 调用视觉 API（如 Qwen-VL、GPT-4V 等）</span></div></div><div><div><div>10</div></div><div><span>    </span><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span>(</span><span>'...'</span><span>, { </span><span>...</span><span> });</span></div></div><div><div><div>11</div></div><div><span>    </span><span>const</span><span> </span><span>description</span><span> </span><span>=</span><span> </span><span>...</span><span>; </span><span>// 从 API 响应中提取描述</span></div></div><div><div><div>12</div></div><div><span>    </span><span>// 查找页面中对应的 img 元素，写入 alt</span></div></div><div><div><div>13</div></div><div><span>    </span><span>const</span><span> </span><span>img</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelector</span><span>(</span><span>`img[src="</span><span>${</span><span>imageUrl</span><span>}</span><span>"]`</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>if</span><span><span> (</span><span>img</span><span>) </span><span>img</span><span>.</span></span><span>alt</span><span> </span><span>=</span><span><span> </span><span>description</span><span>;</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span> </span><span>`图片识别完成，描述为：“</span><span>${</span><span>description</span><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></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>
<section><h2>总结<a href="#总结"><span>#</span></a></h2><p>于是乎，在经过了以上这些操作，成功让原项目支持了图片识别以及解除了一些网页文字识别的限制。以下是我改进版的仓库地址
<a href="https://github.com/iliaoke/page-agent" target="_blank">https://github.com/iliaoke/page-agent</a></p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/first-open-source-pr/</id>
      <title type="text">随笔杂谈 | 我的第一个开源 PR：适配 APatch 的 gbl_root_canoe 兼容性修复</title>
      <published>2026-05-24T00:00:00.000Z</published>
      <updated>2026-05-24T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/first-open-source-pr/"/>
      <summary type="text">记录在一加15T上折腾ESP分区漏洞Root方案时，发现模块在APatch上WebUI初始化失败的问题，通过阅读源码定位原因并提交修复，完成了自己的第一个开源项目Pull Request。</summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><p>在我初中接触Github并逐渐了解之后，我一直也想为开源社区做份贡献，也想给项目提交Pr。不过受限于自己的技术力还不够，也没有足够的时间与精力，以及合适的项目与机会。不过这个想法一直扎根在我的心头。</p></section>
<section><h2>机会<a href="#机会"><span>#</span></a></h2><p>在上了大学之后，我换了新手机一加15T，芯片骁龙8 Elite5。而在这一代手机刚好引入了esp分区，而且由于高通的疏忽，没有验证esp分区，导致部分版本的系统可以执行任意efi底层代码，于是便有了这个<a href="https://github.com/superturtlee/gbl_root_canoe" target="_blank">项目</a>。简单来说，这个项目可以在手机启动前无论手机有没有解锁bl，都返回bl已经上锁的信息，来通过手机的keystore效验。于是我尝试了这个项目的magisk模块，但是在<a href="https://blog.liao-ke.com/p/android-root/">APatch</a>上发现模块的WebUI无法初始化成功。</p></section>
<section><h2>解决<a href="#解决"><span>#</span></a></h2><p>于是乎，我翻了一下<a href="https://blog.liao-ke.com/p/android-root/">APatch</a>和这个<a href="https://github.com/superturtlee/gbl_root_canoe/blob/main/targets/magisk_module/module/webroot/app.js" target="_blank">项目</a>的源代码，我发现了问题的原因，这个项目模块在初始化的时候会调用KSU里面内置的一些函数方法，但是<a href="https://blog.liao-ke.com/p/android-root/">APatch</a>并没有内置这个方法函数，所以会导致在<a href="https://blog.liao-ke.com/p/android-root/">APatch</a>上面，这个模块会初始化失败，于是乎，我改了一下<a href="https://github.com/superturtlee/gbl_root_canoe/blob/main/targets/magisk_module/module/webroot/app.js" target="_blank">项目</a>的源代码，当检测到没有这个方法函数的时候，将会采取手动历遍，并且返回相对应的数据结构的方法来兼容<a href="https://blog.liao-ke.com/p/android-root/">APatch</a>。</p></section>
<section><h2>灵机一动<a href="#灵机一动"><span>#</span></a></h2><p>当我完成了以上事宜之后，突然想到，我做的这些不正好可以提交一个Pr来改进项目吗？于是乎抱着坎坷的心情，我提交了我第1个<a href="https://github.com/superturtlee/gbl_root_canoe/pull/44#event-25855613583" target="_blank">Pr</a>。</p></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p>在怀着坎坷的心情等了几天之后，原作者受理并成功通过了我的PR请求，这次经历无疑是圆了我初中时一直有的一个愿望。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/bypass-campus-wifi/</id>
      <title type="text">学习笔记 | 最简单的绕过校园网多设备热点检测方案</title>
      <published>2026-03-09T00:00:00.000Z</published>
      <updated>2026-03-09T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/bypass-campus-wifi/"/>
      <summary type="text">通过 tun 虚拟网卡与网络共享手动指定热点上级网卡，绕过校园网热点设备数检测</summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><p>弄到了一个白嫖的校园网账号,但是触发设备连接上限了(电脑只要开热点，wifi就无法访问互联网),我手机电脑无法同时连接,于是便有了接下来这篇教程</p></section>
<section><h2>原因解析<a href="#原因解析"><span>#</span></a></h2><ol>
<li>当电脑连接WIFI的时候,网络适配器中的wlan网卡便是已连接的状态</li>
<li>此时我们打开热点,在网络适配器中会自动创建一个新的本地连接的网卡</li>
<li>在默认情况下,热点的网卡会自动寻找可以访问互联网的网卡，作为上级网卡.</li>
<li>但是由于校园网有检测,当热点网卡的流量转发到wlan时,校园网就会检测到有多个设备，从而断开访问
<img alt="" loading="lazy" width="2536" height="1021" src="/_astro/1.Bu9y58EQ_1o6Lew.webp" /></li>
</ol></section>
<section><h2>解决方法<a href="#解决方法"><span>#</span></a></h2><ol>
<li>通过手动设置热点网卡的上级网卡到其他的网卡,然后其他网卡的流量(经过这个网卡，对流量进行处理)再流入wlan,这样便不会被校园网检测.</li>
<li>于是我们便要用到clash/singbox这样的代理工具的tun模式创造另外一个网卡(其实也可以用wintun驱动手动创建,不过得手动配置一些东西，比较麻烦)
<img alt="" loading="lazy" width="2524" height="1522" src="/_astro/4.B6jJdMyP_1gjzNJ.webp" /></li>
<li>不一定需要添加clash的代理配置文件,只要打开tun模式即可.</li>
<li>此时网络适配器中将会多出一张其他的网络设备
<img alt="" loading="lazy" width="2536" height="1279" src="/_astro/2.C1_-sxVc_1umMY0.webp" /></li>
<li>此时右击该网卡属性点击共享，然后设置共享的网络为本地连接(相当手动指定热点的上级网络),便可以实现绕过校园网检测
<img alt="" loading="lazy" width="2548" height="1455" src="/_astro/3.CJUm1Ihm_1pM0Pr.webp" /></li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/openwrt-vlan/</id>
      <title type="text">学习笔记 | openwrt25.10设置vlan实现单线复用</title>
      <published>2026-02-25T00:00:00.000Z</published>
      <updated>2026-02-25T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/openwrt-vlan/"/>
      <summary type="text">通过在光猫、主路由与房间子路由三端统一配置vlanid，实现桥接模式下的单线复用，让光猫其他网口/房间网口也能正常获取主路由DHCP下发的IP，并给出openwrt25.10的配置步骤与注意事项。</summary>
      <content type="html"><![CDATA[<section><h2>原理解析<a href="#原理解析"><span>#</span></a></h2><p>(特别注意&lt;有一些路由器的系统是不支持设置vlanid的&gt;,所以建议所有路由器都为openwrt软路由，方便操作.)</p><p>在传统方案中,当光猫设置<strong>桥接模式</strong>,软路由负责拨号,此时光猫的其他网口在正常情况下是无法工作的,因为光猫此时只做光电转换，不做路由ip下发.</p><p>此时我们便需要用到一项技术实现光猫在桥接模式下光猫的其他网口也能正常工作.这便是<strong>vlanid</strong>.vlanid并不只有<strong>单线复用</strong>这一个功能,它也可以用来划分局域网,从本质上来讲，它的作用就是给流量打上标记.</p><p>当光猫桥接主路由拨号时,其他房间网口的流量是流向光猫的网口,在流量没有打<strong>vlanid</strong>标识的情况下，光猫也不知道要把这些流量弄到哪里去,所以光猫其他网口便无法正常通讯.</p><p>所以这个时候我们便用<strong>vlanid</strong>来实现<strong>单线复用</strong>(<strong>即一条网线，负责实现两个功能,也就是说我们主路由与光猫连接的那根网线既负责主路由的拨号，又负责主路由dhcp服务器下发ip到光猫的其他网口</strong>),我们在其他网口的房间里的路由器里面设置<strong>vlanid</strong>,然后在光猫的管理员后台里也设置的<strong>vlanid</strong>,最后在主路由里面设置一样的<strong>vlanid</strong>.就这样在三台设备中都设置了<strong>vlanid</strong>,设备就知道这些流量该如何传输.</p></section>
<section><h2>具体操作<a href="#具体操作"><span>#</span></a></h2><ol>
<li>
<p>在房间的子路由中把<strong>wan接口删除</strong>,只留lan口,然后在设备中添加连接网线的网口的vlan设备,然后把这个设备添加到<strong>br-lan</strong>的网桥设备集合之中
<img alt="" loading="lazy" width="2383" height="991" src="/_astro/1.J30e56H1_Z105urz.webp" />
<img alt="" loading="lazy" width="2281" height="1236" src="/_astro/2.YPhIfmZg_ZviXXm.webp" /></p>
</li>
<li>
<p>在光猫的管理员后台(<strong>不同的型号的光猫进入管理员后台的方法不同，有一些甚至进不去管理员后台，这里就不细讲了</strong>),把对应连接方式的lan口绑定全部取消,然后设置对应网口的<strong>vlanid和vlan绑定</strong>
<img alt="" loading="lazy" width="1228" height="985" src="/_astro/3.CFXcMmDn_24uRKX.webp" />
<img alt="" loading="lazy" width="1219" height="998" src="/_astro/4.DXqJFPL7_ZcLKtd.webp" />
<img alt="" loading="lazy" width="1200" height="1180" src="/_astro/5.DrBrQrGh_22cd5M.webp" />
以上这张图中，网络连接二是用于主路由和光猫拨号的，而网络连接五则是用于设置vlanid来实现单线复用的。</p>
</li>
<li>
<p>在主路由进行和子路由中一样的操作,只是这一次不用把wan口删除了,把wan口对应的连接端口设置<strong>vlanid</strong>,然后也一并添加到<strong>br-lan</strong>中即可完成配置
<img alt="" loading="lazy" width="2553" height="1320" src="/_astro/6.2Nt2Jjm6_Z1X1Eon.webp" />
<img alt="" loading="lazy" width="2553" height="1351" src="/_astro/7.BvTVToMy_Z1dovC6.webp" /></p>
</li>
</ol><p>完成了以上三个步骤,就可以实现光猫在<strong>桥接模式</strong>下，房间的子路由仍然可以接收到主路由下发的ip了</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/routeros-choose/</id>
      <title type="text">方案分析 | 2026年软路由系统分析和推荐</title>
      <published>2026-02-18T00:00:00.000Z</published>
      <updated>2026-02-18T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/routeros-choose/"/>
      <summary type="text">2026年软路由系统分析和推荐，涵盖openwrt系、ikuai、RouterOS的对比与分支选择建议。</summary>
      <content type="html"><![CDATA[<section><h2>2026年软路由系统分析和推荐<a href="#2026年软路由系统分析和推荐"><span>#</span></a></h2><p>截止到目前为止,还在持续维护更新的软路由系统有<code>openwrt</code>系,<code>ikuai</code>,<code>RouterOS</code>.</p><section><h3>三大系统简述<a href="#三大系统简述"><span>#</span></a></h3><ul>
<li><strong>ikuai系统</strong>不开源,可玩性和软件生态上比openwrt和RouterOS差,但是稳定,适用于对稳定性和兼容性要求比较高的工作环境，比如说企业.</li>
<li><strong>Routeros系统</strong>是开源的,基于linux,但是软件生态和可玩性也不如openwrt,Linux内核版本也较低,主打的也是偏稳定向的.</li>
<li><strong>openwrt系统</strong>是开源的,相当于阉割魔改版之后的linux（linux可以干的,openwrt通过补全环境和其他方法，基本都可以干,如docker,php,python等）,且积极更新Linux内核和对应s组件依赖,社区力量和软件包是远远大于另外两个软路由系统的,由于openwrt系统非常轻量，所以适配的硬件特别广.而另外两个软路由系统对硬件的要求多相对较高,而openwrt各种嵌入式设备也可以刷入,很多厂商的硬路由openwrt也会适配.所以对于个人玩家来说,强烈推荐openwrt系.</li>
</ul></section><section><h3>openwrt系分支<a href="#openwrt系分支"><span>#</span></a></h3><p>而openwrt系又主要分<code>openwrt官方系统</code>,<code>immortalwrt</code>,<code>kwrt</code>,<code>lead</code>,<code>x-wrt</code>,<code>istoreos</code></p><section><h4>openwrt官方系统:<a href="#openwrt官方系统"><span>#</span></a></h4><p>是所有其他分支的源头，更新速度最快,但是软件包源服务器是在国外，国内用户无法直接更新里面的软件包,而且官方服务器里面的软件包数量很少.</p></section><section><h4>immortalwrt:<a href="#immortalwrt"><span>#</span></a></h4><p>基于openwrt源代码轻量改动,可以看成openwrt中国特供版,而且极大的丰富了软件包数量,有很多github上面的第三方的软件包，以及把软件包服务器改成了在国内,还有一些其他的优化以及添加了更多的硬件支持,但是每当openwrt新的版本出来,immortalwrt需要过一小段时间才能适配完</p></section><section><h4>kwrt:<a href="#kwrt"><span>#</span></a></h4><p>基于openwrt,可以看成在openwrt源代码的基础上，自己写了一套代码流程来自定义一些东西,有自己自建的软件仓库,里面软件的数量是openwrt系里面最多的,构建系统镜像的时候还可以提前自定义后台IP地址，宽带账号之类的东西,但是魔改的比较多，特别容易出bug（比如我的r2s固件直接开不了机）,而且这个系统作者的名声在圈内不太好,合并上游系统速度还算及时(immortal需要一点时间)</p></section><section><h4>lede:<a href="#lede"><span>#</span></a></h4><p>准确来说是lean的lede,最原始的lede是openwrt里面分出来，后面又重新与openwrt合并,而github上面的lede是lean(作者)基于最后的lede源代码的个人维护版本,也就是说基于的openwrt主线版本已经十分老旧了,这个系统的优点是有一些闭源驱动，性能非常强大,但由于已经和新版的openwrt代码相差很大，所以很多软件包已经不兼容了.</p></section><section><h4>x-wrt:<a href="#x-wrt"><span>#</span></a></h4><p>基于openwrt的主线分支(不是release分支),所以代码更新速度很快,有自己的软件仓库，国内用户可以直连,相对于官方的系统，已经内置集成的一些功能</p></section><section><h4>istoreos:<a href="#istoreos"><span>#</span></a></h4><p>基于openwrt以前的分支,一般不会基于最新的主线分支，一般会隔一两个大版本,有自己的软件源，国内用户可直接连,主要就是ui和功能改动,openwrt官方系统或其他分支系统也可以安装istoreos的软件包来实现成istoreos的ui和功能.主打的是将路由器和nas结合，里面内置了很多跟nas有关的功能。</p></section></section><section><h3>结论<a href="#结论"><span>#</span></a></h3><p>综上所述目前软路由系统，我个人推荐openwrt系,openwrt系里面结合更新速度和软件包数量以及稳定性,lede基于的主线版本太老,istoreos更新上游的速度较慢,kwrt魔改openwrt有点多(不是基于源代码魔改),稳定性较差.x-wrt特色不是很强,所以我个人综合起来更推荐immortalwrt</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/llm-tool-calls/</id>
      <title type="text">方案分析 | 我对几种LLM扩展工具的区别与使用心得</title>
      <published>2026-02-02T00:00:00.000Z</published>
      <updated>2026-02-02T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/llm-tool-calls/"/>
      <summary type="text">从“LLM 只能决策不能执行”出发，对 Tool Calls、MCP、Skills 三种扩展方式的调用链路、桥接成本与上下文风险做对比，并给出适用场景与个人取舍。</summary>
      <content type="html"><![CDATA[<section><h2>关于LLM的几种工具扩展方法的区别和见解<a href="#关于llm的几种工具扩展方法的区别和见解"><span>#</span></a></h2><p>LLM本身只能当做<strong>聊天对话机器</strong>,他只可以接收对话，然后返回回答,本身是<strong>不能干任何额外的事情</strong>.</p><p>我们使用ai的一些扩展功能，比如联网搜索和其他的工具本质上是让<strong>AI返回指定参数</strong>,<strong>交由外部工具去完成</strong>,然后在拿<strong>外部工具执行到的信息作为上下文</strong>,再返回给AI,<strong>llm只是决策者而并非执行者</strong>,<strong>工具真正的逻辑执行部分和判断部分需要人为的完成</strong>.</p><p>而市面上主流的LLM主要有以下几种方法扩展工具</p><section><h3>1.Tool Calls(很少使用)<a href="#1tool-calls很少使用"><span>#</span></a></h3><p>这个是一些大语言模型本身自带的功能,可以让用户<strong>自定义一段工具介绍</strong>，然后让LLM在适当的时候<strong>返回工具调用的结构体</strong>,然后由用户<strong>解析结构体执行程序</strong>之后，再返回获取到的结果来完善上下文</p><p><strong>缺点:</strong>
LLM和工具的衔接部分<strong>需要自己人为书写</strong>,比如需要<strong>判断LLM有没有返回工具调用信息</strong>,如果返回了，要根据返回的信息进行选择调用,<strong>写的工具越复杂</strong>，衔接部分需要花费的时间就越大,并且不同的模型的Tool Calls返回的信息可能还有所不同，<strong>不同模型的Tool Calls标准不一样，同一个工具需要对不同的模型单独做适配</strong>,所以并不流行(不过算是最早的给llm扩展工具的方法)</p></section><section><h3>2.Mcp(流行)<a href="#2mcp流行"><span>#</span></a></h3><p>这是一套<strong>严格的标准</strong>,不会像Tool Calls一样可能因为模型的问题，返回的结构体有所差异,有<strong>mcp客户端</strong>,还有<strong>mcp服务器</strong>,工具的连接方式有<strong>远程</strong>，也有<strong>本地调用</strong>,是一套很完善的规则.</p><p><strong>缺点:</strong>
因为<strong>调用流程过于严谨和繁琐</strong>,所以很容易引起<strong>llm上下文直接爆炸</strong>,让llm抓不到重点,对话越到后面效果越差。<strong>部署和开发也相对麻烦</strong>,因为有服务端，客户端，还有一些七七八八.而Tool Calls,可以直接在一个片段代码里面完成开发.</p></section><section><h3>3.Skills(流行)<a href="#3skills流行"><span>#</span></a></h3><p>这个是最近claude推出,我对他的理解是<strong>最高级别的prompt</strong>,这个严格来说<strong>并不是一个标准,而是共识</strong>,更偏向于一个技术理念,<strong>需要IDE自己去实现这个理念（<a href="https://github.com/anthropics/skills" target="_blank">Claude开源了他们官方skills的实现</a>）</strong>,不像mcp和Tool Calls是有一定的标准的,要严格按照官方的标准来。</p><p>Skills某种程度上来说和Tool Calls有点类似,但是<strong>他不需要写LLM和工具之间的桥接代码</strong>,并且还可以完成一些prompt才可以做的事，比如说<strong>自定义一下语气和注意事项</strong>.</p><p>Tool Calls每写一个工具，我需要自己再写程序去判断LLM返回的工具调用信息，然后再去执行运算(并且还可能因为不同的模型返回的信息有差异而导致兼容性问题),<strong>而放到Skills</strong>,我只需要<strong>自定义一段话</strong>,<strong>不需要写具体的代码</strong>(工具本身是要写代码的，只是<strong>调用不需要写代码</strong>),比如在特定的时候执行某个文件目录下面的某个代码片段传入某些参数,相当于把<strong>桥接部分交由了程序本身</strong>(如Vscode和windsurf)去执行,所以说需要程序去适配Skills,<strong>它是共识和理念，而不是一套标准</strong>.(不过当流行起来的时候，共识理念最终也会成为大家默认的标准)</p></section></section>
<section><h2>总结<a href="#总结"><span>#</span></a></h2><p>总结&lt;以上三种给llm扩展工具的方式&gt;,我个人对未来最看好的是<strong>Skills</strong>,我认为把这套逻辑和共识再进行优化加强是完全可以比过Tool Calls和mcp。Tool Calls桥接部分比较麻烦,<strong>一切运转都要依靠代码</strong>,还有<strong>兼容性问题</strong>;mcp<strong>开发部署繁琐,且容易引起上下文爆炸</strong>,LLM运行缓慢.Skills<strong>恰好能解决这两者的缺点</strong></p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/hugo-add-lastmod-to-posts/</id>
      <title type="text">学习笔记 | Hugo自动修正文章最后修改时间的最佳方案</title>
      <published>2026-01-25T00:00:00.000Z</published>
      <updated>2026-01-25T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/hugo-add-lastmod-to-posts/"/>
      <summary type="text">面向 Serverless/CI 自动部署场景，分析 Hugo 获取 lastmod 的几种方式（:filemodtime/:git/lastmod/date）的坑点，并给出“用 Git 提交时间回写文件时间戳 + :filemodtime”的稳定方案与脚本思路。</summary>
      <content type="html"><![CDATA[<section><h1>Hugo 全自动修正文章修改时间的最佳实践（Serverless 场景）<a href="#hugo-全自动修正文章修改时间的最佳实践serverless-场景"><span>#</span></a></h1><p>在使用 Hugo 构建静态博客时，<strong>文章的修改时间（Last Modified Time）</strong> 是一个非常重要但又容易踩坑的问题，尤其是在 <strong>Serverless / CI 自动部署</strong> 场景下。</p><p>Hugo 官方目前提供了多种用于确定文章修改时间的方式：</p><section><h2>Hugo 支持的修改时间来源<a href="#hugo-支持的修改时间来源"><span>#</span></a></h2><ul>
<li>
<p><strong><code>:filemodtime</code></strong><br />
使用文件本身的最后修改时间作为文章的修改时间。</p>
</li>
<li>
<p><strong><code>lastmod</code></strong><br />
需要在文章 Front Matter 中手动维护 <code>lastmod</code> 字段。</p>
</li>
<li>
<p><strong><code>:git</code></strong><br />
使用 Git 中该文件的<strong>最后一次提交时间</strong>作为修改时间（依赖 <code>gitinfo</code>）。</p>
</li>
<li>
<p><strong><code>date</code></strong><br />
使用文章的发布时间作为修改时间。</p>
</li>
</ul><hr /></section><section><h2>各方案的自动化可行性分析<a href="#各方案的自动化可行性分析"><span>#</span></a></h2><p>在上述方案中：</p><ul>
<li>
<p>✅ <strong>可自动化</strong></p>
<ul>
<li><code>:filemodtime</code></li>
<li><code>:git</code></li>
</ul>
</li>
<li>
<p>❌ <strong>不适合全自动</strong></p>
<ul>
<li><code>lastmod</code>（需要人工维护）</li>
<li><code>date</code>（语义不符）</li>
</ul>
</li>
</ul><hr /></section><section><h2> 在 Serverless 场景下的问题<a href="#-在-serverless-场景下的问题"><span>#</span></a></h2><p>使用 Hugo 的一个重要原因，正是为了 <strong>方便地将网站部署到 Serverless 平台</strong>（如 Cloudflare Pages、Vercel、Netlify 等）。</p><p>然而在 Serverless / CI 场景中：</p><ul>
<li>每次部署都会重新 <code>git clone</code> 仓库</li>
<li>所有文件的 <strong>文件系统修改时间</strong> 都会变成「拉取时间」</li>
<li>这会导致：
<ul>
<li>所有文章的修改时间被错误地更新为“本次部署时间”</li>
<li>完全失去文章真实修改时间的意义</li>
</ul>
</li>
</ul><p>因此，<strong><code>:filemodtime</code> 在默认情况下并不适合 Serverless 场景</strong>。</p><hr /></section><section><h2> 方案的现实问题<a href="#-方案的现实问题"><span>#</span></a></h2><p>理论上，<code>:git</code> 是最理想的方案：</p><ul>
<li>修改时间与 Git 提交严格一致</li>
<li>完全自动化</li>
<li>不依赖文件系统时间戳</li>
</ul><p>但在实际使用中：</p><ul>
<li>即使开启了 <code>enableGitInfo = true</code></li>
<li>某些情况下 <code>:git</code> <strong>无法正确生效</strong></li>
<li>在 CI / Serverless 环境中尤为明显</li>
</ul><p>不确定这是环境限制、Git shallow clone、还是 Hugo 本身的 Bug，但结果是：<strong>该方案在实践中不够稳定</strong>。</p><hr /></section><section><h2>最终解决方案：修改文件时间戳为Git 提交时间 + <a href="#最终解决方案修改文件时间戳为git-提交时间--"><span>#</span></a></h2><p>综合以上问题，我最终总结出了一套<strong>最适合 Serverless 管理的全自动方案</strong>：</p><section><h3>核心思路<a href="#核心思路"><span>#</span></a></h3><blockquote><p><strong>在 Hugo 构建之前，将文件的修改时间戳“伪装”为 Git 最后提交时间。</strong></p></blockquote><p>具体流程如下：</p><ol>
<li>Hugo 中使用 <code>:filemodtime</code> 作为文章修改时间来源</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[frontmatter]</span></div></div><div><div><div>2</div></div><div><span>lastmod = [':fileModTime']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li>将脚本放在文件夹根目录</li>
<li>将自动执行脚本的命令和正式部署命令合二为一(<strong>这里需要注意的是一般的serverless平台拉取项目只是浅层克隆不会把文件的最后提交信息给拉取过来，所以我们要在最前面要执行完整的拉取命令。</strong>)</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>git fetch --unshallow &amp;&amp; chmod +x 1.sh &amp;&amp; ./1.sh &amp;&amp; hugo</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li>Serverless平台设置环境变量TZ为Asia/Shanghai(不然的话，一般Serverless平台默认应该是美国的时间,会影响构建出来的网站的文章的最后修改日期的准确性)</li>
</ol><p>这样就可以做到：</p><ul>
<li>✅ 修改时间与 Git 提交时间一致</li>
<li>✅ 完全自动化</li>
<li>✅ 与 Serverless / CI 环境完美兼容</li>
</ul><hr /></section></section><section><h2>自动同步 Git 提交时间到文件修改时间的脚本<a href="#自动同步-git-提交时间到文件修改时间的脚本"><span>#</span></a></h2><p>下面的脚本用于：</p><blockquote><p><strong>将当前目录下所有文件的最后修改时间，统一修改为对应的 Git 最后提交时间</strong></p></blockquote><div><div><div><figure><figcaption><span>git-set-filetime.sh</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span># （此处放置你的脚本内容）</span></div></div><div><div><div>2</div></div><div><span>#!/bin/bash</span></div></div><div><div><div>3</div></div><div><span># 功能：将 git 仓库中所有被追踪文件的修改时间改为最后一次提交时间</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 确保当前目录是 git 仓库</span></div></div><div><div><div>6</div></div><div><span>if</span><span><span> [ </span><span>!</span><span> </span><span>-d</span><span> </span></span><span>".git"</span><span> ]; </span><span>then</span></div></div><div><div><div>7</div></div><div><span>    </span><span>echo</span><span> </span><span>"Error: 当前目录不是 git 仓库根目录"</span></div></div><div><div><div>8</div></div><div><span>    </span><span>exit</span><span> </span><span>1</span></div></div><div><div><div>9</div></div><div><span>fi</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># 获取所有被追踪文件（包含特殊字符/空格/中文）</span></div></div><div><div><div>12</div></div><div><span>git</span><span> </span><span>ls-files</span><span> </span><span>-z</span><span> | </span><span>while</span><span> </span><span>IFS</span><span><span>=</span><span> </span></span><span>read</span><span> </span><span>-r</span><span> </span><span>-d</span><span> </span><span>''</span><span> </span><span>file</span><span>; </span><span>do</span></div></div><div><div><div>13</div></div><div><span>    </span><span># 获取最后一次提交的 Unix 时间戳</span></div></div><div><div><div>14</div></div><div><span>    </span><span>timestamp</span><span><span>=</span><span>$(</span></span><span>git</span><span> </span><span>log</span><span> </span><span>-1</span><span> </span><span>--format=</span><span>"%ct"</span><span> </span><span>--</span><span> </span><span>"</span><span>$file</span><span>"</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span><span> [ </span><span>-n</span><span> </span></span><span>"</span><span>$timestamp</span><span>"</span><span> ]; </span><span>then</span></div></div><div><div><div>17</div></div><div><span>        </span><span># 使用 touch 修改文件修改时间</span></div></div><div><div><div>18</div></div><div><span>        </span><span># -d "@$timestamp" 表示从 Unix 时间戳设置时间</span></div></div><div><div><div>19</div></div><div><span>        </span><span>touch</span><span> </span><span>-d</span><span> </span><span>"@</span><span>$timestamp</span><span>"</span><span> </span><span>"</span><span>$file</span><span>"</span></div></div><div><div><div>20</div></div><div><span>        </span><span>echo</span><span> </span><span>"Updated: </span><span>$file</span><span> -&gt; $(</span><span>date</span><span> </span><span>-d</span><span> "@</span><span>$timestamp</span><span>" '+%Y-%m-%d %H:%M:%S')"</span></div></div><div><div><div>21</div></div><div><span>    </span><span>else</span></div></div><div><div><div>22</div></div><div><span>        </span><span>echo</span><span> </span><span>"Skipped (not tracked): </span><span>$file</span><span>"</span></div></div><div><div><div>23</div></div><div><span>    </span><span>fi</span></div></div><div><div><div>24</div></div><div><span>done</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>echo</span><span> </span><span>"All tracked files' modification times updated."</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/ai-understanding/</id>
      <title type="text">随笔杂谈 | 谈谈我对AI的一些理解</title>
      <published>2025-12-09T00:00:00.000Z</published>
      <updated>2025-12-09T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/ai-understanding/"/>
      <summary type="text">结合个人使用大模型的经历，聊聊我对AI的理解：AI是提升效率的工具而非核心，关键在于掌握AI而不是依赖AI。</summary>
      <content type="html"><![CDATA[<section><h2>前言:<a href="#前言"><span>#</span></a></h2><p>用ai也用了这么久了,今天想谈一下自己对AI的理解.从chatgpt刚问世开始,我就在开始使用大模型.也算是最早使用大模型的那批人.随着大模型的发展愈演愈烈,也衍生出了很多分支比如<strong>AI IDE</strong>,应用内集成AI聊天助手,大模型识别图像等等.</p><p>而随着AI能干的事越来越多,有些人产生了<strong>自我怀疑</strong>(我能做的事AI都能做,那还需要我干什么),而另外一部分人则产生了<strong>依赖</strong>(你看ai都是这么说的了(盲目相信AI)).</p><p>而我也用大模型用了这么久了,今天便在这里谈一下我自己的认知</p></section>
<section><h2>我的认知<a href="#我的认知"><span>#</span></a></h2><p>我始终认为<strong>Ai始终是作为一个工具,用来提升工作效率,而非核心</strong>.大模型本身的特点就注定了有一些工作是无法代替人类.比如<strong>项目的一些宏观设计和管理</strong>,一些最新的技术的使用和获取.它是一个超大知识的集合体,它的作用是降低我在知道该怎么做的情况下，让我避免去查代码文档，一步一步慢慢实现功能所带来的时间成本.而不是想做什么，跟AI说一句话，让他帮我实现就行了,我们要做的是工程师，是设计师，而不是让AI作为核心，我们负责提要求。不然就相当于我们是甲方，AI是乙方。<strong>我们应该当的是boss，是甲方,AI当我们底下的员工，来负责完成我设计的东西（乙方需求）。</strong></p></section>
<section><h2>所以在我看来,使用AI无非就两个档次<a href="#所以在我看来使用ai无非就两个档次"><span>#</span></a></h2><ul>
<li>初级者&lt;什么都不懂&gt;,只知道自己想要什么,反正让AI完成就行了。(这在学习的初期确实可以很快带来成就感,但始终无法避免一个问题,<strong>你不懂的始终不懂，懂的是AI而不是你</strong>,你并没有任何的核心竞争力)</li>
<li>真正掌握AI的人&lt;自己设置架构&gt;，自己设置语法要求,自己设置UI层级和UI设计风格,规定使用的技术栈,让AI按照自己的规定去完成对应的工作。<strong>AI是提升效率的工具，而不是自己的大脑</strong>,真正应该怎么做，应该是自己知道，而不是AI知道。</li>
</ul></section>
<section><h2>所以在这个时代,有人害怕ai,有人依赖AI,那我的答案是掌握AI.<a href="#所以在这个时代有人害怕ai有人依赖ai那我的答案是掌握ai"><span>#</span></a></h2></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/Android-root/</id>
      <title type="text">方案分析 | Android Root 的演进与抉择</title>
      <published>2025-12-09T00:00:00.000Z</published>
      <updated>2025-12-09T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/Android-root/"/>
      <summary type="text">从技术路线与时间线梳理 Android Root 的演进：用户态/内核态/漏洞 Root 的差异与适用条件，并对 KingRoot、SuperSU、Magisk、KernelSU、APatch 等方案做原理拆解与优缺点对比，给出选择建议。</summary>
      <content type="html"><![CDATA[<section><h2>Android Root 发展历程和方案分析<a href="#android-root-发展历程和方案分析"><span>#</span></a></h2><p>Android Root 顾名思义即给安卓系统获取根权限，让用户拥有系统级权限，极大提升系统可玩性。<br />
一般来说，Root 需要先解锁 bootloader（BL），解锁的目的，是让用户有权限刷写各个分区，从而能：</p><ul>
<li>更换系统（system / vendor / product 等）</li>
<li>更换内核（boot ）</li>
<li>注入 Root / 模块方案</li>
</ul><p><strong>注意：解锁 BL ≠ 必然 Root</strong>，也可以只刷机不 Root。<br />
主流 Root 方案按时间大致可以排成这样：</p><blockquote><p><strong>KingRoot → SuperSU → Magisk → SKRoot（小众） → KernelSU → APatch</strong></p></blockquote><hr /><section><h3>一、Root 实现思路：按技术路线分类<a href="#一root-实现思路按技术路线分类"><span>#</span></a></h3><section><h4>用户态 Root（User-space Root）<a href="#用户态-rootuser-space-root"><span>#</span></a></h4><ul>
<li>不直接修改内核代码</li>
<li>修改 <strong>boot.img → ramdisk → init 脚本</strong>，在早期启动阶段插入自己的用户态 root 程序</li>
<li>通过 <strong>用户态守护进程</strong> 管理权限（例如 <code>magiskd</code>）</li>
<li>一般依赖一个 <strong>管理 App</strong> 配合使用（授权弹窗、模块管理等）</li>
<li><strong>代表方案：Magisk</strong></li>
<li><strong>优点</strong>：对内核无强依赖，兼容面最广</li>
<li><strong>缺点</strong>：本质不在内核，某些内核安全策略/厂商定制下会有局限</li>
</ul></section><section><h4>内核态 Root（Kernel-space Root）<a href="#内核态-rootkernel-space-root"><span>#</span></a></h4><ul>
<li>直接在内核中 <strong>Hook 权限相关函数 / LSM / syscall</strong></li>
<li>在内核层修改 <code>cred</code>、<code>capabilities</code> 等结构，获得最高权限</li>
<li>部分方案需要内核源码（早期 KernelSU），部分不需要（APatch、SKRoot）</li>
<li><strong>代表方案：KernelSU、APatch、SKRoot</strong></li>
<li><strong>优点</strong>：权限层级最高，可做更“隐蔽”和细粒度的控制</li>
<li><strong>缺点</strong>：与内核版本/厂商定制强相关，适配成本较高</li>
</ul></section><section><h4>漏洞 Root（Exploit-based Root）<a href="#漏洞-rootexploit-based-root"><span>#</span></a></h4><ul>
<li>利用系统或内核漏洞临时/半永久获取 Root</li>
<li>通常依赖特定 Android / 内核版本，系统一更新就失效</li>
<li><strong>代表方案：KingRoot 等一键 Root 工具</strong></li>
<li><strong>优点</strong>：用户体验简单，历史上对锁 BL 的机器也有机会</li>
<li><strong>缺点</strong>：完全吃漏洞红利，维护性差，安全风险高</li>
</ul><hr /></section></section><section><h3>二、按时间线看典型方案<a href="#二按时间线看典型方案"><span>#</span></a></h3><section><h4>KingRoot：漏洞驱动的早期 Root<a href="#kingroot漏洞驱动的早期-root"><span>#</span></a></h4><ul>
<li>主要活跃在 <strong>Android 4.4 ~ 6.0</strong> 时代</li>
<li>通过 <strong>内核 / 系统漏洞</strong>（如提权漏洞）获得 Root</li>
<li>多为 <strong>临时或半永久 Root</strong>，重启或 OTA 后经常失效</li>
<li>随着漏洞不断被修复，这类方案很快被淘汰</li>
</ul><p><strong>定位</strong>：<br />
完全基于漏洞的时代产物，如今更多是历史意义。</p><hr /></section><section><h4>SuperSU：system 分区注入 su 的经典方案<a href="#supersusystem-分区注入-su-的经典方案"><span>#</span></a></h4><ul>
<li>思路：直接修改 <code>system.img</code>，在 <code>/system/bin</code> 或 <code>/system/xbin</code> 注入 <code>su</code> 二进制</li>
<li>通过 <code>su</code> 的 setuid 机制实现提权</li>
<li>典型适用 <strong>Android 4.x ~ 6.x</strong>，system 还比较“好改”的时代</li>
<li>随着：
<ul>
<li>system 分区逐渐只读 / 受更严格完整性保护</li>
<li><strong>Magisk 引入 systemless Root</strong></li>
</ul>
</li>
</ul><p>SuperSU 很快失去优势。</p><p><strong>定位</strong>：<br />
传统“改 system.img 注入 su”的代表</p><hr /></section><section><h4><a href="https://github.com/topjohnwu/Magisk" target="_blank">Magisk</a>：用户态 systemless Root 的时代<a href="#magisk用户态-systemless-root-的时代"><span>#</span></a></h4><p>Magisk 把 Root 方式拉到了一个新高度。</p><section><h5>核心实现<a href="#核心实现"><span>#</span></a></h5><ul>
<li>修改 <strong>boot.img → ramdisk → init</strong>：
<ul>
<li>用 <code>magiskinit</code> 接管早期启动，作为“第一个用户态进程”</li>
<li>启动 root 守护进程 <code>magiskd</code></li>
</ul>
</li>
<li><code>/system/bin/su</code> 在 Magisk 中 <strong>只是前端/中转</strong>：
<ul>
<li>解析参数 → 连接 <code>magiskd</code> → 由 <code>magiskd</code> 以 root fork/exec 真正的命令</li>
<li><strong>提权的决策和执行</strong>在 <code>magiskd</code> 中完成，而不是在 <code>su</code> 本身</li>
</ul>
</li>
<li>不在 system 分区直接写入文件，做到 <strong>systemless</strong><br />
（实际上是用挂载/覆盖的方式“虚拟出”修改过的系统视图）</li>
</ul></section><section><h5>关键特点<a href="#关键特点"><span>#</span></a></h5><ul>
<li><strong>首创 magic mount 模块系统</strong>：
<ul>
<li>在不实际修改系统文件的情况下，实现对 <code>/system</code>、<code>/vendor</code> 等目录的文件覆盖</li>
<li>极大提升“玩机模块生态”</li>
</ul>
</li>
<li><strong>强兼容性</strong>：
<ul>
<li>只要能改 boot.img中的init脚本，大多数内核版本都能使用</li>
</ul>
</li>
<li><strong>依赖管理 App</strong>（Magisk App）：
<ul>
<li>管理模块、处理 su 授权弹窗、升级/卸载</li>
</ul>
</li>
</ul></section><section><h5>局限<a href="#局限"><span>#</span></a></h5><ul>
<li>纯用户态方案，需要搭配app</li>
<li>模块生态与内核态模块（如 APatch 的 KPM ）相比，在<strong>内核能力利用上</strong>略逊一筹</li>
<li>不支持kernelsu后来的模块webui功能,需要额外安装app解决</li>
</ul><hr /></section></section><section><h4><a href="https://github.com/abcz316/SKRoot-linuxKernelRoot" target="_blank">SKRoot</a>：早期“内核 Root + su 环境注入”的探索者<a href="#skroot早期内核-root--su-环境注入的探索者"><span>#</span></a></h4><ul>
<li>时间上早于目前主流内核 Root（KernelSU / APatch）</li>
<li><strong>无需内核源码</strong>：
<ul>
<li>离线分析目标内核镜像（ELF/镜像格式）</li>
<li>找到如 <code>do_execve</code> 等关键函数与 <code>task_struct/cred/seccomp</code> 的偏移</li>
<li>插入自定义 shellcode 实现内核级提权</li>
</ul>
</li>
<li>号称“<strong>隐藏性很强</strong>”：
<ul>
<li>没有模块系统</li>
<li>常见用法是：针对<strong>单个或少数进程</strong>注入 su 环境</li>
<li>通过 PATH / so 寄生等方式，让特定 APP 获得 su，而不是全局暴露</li>
</ul>
</li>
<li>适配范围：<strong>大致 3.10 ~ 6.6 内核</strong></li>
</ul><p><strong>优点</strong>：</p><ul>
<li>不依赖内核源码，适配范围相对广</li>
<li>su 环境可以“定向注入”，对其他进程很隐蔽</li>
</ul><p><strong>缺点</strong>：</p><ul>
<li>没有完整的模块系统，操作较繁琐</li>
<li>生态小众，文档和社区支持相对较弱</li>
</ul><hr /></section><section><h4><a href="https://github.com/tiann/KernelSU" target="_blank">KernelSU</a>：主流内核态 Root + overlayfs 模块<a href="#kernelsu主流内核态-root--overlayfs-模块"><span>#</span></a></h4><section><h5>基本定位<a href="#基本定位"><span>#</span></a></h5><ul>
<li>代表性的 <strong>内核态 Root</strong>：
<ul>
<li>在内核中安装 hook（LSM / cred 修改等）</li>
<li>在执行 <code>/system/bin/su</code> 时，内核直接修改进程 cred 实现提权</li>
</ul>
</li>
<li><code>/system/bin/su</code> 在 KernelSU 中是真正的 <strong>提权入口</strong>：
<ul>
<li>它触发内核 hook</li>
<li>提权是在内核里完成的，而不是转发给某个守护进程（区别于 Magisk）</li>
</ul>
</li>
</ul></section><section><h5>技术路径演进<a href="#技术路径演进"><span>#</span></a></h5><ul>
<li>早期：通过 <strong>谷歌GKI 内核（已被官方淘汰） / 内核源码集成（已被官方淘汰）</strong> 集成 KernelSU</li>
<li>现在主流：<strong>LKM（ko 模块）方式</strong>：
<ul>
<li>把修改封装成内核模块加载进 GKI 内核</li>
<li>同一大版本Gki内核编译出来的ko模块具有一定通用性,所以安装体验上不需要内核源代码</li>
<li>但 <strong>ko 的编译仍依赖内核的源码</strong>，只是这部分由项目方/维护者完成</li>
<li>对用户体验来说“不需要自己改源码”</li>
</ul>
</li>
</ul></section><section><h5>模块系统：元模块（支持overlyfs/magic mount或者自定义挂载方式）<a href="#模块系统元模块支持overlyfsmagic-mount或者自定义挂载方式"><span>#</span></a></h5><ul>
<li>默认使用 <strong>overlayfs</strong> 实现模块系统(首创)：
<ul>
<li>支持对 <code>/system</code> 等只读分区做“上层覆盖”(模块安装后通常需要 <strong>重启才能生效对/system的修改</strong>)</li>
</ul>
</li>
<li>对比 Magisk 的 magic mount：
<ul>
<li>overlayfs 是内核特性，语义更标准</li>
<li>但实时性稍差，多数变更需要重启</li>
</ul>
</li>
</ul></section><section><h5>特点与限制<a href="#特点与限制"><span>#</span></a></h5><ul>
<li>
<p><strong>特点</strong>：</p>
<ol>
<li>内核态 su：安全模型清晰，可做细粒度 App profile（按 UID/GID 控制权限大小）</li>
<li>官方overlayfs 模块系统：结构正规，可与 GKI 思路统一</li>
</ol>
</li>
<li>
<p><strong>限制</strong>：</p>
<ul>
<li>对低版本 / 非 GKI 内核兼容性差</li>
<li>一些魔改了内核且不开源的gki设备对谷歌官方gki内核支持差,刷了可能开不了机</li>
<li>官方更推荐搭配 <strong>5.10以上的 GKI 内核搭配ko模块使用</strong></li>
</ul>
</li>
</ul><hr /></section></section><section><h4><a href="https://github.com/bmax121/APatch" target="_blank">APatch</a>：无需源码的 kpimg 内核 Root + KPM 模块<a href="#apatch无需源码的-kpimg-内核-root--kpm-模块"><span>#</span></a></h4><p>在内核 Root 方案中，APatch 的技术路线是目前<strong>最“工程化 + 通用”的一类</strong>。</p><section><h5>核心实现方式<a href="#核心实现方式"><span>#</span></a></h5><ul>
<li>仍然是 <strong>内核态 Root</strong>，但：
<ul>
<li><strong>不需要内核源码</strong></li>
<li>使用 KernelPatch 工具链解析并 patch 目标内核镜像</li>
</ul>
</li>
<li>通过在内核镜像中注入 <strong><code>kpimg</code></strong>：
<ul>
<li>修改内核启动入口 / early init 流程</li>
<li>把自定义 hook 注入到内核的启动过程</li>
<li>重启后，<code>kpimg</code> 会在早期阶段接管部分逻辑，实现：
<ul>
<li>Root 提权</li>
<li>内核 hook（supercall / inline hook）</li>
<li>启动用户态守护/事件（<a>apd</a> 等）</li>
</ul>
</li>
</ul>
</li>
</ul></section><section><h5>兼容性<a href="#兼容性"><span>#</span></a></h5><ul>
<li>内核版本范围广：<strong>3.18 ~ 6.12</strong></li>
<li>不依赖 GKI，不要求厂商公开内核源码</li>
<li>对各家定制内核更友好（因为是直接对内核二进制镜像做符号分析与 patch）</li>
</ul></section><section><h5>特点<a href="#特点"><span>#</span></a></h5><ol>
<li>
<p><strong>KPM 内核模块</strong>：</p>
<ul>
<li>在已经被 kpimg 接管的内核里，动态加载 KPM 模块</li>
<li>支持对内核函数进行 hook，部分 hook 即时生效</li>
<li>无<code>/system/bin/su</code>文件，通过监听拦截<code>/system/bin/su</code>或<code>su</code>指令来实现提权</li>
</ul>
</li>
<li>
<p><strong>模块系统 + Lua 脚本</strong>：</p>
<ul>
<li>用户态 <a>apd</a> 管理模块目录、事件（post-fs-data / boot-completed 等）</li>
<li>最新版本中可以用 <strong>Lua</strong> 替代传统 shell 脚本，增强复杂逻辑可维护性</li>
</ul>
</li>
<li>
<p><strong>不依赖内核源码的工程化链路</strong>：</p>
<ul>
<li>用 <a>tools/kallsym.c</a>、<a>tools/patch.c</a> 等做符号解析、patch</li>
<li>用 <a>kernel/patch/android/user_init.sh</a> 等把 apd 接到系统启动阶段</li>
</ul>
</li>
<li>
<p>支持kernelsu的元模块挂载方案(2026.1.12补)</p>
</li>
</ol><hr /></section></section></section><section><h3>三、综合对比与结论<a href="#三综合对比与结论"><span>#</span></a></h3><section><h4>技术路线对比（简表）<a href="#技术路线对比简表"><span>#</span></a></h4>

<table><thead><tr><th>方案</th><th>类型</th><th>是否改内核代码</th><th>是否需Kernel源码</th><th>是否依赖 App</th><th>su 角色</th><th>模块系统</th></tr></thead><tbody><tr><td>KingRoot</td><td>漏洞 Root</td><td>否</td><td>否</td><td>需要</td><td>各家自定义</td><td>无 / 简单补丁</td></tr><tr><td>SuperSU</td><td>system 注入 su</td><td>否</td><td>否</td><td>可选</td><td>传统 setuid su</td><td>无</td></tr><tr><td>Magisk</td><td>用户态 Root</td><td>否（但改boot.img中的ramdisk）</td><td>否</td><td>强依赖</td><td><code>/system/bin/su</code>中转到 <code>magiskd</code></td><td>magic mount</td></tr><tr><td>SKRoot</td><td>内核 Root</td><td>是（patch 镜像）</td><td>否</td><td>有工具/库</td><td>隐藏 su + 进程定向注入</td><td>无完整模块系统</td></tr><tr><td>KernelSU</td><td>内核 Root</td><td>是（源码/GKI/LKM）</td><td>技术层面上需要</td><td>App 非必须</td><td><code>/system/bin/su</code> 触发内核提权</td><td>元模块</td></tr><tr><td>APatch</td><td>内核 Root</td><td>是（kpimg patch）</td><td>否</td><td>App 非必须</td><td>内核 hook su/execve 等</td><td>APM/KPM + Lua</td></tr></tbody></table></section><section><h4>目前最主流的三款root方案分析<a href="#目前最主流的三款root方案分析"><span>#</span></a></h4><ul>
<li>
<p><strong>Magisk</strong>：<br />
适合追求 <strong>广泛兼容 + 成熟模块生态</strong> 的用户，一般设备能解锁 boot.img 就能用。</p>
</li>
<li>
<p><strong>KernelSU</strong>：<br />
适合有 <strong>GKI 内核</strong>，且希望在内核层面细控 Root 权限（App profile）的人，<br />
更偏“官方化”的内核模块方案，但对旧机型支持有限
可以自定义挂载</p>
</li>
<li>
<p><strong>APatch</strong>：<br />
从技术理念上最“硬核”，不依赖源码、支持广泛内核版本，<br />
内核模块 hook + Lua 模块系统，对研究者 / 高级玩家非常友好。</p>
</li>
</ul></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/ai-ide/</id>
      <title type="text">方案分析 | 如何选择最适合你的 AI IDE 与扩展插件</title>
      <published>2025-12-08T00:00:00.000Z</published>
      <updated>2025-12-08T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/ai-ide/"/>
      <summary type="text">从使用场景与评价指标出发，梳理 CLI/IDE 两类 AI 编程工具，并对比国内外主流 AI IDE 的特点、限制与适用人群，给出个人的选型建议与推荐。</summary>
      <content type="html"><![CDATA[<section><h2>AI IDE 工具推荐<a href="#ai-ide-工具推荐"><span>#</span></a></h2><section><h3>为什么使用 AI IDE？<a href="#为什么使用-ai-ide"><span>#</span></a></h3><ul>
<li>传统 AI 聊天无法：
<ul>
<li>实时读取本地文件</li>
<li>兼顾项目的宏观框架管理</li>
</ul>
</li>
<li>偏商业化的 AI IDE：
<ul>
<li>一般不允许自定义第三方服务商接口</li>
<li>强制使用平台模型收取订阅费</li>
<li>实时读取本地文件</li>
<li>适配开发的执行流程与逻辑</li>
</ul>
</li>
</ul><p>因此，为了高效开发和项目管理，需要使用 AI IDE。</p><hr /></section><section><h3>AI IDE 的能力评价指标<a href="#ai-ide-的能力评价指标"><span>#</span></a></h3><ol>
<li><strong>硬实力：自带模型能力</strong></li>
<li><strong>软实力：上下文补全与逻辑能力</strong>
<ul>
<li>通过插件、执行策略、隐藏的上下文prompt 等实现</li>
</ul>
</li>
</ol><ul>
<li>两者缺一不可</li>
</ul><hr /></section><section><h3>AI 编程工具分类<a href="#ai-编程工具分类"><span>#</span></a></h3><section><h4>1. CLI 命令行AI开发工具<a href="#1-cli-命令行ai开发工具"><span>#</span></a></h4><ul>
<li>开发效率比 IDE 差
<ul>
<li>文件引用需手动输入</li>
</ul>
</li>
<li>不适合宏观项目管理</li>
<li>适合单一功能的改进</li>
<li>代表工具：
<ul>
<li><a href="https://openai.com/blog/openai-codex" target="_blank">OpenAI Codex★★★</a>(也有vscode 插件版本,但开发效率不如集成式ai ide,仍然是片段式更改)</li>
<li><a href="https://claude.com/product/claude-code" target="_blank">Claude Code★★★</a>(也有vscode 插件版本,但开发效率不如集成式ai ide,仍然是片段式更改)</li>
<li><a href="https://github.com/sst/opencode" target="_blank">OpenCode（开源）</a>（可自定义服务商接口，但上下文能力较弱）</li>
</ul>
</li>
</ul></section><section><h4>2. IDE AI 开发工具<a href="#2-ide-ai-开发工具"><span>#</span></a></h4><ul>
<li>直接从文件管理器拖入文件即可</li>
<li>支持项目级宏观管理</li>
</ul><hr /></section></section><section><h3>国内 AI IDE<a href="#国内-ai-ide"><span>#</span></a></h3><ul>
<li><strong><a href="https://www.trae.ai/" target="_blank">Trae</a></strong>
<ul>
<li>特有 Solo 模式（和普通模式感觉主要是 UI 差别，在功能上感觉差的不是很大，可能隐藏 prompt 有所优化）</li>
<li>可使用规定的国内其他服务商接口</li>
</ul>
</li>
<li><strong><a href="https://qoder.ai/" target="_blank">Qoder</a></strong>
<ul>
<li>无特色</li>
<li>上下文消耗快</li>
<li>无法使用国外模型或其他接口</li>
</ul>
</li>
<li><strong><a href="https://lingma.aliyun.com/" target="_blank">通义灵码</a></strong>
<ul>
<li>无特色</li>
<li>无法使用国外模型或其他接口</li>
</ul>
</li>
</ul><section><h4>国内 IDE 的共性<a href="#国内-ide-的共性"><span>#</span></a></h4><ul>
<li>喜欢构建工作区文件全量索引，每次提问前先查索引文件，消耗大量上下文 token，有时查找的文件过多反而忽略关键问题</li>
<li>国内模型编码能力较弱，且不允许使用国外更强的模型</li>
<li>方便支付订阅, 界面中文支持较好</li>
</ul><hr /></section></section><section><h3>国外 AI IDE<a href="#国外-ai-ide"><span>#</span></a></h3><ul>
<li>
<p><strong><a href="https://github.com/features/copilot" target="_blank">GitHub Copilot★★</a></strong></p>
<ul>
<li>VSCode 插件形式</li>
<li>适合片段式的文件</li>
<li>上下文补全能力较弱</li>
<li>无法全面定制化</li>
</ul>
</li>
<li>
<p><strong><a href="https://github.com/cline/cline" target="_blank">Cline★★</a>（开源）</strong></p>
<ul>
<li><strong>可自定义任意服务商</strong></li>
<li>VSCode 插件形式</li>
<li>适合片段式的文件</li>
<li>上下文补全能力较弱</li>
<li>无法全面定制化</li>
</ul>
</li>
<li>
<p><strong><a href="https://github.com/Kilo-Org/kilocode" target="_blank">Kilo</a>（开源）</strong></p>
<ul>
<li>可使用规定的第三方服务商api</li>
<li>VSCode 插件形式</li>
<li>适合片段式的文件</li>
<li>上下文补全能力较弱</li>
<li>无法全面定制化</li>
</ul>
</li>
<li>
<p><strong><a href="https://www.cursor.so/" target="_blank">Cursor★★★</a></strong></p>
<ul>
<li>基于 VSCode 深度定制</li>
<li>自研模型 Composer 1 (专为编程训练, 功能强大)</li>
<li>可使用规定的其他服务商 API</li>
<li>agent可以直接连接github远程储存库(特色功能)</li>
<li>预设 Prompt 优秀输出质量高，但免费额度少，订阅贵</li>
<li>亮眼功能：
<ul>
<li><strong>浏览器设计模式</strong>：可以直接在ide里面打开网页，并且选择元素，方便设计网页</li>
<li><strong>debug</strong>：可以根据log寻找隐藏bug</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong><a href="https://kiro.dev" target="_blank">kiro</a></strong>
电脑安装闪退，我用不了(哭)</p>
</li>
<li>
<p><strong><a href="https://windsurf.ai/" target="_blank">Windsurf★★★</a></strong></p>
<ul>
<li>基于 VSCode 深度定制</li>
<li>自研模型 SWE-1.5 (专为编程训练, 功能强大)</li>
<li>亮眼功能：
<ul>
<li><strong>Memories</strong>：可以根据对话和习惯生成对应的记忆（ai自动生成记忆，以在对话之间保持上下文连贯性）</li>
<li><strong>DeepWiki</strong>：独家功能,定位项目中关键函数或变量，了解用途和在项目其他地方引用 (方便学习他人的开源项目)</li>
<li><strong>CodeMap</strong>：独家功能,用于分析现有项目代码结构,可以根据输入的问题生成项目代码实现的流程图,点击流程图可以定位对应代码实现的部分（对于解剖他人项目的功能实现很有用）</li>
<li><strong>浏览器设计模式</strong>：可以直接在ide里面打开网页，并且选择元素，方便设计网页</li>
<li><strong>对话消息互通</strong>：独家功能,不同对话之间可以选择互通消息，如两个不同的项目,具有一定的关联性，我们就可以通过这个功能实现两个项目在对话层面上的互通，而不需要ai去研究另外一个项目的代码，防止添加过多的内容，导致上下文混乱</li>
</ul>
</li>
<li>免费额度高，价格便宜</li>
<li>支持规定的其他服务商 API</li>
</ul>
</li>
<li>
<p><strong><a href="https://zed.dev" target="_blank">Zen Editor</a>（开源）</strong></p>
<ul>
<li>可使用任意服务商接口</li>
<li>自带免费额度高</li>
<li>上下文补全能力较弱</li>
</ul>
</li>
</ul><hr /></section><section><h3>总结推荐<a href="#总结推荐"><span>#</span></a></h3><ul>
<li>综合价格、模型能力和上下文补全能力，我个人首推 <strong><a href="https://windsurf.ai/" target="_blank">Windsurf</a></strong></li>
<li>DeepWiki、CodeMap 和消息互通是其最大亮点，让其上下文补全能力非常强大</li>
<li>其他 AI IDE 在 UI 功能上同质化严重，差别主要在 AI 模型、执行逻辑和预设 Prompt</li>
<li>如果不考虑开发效率，单论编码能力,openai的codex和claude code这种命令行式开发可能是最强的一批,其次就是cursor和windsurf</li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/android-gki-kernel-compile/</id>
      <title type="text">学习笔记 | GKI 内核不完全编译实战</title>
      <published>2025-12-05T00:00:00.000Z</published>
      <updated>2025-12-05T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/android-gki-kernel-compile/"/>
      <summary type="text">以 GKI 5.10+ 为例，记录 Android 内核“不完全编译”的完整流程：环境依赖、编译链选择、编译命令到 AnyKernel3 打包刷入，并附常见坑与排查思路。</summary>
      <content type="html"><![CDATA[<section><h2>Android内核编译<a href="#android内核编译"><span>#</span></a></h2><p><strong>以编译5.10以上的内核为例,所需资源:</strong></p><ol>
<li>一台配置较好的电脑</li>
<li>linux系统</li>
<li>内核源代码</li>
<li>编译链工具</li>
</ol><section><h3>linux系统<a href="#linux系统"><span>#</span></a></h3><ul>
<li>各大Linux系统(Ubuntu系/arch系/fedora系/Wsl2)都可以，网上教程多以Ubuntu lts版本为例(<strong>便于找依赖和教程</strong>)</li>
</ul></section><section><h3>内核源代码<a href="#内核源代码"><span>#</span></a></h3><p>从github上搜索自己机型的内核源代码<br />
如<code>https://github.com/Evolution-X-Devices/kernel_xiaomi_sm8450</code>(<strong>项目名一般采用   kernel_厂家名_芯片组代号   的形式,5.10之后都是gki内核,所以内核名称基本是以芯片组命名如sm8450,有些项目也可能是按机型代号命名，而不是芯片组</strong>)</p></section><section><h3>编译链工具<a href="#编译链工具"><span>#</span></a></h3><p>5.10以上的内核一般采用纯clang/llvm进行编译(低版本内核也可尝试，但有可能能成功编译但会开不了机)</p><section><h4>Clang/llvm<a href="#clangllvm"><span>#</span></a></h4><p>Clang 一般情况下采用的是<a href="https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/+/refs/heads/main" target="_blank">google</a>的正式分支,也可以采用<a href="https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/+/refs/heads/mirror-goog-main-prebuilts" target="_blank">测试分支</a></p></section></section><section><h3>编译过程<a href="#编译过程"><span>#</span></a></h3><section><h4>更新环境依赖<a href="#更新环境依赖"><span>#</span></a></h4><p>以Ubuntu24.04为例,执行以下命令安装依赖</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>apt-get</span><span> </span><span>install</span><span> </span><span>git-core</span><span> </span><span>gnupg</span><span> </span><span>flex</span><span> </span><span>bison</span><span> </span><span>build-essential</span><span> </span><span>zip</span><span> </span><span>curl</span><span> </span><span>zlib1g-dev</span><span> </span><span>gcc-multilib</span><span> </span><span>g++-multilib</span><span> </span><span>libc6-dev-i386</span><span> </span><span>libncurses5</span><span> </span><span>lib32ncurses5-dev</span><span> </span><span>x11proto-core-dev</span><span> </span><span>libx11-dev</span><span> </span><span>lib32z1-dev</span><span> </span><span>libgl1-mesa-dev</span><span> </span><span>libxml2-utils</span><span> </span><span>xsltproc</span><span> </span><span>tar</span><span> </span><span>xz-utils</span><span> </span><span>cpio</span><span> </span><span>findutils</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>克隆内核源代码和编译链工具<a href="#克隆内核源代码和编译链工具"><span>#</span></a></h4><p>内核:<code>git clone https://github.com/Evolution-X-Devices/kernel_xiaomi_sm8450.git --depth=1</code><br />
编译链工具:<code>git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86.git --depth=1</code><br />
<strong>depth=1</strong>只拉取文件，不拉取历史提交，可以极大减少下载时间<br />
如果想拉取其他的测试分支，也可以在后面加<code>-b 测试分支名字</code><br />
如拉取测试版的编译链则添加<code>-b mirror-goog-main-prebuilts</code></p></section><section><h4>设置环境变量<a href="#设置环境变量"><span>#</span></a></h4><p>注:<code>以下操作需要在同一个终端环境下执行，因为添加的变量是临时变量</code><br />
此时在你的工作目录下面应该会有两个文件夹，一是<code>内核文件夹</code>，二是<code>编译链工具文件夹</code><br />
打开终端,执行<code>cd 编译链工具文件夹路径</code>,当前目录下应该会有若干个<code>clang-rxxxx</code>子文件夹，选择后缀数字最大的最新版本即可,将它下面的/bin添加到环境变量中</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>PATH</span><span>=</span><span>"</span><span>$PATH</span><span>:/../../编译链工具文件夹/clang-rxxxx/bin"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后再<code>cd 内核源代码文件夹</code><br />
设置编译环境变量</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>ARCH</span><span>=</span><span>"arm64"</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>SUBARCH</span><span>=</span><span>"arm64"</span></div></div><div><div><div>3</div></div><div><span>export</span><span> </span><span>CC</span><span>=</span><span>"clang"</span></div></div><div><div><div>4</div></div><div><span>export</span><span> </span><span>LLVM</span><span>=</span><span>1</span></div></div><div><div><div>5</div></div><div><span>export</span><span> </span><span>LLVM_IAS</span><span>=</span><span>1</span></div></div><div><div><div>6</div></div><div><span>export</span><span> </span><span>HOSTCC</span><span>=</span><span>clang</span></div></div><div><div><div>7</div></div><div><span>export</span><span> </span><span>LD</span><span>=</span><span>ld</span><span>.</span><span>lld</span></div></div><div><div><div>8</div></div><div><span>export</span><span> </span><span>CLANG_TRIPLE</span><span>=</span><span>"aarch64-linux-gnu-"</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>LTO</span><span>=</span><span>thin</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>执行编译命令<a href="#执行编译命令"><span>#</span></a></h4><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>ARCH="arm64"</span><span> </span><span>gki_defconfig</span></div></div><div><div><div>2</div></div><div><span>make</span><span> </span><span>-j$(</span><span>nproc</span><span> --all)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行命令是加载配置文件gki_defconfig,他所在的目录是<code>/内核源代码/arch/arm64/configs/gki_defconfig</code>（可以自行修改里面的配置项）<br />
编译出来的内核二进制文件会在<code>/内核源代码/arch/arm64/boot/Image</code></p></section></section><section><h3>打包内核<a href="#打包内核"><span>#</span></a></h3><p>由于没有编译kernel-devicetree,也没ramdisk,所以无法和内核二进制文件打包成boot.img刷入手机分区(也可以用magiskboot把手机原本的boot.img进行解包，把其中的内核二进制文件替换之后,再打包就行了)<br />
这个时候就需要<a href="https://github.com/osm0sis/AnyKernel3" target="_blank">AnyKernel3</a>对内核二进制文件进行打包<br />
详细教程参考<br />
<a href="https://github.com/tiann/KernelSU/discussions/952" target="_blank">https://github.com/tiann/KernelSU/discussions/952</a></p></section><section><h3>刷入内核<a href="#刷入内核"><span>#</span></a></h3><p>可以在twrp或一些用于刷写内核的app中刷入打包后的AnyKernel3.zip</p></section><section><h3>注意<a href="#注意"><span>#</span></a></h3><p><strong>不同型号内核以及不同版本的编译器，可能对依赖和代码语法要求也有所不同，需要自行解决。</strong></p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/proxy-tools/</id>
      <title type="text">方案分析 | 代理工具的选择</title>
      <published>2025-12-03T00:00:00.000Z</published>
      <updated>2025-12-03T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/proxy-tools/"/>
      <summary type="text">从“代理核心 + GUI 前端”的视角，介绍并对比 Clash/Mihomo、Sing-Box、V2Ray、Xray、dae 等主流核心的特点与取舍，同时按系统整理常见客户端选择，便于快速完成选型。</summary>
      <content type="html"><![CDATA[<section><h2>主流代理核心简介（代理核心 + 前端 UI）<a href="#主流代理核心简介代理核心--前端-ui"><span>#</span></a></h2><p>所有代理工具本质上都是 <strong>代理核心 + 前端图形界面（UI）</strong> 组成。<br />
前端 UI 可以千变万化，但真正承担流量处理的 <strong>代理核心只有少数几种</strong>。</p><p>以下是目前主流、最常见的核心介绍。</p><section><h3><a href="https://github.com/Dreamacro/clash" target="_blank">Clash</a>(更新停滞)<a href="#clash更新停滞"><span>#</span></a></h3><p>Clash 是一个最初由 Go 编写的多协议代理核心（原项目已删除）。<br />
使用广泛，生态庞大。</p><p><strong>特点：</strong></p><ul>
<li>使用统一的 YAML 配置，无需因平台不同而修改</li>
<li>GUI 客户端数量最多、生态最大</li>
<li>跨平台兼容性极强</li>
</ul><p><strong>缺点：</strong></p><ul>
<li>原项目已停止维护</li>
<li>新协议支持较慢（如 Reality、Hysteria2 等）</li>
</ul></section><section><h3><a href="https://github.com/MetaCubeX/mihomo" target="_blank">Mihomo（Clash Meta）</a>★★★<a href="#mihomoclash-meta"><span>#</span></a></h3><p>MetaCubeX 社区维护的 Clash 增强版，也是目前最主流的 Clash 继承者。</p><p><strong>特点：</strong></p><ul>
<li>完全兼容 Clash 配置</li>
<li>支持现代协议（Reality、Hysteria2、TUIC 等）</li>
<li>活跃维护，性能更高</li>
</ul></section><section><h3><a href="https://github.com/SagerNet/sing-box" target="_blank">Sing-Box（sb）</a>★★<a href="#sing-boxsb"><span>#</span></a></h3><p>由 SagerNet 开发的现代化代理核心，目前增长最快、架构最先进。</p><p><strong>特点：</strong></p><ul>
<li>极度轻量，理论性能高</li>
<li>高度可定制的网络与路由配置</li>
<li>支持大量现代协议</li>
<li>跨平台优秀</li>
</ul><p><strong>缺点：</strong></p><ul>
<li>配置难度高，对新手不友好</li>
<li>GUI 生态较少</li>
</ul></section><section><h3><a href="https://github.com/v2fly/v2ray-core" target="_blank">V2Ray</a>(更新缓慢)<a href="#v2ray更新缓慢"><span>#</span></a></h3><p>由 v2fly 社区维护的原始 V2Ray 核心。</p><p><strong>特点：</strong></p><ul>
<li>配置灵活</li>
<li>结构成熟</li>
<li>较为轻量</li>
</ul><p><strong>缺点：</strong></p><ul>
<li>开发速度慢</li>
<li>新协议支持不佳</li>
<li>配置复杂</li>
<li>GUI 生态弱</li>
</ul></section><section><h3><a href="https://github.com/XTLS/Xray-core" target="_blank">Xray</a><a href="#xray"><span>#</span></a></h3><p><a href="https://github.com/v2fly/v2ray-core" target="_blank">V2Ray</a> 的增强版核心，由 XTLS 项目维护。</p><p><strong>特点：</strong></p><ul>
<li>完全兼容 V2Ray 配置</li>
<li>支持 Reality</li>
<li>支持 XTLS，性能大幅提升</li>
<li>性能明显高于官方 V2Ray</li>
</ul><p><strong>缺点：</strong></p><ul>
<li>配置复杂</li>
<li>GUI 生态较弱</li>
</ul></section><section><h3><a href="https://github.com/daeuniverse/dae" target="_blank">dae</a><a href="#dae"><span>#</span></a></h3><p>基于 eBPF 的 Linux / 路由器高性能代理引擎，更像“下一代分流框架”。</p><p><strong>特点：</strong></p><ul>
<li>基于 eBPF，性能极高</li>
<li>几乎不占 CPU</li>
<li>延迟极低</li>
<li>路由分流能力极强</li>
</ul><p><strong>缺点：</strong></p><ul>
<li>仅支持 Linux / OpenWRT</li>
<li>配置复杂</li>
<li>GUI少</li>
</ul></section><section><h3>各操作系统主流GUI代理客户端(整体按照知名度排序)<a href="#各操作系统主流gui代理客户端整体按照知名度排序"><span>#</span></a></h3><section><h4>Windows<a href="#windows"><span>#</span></a></h4><ul>
<li><a href="https://github.com/2dust/v2rayN" target="_blank">v2rayN★★</a> — 支持 Xray / sing-box / clash/others 内核,支持Windows/Linux/macOS</li>
<li><a href="https://github.com/Clash-Verge-rev/clash-verge-rev" target="_blank">Clash Verge Rev★★</a> — 内置 Mihomo内核,支持 Linux/macOS/Windows</li>
<li><a href="https://github.com/mihomo-party-org/clash-party" target="_blank">Mihomo Party★★★</a>(推荐)  - 支持linux/windows/macos</li>
<li><a href="https://github.com/KaringX/karing" target="_blank">Karing</a> — 基于 sing-box 内核,支持Android/ios/linux/windows/macos</li>
<li><a href="https://github.com/xishang0128/sparkle" target="_blank">Sparkle★★</a> — 基于 Mihomo 内核，支持 Windows / macOS / Linux</li>
<li><a href="https://github.com/KaringX/clashmi" target="_blank">Clash Mi</a> —支持android/ios/Windows / macOS / Linux</li>
<li><a href="https://github.com/chen08209/FlClash" target="_blank">FlClash</a> — 基于 Mihomo 内核,支持Android/linux/windows、macos</li>
<li><a href="https://github.com/GUI-for-Cores/GUI.for.Clash" target="_blank">GUI.for.Clash</a> — Windows / macOS / Linux</li>
<li><a href="https://github.com/snakem982/Pandora-Box" target="_blank">Pandora-Box</a> — 基于 Mihomo 内核,支持linux/windows/macos</li>
</ul></section><section><h4>macOS<a href="#macos"><span>#</span></a></h4><ul>
<li><a href="https://nssurge.com" target="_blank">Surge★★★</a> — 付费高级客户端,支持ios/macos</li>
<li><a href="https://apps.apple.com/us/app/quantumult-x/id1443988620" target="_blank">Quantumult X★★★</a> — 付费,支持ios/macos</li>
<li><a href="https://apps.apple.com/us/app/loon/id1373567447" target="_blank">Loon</a> —  付费,支持ios/macos</li>
<li><a href="https://github.com/2dust/v2rayN" target="_blank">v2rayN</a> — 支持 Xray / sing-box / clash/others 内核,支持Windows/Linux/macOS</li>
<li><a href="https://github.com/Clash-Verge-rev/clash-verge-rev" target="_blank">Clash Verge Rev</a> — 内置 Mihomo 内核,支持 Linux/macOS/Windows</li>
<li><a href="https://github.com/mihomo-party-org/clash-party" target="_blank">Mihomo Party</a>  - 支持linux/windows/macos</li>
<li><a href="https://github.com/KaringX/karing" target="_blank">Karing</a> — 基于 sing-box 内核,支持Android/ios/linux/windows/macos</li>
<li><a href="https://github.com/xishang0128/sparkle" target="_blank">Sparkle</a> — 基于 Mihomo 内核，支持 Windows / macOS / Linux</li>
<li><a href="https://github.com/chen08209/FlClash" target="_blank">FlClash</a> — 基于 Mihomo 内核,支持Android/linux/windows、macos</li>
<li><a href="https://github.com/KaringX/clashmi" target="_blank">Clash Mi</a> —支持android/ios/Windows / macOS / Linux</li>
<li><a href="https://github.com/GUI-for-Cores/GUI.for.Clash" target="_blank">GUI.for.Clash</a> — Windows / macOS / Linux</li>
<li><a href="https://stash.ws" target="_blank">Stash★★</a> — 基于 sing-box（付费）</li>
<li><a href="https://github.com/snakem982/Pandora-Box" target="_blank">Pandora-Box</a> — 基于 Mihomo 内核,支持linux/windows/macos</li>
</ul></section><section><h4>Linux<a href="#linux"><span>#</span></a></h4><ul>
<li><a href="https://github.com/2dust/v2rayN" target="_blank">v2rayN★★</a> — 支持 Xray / sing-box / clash/others 内核,支持Windows/Linux/macOS</li>
<li><a href="https://github.com/Clash-Verge-rev/clash-verge-rev" target="_blank">Clash Verge Rev★★</a> — 内置 Mihomo 内核,支持 Linux/macOS/Windows</li>
<li><a href="https://github.com/mihomo-party-org/clash-party" target="_blank">Mihomo Party★★★</a>  - 支持linux/windows/macos</li>
<li><a href="https://github.com/KaringX/karing" target="_blank">Karing</a> — 基于 sing-box 内核,支持Android/ios/linux/windows/macos</li>
<li><a href="https://github.com/xishang0128/sparkle" target="_blank">Sparkle</a> — 基于 Mihomo 内核，支持 Windows / macOS / Linux</li>
<li><a href="https://github.com/chen08209/FlClash" target="_blank">FlClash</a> — 基于 Mihomo 内核,支持Android/linux/windows、macos</li>
<li><a href="https://github.com/KaringX/clashmi" target="_blank">Clash Mi</a> —支持android/ios/Windows / macOS / Linux</li>
<li><a href="https://github.com/GUI-for-Cores/GUI.for.Clash" target="_blank">GUI.for.Clash</a>  — 支持 Linux/macOS/Windows</li>
<li><a href="https://github.com/snakem982/Pandora-Box" target="_blank">Pandora-Box</a> — 基于 Mihomo 内核,支持linux/windows/macos</li>
</ul></section><section><h4>Android<a href="#android"><span>#</span></a></h4><ul>
<li><a href="https://github.com/MetaCubeX/ClashMetaForAndroid" target="_blank">Clash Meta for Android★★★</a>(推荐)</li>
<li><a href="https://github.com/KaringX/karing" target="_blank">Karing</a> — 基于 sing-box 内核,支持Android/ios/linux/windows/macos</li>
<li><a href="https://github.com/KaringX/clashmi" target="_blank">Clash Mi</a> —支持android/ios/Windows / macOS / Linux</li>
<li><a href="https://github.com/chen08209/FlClash" target="_blank">FlClash★★</a> — 基于 Mihomo 内核,支持Android/linux/windows、macos</li>
</ul></section><section><h4>iOS<a href="#ios"><span>#</span></a></h4><ul>
<li><a href="https://nssurge.com" target="_blank">Surge★★★</a> — 付费高级客户端,支持ios/macos</li>
<li><a href="https://apps.apple.com/us/app/shadowrocket/id932747118" target="_blank">Shadowrocket★★★</a> — 付费,支持多协议</li>
<li><a href="https://apps.apple.com/us/app/quantumult-x/id1443988620" target="_blank">Quantumult X★★★</a> — 付费,支持ios/macos</li>
<li><a href="https://apps.apple.com/us/app/loon/id1373567447" target="_blank">Loon</a> —  付费,支持ios/macos</li>
<li><a href="https://github.com/KaringX/karing" target="_blank">Karing</a> — 基于 sing-box 内核,支持Android/ios/linux/windows/macos</li>
<li><a href="https://github.com/KaringX/clashmi" target="_blank">Clash Mi</a> —支持android/ios/Windows / macOS / Linux</li>
</ul></section><section><h4>HarmonyOS NEXT<a href="#harmonyos-next"><span>#</span></a></h4><ul>
<li><a href="https://github.com/xiaobaigroup/ClashBox" target="_blank">ClashBox</a> — 独苗这一块</li>
</ul></section><section><h4>路由器（OpenWRT / Linux）<a href="#路由器openwrt--linux"><span>#</span></a></h4><ul>
<li><a href="https://github.com/vernesong/OpenClash" target="_blank">OpenClash★★</a> - OpenWRT 上主流的 Clash 客户端</li>
<li><a href="https://github.com/nikkinikki-org/OpenWrt-nikki" target="_blank">OpenWrt-nikki★★★</a>(推荐) —  基于clash核心</li>
<li><a href="https://github.com/daeuniverse/daed" target="_blank">daed</a> — 基于dae内核</li>
<li><a href="https://github.com/xiaorouji/openwrt-passwall" target="_blank">Passwall2</a></li>
<li><a href="https://github.com/Thaolga/openwrt-nekobox" target="_blank">openwrt-nekobox</a> — 基于 Sing-box核心</li>
</ul></section></section><section><h3>总结<a href="#总结"><span>#</span></a></h3><p>小白无脑选择clash系客户端，有其他要求可自定义选择。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/steam-fake-inventory/</id>
      <title type="text">学习笔记 | Steam 假入库那些事</title>
      <published>2025-12-03T00:00:00.000Z</published>
      <updated>2025-12-03T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/steam-fake-inventory/"/>
      <summary type="text">从 Steam 授权与清单文件机制出发，拆解“假入库”的实现思路（清单/密钥获取、Steamworks API 劫持等），对常见做法的局限与风险做说明，并给出辨别与处理建议。</summary>
      <content type="html"><![CDATA[<section><h2>Steam假入库到底是什么？<a href="#steam假入库到底是什么"><span>#</span></a></h2><p>淘宝上有很多低价的steam游戏cdk,但购买之后实际是运行脚本入库(有自己steam账号信息泄露的风险),可以在本地游玩.（<strong>换台设备重新登录账号，假入库游戏将会被清空。</strong>）</p><p><strong>实际上账号并没有拥有游戏,别人是看不到你的账号拥有该游戏的</strong></p><section><h3>假入库原理分析:<a href="#假入库原理分析"><span>#</span></a></h3><p><strong>Steam购买游戏相当于购买游戏的游玩资格，购买完成后，将会下载游戏的清单文件(相当于下载列表),然后steam会用游戏的depot key(正版购买用户自动下发)对清单文件进行解密下载,下载完成后，打开游戏会调用steamwork api进行验证当前运行的steam账号是否拥有该游戏,认证通过后即可正常游玩。</strong></p><p>我们可以在其中做些手脚,便可以实现假入库。</p><ol>
<li>
<p>游戏清单文件是可以公开找到的，但是需要正版账号的depot key解密才可以解密下载(<strong>游戏每一个版本的清单文件对应着一个depot key,所以想要玩到最新的游戏内容，密钥文件也需要及时更新</strong>)。假入库的商家可能会根据爬虫或者其他技术手段(如通过木马病毒从其他steam玩家账号中提取depot key)，可以搞到最新的游戏depot key</p>
</li>
<li>
<p>通过解密清单将游戏完整下下来后,通过steamtool/GreenLuma hook注入自己的.dll文件,对steam原有的steamwork api进行劫持,便可以实现让游戏认为账号已经获取了该游戏的游玩权限。</p>
</li>
</ol><p>于是乎通过以上手段，便实现了一套完整的steam游戏假入库</p><p><strong>在steam游戏破解游玩方面,主要有两种破解方法</strong></p></section><section><h3>一是对steam客户端进行crak<a href="#一是对steam客户端进行crak"><span>#</span></a></h3><p>让steam认为你拥有该游戏的游玩权限,再搭配实时获取别人最新的depot key,便可实现几乎与正版购买一致的游戏体验(云存档,成就系统等等)</p><section><h4>弊端:<a href="#弊端"><span>#</span></a></h4><ol>
<li>无法使用联机功能</li>
<li>别人看不到你库中的游戏(无伤大雅,除非你想要通过玩游戏来证明自己的老资历,在某盒软件舆论战中占上风)</li>
<li>系统重装后，steam假入库游戏入库数据会消失(要玩的时候重新入一下库就行了，问题不大)</li>
</ol></section><section><h4>典型代表:<a href="#典型代表"><span>#</span></a></h4><ul>
<li><a href="https://www.steamtools.net" target="_blank">steamtool</a> (闭源，国人大佬开发,功能强大)</li>
<li><a href="https://cs.rin.ru/forum/viewtopic.php?t=103709" target="_blank">GreenLuma</a>(原版纯命令行,操作较难,闭源,外国论坛发布)</li>
<li><a href="https://github.com/clinlx/CN_GreenLumaGUI" target="_blank">GreenLuma_CN</a>(原版加上前端，方便操作)</li>
</ul></section></section><section><h3>二是对游戏文件本身进行修改<a href="#二是对游戏文件本身进行修改"><span>#</span></a></h3><p>对游戏文件夹本身进行修改,内置一个模仿steam api行为的第三方轻量steam client,实现steam游戏脱离steam官方客户端运行,方便游戏在互联网上传播</p><section><h4>弊端:<a href="#弊端-1"><span>#</span></a></h4><ol>
<li>不方便游戏新版本的更新和获取</li>
<li>无法使用steam官方客户端的一些功能</li>
</ol></section><section><h4>典型代表:<a href="#典型代表-1"><span>#</span></a></h4><ul>
<li><a href="https://github.com/Detanup01/gbe_fork" target="_blank">gbe_fork</a></li>
<li><a href="https://github.com/SteamAutoCracks/Steam-auto-crack" target="_blank">steam-auto-crak</a></li>
</ul></section></section><section><h3>其他:<a href="#其他"><span>#</span></a></h3><ul>
<li>steamlan/gbe 通过模仿steam lan api,可以实现游戏在线/离线模式下局域网联机,而不需要走steam官方服务器</li>
<li><a href="https://github.com/atom0s/Steamless" target="_blank">steamless</a> 移除steam端的d加密(不会绕过游戏游玩验证,并且非steam的d加密无法破解，比如说黑猴,需要使用别人的d加密密钥才可以认证游玩)</li>
</ul></section><section><h3>总结:<a href="#总结"><span>#</span></a></h3><p>Steam假入库商家通常通过劫持steam根目录的hid.dll文件进行自定义操作,hook核心一般采用steamtool,加上不知道哪里搞来的清单depot key源,由于是黑箱代码,无法准确判断是否含有其他的木马病毒,来获取自己steam的一些信息,对账号造成隐性风险,所以不建议使用淘宝上的 steam假入库。</p><section><h4>辨别方法:<a href="#辨别方法"><span>#</span></a></h4><p>由于steam假入库这件事已经有一定的知名度了,有些商家也学聪明了，把正版的激活流程放在介绍页，然后下面配一行小字，如果正版激活不行(100%是不成功的)，再采用其他方法(他们自己的脚本).</p><p>主要由以下两个方法判断:</p><ol>
<li>cdk价格过低的，100%都是假入库</li>
<li>直接询问客服是否可以在手机上通过steam客户端cdk激活（假入库的脚本程序无法在手机上运行）</li>
</ol></section><section><h4>解除假入库<a href="#解除假入库"><span>#</span></a></h4><ol>
<li>使用<a href="https://cloud.ksmlc.cn/home?path=cloudreve%3A%2F%2FPDSB%40share" target="_blank">steam解锁文件管理器</a>删除假入库的游戏</li>
<li>然后删除steam根目录的hid.dll(steam更新64位后,hid.dll劫持的方案已经失效了,换别的方案了)</li>
</ol><p><strong>最后严肃声明，本文章仅供技术探讨，支持正版从我做起</strong></p></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/blog-framework-choose/</id>
      <title type="text">方案分析 | 博客框架的选择</title>
      <published>2025-12-02T00:00:00.000Z</published>
      <updated>2025-12-02T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/blog-framework-choose/"/>
      <summary type="text">从静态博客与动态博客的架构差异出发，对资源占用、部署方式、运维成本与可扩展性做对比，并给出 WordPress/Typecho/Halo 与 Hexo/Hugo/Jekyll/Astro 等常见框架的选择建议。</summary>
      <content type="html"><![CDATA[<section><h2>博客框架我们应该如何选择？<a href="#博客框架我们应该如何选择"><span>#</span></a></h2><p>博客大体上可以分为<strong>静态博客</strong>和<strong>动态博客</strong>。</p><p>动态博客采用的是<strong>前端+后端</strong>的模式,而静态博客就是<strong>纯前端静态文件</strong>（方便托管）</p><section><h3>动态博客的优缺点:<a href="#动态博客的优缺点"><span>#</span></a></h3><section><h4>优点:<a href="#优点"><span>#</span></a></h4><ol>
<li>适合大型博客或者论坛</li>
<li>自带后台管理评论</li>
<li>功能集成很多，适合小白，基本不需要额外去弄什么</li>
</ol></section><section><h4>缺点:<a href="#缺点"><span>#</span></a></h4><ol>
<li>资源占用大</li>
<li>必须依赖服务器运行，不可使用静态host主机/serverless服务提供商(cloudflare/vercle/github pages)</li>
<li>网站加载速度相对较慢</li>
<li>有一定的运维成本(服务器/防火墙等费用)</li>
</ol></section></section><section><h3>静态博客的优缺点:<a href="#静态博客的优缺点"><span>#</span></a></h3><section><h4>优点:<a href="#优点-1"><span>#</span></a></h4><ol>
<li>加载速度相对较快</li>
<li>资源占用少。</li>
<li>可以将网站交给serverless服务商托管（白嫖党狂喜，不需要专门的服务器）</li>
</ol></section><section><h4>缺点:<a href="#缺点-1"><span>#</span></a></h4><ol>
<li>要有一定的web技术功底，会手动改配置文件、布局,写文章模板</li>
<li>框架集成的功能较少，如评论功能，音频播放器，需要自行对接第三方接口（不过很多静态博客主题里面已经把功能模板写好了，只需要更换接口地址就行）</li>
<li>缺乏统一的后台管理功能</li>
</ol></section></section><section><h3>动态博客框架<a href="#动态博客框架"><span>#</span></a></h3><section><h4><a href="https://wordpress.org/" target="_blank">WordPress★★★</a><a href="#wordpress"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>世界知名动态博客框架，主题资源插件丰富，教程多。</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><a href="https://typecho.org/" target="_blank">Typecho</a><a href="#typecho"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>轻量级动态博客框架，主打一个快速部署，快速上线</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><a href="https://halo.run/" target="_blank">Halo★★</a><a href="#halo"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>界面很漂亮，资源插件相对来说比较丰富，社区正在逐步发展，安装部署方法较为麻烦，比较吃服务器性能</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>静态博客框架<a href="#静态博客框架"><span>#</span></a></h3><section><h4><a href="https://hexo.io/" target="_blank">Hexo★★★</a><a href="#hexo"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>node.js开发,教程很多，社区插件主题资源丰富，缺点是当文章数量&gt;1k时，构建速度会显著减缓</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><a href="https://gohugo.io/" target="_blank">Hugo★★★</a><a href="#hugo"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>go语言开发,使用go template模板，学习门槛相对其他的来说高点。运行build效率很高,支持热加载,更改不需要刷新网页页面即可生效。教程资源丰富,社区插件资源也很多，稍逊hexo,最大的优势是当文章数量很多时，构建静态网站的速度可以快其他框架几倍.</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><a href="https://jekyllrb.com/" target="_blank">Jekyll★★</a><a href="#jekyll"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>资源插件相对较少,界面简洁直观,但有github官方支持,可以直接将存入仓库的md文件转换成网页</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><a href="https://vitepress.dev/" target="_blank">VitePress</a><a href="#vitepress"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>资源插件丰富，可扩性强,常用于构建文档，不用于博客</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><a href="https://astro.build/" target="_blank">Astro★★</a><a href="#astro"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>新一代静态框架,0 javascript的技术理念让网站首页加载起来非常快,采用服务端渲染的技术理念</span></div></div><div><div><div>2</div></div><div><span>可局部初始化组件为spa,增强网站运行效率,可搭配其他前端框架组件使用，但主题资源相对较少。</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>总结<a href="#总结"><span>#</span></a></h3><p>对于零基础的小白，想搭建博客，首先推荐动态博客框架，首选wordpress,有一定技术基础，搭建自己个人的轻量博客推荐静态框架,首选hexo/hugo,想要更快的构建速度和热加载，选择hugo.如果想尝试更先进的技术理念和更快的首页加载速度，可以尝试astro</p></section><section><h3>杂谈<a href="#杂谈"><span>#</span></a></h3><p>本站采用hugo构建,github仓库储存,cloudflare远程克隆部署,评论功能对接waline。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.liao-ke.com/posts/cloudflare-worker-telegram-bot/</id>
      <title type="text">开发日记 | 用cloudflare worker实现telegram bot</title>
      <published>2025-10-20T00:00:00.000Z</published>
      <updated>2025-10-20T00:00:00.000Z</updated>
      <author><name>LiaoKe</name></author>
      <link rel="alternate" href="https://blog.liao-ke.com/posts/cloudflare-worker-telegram-bot/"/>
      <summary type="text">使用Cloudflare Worker和KV数据库实现带有记忆功能的Telegram聊天机器人，无需服务器即可部署AI对话功能</summary>
      <content type="html"><![CDATA[<p>这段时间闲着没事，想做一个 Telegram Bot 来玩玩。了解了一下，可实施的开发方案还挺多的，Java/Python 等等都有相对应的库。但是奈何<strong>我没有服务器</strong>，无法进行传统的前后端开发。</p>
<p>于是乎，我想到了另一种开发思路：利用 <strong>Cloudflare 提供的 Serverless 无状态 JavaScript Worker</strong> 以及 <strong>Telegram Bot 官方支持的 Webhook 功能</strong>来实现无服务器聊天机器人。</p>
<section><h2>实现原理<a href="#实现原理"><span>#</span></a></h2><p>我们在聊天机器人的界面发送命令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/chat 聊天内容</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>通过设置 Webhook 地址指向 Worker，Worker 中写好处理指令的代码，截取 <code>/开始</code> 后面的文字，然后转发给 <strong>Cloudflare 官方提供的免费的大语言模型后端</strong>，然后再处理返回的信息，最后再通过 <strong>Telegram Bot API</strong> 再返回消息到客户端。</p><p>这样便实现了<strong>无服务器的聊天 AI Bot</strong>，但是这有个缺点：用户的每一次聊天都是单独的一次请求，bot没有上下文记忆,因为 Worker 本身是个<strong>无状态的js执行器</strong>，所以这个 Bot 其实相当于是一个<strong>图形化的终端命令请求器</strong>。</p></section>
<section><h2>但是 <strong>Cloudflare 有提供免费的额外的 KV 数据库</strong>，于是我们再改动一下代码：<a href="#但是-cloudflare-有提供免费的额外的-kv-数据库于是我们再改动一下代码"><span>#</span></a></h2><ol>
<li>用户发来的文字不直接请求大语言模型后端</li>
<li>而是先<strong>存入 KV 数据库</strong></li>
<li>最后将 KV 数据库里的数据一起再发给大语言模型后端</li>
<li>处理返回的信息</li>
<li>返回 Telegram 客户端</li>
</ol><p>于是Worker通过外挂KV数据库，最终便实现了<strong>带有记忆的无服务器聊天 AI 机器人</strong>。</p></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>const</span><span> </span><span>TELEGRAM_BOT_TOKEN</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>CF_API_TOKEN</span><span> </span><span>=</span><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>async</span><span> </span><span>function</span><span> </span><span>run</span><span><span>(</span><span>model</span><span>, </span><span>input</span><span>) {</span></span></div></div><div><div><div>5</div></div><div><span>  </span><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span>(</span></div></div><div><div><div>6</div></div><div><span>    </span><span>`https://api.cloudflare.com/client/v4/accounts/1f5b3d5417456835c8191a2ec42f449e/ai/run/</span><span>${</span><span>model</span><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>headers</span><span>:</span><span> { </span><span>Authorization</span><span>:</span><span> </span><span>`Bearer </span><span>${</span><span>CF_API_TOKEN</span><span>}</span><span>`</span><span> },</span></div></div><div><div><div>9</div></div><div><span>      </span><span>method</span><span>:</span><span> </span><span>"POST"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>      </span><span>body</span><span>:</span><span> </span><span>JSON</span><span>.</span><span>stringify</span><span><span>(</span><span>input</span><span>),</span></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>return</span><span> </span><span>await</span><span><span> </span><span>response</span><span>.</span></span><span>json</span><span>();</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>async</span><span> </span><span>function</span><span> </span><span>sendTelegramMessage</span><span><span>(</span><span>chat_id</span><span>, </span><span>text</span><span>) {</span></span></div></div><div><div><div>17</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>await</span><span> </span><span>fetch</span><span>(</span><span>`https://api.telegram.org/bot</span><span>${</span><span>TELEGRAM_BOT_TOKEN</span><span>}</span><span>/sendMessage`</span><span>, {</span></div></div><div><div><div>19</div></div><div><span>      </span><span>method</span><span>:</span><span> </span><span>"POST"</span><span>,</span></div></div><div><div><div>20</div></div><div><span>      </span><span>headers</span><span>:</span><span> { </span><span>"Content-Type"</span><span>:</span><span> </span><span>"application/json"</span><span> },</span></div></div><div><div><div>21</div></div><div><span>      </span><span>body</span><span>:</span><span> </span><span>JSON</span><span>.</span><span>stringify</span><span>({</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>chat_id</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>text</span><span>,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>parse_mode</span><span>:</span><span> </span><span>"MarkdownV2"</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>      </span></span><span>}),</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span><span> (</span><span>err</span><span>) {</span></span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>error</span><span>(</span><span>"发送 Telegram 消息失败:"</span><span><span>, </span><span>err</span><span>);</span></span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>export</span><span> </span><span>default</span><span> {</span></div></div><div><div><div>33</div></div><div><span>  </span><span>async</span><span> </span><span>fetch</span><span><span>(</span><span>request</span><span>, </span><span>env</span><span>, </span><span>ctx</span><span>) {</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>if</span><span><span> (</span><span>request</span><span>.</span></span><span>method</span><span> </span><span>!==</span><span> </span><span>"POST"</span><span>) </span><span>return</span><span> </span><span>new</span><span> </span><span>Response</span><span>(</span><span>"ok"</span><span>, { </span><span>status</span><span>:</span><span> </span><span>200</span><span> });</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>37</div></div><div><span>      </span><span>const</span><span> </span><span>data</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>request</span><span>.</span></span><span>json</span><span>();</span></div></div><div><div><div>38</div></div><div><span><span>      </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"Telegram Update:"</span><span>, </span><span>JSON</span><span>.</span><span>stringify</span><span>(</span><span>data</span><span>));</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>      </span><span>const</span><span> </span><span>message</span><span> </span><span>=</span><span><span> </span><span>data</span><span>.</span></span><span>message</span><span>;</span></div></div><div><div><div>41</div></div><div><span>      </span><span>if</span><span> (</span><span>!</span><span><span>message</span><span>?.</span></span><span>text</span><span>) </span><span>return</span><span> </span><span>new</span><span> </span><span>Response</span><span>(</span><span>"ok"</span><span>, { </span><span>status</span><span>:</span><span> </span><span>200</span><span> });</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>      </span><span>const</span><span> </span><span>text</span><span> </span><span>=</span><span><span> </span><span>message</span><span>.</span></span><span>text</span><span>;</span></div></div><div><div><div>44</div></div><div><span>      </span><span>const</span><span> </span><span>match</span><span> </span><span>=</span><span><span> </span><span>text</span><span>.</span></span><span>match</span><span>(</span><span>/</span><span>^</span><span><span>\/</span><span>chat(?:@</span></span><span>\w</span><span><span>+</span><span>)</span><span>?</span><span>(?:</span></span><span>\s</span><span><span>+</span><span>(</span></span><span><span>[</span><span>\s\S</span><span>]</span></span><span><span>+</span><span>))</span><span>?</span></span><span>$</span><span>/</span><span>);</span></div></div><div><div><div>45</div></div><div><span>      </span><span>const</span><span> </span><span>arg</span><span> </span><span>=</span><span> </span><span>match</span><span> </span><span>?</span><span> (</span><span>match</span><span>[</span><span>1</span><span>] </span><span>||</span><span> </span><span>""</span><span>).</span><span>trim</span><span>() </span><span>:</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span><span>      </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"完整消息文本:"</span><span>, </span><span>text</span><span>);</span></div></div><div><div><div>48</div></div><div><span><span>      </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"命令参数:"</span><span>, </span><span>arg</span><span>);</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>      </span><span>if</span><span> (</span><span>!</span><span>arg</span><span>) {</span></div></div><div><div><div>51</div></div><div><span>        </span><span>await</span><span> </span><span>sendTelegramMessage</span><span><span>(</span><span>message</span><span>.</span></span><span>chat</span><span>.</span><span>id</span><span>, </span><span>"请在 /chat 后输入要生成的内容，例如：/chat 写一个小故事"</span><span>);</span></div></div><div><div><div>52</div></div><div><span><span>      </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>        </span><span>let</span><span> </span><span>history</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>env</span><span>.</span></span><span>bot</span><span>.</span><span>get</span><span><span>(</span><span>message</span><span>.</span></span><span>chat</span><span>.</span><span>id</span><span>);</span></div></div><div><div><div>55</div></div><div><span>        </span><span>history</span><span> </span><span>=</span><span> </span><span>history</span><span> </span><span>?</span><span> </span><span>JSON</span><span>.</span><span>parse</span><span>(</span><span>history</span><span>) </span><span>:</span><span> [];</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>history</span><span>.</span><span>push</span><span>({ </span><span>role</span><span>:</span><span> </span><span>"user"</span><span>, </span><span>content</span><span>:</span><span> </span><span>arg</span><span> });</span></div></div><div><div><div>57</div></div><div><span>        </span><span>await</span><span><span> </span><span>env</span><span>.</span></span><span>bot</span><span>.</span><span>put</span><span><span>(</span><span>message</span><span>.</span></span><span>chat</span><span>.</span><span>id</span><span>, </span><span>JSON</span><span>.</span><span>stringify</span><span>(</span><span>history</span><span>));</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>        </span><span>const</span><span> </span><span>aiResponse</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>run</span><span>(</span><span>"@cf/openai/gpt-oss-120b"</span><span>, {</span></div></div><div><div><div>60</div></div><div><span>          </span><span>instructions</span><span>:</span><span> </span><span>'你是liaoke的私人中文AI助手，简洁地回答用户的问题。'</span><span>,</span></div></div><div><div><div>61</div></div><div><span>          </span><span>reasoning</span><span>:</span><span> { </span><span>"effort"</span><span>:</span><span> </span><span>"medium"</span><span> },</span></div></div><div><div><div>62</div></div><div><span>          </span><span>input</span><span>:</span><span> </span><span>history</span><span>,</span></div></div><div><div><div>63</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span><span>        </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"AI 原始响应:"</span><span>, </span><span>JSON</span><span>.</span><span>stringify</span><span>(</span><span>aiResponse</span><span>));</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>        </span><span>const</span><span> </span><span>reasonText</span><span> </span><span>=</span><span><span> </span><span>aiResponse</span><span>?.</span></span><span>result</span><span>?.</span><span>output</span></div></div><div><div><div>69</div></div><div><span><span>          </span></span><span>?.</span><span>filter</span><span><span>(</span><span>o</span><span> </span></span><span>=&gt;</span><span><span> </span><span>o</span><span>.</span></span><span>type</span><span> </span><span>===</span><span> </span><span>"reasoning"</span><span>)</span></div></div><div><div><div>70</div></div><div><span><span>          </span></span><span>?.</span><span>map</span><span><span>(</span><span>o</span><span> </span></span><span>=&gt;</span><span><span> </span><span>o</span><span>.</span></span><span>content</span><span>?.</span><span>map</span><span><span>(</span><span>c</span><span> </span></span><span>=&gt;</span><span><span> </span><span>c</span><span>.</span></span><span>text</span><span>).</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>))</span></div></div><div><div><div>71</div></div><div><span><span>          </span></span><span>.</span><span>filter</span><span>(</span><span>Boolean</span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>          </span></span><span>.</span><span>join</span><span>(</span><span>"</span><span>\n\n</span><span>"</span><span>) </span><span>||</span><span> </span><span>"AI 无法生成内容"</span><span>;</span></div></div><div><div><div>73</div></div><div>
</div></div><div><div><div>74</div></div><div><span>        </span><span>const</span><span> </span><span>outputText</span><span> </span><span>=</span><span><span> </span><span>aiResponse</span><span>?.</span></span><span>result</span><span>?.</span><span>output</span></div></div><div><div><div>75</div></div><div><span><span>          </span></span><span>?.</span><span>filter</span><span><span>(</span><span>o</span><span> </span></span><span>=&gt;</span><span><span> </span><span>o</span><span>.</span></span><span>type</span><span> </span><span>===</span><span> </span><span>"message"</span><span>)</span></div></div><div><div><div>76</div></div><div><span><span>          </span></span><span>?.</span><span>map</span><span><span>(</span><span>o</span><span> </span></span><span>=&gt;</span><span><span> </span><span>o</span><span>.</span></span><span>content</span><span>?.</span><span>map</span><span><span>(</span><span>c</span><span> </span></span><span>=&gt;</span><span><span> </span><span>c</span><span>.</span></span><span>text</span><span>).</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>))</span></div></div><div><div><div>77</div></div><div><span><span>          </span></span><span>.</span><span>filter</span><span>(</span><span>Boolean</span><span>)</span></div></div><div><div><div>78</div></div><div><span><span>          </span></span><span>.</span><span>join</span><span>(</span><span>"</span><span>\n\n</span><span>"</span><span>) </span><span>||</span><span> </span><span>"AI 无法生成内容"</span><span>;</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div>
</div></div><div><div><div>81</div></div><div><span>        </span><span>const</span><span> </span><span>escapeMD</span><span> </span><span>=</span><span><span> (</span><span>text</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>82</div></div><div><span>          </span><span>if</span><span> (</span><span>!</span><span>text</span><span>) </span><span>return</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>83</div></div><div><span>          </span><span>return</span><span> </span><span>text</span></div></div><div><div><div>84</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\\</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\\\</span><span>"</span><span>)</span></div></div><div><div><div>85</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/`/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>`"</span><span>)</span></div></div><div><div><div>86</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/_/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>_"</span><span>)</span></div></div><div><div><div>87</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\*</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>*"</span><span>)</span></div></div><div><div><div>88</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\[</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>["</span><span>)</span></div></div><div><div><div>89</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\]</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>]"</span><span>)</span></div></div><div><div><div>90</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/~/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>~"</span><span>)</span></div></div><div><div><div>91</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/&gt;/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>&gt;"</span><span>)</span></div></div><div><div><div>92</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/#/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>#"</span><span>)</span></div></div><div><div><div>93</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\+</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>+"</span><span>)</span></div></div><div><div><div>94</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/-/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>-"</span><span>)</span></div></div><div><div><div>95</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/=/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>="</span><span>)</span></div></div><div><div><div>96</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\|</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>|"</span><span>)</span></div></div><div><div><div>97</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\{</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>{"</span><span>)</span></div></div><div><div><div>98</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\}</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>}"</span><span>)</span></div></div><div><div><div>99</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\.</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\</span><span>."</span><span>)</span></div></div><div><div><div>100</div></div><div><span><span>            </span></span><span>.</span><span>replace</span><span>(</span><span>/!/</span><span>g</span><span>, </span><span>"</span><span>\\</span><span>!"</span><span>);</span></div></div><div><div><div>101</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div>
</div></div><div><div><div>104</div></div><div><span>        </span><span>const</span><span> </span><span>messageMD</span><span> </span><span>=</span><span> </span><span>`</span></div></div><div><div><div>105</div></div><div><span>🧠 推理过程：</span></div></div><div><div><div>106</div></div><div><span>\`\`\`</span></div></div><div><div><div>107</div></div><div><span>${</span><span>escapeMD</span><span>(</span><span>reasonText</span><span>)</span><span>}</span></div></div><div><div><div>108</div></div><div><span>\`\`\`</span></div></div><div><div><div>109</div></div><div>
</div></div><div><div><div>110</div></div><div><span>💬 最终回答：</span></div></div><div><div><div>111</div></div><div><span>${</span><span>escapeMD</span><span>(</span><span>outputText</span><span>)</span><span>}</span></div></div><div><div><div>112</div></div><div><span><span>        </span></span><span>`</span><span>;</span></div></div><div><div><div>113</div></div><div>
</div></div><div><div><div>114</div></div><div>
</div></div><div><div><div>115</div></div><div><span>        </span><span>await</span><span> </span><span>sendTelegramMessage</span><span><span>(</span><span>message</span><span>.</span></span><span>chat</span><span>.</span><span>id</span><span>, </span><span>messageMD</span><span>);</span></div></div><div><div><div>116</div></div><div><span><span>        </span></span><span>history</span><span>.</span><span>push</span><span>({ </span><span>role</span><span>:</span><span> </span><span>"assistant"</span><span>, </span><span>content</span><span>:</span><span> </span><span>outputText</span><span> });</span></div></div><div><div><div>117</div></div><div><span>        </span><span>await</span><span><span> </span><span>env</span><span>.</span></span><span>bot</span><span>.</span><span>put</span><span><span>(</span><span>message</span><span>.</span></span><span>chat</span><span>.</span><span>id</span><span>, </span><span>JSON</span><span>.</span><span>stringify</span><span>(</span><span>history</span><span>));</span></div></div><div><div><div>118</div></div><div><span><span>        </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"AI 输出发送给用户:"</span><span>, </span><span>messageMD</span><span>);</span></div></div><div><div><div>119</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>120</div></div><div>
</div></div><div><div><div>121</div></div><div><span>      </span><span>return</span><span> </span><span>new</span><span> </span><span>Response</span><span>(</span><span>"ok"</span><span>, { </span><span>status</span><span>:</span><span> </span><span>200</span><span> });</span></div></div><div><div><div>122</div></div><div><span><span>    </span></span><span>} </span><span>catch</span><span> (</span><span>err</span><span>) {</span></div></div><div><div><div>123</div></div><div><span><span>      </span></span><span>console</span><span>.</span><span>error</span><span>(</span><span>"解析 JSON 或处理失败:"</span><span>, </span><span>err</span><span>);</span></div></div><div><div><div>124</div></div><div><span>      </span><span>return</span><span> </span><span>new</span><span> </span><span>Response</span><span>(</span><span>"error"</span><span>, { </span><span>status</span><span>:</span><span> </span><span>400</span><span> });</span></div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>126</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>127</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></section>]]></content>
    </entry>
</feed>
