{
    "version": "https://jsonfeed.org/version/1",
    "title": "vw2x",
    "home_page_url": "https://vw2x.vercel.app",
    "feed_url": "https://vw2x.vercel.app/feed.json",
    "description": "Notes on learning, building, and thinking",
    "icon": "https://vw2x.vercel.app/favicon.png",
    "author": {
        "name": "vw2x",
        "url": "https://vw2x.vercel.app"
    },
    "items": [
        {
            "id": "https://vw2x.vercel.app/en/blog/20260327_rethinking-zsh-in-codex",
            "content_html": "<h2>Why I Started Rethinking zsh</h2>\n<p>Recently I was debugging an Android native build issue in Codex.</p>\n<p>The symptom was odd: on the same machine, in the same repository, the same <code>./gradlew clean :app:assembleDebug</code> command would succeed when I ran it manually in Codex Desktop&#39;s interactive terminal, but fail consistently when the assistant ran it through the tool.</p>\n<p>At first glance it looked like an ordinary build failure. I suspected Gradle, the NDK, the JDK, and the cache.</p>\n<p>But after checking the daemon process arguments, none of those were it.</p>\n<p><strong>The command did not change, the entry point did, and the result changed.</strong></p>\n<p>That reminded me of an environment issue I had only half-understood before: what is the difference between <code>zsh -c</code>, <code>zsh -lc</code>, and <code>zsh -ic</code>?</p>\n<p>I used to think of them as just a few shell startup variants.</p>\n<p>This time, though, I realized the difference is not a syntax detail. It directly decides whether you are getting the same environment.</p>\n<h2>I First Treated It as a Build Problem</h2>\n<p>The failure eventually landed in the native build chain, and the errors looked like a broken toolchain:</p>\n<ul>\n<li><code>fatal error: &#39;stdio.h&#39; file not found</code></li>\n<li><code>fatal error: &#39;stdlib.h&#39; file not found</code></li>\n</ul>\n<p>That kind of error is easy to misread.</p>\n<p>Because it happened during the build, my attention naturally went to:</p>\n<ol>\n<li>Whether Gradle was broken.</li>\n<li>Whether the NDK version was wrong.</li>\n<li>Whether the JDK was incompatible.</li>\n<li>Whether the CMake/Ninja cache was dirty.</li>\n</ol>\n<p>At the same time, one signal was much more important:</p>\n<p><strong>Manual terminal execution succeeded, but the tool subprocess failed</strong></p>\n<p>That should change the priority.</p>\n<p>At that point, the better question was not &quot;is the project broken?&quot; but:</p>\n<p><strong>Are those two entry points actually running in the same environment?</strong></p>\n<p>That was the key step.</p>\n<p>We often assume that because a command runs inside the same app, the environment should be close enough.</p>\n<p>What I came to understand here is:</p>\n<p><strong>Different execution entry points in the same product can still map to different shell startup environments.</strong></p>\n<h2><code>zsh -c</code>, <code>zsh -lc</code>, and <code>zsh -ic</code> Differ in Environment, Not Syntax</h2>\n<p>Instead of guessing inside the Gradle wrapper, I first cut the problem by shell startup mode.</p>\n<p>The result was very direct:</p>\n<ul>\n<li><code>zsh -c</code> failed.</li>\n<li><code>zsh -lc</code> still failed.</li>\n<li><code>zsh -ic</code> succeeded.</li>\n</ul>\n<p>Once those three results were in, the problem had already been narrowed down a lot.</p>\n<p>It told me at least three things:</p>\n<ol>\n<li>The command text itself was not the issue.</li>\n<li>The repository itself was not the issue.</li>\n<li>The issue was in the shell initialization chain.</li>\n</ol>\n<p>If I reduce it to a rough troubleshooting model, these flags can be remembered like this:</p>\n<ul>\n<li><code>zsh -c</code>: <code>-c</code> means command, so it runs the command string and exits. That is usually a non-interactive, non-login subprocess mode.</li>\n<li><code>zsh -lc</code>: <code>-l</code> means login, <code>-c</code> means command. In other words, start as a login shell, then run only one command.</li>\n<li><code>zsh -ic</code>: <code>-i</code> means interactive, <code>-c</code> means command. In other words, force interactive shell semantics, then run only one command.</li>\n</ul>\n<p>What matters is that the startup chain behind them is different.</p>\n<p><img src=\"/blog/260327_codex-zsh-interactive-shell/image.png\" alt=\"Screenshot\"></p>\n<p>From the <code>zsh</code> manual, the startup order can be remembered like this:</p>\n<ul>\n<li>Every <code>zsh</code> instance reads <code>/etc/zshenv</code> and <code>~/.zshenv</code></li>\n<li>A <code>login shell</code> also reads <code>/etc/zprofile</code> and <code>~/.zprofile</code></li>\n<li>An <code>interactive shell</code> also reads <code>/etc/zshrc</code> and <code>~/.zshrc</code></li>\n<li>A <code>login shell</code> finally reads <code>/etc/zlogin</code> and <code>~/.zlogin</code></li>\n</ul>\n<p>Mapped back to the three startup modes here, that becomes roughly:</p>\n<ul>\n<li><code>zsh -c</code><ul>\n<li>You can think of it as: execute command</li>\n<li>Typical chain: <code>/etc/zshenv</code> -&gt; <code>~/.zshenv</code> -&gt; execute command -&gt; exit</li>\n</ul>\n</li>\n<li><code>zsh -lc</code><ul>\n<li>You can think of it as: login shell + execute command</li>\n<li>Typical chain: <code>/etc/zshenv</code> -&gt; <code>~/.zshenv</code> -&gt; <code>/etc/zprofile</code> -&gt; <code>~/.zprofile</code> -&gt; <code>/etc/zlogin</code> -&gt; <code>~/.zlogin</code> -&gt; execute command -&gt; exit</li>\n</ul>\n</li>\n<li><code>zsh -ic</code><ul>\n<li>You can think of it as: interactive shell + execute command</li>\n<li>Typical chain: <code>/etc/zshenv</code> -&gt; <code>~/.zshenv</code> -&gt; <code>/etc/zshrc</code> -&gt; <code>~/.zshrc</code> -&gt; execute command -&gt; exit</li>\n</ul>\n</li>\n</ul>\n<p>That also explains why <code>zsh -lc</code> and <code>zsh -ic</code> are both different from plain <code>zsh -c</code>, yet can still end up with different environments:</p>\n<ul>\n<li>If the key variables live in <code>~/.zprofile</code>, <code>-lc</code> may get them but <code>-ic</code> may not.</li>\n<li>If the key variables live in <code>~/.zshrc</code>, <code>-ic</code> may get them but <code>-lc</code> may not.</li>\n<li>If the key variables are moved into <code>~/.zshenv</code>, then all three should see them in theory.</li>\n</ul>\n<p>The easiest mistake here is to treat &quot;it is also zsh&quot; as &quot;the environment should be roughly the same&quot;.</p>\n<p>That is not true.</p>\n<p>Once the startup flags differ, the initialization file chain can differ too, and so can the final environment variables.</p>\n<h2>How I Narrowed It Down to Shell Environment</h2>\n<p>Debugging is not &quot;guessing with experience&quot;. It is knowing that you do not control the whole system, stepping back, and using variable control and binary search.</p>\n<h3>1. Admit It Is Not a Single-Command Problem, but an Entry-Point Problem</h3>\n<p>If the same command gives opposite results through two entry points, the first thing to do is: <strong>freeze the command text and compare the entry points.</strong></p>\n<p><strong>Control variables before running experiments.</strong></p>\n<p>Do not keep changing the project, guessing the cache, and trying new commands at the same time, or you will just confuse yourself.</p>\n<h3>2. Check Whether the Repository Itself Is the Problem</h3>\n<p>If manual execution succeeds on the same machine and the same repository, then the repository itself is probably not the main cause.</p>\n<p>That does not mean the code is definitely fine. It means:</p>\n<p><strong>It should not be the first thing you suspect.</strong></p>\n<h3>3. Use <code>zsh -c</code>, <code>zsh -lc</code>, and <code>zsh -ic</code> to Slice the Environment</h3>\n<p>Recently I heard someone say that once a problem becomes concrete, it is no longer scary. It is like the boss finally showing its health bar.</p>\n<p><code>Tool execution in Codex is different from manual terminal use</code> needs to become a more concrete statement: <code>the difference is whether the shell reaches the interactive initialization chain</code></p>\n<ul>\n<li><code>-c</code> does not work</li>\n<li><code>-lc</code> does not work</li>\n<li><code>-ic</code> does work</li>\n</ul>\n<p>So the issue is not &quot;whether there is zsh&quot;, but &quot;what semantics zsh is started with&quot;.</p>\n<h3>4. Validate the Smallest Failure Point</h3>\n<p><strong>Do not keep guessing while wrapped inside the whole system.</strong></p>\n<p>Gradle wraps CMake, CMake wraps Ninja, and Ninja calls the compiler. That is too many layers, and the real signal gets buried.</p>\n<p>It wastes time, and it wastes tokens.</p>\n<p>Once I kept tracing, I found the failure path was actually using the NDK host <code>clang</code>, and then I checked its header search paths directly. The problem became much clearer.</p>\n<p>In the failing environment, the Darwin SDK system header search path was missing.</p>\n<p>I asked the AI, and it suggested trying to add <code>SDKROOT</code>.</p>\n<p>That turned &quot;the build system may have many complicated causes&quot; into <strong>&quot;the current shell did not carry in the key environment&quot;</strong></p>\n<h2>The Core Issue</h2>\n<p>Looking back, the most important thing here was not how to fix one Android native project, but:</p>\n<p><strong>In Codex, the interactive terminal and the tool subprocess are not the same shell environment.</strong></p>\n<p>Once that premise is true, many later symptoms stop being strange:</p>\n<ul>\n<li>Running inside Codex but behaving like two different machines.</li>\n<li>Both using <code>zsh</code>, yet loading different variables.</li>\n<li>Manual execution succeeds, but automation fails.</li>\n</ul>\n<p>Commands are never executed outside context.</p>\n<p>A command&#39;s real execution conditions include at least:</p>\n<ul>\n<li>the current directory</li>\n<li>the current process environment variables</li>\n<li>the shell startup semantics</li>\n<li>the actual toolchain path that gets resolved</li>\n</ul>\n<p>If you only look at the command text, it is easy to get stuck thinking: &quot;I ran the exact same command.&quot;</p>\n<p>From a systems perspective, if those conditions differ, it is not the same execution at all.</p>\n<h2>How I Will Debug This Kind of Environment Issue Next Time</h2>\n<h3>1. When Manual Works and Automation Fails, Do Not Rush to Change Code</h3>\n<p>Suspect the process environment first, not the repository contents.</p>\n<p>Especially in IDEs, agents, terminal tools, and CI systems, never assume the environments are identical by default.</p>\n<h3>2. Compare Shell Semantics Before Command Text</h3>\n<p>The difference between <code>zsh -c</code>, <code>zsh -lc</code>, and <code>zsh -ic</code> is not a memorization question. It is a startup-chain difference.</p>\n<p>Once you know:</p>\n<ul>\n<li><code>-c</code> is closer to a run-and-exit command mode</li>\n<li><code>-lc</code> carries login-shell semantics</li>\n<li><code>-ic</code> carries interactive-shell semantics</li>\n</ul>\n<p>a lot of &quot;same command, different result&quot; cases suddenly become concrete.</p>\n<h3>3. Validate the Smallest Failure Point, Not the Outer System</h3>\n<p>If there is a Gradle wrapper, script, or task orchestrator around it, push inward.</p>\n<p>Find the layer where it actually fails and inspect what it is missing.</p>\n<p>Many environment issues become much easier once you can reduce them to the smallest possible command.</p>\n",
            "url": "https://vw2x.vercel.app/en/blog/20260327_rethinking-zsh-in-codex",
            "title": "Rethinking zsh in Codex",
            "summary": "One software, one command, two outcomes. That made me start separating different zsh startup modes.",
            "date_modified": "2026-03-27T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/blog/260327_codex-zsh-interactive-shell",
            "content_html": "<h2>为什么我会重新理解 zsh</h2>\n<p>最近在 Codex 里排一个 Android native 构建问题.</p>\n<p>现象有点别扭: 同一台机器, 同一份仓库, 同一条 <code>./gradlew clean :app:assembleDebug</code>, 在 Codex 桌面里的交互终端手动执行能成功, assistant 通过工具去执行却稳定失败.</p>\n<p>第一眼看上去像是普通的构建失败. 怀疑 Gradle, 怀疑 NDK, 怀疑 JDK, 怀疑缓存.</p>\n<p>但查看 daemon 进程的运行参数发现都不是.</p>\n<p><strong>命令没变, 执行入口变, 结果变.</strong></p>\n<p>这件事让我想起曾经一笔带过的环境配置问题: <code>zsh -c</code>、<code>zsh -lc</code>、<code>zsh -ic</code> 这些启动方式的差异是什么?</p>\n<p>以前对这些参数的理解比较松散, 觉得无非就是 shell 的几种启动形式.</p>\n<p>但这次排查到后面发现, 差异不是“语法细节”, 而是会直接决定你拿到的是不是同一套环境.</p>\n<h2>一开始把它当成构建问题</h2>\n<p>这次失败最终落在 native 构建链路里, 报错也很像工具链坏了:</p>\n<ul>\n<li><code>fatal error: &#39;stdio.h&#39; file not found</code></li>\n<li><code>fatal error: &#39;stdlib.h&#39; file not found</code></li>\n</ul>\n<p>这类错误很容易让人想歪.</p>\n<p>因为它出现在构建阶段, 自然把注意力放在:</p>\n<ol>\n<li>Gradle 有没有问题.</li>\n<li>NDK 版本是不是不对.</li>\n<li>JDK 是不是和项目不兼容.</li>\n<li>CMake/Ninja 缓存是不是脏了.</li>\n</ol>\n<p>与此同时发现一个更值得警觉的信号:</p>\n<p><strong>用户手动终端能成功, 工具子进程却失败</strong></p>\n<p>那优先级其实应该先切换一下.</p>\n<p>这时候比 &quot;项目是不是坏了&quot; 更值得问的是:</p>\n<p><strong>这两个入口, 真的是同一种运行环境吗?</strong></p>\n<p>这一步很关键.</p>\n<p>很多时候我们会下意识觉得, 都在同一个 App 里执行命令, 差别不该太大.</p>\n<p>但这次让我真正建立起来的直觉是:</p>\n<p><strong>同一个产品里的不同执行入口, 完全可能对应不同的 shell 启动环境.</strong></p>\n<h2><code>zsh -c</code>、<code>zsh -lc</code>、<code>zsh -ic</code> 差的不是语法, 是环境</h2>\n<p>这次我没有一上来继续包在 Gradle 外面猜, 而是先拿 shell 启动方式做切片.</p>\n<p>结果非常直接:</p>\n<ul>\n<li><code>zsh -c</code> 下构建失败.</li>\n<li><code>zsh -lc</code> 下仍然失败.</li>\n<li><code>zsh -ic</code> 下构建成功.</li>\n</ul>\n<p>这三个结果一出来, 问题其实已经被压缩得很小了.</p>\n<p>它至少说明了三件事:</p>\n<ol>\n<li>问题不在命令文本本身.</li>\n<li>问题不在仓库本身.</li>\n<li>问题在 shell 初始化链条.</li>\n</ol>\n<p>如果只从排障角度理解, 这几个参数可以先粗暴地记成:</p>\n<ul>\n<li><code>zsh -c</code>: <code>-c</code> 是 command, 表示执行后面给的一段命令后退出. 这通常是非交互、非 login 的子进程执行方式.</li>\n<li><code>zsh -lc</code>: <code>-l</code> 是 login, <code>-c</code> 是 command. 也就是“以 login shell 的方式启动, 但只执行一段命令”.</li>\n<li><code>zsh -ic</code>: <code>-i</code> 是 interactive, <code>-c</code> 是 command. 也就是“强制走交互 shell 语义, 但只执行一段命令”.</li>\n</ul>\n<p>这三个参数真正有价值的地方, 是它们背后的启动链路不同.</p>\n<p><img src=\"/blog/260327_codex-zsh-interactive-shell/image.png\" alt=\"截图\"></p>\n<p>翻了 <code>zsh</code> 手册, 启动顺序可以先记成:</p>\n<ul>\n<li>所有 <code>zsh</code> 实例都会先读 <code>/etc/zshenv</code> 和 <code>~/.zshenv</code></li>\n<li>如果是 <code>login shell</code>, 会继续读 <code>/etc/zprofile</code> 和 <code>~/.zprofile</code></li>\n<li>如果是 <code>interactive shell</code>, 会继续读 <code>/etc/zshrc</code> 和 <code>~/.zshrc</code></li>\n<li>如果是 <code>login shell</code>, 最后还会继续读 <code>/etc/zlogin</code> 和 <code>~/.zlogin</code></li>\n</ul>\n<p>所以把它映射回这次遇到的三种启动方式, 大概就是:</p>\n<ul>\n<li><code>zsh -c</code><ul>\n<li>全称可以理解为: execute command</li>\n<li>常见链路: <code>/etc/zshenv</code> -&gt; <code>~/.zshenv</code> -&gt; 执行命令 -&gt; 退出</li>\n</ul>\n</li>\n<li><code>zsh -lc</code><ul>\n<li>全称可以理解为: login shell + execute command</li>\n<li>常见链路: <code>/etc/zshenv</code> -&gt; <code>~/.zshenv</code> -&gt; <code>/etc/zprofile</code> -&gt; <code>~/.zprofile</code> -&gt; <code>/etc/zlogin</code> -&gt; <code>~/.zlogin</code> -&gt; 执行命令 -&gt; 退出</li>\n</ul>\n</li>\n<li><code>zsh -ic</code><ul>\n<li>全称可以理解为: interactive shell + execute command</li>\n<li>常见链路: <code>/etc/zshenv</code> -&gt; <code>~/.zshenv</code> -&gt; <code>/etc/zshrc</code> -&gt; <code>~/.zshrc</code> -&gt; 执行命令 -&gt; 退出</li>\n</ul>\n</li>\n</ul>\n<p>这也解释了为什么 <code>zsh -lc</code> 和 <code>zsh -ic</code> 虽然都不像纯粹的 <code>zsh -c</code>, 但它们拿到的环境仍然可能不同:</p>\n<ul>\n<li>如果关键变量放在 <code>~/.zprofile</code>, <code>-lc</code> 可能拿得到, <code>-ic</code> 不一定.</li>\n<li>如果关键变量放在 <code>~/.zshrc</code>, <code>-ic</code> 可能拿得到, <code>-lc</code> 不一定.</li>\n<li>如果关键变量下沉到了 <code>~/.zshenv</code>, 那三者理论上都能拿到.</li>\n</ul>\n<p>这里最容易犯的错是:</p>\n<p>把 &quot;也是 zsh&quot; 误解成 &quot;环境应该差不多&quot; .</p>\n<p>其实不是.</p>\n<p>同样叫 <code>zsh</code>, 只要启动参数不同, 加载的初始化文件链就可能不同, 最后拿到的环境变量也可能不同.</p>\n<h2>我是怎么把问题收敛到 shell 环境上的</h2>\n<p>排障不是 &quot;经验丰富地乱猜&quot;, 而是遇到难题先知道自己不能掌控全局, 先退一步, 控制变量, 二分法排查.</p>\n<h3>1. 承认这不是一条命令的问题, 而是两个入口的问题</h3>\n<p>如果同一条命令在两个入口结果相反, 那最应该先做的是: <strong>冻结命令文本, 去比较入口差异.</strong></p>\n<p><strong>做实验时先控制变量.</strong></p>\n<p>不能一边改项目, 一边猜缓存, 一边再换命令, 最后把自己绕晕.</p>\n<h3>2. 判断是不是仓库代码本身的问题</h3>\n<p>如果用户在同一台机器、同一份仓库里手动执行可以成功, 那仓库代码本身大概率不是主因.</p>\n<p>这里不是说代码一定没问题, 而是说:</p>\n<p><strong>它不值得成为第一怀疑对象.</strong></p>\n<h3>3. 用 <code>zsh -c</code>、<code>zsh -lc</code>、<code>zsh -ic</code> 切环境</h3>\n<p>前段时间听到别人说, 问题一旦具体起来, 就不再可怕, 相当于 boss 亮血条了.</p>\n<p><code>Codex 里的工具执行和手动终端不一样</code> 需要切换成更加具体的语言: <code>差异在 shell 是否走到了交互初始化链条上</code></p>\n<ul>\n<li><code>-c</code> 不行</li>\n<li><code>-lc</code> 也不行</li>\n<li><code>-ic</code> 可以</li>\n</ul>\n<p>所以, 问题不是 &quot;有没有 zsh&quot;, 而是 &quot;zsh 是按什么语义启动的&quot;.</p>\n<h3>4. 验证最小失败点</h3>\n<p><strong>环境问题不能一直包在大系统外面猜.</strong></p>\n<p>Gradle 套 CMake, CMake 套 Ninja, Ninja 再去调编译器, 中间层太多了, 就会把真正的信号埋掉.</p>\n<p>不仅浪费时间, 还浪费我的 token.</p>\n<p>继续往下追后, 看到失败链路里实际调用的是 NDK 的 host <code>clang</code>, 然后再去直接看它的头文件搜索路径, 问题就更清楚了.</p>\n<p>失败环境里看不到 Darwin SDK 的系统头搜索路径.</p>\n<p>问 AI, 说是补上 <code>SDKROOT</code> 试试.</p>\n<p>&quot;构建系统可能有很多复杂原因&quot; 的问题收敛为 <strong>&quot;当前 shell 没把关键环境带进来&quot;</strong></p>\n<h2>问题本质</h2>\n<p>回头看, 这次最值得记住的东西并不是某个 Android native 项目该怎么修, 而是:</p>\n<p><strong>在 Codex 里, 交互终端和工具子进程不是同一种 shell 环境.</strong></p>\n<p>只要这个前提成立, 后面很多现象就都不奇怪了:</p>\n<ul>\n<li>同样都在 Codex 里执行, 结果却像两台机器.</li>\n<li>同样写的是 <code>zsh</code>, 加载到的变量却不一样.</li>\n<li>手工执行能成功, 自动化执行却失败.</li>\n</ul>\n<p>说到底, 命令从来都不是脱离上下文运行的.</p>\n<p>一条命令真正的执行条件至少包括:</p>\n<ul>\n<li>当前目录</li>\n<li>当前进程环境变量</li>\n<li>shell 的启动语义</li>\n<li>实际解析到的工具链路径</li>\n</ul>\n<p>只盯着命令文本, 很容易卡住: &quot;我运行的明明是同一条命令.&quot; </p>\n<p>但从系统视角看, 如果这几个前提不同, 那它根本就不是同一次执行.</p>\n<h2>以后怎么排这类环境问题</h2>\n<h3>1. 看到 &quot;手工成功, 自动化失败&quot;, 不急改代码</h3>\n<p>先怀疑进程环境, 不是仓库内容.</p>\n<p>尤其是在 IDE、Agent、终端工具、CI 这些多入口系统里, 默认不要假设环境一致.</p>\n<h3>2. 先比较 shell 语义, 不只比较命令文本</h3>\n<p><code>zsh -c</code>、<code>zsh -lc</code>、<code>zsh -ic</code> 的差异, 本质不是参数背诵题, 而是启动链路差异.</p>\n<p>一旦你知道:</p>\n<ul>\n<li><code>-c</code> 更接近“跑完即走”的 command mode</li>\n<li><code>-lc</code> 是 login shell 语义</li>\n<li><code>-ic</code> 是 interactive shell 语义</li>\n</ul>\n<p>很多“同命令不同结果”的问题就会突然具体起来.</p>\n<h3>3. 验证最小失败点, 不要一直包在大系统外面猜</h3>\n<p>如果外面套着 Gradle、脚本、任务编排器, 就尽量往里打.</p>\n<p>打到真正失败的那一层工具, 看它到底缺什么.</p>\n<p>很多环境问题一旦能落到最小命令, 判断速度会快很多.</p>\n<h3>4. 看到标准头文件缺失, 优先怀疑 sysroot/SDK</h3>\n<p>像 <code>stdio.h</code>、<code>stdlib.h</code> 这种系统标准头文件找不到时, 第一反应通常不该是 &quot;业务代码写错了&quot;</p>\n<p>更应该先看:</p>\n<ul>\n<li>当前编译器的搜索路径</li>\n<li>当前进程有没有正确的 SDK 线索</li>\n<li>当前 shell 初始化链有没有把关键变量带进来</li>\n</ul>\n<h2>最后</h2>\n<p>以前我会把 <code>zsh -c</code>、<code>zsh -lc</code>、<code>zsh -ic</code> 这种区别当成 shell 使用细节.</p>\n<p>但这次在 Codex 里排完问题后, 我对它们的理解变了.</p>\n<p>它们不只是 &quot;几种启动参数&quot; , 而是几种不同的环境入口.</p>\n<p>而很多构建问题、自动化问题、Agent 跑命令的问题, 本质上都不是命令写错了, 而是你没有先分清:</p>\n<p><strong>你到底是在同一个环境里执行, 还是只是在看起来相似的两个入口里执行.</strong></p>\n",
            "url": "https://vw2x.vercel.app/blog/260327_codex-zsh-interactive-shell",
            "title": "在 Codex 里重新理解 zsh",
            "summary": "同一软件, 一条命令, 两种结果. 让我开始区分 zsh 的不同启动方式. ",
            "date_modified": "2026-03-27T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/en/blog/20260323_mobileagent-v3-testing",
            "content_html": "<p>Today I ran a side-by-side experiment on the same problem across two projects and spent 3 hours on it. The conclusion was clear:</p>\n<p>In the business environment of mobile security and gray-market app testing, chasing <code>full automation coverage</code> can take far more time than doing the clicks by hand.</p>\n<p>I spent enough time on this that I want to record how this should be designed from an engineering point of view when a process needs to run stably for a long time inside a private tool app.</p>\n<h2>Full-AI Automation Attempts: Open-AutoGLM and MobileAgent-v3</h2>\n<p>Once the goal shifts from <code>can run</code> to <code>close to 100% stable</code>, the difference becomes obvious:</p>\n<ol>\n<li><code>Open-AutoGLM</code> is a single loop: screenshot -&gt; action -&gt; execute.</li>\n<li><a href=\"https://github.com/X-PLUG/MobileAgent\">MobileAgent-v3</a> adds post-action reflection and failure branching, and repeated failures can trigger replanning.</li>\n</ol>\n<p><img src=\"/blog/260323_MobileAgentv3/image-2.png\" alt=\"Screenshot\"></p>\n<p>Pretty impressive. It feels like it grew a brain.</p>\n<p>I turned one gray-market test flow into a workflow file similar to GitHub Workflow and even gave screenshots to the AI for context, hoping to lock it down once and trust it every time.</p>\n<p>But <code>MobileAgent-v3</code> was very slow at post-action verification and failure branching. <strong>A single judgment took about 10 seconds</strong>, which meant the app could ask for root permission and the 10-second countdown would expire before the agent reacted. Then the flow had to roll back and start over. A setup that depends on server-side judgment is really too slow. One full round took 10 minutes. I was getting anxious next to it, and in the end I granted root, the device rebooted because of a device issue, and everything had to start again.</p>\n<p>That was awkward.</p>\n<p>Beyond that, gray-market environments usually share three traits:</p>\n<ol>\n<li>They are private apps with a lot of startup mapping and permission pre-processing.</li>\n<li>UI disturbance is frequent: pop-ups, ads, network state changes, and even device reboots can break the chain.</li>\n<li>The regression chain is long, and one complete run can easily exceed 15 minutes.</li>\n</ol>\n<p>For the same chain, the critical click may only take a few seconds if a human does it.</p>\n<p>The more you push for end-to-end full automation, the more time you may waste in the least stable part.</p>\n<p>So the metric can be split out from <code>automation coverage</code> into:</p>\n<ol>\n<li>Time per successful run</li>\n<li>Recovery cost after failure</li>\n</ol>\n<h2>Back to Scripts and Humans</h2>\n<p>For the Fake Location chain, I changed the flow to <code>stable segments automated + script prompts x human actions</code>.</p>\n<ol>\n<li>The machine handles stable steps: root, install, server, health check.</li>\n<li>A human handles high-variance steps: critical taps, temporary pop-up handling, confirmation of file selection paths.</li>\n<li>After the human action, the script must immediately return to assertions. No black box is allowed to keep running.</li>\n</ol>\n<p>I also kept the GitHub Workflow-like approach, but turned the protocol into a verifiable contract instead of documentation:</p>\n<ol>\n<li>Use JSON Schema for the <code>request</code> / <code>candidate</code> / <code>trace</code> / <code>verification</code> fields.</li>\n<li>Provide matching example JSON.</li>\n<li>Run the validator and workspace validation scripts on every change.</li>\n</ol>\n<p><img src=\"/blog/260323_MobileAgentv3/image-1.png\" alt=\"Screenshot\"></p>\n<h2>The Core Point</h2>\n<p>The core of automation is not to replace humans. It is to script what is deterministic as much as possible, and to find determinism inside what is not.</p>\n<p>If something really is uncertain, do not force it. Separate the uncertainty and use verifiable mechanisms to keep the cost of each layer as low as possible.</p>\n<p>Oh, and I also fixed a BUG along the way.</p>\n<p><img src=\"/blog/260323_MobileAgentv3/image.png\" alt=\"BUG\"></p>\n<p>I did not expect the PR to merge so quickly, and the homepage even got a small icon. That was a nice surprise.</p>\n",
            "url": "https://vw2x.vercel.app/en/blog/20260323_mobileagent-v3-testing",
            "title": "MobileAgent-v3 Testing",
            "summary": "If you want stable automation for gray-market app testing, abandon the fantasy of full automation first.",
            "date_modified": "2026-03-23T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/blog/260323_MobileAgentv3",
            "content_html": "<p>今天把同个问题在两个项目里做了对照实验, 花了 3h 时间, 得到一个结论: </p>\n<p>在移动安全黑灰产测试的业务环境, 追求 <code>全自动覆盖率</code> 会花费比人手点多得多的时间</p>\n<p>花了这么多时间, 我得记录一下, 当要在私有工具 App 上长期稳定执行流程时, 工程上该怎么设计.</p>\n<h2>全 AI 自动化尝试: Open-AutoGLM 和 MobileAgent-v3 尝试</h2>\n<p>但当目标从 <code>能跑</code> 变成 <code>接近 100% 稳定</code>, 差异会出现:</p>\n<ol>\n<li><code>Open-AutoGLM</code> 是单环路: 截图 -&gt; 动作 -&gt; 执行.</li>\n<li><a href=\"https://github.com/X-PLUG/MobileAgent\">MobileAgent-v3</a> 多了动作后反射与失败分流机制, 连续失败可触发重新规划.</li>\n</ol>\n<p><img src=\"/blog/260323_MobileAgentv3/image-2.png\" alt=\"截图\"></p>\n<p>挺厉害的, 感觉长脑子了.</p>\n<p>我将某个黑灰产测试的流程, 固定为类似 Github Workflow 的流程文件, 甚至截图给到 AI 辅助理解, 以期一次固定, 次次放心.</p>\n<p>但 <code>MobileAgent-v3</code> 的动作后验证和失败分流很慢, <strong>一次判断居然需要 10s 左右</strong>, 以至于这个黑灰产 App 问我要 root 权限 10s 倒计时一过就过了, 然后流程需要回滚, 需要从头再来 (这种依赖服务端判断的方案真的是太慢了), 一轮下来, 跑通用了 10min, 我在旁边急急急, 最后给了 root 权限, 因为设备问题, 重启了, 一切又重来.</p>\n<p>尬住.</p>\n<p>除此之外, 对灰黑产环境进行复现, 往往有 3 个共性:</p>\n<ol>\n<li>私有 app, 启动映射和权限前置处理较多.</li>\n<li>UI 扰动频繁, 弹窗, 广告, 网络态变化, 甚至导致设备重启都会打断链路.</li>\n<li>回归链路长, 一次完整执行可能超过 15 分钟.</li>\n</ol>\n<p>而同一条链路里, 关键点击如果交给人做, 可能只需要几秒. </p>\n<p>越追求端到端全自动, 越可能在最不稳定的段落浪费最多时间.</p>\n<p>所以指标可以从 <code>自动化覆盖率</code> 拆分为:</p>\n<ol>\n<li>单位成功耗时</li>\n<li>失败后恢复成本</li>\n</ol>\n<h2>回到脚本和人工</h2>\n<p>在 Fake Location 链路里, 我把流程改成 <code>稳定段自动化 + 脚本提示x人工动作</code>.</p>\n<ol>\n<li>机器负责稳定步骤: root, install, server, healthcheck.</li>\n<li>人负责高波动步骤: 关键点击, 临时弹窗处理, 文件选择路径确认.</li>\n<li>人工动作后, 必须立刻回到脚本断言, 不允许黑盒继续跑.</li>\n</ol>\n<p>除此之外, 仍然使用类 Github Workflow 的方案, 把协议做成可校验契约, 而不是文档描述</p>\n<ol>\n<li>用 JSON Schema 写 request/candidate/trace/verification 字段.</li>\n<li>配套 example JSON.</li>\n<li>每次改动都跑 validator 与 workspace 校验脚本.</li>\n</ol>\n<p><img src=\"/blog/260323_MobileAgentv3/image-1.png\" alt=\"截图\"></p>\n<h2>本质</h2>\n<p>自动化的核心不是替代人, 而是尽可能把确定性的东西脚本化, 不确定的东西寻找其确定性. </p>\n<p>实在不确定咱也别犟, 把不确定性分离, 用可验证机制把每层的成本压到最低.</p>\n<p>哦对了, 顺手修了个 BUG</p>\n<p><img src=\"/blog/260323_MobileAgentv3/image.png\" alt=\"BUG\"></p>\n<p>没想到 pr 提上去没一会就合并了, 主页还多了个小图标, 蛮开心的.</p>\n",
            "url": "https://vw2x.vercel.app/blog/260323_MobileAgentv3",
            "title": "MobileAgent-v3 测试",
            "summary": "黑灰产 App 自动化测试想稳定, 先放弃全自动幻觉.",
            "date_modified": "2026-03-23T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/en/blog/20260319_nextjs-auth-bypass-cases",
            "content_html": "<h2>Why I Wrote This</h2>\n<p>Recently I was working on a Next.js app. While discussing one middleware implementation, I started thinking about a possible authorization bypass.</p>\n<p>AI mentioned <code>CVE-2025-29927</code>, which is a Next.js middleware authorization bypass. Many Next.js apps put access control in middleware, and if that layer can be skipped, the page, route handler, or API route behind it may be exposed directly.</p>\n<p>The core issue is that an internal control signal was influenced by external input, and because it was not tightly bound to context, the authorization check could be bypassed.</p>\n<p>The entry points for <code>CVE-2023-48309</code>, <code>CVE-2022-24858</code>, and <code>CVE-2023-27490</code> are similar.</p>\n<p>With AI pushing more people to build their own sites and apps, the usual pattern is &quot;make it work first&quot;, which makes these mistakes very easy to introduce. If you do not pay attention, they can become real loss events.</p>\n<h2>Context</h2>\n<h3>1) <code>CVE-2025-29927</code>, Next.js middleware authorization bypass</h3>\n<ul>\n<li>Scenario: the app puts access control in middleware and decides internal access control based on a request header.</li>\n<li>Attack path: an external request injects a signal that affects internal semantics, causing middleware to be skipped.</li>\n<li>Risk: once the front authorization layer is bypassed, the downstream routes are exposed.</li>\n</ul>\n<p>External input controls an internal flag -&gt; possible bypass.</p>\n<h3>2) <code>CVE-2023-48309</code>, NextAuth possible user mocking</h3>\n<ul>\n<li>Scenario: default middleware plus overly weak authorization checks.</li>\n<li>Attack path: after the system produces a token, it only checks <code>if (token)</code> and lets the request through.</li>\n</ul>\n<p>If it exists, it is treated as valid. That is a bit absurd.</p>\n<h3>3) <code>CVE-2022-24858</code>, NextAuth open redirect</h3>\n<ul>\n<li>Scenario: the login callback accepts a user-controlled <code>callbackUrl</code>.</li>\n<li>Attack path: combined with phishing, the trust chain gets used for <code>trust laundering</code>.</li>\n</ul>\n<p>The redirect is not bound-checked.</p>\n<h3>4) <code>CVE-2023-27490</code>, missing OAuth checks (<code>state/pkce/nonce</code>)</h3>\n<ul>\n<li>Scenario: OAuth login does not finish in one request. It goes through several hops, including browser redirects, third-party authorization, and the server callback. If any of <code>state</code>, <code>pkce</code>, or <code>nonce</code> is not checked correctly, the whole chain can get bound to the wrong identity.</li>\n<li>The three fields:<ul>\n<li><code>state</code>: confirms whether this callback belongs to the login request I just started, so nobody can inject a fake callback.</li>\n<li><code>pkce</code>: confirms that the person who received the <code>code</code> and the person who exchanges it for a token are the same client, so a stolen code cannot be reused by someone else.</li>\n<li><code>nonce</code>: confirms that the returned token really belongs to this login, so old tokens are not replayed and identities are not mixed up.</li>\n</ul>\n</li>\n<li>The attack path can be understood in five steps:<ol>\n<li>The attacker tampers with the authorization entry parameters and tricks the user into opening a modified OAuth link.</li>\n<li>The user logs in normally at the IdP, and the platform returns the <code>code</code> and related parameters.</li>\n<li>The affected implementation does not fully verify <code>state/pkce/nonce</code>, or it fails to stop when the check fails.</li>\n<li>Some failure branches do not terminate or clean up completely, so intermediate artifacts can still be reused.</li>\n<li>In the end, the system may bind the login state to the wrong person. Best case, account state becomes inconsistent; worst case, the identity is taken over.</li>\n</ol>\n</li>\n<li>Risk: it does not always show up as &quot;account takeover in seconds&quot;, but it does blur identity boundaries. For an authentication system, that is a high-risk signal.</li>\n<li>Defense: verification must be mandatory, the flow must be atomic, failures must fail closed, and checks must happen before side effects.</li>\n<li>What I took away: having these fields is not enough. The checks must be impossible to skip, impossible to downgrade, and must terminate on failure.</li>\n</ul>\n<h2>Notes</h2>\n<p>For an attacker:</p>\n<ol>\n<li>Which inputs does this authorization depend on</li>\n<li>Which of those inputs can I control</li>\n<li>Can I intervene in the middle, test the binding, and capture intermediate state</li>\n</ol>\n<p>For the code author:</p>\n<ol>\n<li>Which inputs does this authorization depend on</li>\n<li>Can the client influence those inputs</li>\n<li>Does the failure path really fail closed</li>\n<li>Does the fix restore the invariant instead of only blocking a known payload</li>\n</ol>\n<p>If any layer treats untrusted input as a trusted control signal, the system will leak.</p>\n<h2>Hardening</h2>\n<p>In my Next.js project, I would make these changes:</p>\n<ul>\n<li>middleware layer: keep front access control and early rejection so the user experience stays responsive.</li>\n<li>page/api layer: even if the front layer passes, still verify identity and authorization truth.</li>\n<li>data layer: bind real queries.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>NVD: <a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-29927\">CVE-2025-29927</a></li>\n<li>GHSA: <a href=\"https://github.com/vercel/next.js/security/advisories/GHSA-f82v-jwr5-mffw\">Next.js advisory</a></li>\n<li>Patch:<ul>\n<li><a href=\"https://github.com/vercel/next.js/commit/52a078da3884efe6501613c7834a3d02a91676d2\">commit-1</a></li>\n<li><a href=\"https://github.com/vercel/next.js/commit/5fd3ae8f8542677c6294f32d18022731eab6fe48\">commit-2</a></li>\n</ul>\n</li>\n<li>CVE refs:<ul>\n<li><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2023-48309\">CVE-2023-48309</a></li>\n<li><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2022-24858\">CVE-2022-24858</a></li>\n<li><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2023-27490\">CVE-2023-27490</a></li>\n</ul>\n</li>\n<li>Research writeup: <a href=\"https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware\">zhero</a></li>\n</ul>\n",
            "url": "https://vw2x.vercel.app/en/blog/20260319_nextjs-auth-bypass-cases",
            "title": "CVE-2025-29927 and CVE-2023-27490",
            "summary": "I do not try to control the whole system. I focus on the parts I can control and influence.",
            "date_modified": "2026-03-19T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/blog/260319_CVE-2025-29927",
            "content_html": "<h2>为什么写这篇</h2>\n<p>最近在写 Next.js 应用, 聊到某个中间件实现的时候, 想到可能存在越权问题</p>\n<p>AI 提到了 <code>CVE-2025-29927</code> 这个漏洞, 讲的是 Next.js middleware 授权绕过.\n很多 Next.js 应用会在 middLeware 做前置访问控制, 这层如果被跳过, 后面的 page.route handler.api 就可能直接暴露风险</p>\n<p>这个漏洞的本质是, 某个本该内部的控制信号, 被外部输入影响了, 且因为未与上下文强绑定, 导致越权绕过.</p>\n<p><code>CVE-2023-48309</code>, <code>CVE-2022-24858</code>, <code>CVE-2023-27490</code> 这几个洞的切入点也差不多, </p>\n<p>因为 AI 的发展, 未来大家都会想手搓一些自己的网站或者应用, 一般就是能跑就行, 自然容易出现这些漏洞, 如果不怎么在意容易资损.</p>\n<h2>上下文</h2>\n<h3>1) <code>CVE-2025-29927</code> , Next.js middleware authorization bypass</h3>\n<ul>\n<li>场景: 应用把访问控制放在 middleware, 直接通过某个请求头判断内部的访问控制; </li>\n<li>攻击链路: 外部请求注入可影响内部语义的信号, 使得 middleware 跳过.</li>\n<li>风险: 前置授权层被绕过后, 后续路由直接暴露.</li>\n</ul>\n<p>外部输入控制内部 flag -&gt; 可能绕过</p>\n<h3>2) <code>CVE-2023-48309</code> , NextAuth possible user mocking</h3>\n<ul>\n<li>场景: 默认 middleware + 授权判断过弱.</li>\n<li>攻击链路: 拿到流程中产生的 token 后, 系统只用 <code>if (token)</code> 判断通过.</li>\n</ul>\n<p>存在即合理, 有点搞</p>\n<h3>3) <code>CVE-2022-24858</code> , NextAuth open redirect</h3>\n<ul>\n<li>场景: 登录回调接受用户可控 <code>callbackUrl</code>.</li>\n<li>攻击链路: 配合钓鱼, 信任链被用来做 <code>trust laundering</code>.</li>\n</ul>\n<p>redirect 没有做绑定校验</p>\n<h3>4) <code>CVE-2023-27490</code> , OAuth checks missing(<code>state/pkce/nonce</code>)</h3>\n<ul>\n<li>场景: OAuth 登录不是一次请求就结束, 中间会跳好几次, 包括浏览器跳转, 第三方授权, 服务端回调. 只要 <code>state/pkce/nonce</code> 有一项没校验好, 整条链路就可能绑错.</li>\n<li>3 个字段:<ul>\n<li><code>state</code>: 用来确认 &quot;这次回调是不是我刚刚发起的那次登录&quot;, 防别人塞一条假的回调进来.</li>\n<li><code>pkce</code>: 用来确认&quot;拿到 code 的人&quot;和&quot;去换 token 的人&quot;是同一个客户端, 防 code 被偷走后被别人拿去用.</li>\n<li><code>nonce</code>: 用来确认&quot;回来的 token&quot;确实对应这次登录, 防旧 token 重放或账号对错人.</li>\n</ul>\n</li>\n<li>攻击链路可以理解成 5 步:<ol>\n<li>攻击者先改授权入口参数, 诱导用户点一条被动过手脚的 OAuth 链接.</li>\n<li>用户在 IdP 侧正常登录, 平台按流程返回 <code>code</code> 和相关参数.</li>\n<li>受影响实现对 <code>state/pkce/nonce</code> 的检查不完整, 或者检查失败了也没停下来.</li>\n<li>某些失败分支没有彻底中止和清理, 中间产物还能被继续利用.</li>\n<li>最后系统可能把登录态绑错人, 轻则账号状态混乱, 重则身份被冒用.</li>\n</ol>\n</li>\n<li>风险: 它不一定每次都表现成&quot;秒被接管账号&quot;, 但会让身份边界变模糊. 对认证系统来说, 这就是高危信号.</li>\n<li>防守要点: 校验要 mandatory, 流程要 atomic, 失败要 fail-closed, 并且严格先校验再做副作用.</li>\n<li>我抓到的本质: 不只是有这几个字段就行, 还要保证检查不可跳过, 不可降级, 失败即终止.</li>\n</ul>\n<h2>记录</h2>\n<p>对于攻击者:</p>\n<ol>\n<li>这次授权依赖哪些输入</li>\n<li>哪些输入是我能控制的</li>\n<li>是否能在过程中操作, 测试其绑定关系, 获得中间态</li>\n</ol>\n<p>对于代码编写者:</p>\n<ol>\n<li>这次授权依赖哪些输入.</li>\n<li>这个输入是否可能被客户端影响.</li>\n<li>失败路径是否真正 fail-closed.</li>\n<li>修复是否恢复了不变量, 而不是只挡住已知 payload.</li>\n</ol>\n<p>只要任何一层把 &quot;不可信输入&quot; 当成了&quot;可信控制信号&quot;, 系统就会漏.</p>\n<h2>优化</h2>\n<p>在我的 Next.js 项目中, 进行如下更新</p>\n<ul>\n<li>middleware 层: 前置访问控制与早期拒绝, 确保使用体验不卡顿. </li>\n<li>page/api 层: 即使前置层通过, 仍要做身份与权限真相校验.</li>\n<li>数据层: 绑定真实查询.</li>\n</ul>\n<h2>参考</h2>\n<ul>\n<li>NVD: <a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-29927\">CVE-2025-29927</a></li>\n<li>GHSA: <a href=\"https://github.com/vercel/next.js/security/advisories/GHSA-f82v-jwr5-mffw\">Next.js advisory</a></li>\n<li>Patch:<ul>\n<li><a href=\"https://github.com/vercel/next.js/commit/52a078da3884efe6501613c7834a3d02a91676d2\">commit-1</a></li>\n<li><a href=\"https://github.com/vercel/next.js/commit/5fd3ae8f8542677c6294f32d18022731eab6fe48\">commit-2</a></li>\n</ul>\n</li>\n<li>CVE refs:<ul>\n<li><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2023-48309\">CVE-2023-48309</a></li>\n<li><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2022-24858\">CVE-2022-24858</a></li>\n<li><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2023-27490\">CVE-2023-27490</a></li>\n</ul>\n</li>\n<li>Research writeup: <a href=\"https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware\">zhero</a></li>\n</ul>\n",
            "url": "https://vw2x.vercel.app/blog/260319_CVE-2025-29927",
            "title": "CVE-2025-29927 CVE-2023-27490",
            "summary": "不想着掌控全局, 只控制我能控制的, 影响更多我能影响的.",
            "date_modified": "2026-03-19T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/en/blog/20260314_algorithm-assistant-mcp-from-gui-to-api",
            "content_html": "<h2>1. Introduction: What Kind of Reverse-Engineering Infrastructure Do We Need in the AI Era</h2>\n<p>On the first day I touched Android security after graduating, I used a Frida script to successfully change a function return value. Watching the app behave according to my will kept me excited until 3 a.m.</p>\n<p>But as I did more reverse engineering, it started to feel different.\nEvery time I picked up a new app, I had to find the entry point again, write hooks, stitch parameters together, and inspect logs...\nWhat used to feel exciting turned into a chore: &quot;Do I really have to write all those hooks again?&quot;</p>\n<p>So I started &quot;wrapping&quot; things: Frida templates, Python scripts, Xposed modules.\nBut I soon found that requirements are always customized, and my wrappers usually over-engineered themselves. In the end I still had to go back and write the scripts by hand, and then copy a lot of helper functions over manually.\nThe copy-paste happened over and over, easily more than ten times, and it started to feel repetitive and dull.</p>\n<p>As my high school math teacher would say, that is not &quot;beautiful&quot;.</p>\n<p>So what is &quot;beautiful&quot; reverse engineering in the AI era?</p>\n<p>My current view is simple: the less repetitive work I have to do, the more beautiful it is. The more it helps me be lazy, the better.</p>\n<p>With large models like Cursor, Claude, and Codex becoming common, AI-assisted static analysis has already become normal. A few days ago I saw <code>frida-mcp</code>, which made me realize that AI can also do dynamic analysis now.</p>\n<p>My own work also keeps running into small analysis tasks like card-code bypasses and encryption-chain inspection. The app I use most often is Jiang Ge&#39;s Algorithm Assistant Pro, which can handle pop-up dismissal, allow screenshots, enhance Reqable packet capture, monitor file read/write, hook common crypto algorithms, and let me choose which classes to hook. It is much more convenient than writing Xposed scripts.</p>\n<p>But after only a few runs, my &quot;牛夫人&quot; feeling came back.\nWhy do I still have to pick which app to hook every time, manually add methods, restart the process, check logs, and sometimes even drop a Frida script into <code>/sdcard/</code> and open a file manager to load it? Why am I clicking all this again? Half an hour later I end up with a pile of logs that I still have to analyze myself.</p>\n<p>What if AI clicks for me?\nIf you try it, even Opus 4.6 has to think before every click. If it opens a few pages, it has to think ten times. Watching it operate is slower than clicking by hand, and when I remember that it also costs me tokens, I start feeling annoyed.</p>\n<p>That is not just unbeautiful. It is almost ugly.</p>\n<p>In fact, GUIs are designed for humans. For AI, text commands naturally match the input format of LLMs and can be chained into complex workflows. APIs and CLIs are the natural shape for agents.</p>\n<p>Yesterday I also saw <a href=\"https://github.com/HKUDS/CLI-Anything\">CLI-Anything</a>, which takes open-source software and exposes all of its functions through CLI interfaces so agents can use them better.</p>\n<p>That applies to open-source projects. Closed-source tools need a reverse-engineering step.</p>\n<p>So this series starts an efficiency exploration for my own work: <strong>AI-oriented reverse engineering of GUI tools</strong>.</p>\n<p>What I want to do is peel these human-facing GUI apps through reverse engineering and extract APIs that large models can call directly. Things I have already done should not need to be clicked through again and again.</p>\n<h2>2. Methodology: Three Common Entry Points from GUI to API</h2>\n<p>When you get an app that can only be used through UI clicks and want to turn it into a scriptable or AI-controllable interface, do not rush to think about button macros or UI automation testing. That is too human.</p>\n<p>You can hope the author provides an interface, or you can explore it yourself first. Usually there are three common entry points:</p>\n<h3>1. Find the persistent storage point</h3>\n<p>Every UI click eventually maps to some data change somewhere.\n<strong>Idea:</strong> monitor <code>/data/data/&lt;pkg&gt;/shared_prefs</code>, <code>databases</code>, and external storage under <code>/sdcard/Android/data/&lt;pkg&gt;/files</code>. Once you find a config file (XML/JSON/DB), try modifying it with a script to bypass the UI.</p>\n<h3>2. Find the IPC interface</h3>\n<p>If the config is not in normal files, or changing the files does not work, then there is probably in-memory caching or cross-process communication.\n<strong>Idea:</strong> decompile the APK and focus on <code>provider</code>, <code>receiver</code>, and <code>service</code> entries in <code>AndroidManifest.xml</code>, especially components with <code>exported=true</code>. A lot of tools communicate between the UI and backend services through these standard Android mechanisms.</p>\n<h3>3. Find the CLI</h3>\n<p>Some tools hide command-line interfaces to make debugging or power-user workflows easier.\n<strong>Idea:</strong> inspect binaries, install scripts, or search the decompiled code for keywords like <code>Runtime.getRuntime().exec</code> and <code>su -c</code> to uncover hidden shell commands.</p>\n<h2>3. Case Study: Algorithm Assistant MCP</h2>\n<p>The phased goals were:</p>\n<ol>\n<li>Toggle the apps that Algorithm Assistant should affect inside LSPosed</li>\n<li>Toggle the apps inside the Algorithm Assistant UI</li>\n<li>Read, write, and apply configuration for a single app, including options such as hash hooks and custom method hooks</li>\n<li>Write and apply Frida scripts for a single app</li>\n<li>Extract, structure, and query logs</li>\n</ol>\n<p>Everything should be moved toward CLI so an agent can use it directly.</p>\n<h3>1. Persistence point: confirm package-level config first, then trace AppSwitch</h3>\n<p>In the experiment, LSPosed had these apps selected:</p>\n<ul>\n<li><code>System Framework</code></li>\n<li><code>com.reqable.android</code></li>\n<li><code>com.example.app</code></li>\n</ul>\n<p>Algorithm Assistant&#39;s own UI had these selected:</p>\n<ul>\n<li><code>com.lerist.fakelocation</code></li>\n<li><code>com.example.app</code></li>\n</ul>\n<p>I started with the usual directories:</p>\n<ul>\n<li><code>/data/user/0/com.junge.algorithmAidePro/shared_prefs</code></li>\n<li><code>/data/user/0/com.junge.algorithmAidePro/files</code></li>\n<li><code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config</code></li>\n<li><code>/data/adb/lspd/config</code></li>\n</ul>\n<p>One thing became clear quickly: the target package&#39;s <strong>hook config</strong> really lives in the external directory\n<code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config/&lt;targetPackage&gt;.json</code></p>\n<p>But that is only config. The Algorithm Assistant UI&#39;s <strong>&quot;application selection state (AppSwitch)&quot;</strong> does not appear directly in the app&#39;s private directory. Under <code>shared_prefs</code> I saw several <code>.sp</code> files that looked like config, but searching for the package name did not hit anything.</p>\n<p>The easiest mistake here is to assume &quot;no plain-text package name = no local persistence&quot;.\nThat is not true. Since the file layer did not show it, I moved to the code layer instead (<strong>the path of least resistance</strong>).</p>\n<h3>2. IPC interface: the Provider exposed the key read/check surface</h3>\n<p>After pulling <code>base.apk</code>, the point was not to read everything. The point was to find the config read/write path.</p>\n<p>Decompilation quickly surfaced a few key pieces:</p>\n<ol>\n<li><code>ConfigReader.getInstanceByAlgorithmAidePro(String str)</code></li>\n<li><code>ConfigProvider</code></li>\n<li><code>android:authorities=&quot;algorithmAidePro&quot;</code></li>\n<li><code>xposedsharedprefs=true</code></li>\n</ol>\n<p>The most important one was <code>ConfigProvider</code>. It exposed two query dimensions directly:</p>\n<ul>\n<li><code>projection=config</code></li>\n<li><code>projection=AppSwitch</code></li>\n</ul>\n<pre><code class=\"language-bash\"># Query AppSwitch\nadb shell content query --uri content://algorithmAidePro/com.example.app --projection AppSwitch\n\n# Write AppSwitch\nadb shell content insert --uri content://algorithmAidePro/com.example.app --bind AppSwitch:s:true\n</code></pre>\n<p>Use the Provider as the read/check surface first, then infer the real storage point from there.</p>\n<h4>Main AppSwitch location: <code>AppSwitch.json</code>, not the Provider</h4>\n<p>At that point, one odd thing became obvious:</p>\n<ul>\n<li>The Provider could read <code>AppSwitch</code></li>\n<li>But <code>/data/user/0/com.junge.algorithmAidePro/shared_prefs/AppSwitch.xml</code> did not exist</li>\n</ul>\n<p>So it was not that file. Searching for <code>AppSwitch</code> led to the real location:</p>\n<pre><code class=\"language-text\">/data/system/junge/AppSwitch.json\n</code></pre>\n<p>Reading it directly gives a package-name-to-boolean map, for example:</p>\n<pre><code class=\"language-json\">{\n  &quot;com.example.app&quot;: true,\n  &quot;com.lerist.fakelocation&quot;: true\n}\n</code></pre>\n<p>On this device and this version, the Algorithm Assistant UI&#39;s app-selection source of truth already matched the constants in <code>AlgorithmServer</code>:</p>\n<ul>\n<li><code>APP_SWITCH = AppSwitch.json</code></li>\n<li><code>BASE_DIR = /data/system/junge/</code></li>\n</ul>\n<h3>3. Taking over LSPosed scope through CLI: <code>LSPosed_mod</code></h3>\n<p>LSPosed scope is another config set, and it does not live in the Algorithm Assistant directory.</p>\n<p>The real location is:</p>\n<pre><code class=\"language-text\">/data/adb/lspd/config/modules_config.db\n/data/adb/lspd/config/modules_config.db-wal\n</code></pre>\n<p>String hits across the main DB, WAL, and backup DB confirmed that this database stores LSPosed module enablement data.\nBut editing it directly with <code>sqlite3</code> is not ideal, so I switched to the CLI exposed by <code>LSPosed_mod</code>.</p>\n<p>First confirm the environment:</p>\n<ol>\n<li>The device has <code>/data/adb/lspd/bin/cli</code>, and the CLI needs root permission</li>\n<li><code>Enable CLI</code> must be turned on in LSPosed Manager</li>\n</ol>\n<pre><code class=\"language-bash\">su -c /data/adb/lspd/bin/cli scope set -a com.junge.algorithmAidePro com.qiyi.video/0\n</code></pre>\n<h2>4. Continued Exploration: Log Source, Hook DSL, and the Dynamic-Analysis Loop</h2>\n<h3>1. Finding the log source: how it narrowed down to SQLite step by step</h3>\n<pre><code class=\"language-text\">/sdcard/Android/media/&lt;targetPackage&gt;/database/algorithmAidePro.db\n</code></pre>\n<h4>Round 1: start from the UI&#39;s &quot;save all logs&quot; action</h4>\n<p>The most natural first idea was to automate the &quot;save all logs&quot; button on the log page.\nLater I confirmed two facts:</p>\n<ol>\n<li>The UI button eventually reaches <code>ThreadSaveLogList -&gt; ConfigReader.createLogFile(null)</code></li>\n<li>The exported text file lands in:</li>\n</ol>\n<pre><code class=\"language-text\">/sdcard/Android/data/com.junge.algorithmAidePro/files/Log/&lt;yyyy-MM-dd_HH_mm_ss&gt;.log\n</code></pre>\n<p>In the first round, I followed already verified paths:</p>\n<ol>\n<li><code>content://algorithmAidePro/...</code></li>\n<li>logs or config files already written to external storage</li>\n</ol>\n<p>First, <code>content://algorithmAidePro/...</code> could reliably read:</p>\n<ol>\n<li><code>projection=config</code></li>\n<li><code>projection=AppSwitch</code></li>\n</ol>\n<p>But there was no export-log-related projection, and no stable <code>insert/update/call</code> write entry either.</p>\n<p>Second, the Frida log chain existed independently.</p>\n<pre><code class=\"language-text\">/sdcard/Android/data/com.junge.algorithmAidePro/files/files/fridaLog.html\n</code></pre>\n<p>That file can be pulled directly, but it only corresponds to Frida script logs, not native hook logs.\nAnd while <code>fridaLog.html</code> does exist, it is more of a UI/export surface and not necessarily the lowest runtime write surface.\nLater in the actual device run, <code>com.example.app</code> exposed a more direct file:</p>\n<pre><code class=\"language-text\">/sdcard/Android/media/com.example.app/database/frida.log\n</code></pre>\n<p>This file records Frida runtime logs directly, and it is a more direct smoke-test target than <code>fridaLog.html</code>.</p>\n<h4>Round 2: start doubting whether UI text export is the best target</h4>\n<p>At this point it was clear that &quot;save all logs&quot; is itself a human-facing export action. Its essence is:</p>\n<ul>\n<li>read data from the real internal source</li>\n<li>format it as text</li>\n<li>write it to <code>files/Log/*.log</code></li>\n</ul>\n<p>Trying to replace the button click felt like the wrong direction, and it could not avoid the UI trigger anyway.</p>\n<p>The better question was: <strong>where is the raw storage behind the data shown on the log page?</strong>\nThe idea shifted from &quot;simulate UI export&quot; to &quot;find the log source directly&quot;.</p>\n<h4>Round 3: keep tracing from private directories and system-side config</h4>\n<p>Next I checked a few places that looked the most likely to hold logs:</p>\n<ol>\n<li><code>/data/user/0/com.junge.algorithmAidePro/files</code></li>\n<li><code>/data/user/0/com.junge.algorithmAidePro/databases</code></li>\n<li><code>/data/system/junge/</code></li>\n</ol>\n<p><code>/data/system/junge/</code> had a lot of content, and it looked very much like the Algorithm Assistant system-side repository:</p>\n<ul>\n<li><code>AppSwitch.json</code></li>\n<li><code>logList.json</code></li>\n<li><code>com.example.app/config.json</code></li>\n</ul>\n<p>At this point I already knew:</p>\n<ul>\n<li><code>/data/system/junge/</code> is more of a config repository than a log-detail repository</li>\n<li><code>files/Log/*.log</code> is more of an export result than the long-term source</li>\n<li>the real log source is more likely to be a separate database per target package</li>\n</ul>\n<p>Following that line naturally led to <code>Android/media/&lt;pkg&gt;/database</code>.</p>\n<h4>Round 4: search globally by database name</h4>\n<p>Since the UI page is probably backed by structured data, I should search for the database rather than keep staring at text files.</p>\n<p>The turning point was finding:</p>\n<pre><code class=\"language-text\">/data/media/0/Android/media/com.example.app/database/algorithmAidePro.db\n</code></pre>\n<p>Once that path appeared, several things clicked:</p>\n<ol>\n<li>The path is per target package, which fits the idea of one separate log store per app</li>\n<li>The name is directly <code>algorithmAidePro.db</code>, which is clearly tied to the product</li>\n<li>It lives under the target app&#39;s media directory, not the UI export directory</li>\n</ol>\n<p>At that point there was no reason to keep guessing. I pulled it and opened it with SQLite.</p>\n<h4>Round 5: verify whether this DB is actually the log source</h4>\n<p>After pulling the DB, <code>sqlite_master</code> was very clean:</p>\n<pre><code class=\"language-sql\">table|LOG_DATA_V2|LOG_DATA_V2\ntable|android_metadata|android_metadata\ntable|sqlite_sequence|sqlite_sequence\n</code></pre>\n<p>The schema also pointed directly to logging:</p>\n<pre><code class=\"language-sql\">CREATE TABLE IF NOT EXISTS &quot;LOG_DATA_V2&quot; (\n  &quot;_id&quot; INTEGER PRIMARY KEY AUTOINCREMENT,\n  &quot;GROUP&quot; INTEGER NOT NULL,\n  &quot;TYPE&quot; INTEGER NOT NULL,\n  &quot;OBJ_NAME&quot; TEXT,\n  &quot;CLASS_NAME&quot; TEXT,\n  &quot;LOG_NAME&quot; TEXT,\n  &quot;TIME&quot; INTEGER NOT NULL,\n  &quot;IS_READ&quot; INTEGER NOT NULL,\n  &quot;LOG_DETAILS_RAW&quot; BLOB,\n  &quot;CALL_STACK&quot; TEXT\n);\n</code></pre>\n<p>At this point it was basically confirmed:</p>\n<ul>\n<li>this is not a UI export file</li>\n<li>this is the raw structured store behind the log page</li>\n</ul>\n<p>Querying the latest rows also made sense:</p>\n<ul>\n<li><code>com.example.app.MainActivity | unregisterPluginTestReceiver()</code></li>\n<li><code>com.example.app.MainActivity | onDestroy()</code></li>\n<li><code>com.example.app.MainActivity | lambda$setupTestButtons$3$com-example-app-MainActivity()</code></li>\n</ul>\n<p>And the row count on this device was real:</p>\n<pre><code class=\"language-sql\">124\n</code></pre>\n<p>The original idea was:</p>\n<ol>\n<li>Avoid clicking the UI button for &quot;save all logs&quot;</li>\n<li>Make the app generate a <code>.log</code></li>\n<li><code>adb pull</code> it</li>\n</ol>\n<p>What I actually found was:</p>\n<ol>\n<li><code>adb pull /sdcard/Android/media/&lt;pkg&gt;/database/algorithmAidePro.db</code></li>\n<li>Query it directly with <code>sqlite3</code> or a GUI tool</li>\n</ol>\n<p>The latter is much better for future MCP work:</p>\n<ol>\n<li><p>structured</p>\n</li>\n<li><p>filterable</p>\n</li>\n<li><p>sortable</p>\n</li>\n<li><p>incrementally exportable</p>\n</li>\n<li><p>directly convertible to TSV / CSV / JSON</p>\n</li>\n<li><p>The UI text export path has been reverse-engineered, but it is not the best automation target</p>\n</li>\n<li><p><code>content://algorithmAidePro/...</code> is still a reliable read/check surface, not a log-export surface</p>\n</li>\n<li><p>Frida logs still live separately in <code>fridaLog.html</code></p>\n</li>\n<li><p>Native hook logs already have a better non-UI main path: <code>Android/media/&lt;pkg&gt;/database/algorithmAidePro.db</code></p>\n</li>\n</ol>\n<p>The small closed loop I ended up with was not &quot;automatically click save logs&quot;, but &quot;export the structured hook-log database directly through CLI and query it with SQL&quot;.</p>\n<p>Example:</p>\n<pre><code class=\"language-bash\">adb pull /sdcard/Android/media/com.example.app/database/algorithmAidePro.db .\nsqlite3 algorithmAidePro.db &#39;select count(*) from LOG_DATA_V2;&#39;\nsqlite3 -header -column algorithmAidePro.db &quot;\nselect\n  _id,\n  \\&quot;GROUP\\&quot;,\n  TYPE,\n  ifnull(OBJ_NAME,&#39;&#39;) as obj_name,\n  ifnull(CLASS_NAME,&#39;&#39;) as class_name,\n  ifnull(LOG_NAME,&#39;&#39;) as log_name,\n  TIME,\n  length(LOG_DETAILS_RAW) as raw_len\nfrom LOG_DATA_V2\norder by TIME desc\nlimit 10;\n&quot;\n</code></pre>\n<h4>Tip 1: Provider is an extremely strong verification surface</h4>\n<p>If the target exposes a Provider, do not only rely on static analysis.\nBecause a Provider can answer directly:</p>\n<ul>\n<li>whether a key exists</li>\n<li>what its current value is</li>\n<li>which config the business code really reads</li>\n</ul>\n<p>That is much faster than guessing file formats.</p>\n<h4>Tip 2: Xposed modules deserve extra attention under <code>/data/misc/.../prefs</code></h4>\n<p>A lot of people keep staring at:</p>\n<ul>\n<li><code>/data/user/0/&lt;pkg&gt;/shared_prefs</code></li>\n</ul>\n<p>But for modules with <code>xposedsharedprefs</code>, the config may not be in the app-private directory at all. It may live in a shared location that Xposed can read.</p>\n<h3>2. MCP breakdown: separate the three layers of state first</h3>\n<p>If I want to turn Algorithm Assistant into a general Java Hook MCP later, the actions need to be split into at least three layers:</p>\n<ol>\n<li>Write the target package hook config<ul>\n<li>location: <code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config/&lt;targetPackage&gt;.json</code></li>\n</ul>\n</li>\n<li>Turn on the app in the Algorithm Assistant UI<ul>\n<li>current persisted source of truth: <code>/data/system/junge/AppSwitch.json</code></li>\n<li>verification should be split into two layers:<ul>\n<li>storage-layer readback from <code>AppSwitch.json</code></li>\n<li>runtime-layer readback from <code>logList.json</code></li>\n<li>Provider-layer readback from <code>projection=AppSwitch</code> (reference only)</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>Sync LSPosed scope<ul>\n<li>preferred interface: <code>/data/adb/lspd/bin/cli</code></li>\n<li>persistence: <code>/data/adb/lspd/config/modules_config.db</code></li>\n</ul>\n</li>\n</ol>\n<p>If those three layers are not separated, it is very easy to mix states up when building CLI and MCP later.</p>\n<p>What I mainly separated this time was:</p>\n<ul>\n<li>Algorithm Assistant&#39;s own config</li>\n<li>Algorithm Assistant UI&#39;s app-enable state</li>\n<li>LSPosed scope config</li>\n</ul>\n<h3>3. On-device verification: package-level JSON</h3>\n<p>When all default switches are enabled, the package-level JSON on the device roughly looks like this:</p>\n<pre><code class=\"language-json\">{\n  &quot;ApplicationSwitch&quot;: true,\n  &quot;ExceptionSwitch&quot;: true,\n  &quot;SharedPreferencesPutSwitch&quot;: true,\n  &quot;activitySwitch&quot;: true,\n  &quot;assetsSwitch&quot;: true,\n  &quot;cameraHookSwitch&quot;: true,\n  &quot;checkRootSwitch&quot;: true,\n  &quot;cipherSwitch&quot;: true,\n  &quot;closeDialogSwitch&quot;: true,\n  &quot;dialogKeyword&quot;: &quot;注册码,机器码,激活码&quot;,\n  &quot;dialogSwitch&quot;: true,\n  &quot;digestSwitch&quot;: true,\n  &quot;exitSwitch&quot;: true,\n  &quot;fileDeleteSwitch&quot;: true,\n  &quot;fileSwitch&quot;: true,\n  &quot;fileWriteSwitch&quot;: true,\n  &quot;getSharedPreferencesSwitch&quot;: true,\n  &quot;hiddenVpnSwitch&quot;: true,\n  &quot;hiddenWifiProxySwitch&quot;: true,\n  &quot;hiddenXposedSwitch&quot;: true,\n  &quot;justTrustMePlushSwitch&quot;: true,\n  &quot;logSwitch&quot;: true,\n  &quot;macSwitch&quot;: true,\n  &quot;onClickSwitch&quot;: true,\n  &quot;reqableSwitch&quot;: true,\n  &quot;reqableSwitch_native&quot;: true,\n  &quot;screenSwitch&quot;: true,\n  &quot;shellSwitch&quot;: true,\n  &quot;signSwitch&quot;: true,\n  &quot;sqliteDeleteSwitch&quot;: true,\n  &quot;sqliteExecSQLSwitch&quot;: true,\n  &quot;sqliteInsertSwitch&quot;: true,\n  &quot;sqliteOpenSwitch&quot;: true,\n  &quot;sqliteQuerySwitch&quot;: true,\n  &quot;sqliteUpdateSwitch&quot;: true,\n  &quot;textViewSwitch&quot;: true,\n  &quot;webCryptSwitch&quot;: true,\n  &quot;webViewDebugSwitch&quot;: true,\n  &quot;webViewLoadUrlSwitch&quot;: true\n}\n</code></pre>\n<h4>Verification 1: <code>projection=config</code> is not decided by <code>config.xml</code></h4>\n<p>It is easy to assume:</p>\n<ul>\n<li><code>AppSwitch.json</code> controls the UI toggles</li>\n<li><code>config.xml</code> controls the feature config</li>\n<li><code>files/config/&lt;pkg&gt;.json</code> is just an export copy</li>\n</ul>\n<p>But on-device verification showed that at least for <code>projection=config</code>, that is not the case.</p>\n<p>I ran two comparison experiments:</p>\n<ol>\n<li><code>pkg.json.digestSwitch=true</code>, <code>config.xml.digestSwitch=false</code><ul>\n<li><code>adb shell content query --uri content://algorithmAidePro/com.example.app --projection config</code></li>\n<li>returned <code>digestSwitch=true</code></li>\n</ul>\n</li>\n<li><code>pkg.json.digestSwitch=false</code>, <code>config.xml.digestSwitch=false</code><ul>\n<li>same query</li>\n<li>returned <code>digestSwitch=false</code></li>\n</ul>\n</li>\n</ol>\n<p>That is enough to show that:</p>\n<pre><code class=\"language-text\">/sdcard/Android/data/com.junge.algorithmAidePro/files/config/&lt;pkg&gt;.json\n</code></pre>\n<p>is the primary control source for <code>projection=config</code>, and it takes priority over:</p>\n<pre><code class=\"language-text\">/data/misc/&lt;uuid&gt;/prefs/com.junge.algorithmAidePro/config.xml\n</code></pre>\n<p>So when I want to change a target package config by field, I can edit the package-level JSON directly and do not need to touch <code>config.xml</code>.</p>\n<h4>Verification 2: root does not mean unrestricted writes</h4>\n<p>On this machine, the <code>su</code> context is:</p>\n<pre><code class=\"language-text\">uid=0(root) gid=0(root) context=u:r:magisk:s0\n</code></pre>\n<p>SELinux is still <code>Enforcing</code>. Also:</p>\n<ul>\n<li><code>setenforce 0</code> fails directly</li>\n<li>shell redirection that tries to overwrite <code>config.xml</code> gets <code>Permission denied</code></li>\n</ul>\n<p>That means &quot;having root&quot; does not mean &quot;every write path works&quot;. Especially for paths under <code>/data/misc/.../prefs</code>, I need to respect the <code>magisk su</code> context instead of assuming it just works.</p>\n<h4>Verification 3: recommended CLI order for plain file overwrite</h4>\n<p>If the goal is &quot;do not click the UI, just change it correctly from the command line&quot;, the most stable loop has been:</p>\n<ol>\n<li><code>force-stop</code> Algorithm Assistant</li>\n<li>overwrite the package-level JSON</li>\n<li>start Algorithm Assistant</li>\n<li>read back through the Provider for verification</li>\n</ol>\n<p>Command order:</p>\n<pre><code class=\"language-bash\">adb shell am force-stop com.junge.algorithmAidePro\nadb push com.example.app.json /data/local/tmp/com.example.app.json\nadb shell su -c &#39;cp /data/local/tmp/com.example.app.json /sdcard/Android/data/com.junge.algorithmAidePro/files/config/com.example.app.json&#39;\nadb shell monkey -p com.junge.algorithmAidePro -c android.intent.category.LAUNCHER 1\nadb shell content query --uri content://algorithmAidePro/com.example.app --projection config\n</code></pre>\n<h3>4. Custom hook export: the native DSL is already visible</h3>\n<p>I also dumped the &quot;custom hook method&quot; that I can quickly add by hand inside Algorithm Assistant.\nFor <code>com.example.app</code>, there are two files with the same content on the device:</p>\n<ul>\n<li><code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config/com.example.app.json</code></li>\n<li><code>/sdcard/Android/data/com.junge.algorithmAidePro/files/exportConfig/com.example.app.json</code></li>\n</ul>\n<ol>\n<li><code>config/&lt;pkg&gt;.json</code> is the active config</li>\n<li><code>exportConfig/&lt;pkg&gt;.json</code> is the exported snapshot</li>\n<li>The exported file is already the native config syntax accepted by Algorithm Assistant, so I do not need to guess the schema first</li>\n</ol>\n<p>The exported core for <code>com.example.app</code> looked roughly like this:</p>\n<pre><code class=\"language-json\">{\n  &quot;enableScript&quot;: &quot;bezierzhixian.js&quot;,\n  &quot;hookList&quot;: [\n    {\n      &quot;argsValues&quot;: [],\n      &quot;className&quot;: &quot;com.example.app.DemoTarget&quot;,\n      &quot;constructor&quot;: true,\n      &quot;description&quot;: &quot;来自快速添加的Hook&quot;,\n      &quot;enable&quot;: true,\n      &quot;intercept&quot;: false,\n      &quot;methodName&quot;: &quot;&lt;init&gt;&quot;,\n      &quot;parameterSign&quot;: &quot;&quot;,\n      &quot;printLog&quot;: true,\n      &quot;results&quot;: &quot;&quot;\n    },\n    {\n      &quot;argsValues&quot;: [],\n      &quot;className&quot;: &quot;com.example.app.DemoTarget&quot;,\n      &quot;constructor&quot;: false,\n      &quot;description&quot;: &quot;来自快速添加的Hook&quot;,\n      &quot;enable&quot;: true,\n      &quot;intercept&quot;: false,\n      &quot;methodName&quot;: &quot;someMethod&quot;,\n      &quot;parameterSign&quot;: &quot;&quot;,\n      &quot;printLog&quot;: true,\n      &quot;results&quot;: &quot;&quot;\n    }\n  ]\n}\n</code></pre>\n<p>The key takeaway is that the native hook DSL is already there. I do not need to treat the UI as the source of truth.</p>\n<h2>5. What This Means</h2>\n<p>The point is not to turn every GUI into a CLI for the sake of it. The point is to find the points that already have structure:</p>\n<ol>\n<li>storage</li>\n<li>IPC</li>\n<li>command line</li>\n</ol>\n<p>Once those are separated, the UI becomes a presentation layer instead of the only control surface.</p>\n<p>That is the path I want for future reverse-engineering work:\nnot more clicking, but less repeated clicking.</p>\n<h2>6. Architecture Sketch</h2>\n<p>The current chain can be summarized like this:</p>\n<p><img src=\"/blog/260314_algorithm-mcp/architecture.png\" alt=\"Architecture\"></p>\n<h2>7. If I Continue This Line</h2>\n<p>The next step is not to keep staring at the UI. It is to turn these verified surfaces into a repeatable CLI and then into MCP actions:</p>\n<ol>\n<li>package config write</li>\n<li>app enable state read/write</li>\n<li>LSPosed scope sync</li>\n<li>log query and export</li>\n</ol>\n<p>Once that exists, the agent can work with the tool instead of the GUI.</p>\n",
            "url": "https://vw2x.vercel.app/en/blog/20260314_algorithm-assistant-mcp-from-gui-to-api",
            "title": "Algorithm Assistant MCP: Thoughts on Moving from GUI to API",
            "summary": "Exploring how to gradually peel a human-facing reverse-engineering GUI tool into agent-facing APIs and CLIs.",
            "date_modified": "2026-03-14T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/blog/260314_algorithm-mcp",
            "content_html": "<h2>一、引言: AI 时代我们需要什么样的逆向基建</h2>\n<p>毕业接触 Android 安全的第一天, 用 Frida 脚本成功修改函数的返回值，看着 APP 按自己的意志运行，兴奋得半夜 3 点还没睡着觉. </p>\n<p>但随着逆向做得越来越多, 事情变味了. \n每次拿到一个新的 App, 又要重新找入口、写 Hook、拼参数、看日志 … \n曾经的 &quot;小甜甜&quot; 变成了 &quot;牛夫人&quot;. 心里烦躁居多:  &quot;又特么要写遍 Hook?” </p>\n<p>于是开始 &quot;封装&quot;, 写 Frida 模板、Python 脚本, XPosed 模块. \n但很快又发现: 需求总是定制化, 而自己的封装往往过度设计, 最后还是得乖乖回去手写脚本, 然后发现很多工具函数又得手动复制一遍. \n中间的复制粘贴得有十几次, 感觉全是重复性工作, 有些枯燥. </p>\n<p>用我高中数学老师的话来说, 这不 &quot;美&quot;. </p>\n<p>那在 AI 时代, 什么是 &quot;美&quot; 的逆向工程？</p>\n<p>目前我觉得, 要做的重复性工作越少, 就越美. 越能让我偷懒, 就越美. </p>\n<p>随着大模型 (Cursor, Claude, Codex) 的普及, AI 辅助静态代码分析已经成为常态. 前几天看到 frida-mcp, 意识到 AI 现在也可以动态分析了. </p>\n<p>我当前工作也总会有点小外挂分析需求, 什么卡密校验绕过, 加密链路分析之类的活, 最常用的 APP 就是军哥的算法助手 Pro, 什么过弹窗, 允许截屏, 增强 Reqable 抓包, 文件读写监控, 常用的密码算法 hook 之类, 还能自己选择 hook 哪些类, 比写 XPosed 脚本方便多了. </p>\n<p>但果然没干几次我的 &quot;牛夫人感应&quot; 就又出现了. \n怎么每次都得选择 hook 哪个 App, 手动增加要 Hook 的方法, 重启进程, 查看日志, 有时候我还得给 frida 脚本放到 <code>/sdcard/</code> 并打开文件管理器选择载入, 怎么又特么得点一遍? 弄了半小时, 得到了一堆还是需要我自己分析的日志.</p>\n<p>要不让 AI 点?\n试一下会发现, 就算是 Opus4.6, 每点击一下它也得想想下一步做什么, 点几个页面他得想 10 次, 看它操作比我自己手点还慢, 再一想这还要花我 Token, 就开始生闷气了. </p>\n<p>岂止是不美, 简直是有点丑陋. </p>\n<p>事实上, GUI 本身是为人类设计的, 但对 AI 来说, 文本命令天然匹配 LLM 的输入格式, 可自由串联成复杂工作流, API 和 CLI 才是面向 agent 的. </p>\n<p>昨天也看到了 <a href=\"https://github.com/HKUDS/CLI-Anything\">CLI-Anything</a>, 是基于开源代码, 将所有的开源软件功能暴露出 CLI 接口, 让 agent 可以更好的使用. </p>\n<p>这是针对开源项目的, 闭源就得有逆向的环节. </p>\n<p>因此, 本系列文章是给我自己工作的提效探索开个头: <strong>面向 AI 的逆向 GUI 工具利用</strong>. </p>\n<p>我想做的, 就是把这些面向人类的 GUI 软件, 通过逆向分析剥离出可以直接被大模型调用的 API. 已经做过的事, 就别再反复手点了. </p>\n<h2>二、方法论：从 GUI 到 API 的 3 个通用切入点</h2>\n<p>当你拿到一个只能通过 UI 点击的 APP，想要把它变成脚本或 AI 可以自动控制的接口时，先不要去想“按键精灵”或 UI 自动化测试 (那太人类了)</p>\n<p>可以期待作者提供接口, 也可以先尝试自己探索探索, 通常有以下 3 个通用的切入点: </p>\n<h3>1. 寻找持久化落盘点</h3>\n<p>UI 的每次点击，最终必然对应着某处数据的修改\n<strong>思路:</strong> 监控 <code>/data/data/&lt;pkg&gt;/shared_prefs</code>、<code>databases</code>, 以及外部存储 <code>/sdcard/Android/data/&lt;pkg&gt;/files</code>. 只要找到配置文件 (XML/JSON/DB) , 可尝试用脚本修改文件, 就能绕过 UI. </p>\n<h3>2. 寻找跨进程通信 (IPC) 接口</h3>\n<p>如果配置不在常规文件里，或者修改文件后不生效，说明存在内存缓存或跨进程通信。\n<strong>思路:</strong> 反编译工具 APK, 重点排查 <code>AndroidManifest.xml</code> 中的 <code>provider</code>、<code>receiver</code>、<code>service</code> (特别是 <code>exported=true</code> 的组件) . 很多工具的 UI 和后台服务是通过这些 Android 标准机制通信的. </p>\n<h3>3. 寻找命令行 (CLI)</h3>\n<p>有些工具为了方便高级用户或自身调试，会暗藏命令行接口。\n<strong>思路:</strong> 检查工具的二进制文件、安装脚本, 或者在反编译代码中搜索 <code>Runtime.getRuntime().exec</code>、<code>su -c</code> 等关键字, 寻找隐藏的 shell 命令. </p>\n<h2>三、实战解剖：算法助手 MCP</h2>\n<p>几个阶段性目标:</p>\n<ol>\n<li>在 LSPosed 勾选算法助手生效的应用</li>\n<li>在算法助手 UI 里勾选生效的应用</li>\n<li>针对单个应用的配置读取, 写入, 生效 (包括常见的选项如哈希算法的 hook, 自定义方法的 hook 等)</li>\n<li>针对单个应用的 frida 脚本写入和应用</li>\n<li>日志的提取, 结构化和查询\n都尽量改造成 CLI, 让 agent 可以直接使用.</li>\n</ol>\n<h3>1. 持久化落盘点：先确认包级配置, 再追 AppSwitch</h3>\n<p>实验时，LSPosed 里勾选的是：</p>\n<ul>\n<li><code>系统框架</code></li>\n<li><code>com.reqable.android</code></li>\n<li><code>com.example.app</code>\n算法助手自己 UI 里勾选的是：</li>\n<li><code>com.lerist.fakelocation</code></li>\n<li><code>com.example.app</code></li>\n</ul>\n<p>开始找, 先看算法助手自己的常见目录：</p>\n<ul>\n<li><code>/data/user/0/com.junge.algorithmAidePro/shared_prefs</code></li>\n<li><code>/data/user/0/com.junge.algorithmAidePro/files</code></li>\n<li><code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config</code></li>\n<li><code>/data/adb/lspd/config</code>\n很快可以确认一件事：目标包的 <strong>Hook 配置</strong> 确实在外置目录里\n路径是：<code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config/&lt;targetPackage&gt;.json</code>\n但这只是配置, 算法助手 UI 那份 <strong>&quot;应用勾选状态 (AppSwitch)&quot;</strong> 并没有直接出现在应用私有目录里. <code>shared_prefs</code> 下看到的是几个 <code>.sp</code> 文件, 看起来像配置, 但直接搜包名没有命中.</li>\n</ul>\n<p>这里最容易犯的错误, 就是默认认为 &quot;找不到明文包名 = 没有本地持久化&quot; . \n实际不然, 既然文件层面找不到, 我们就转向代码层面 (<strong>寻找阻力最小路径</strong>)</p>\n<h3>2. IPC 接口：Provider 暴露了关键读校验面</h3>\n<p>把 <code>base.apk</code> 拉下来后，重点不是全量看代码，而是找配置读写路径。</p>\n<p>反编译后很快能抓到几个关键点：</p>\n<ol>\n<li><code>ConfigReader.getInstanceByAlgorithmAidePro(String str)</code></li>\n<li><code>ConfigProvider</code></li>\n<li><code>android:authorities=&quot;algorithmAidePro&quot;</code></li>\n<li><code>xposedsharedprefs=true</code></li>\n</ol>\n<p>其中最关键的是 <code>ConfigProvider</code>. 它直接暴露了两个查询维度: </p>\n<ul>\n<li><code>projection=config</code></li>\n<li><code>projection=AppSwitch</code></li>\n</ul>\n<pre><code class=\"language-bash\"># 查询 AppSwitch\nadb shell content query --uri content://algorithmAidePro/com.example.app --projection AppSwitch\n\n# 写入 AppSwitch\nadb shell content insert --uri content://algorithmAidePro/com.example.app --bind AppSwitch:s:true\n</code></pre>\n<p>先拿 Provider 当读校验面, 再反推真实落点</p>\n<h4>AppSwitch 的主落点：<code>AppSwitch.json</code> 不是 <code>Provider</code></h4>\n<p>到这里出现了一个反常现象：</p>\n<ul>\n<li>Provider 能读到 <code>AppSwitch</code></li>\n<li>但 <code>/data/user/0/com.junge.algorithmAidePro/shared_prefs/AppSwitch.xml</code> 并不存在</li>\n</ul>\n<p>说明不是这个文件, 使用 <code>AppSwitch</code> 关键词找到了实际落点</p>\n<pre><code class=\"language-text\">/data/system/junge/AppSwitch.json\n</code></pre>\n<p>直接读取内容会得到一份包名到布尔值的映射，例如：</p>\n<pre><code class=\"language-json\">{\n  &quot;com.example.app&quot;: true,\n  &quot;com.lerist.fakelocation&quot;: true,\n}\n</code></pre>\n<p>当前版本和这台设备上，算法助手 UI 的应用勾选状态主仓库已经和 <code>AlgorithmServer</code> 里的常量对上了：</p>\n<ul>\n<li><code>APP_SWITCH = AppSwitch.json</code></li>\n<li><code>BASE_DIR = /data/system/junge/</code></li>\n</ul>\n<h3>3. CLI 接管 LSPosed 作用域: <code>LSPosed_mod</code></h3>\n<p>LSPosed 的作用域是另一份配置, 不在算法助手目录里.</p>\n<p>真实位置在:</p>\n<pre><code class=\"language-text\">/data/adb/lspd/config/modules_config.db\n/data/adb/lspd/config/modules_config.db-wal\n</code></pre>\n<p>通过主库、WAL 和备份库的字符串命中, 可以确认这个 db 里存的是 LSPosed 的模块生效信息.\n但直接改 <code>sqlite3</code> 不太优雅, 所以后面直接转向 <code>LSPosed_mod</code> 提供的 CLI.</p>\n<p>先确认环境：</p>\n<ol>\n<li>设备存在 <code>/data/adb/lspd/bin/cli</code>, CLI 需要 root 权限</li>\n<li>需要在 LSPosed Manager 里开启 <code>Enable CLI</code></li>\n</ol>\n<pre><code class=\"language-bash\">su -c /data/adb/lspd/bin/cli scope set -a com.junge.algorithmAidePro com.qiyi.video/0\n</code></pre>\n<h2>四、继续探索：日志源, Hook DSL 与动态分析闭环</h2>\n<h3>1. 日志源定位：如何一步步收敛到 SQLite</h3>\n<pre><code class=\"language-text\">/sdcard/Android/media/&lt;targetPackage&gt;/database/algorithmAidePro.db\n</code></pre>\n<h4>第一轮：先从 UI 的 &quot;保存所有日志&quot; 反推</h4>\n<p>一开始最自然的思路, 是围绕日志页面里的 &quot;保存所有日志&quot; 做自动化. \n这条路后来确认过两个事实: </p>\n<ol>\n<li>UI 按钮最终会走到 <code>ThreadSaveLogList -&gt; ConfigReader.createLogFile(null)</code></li>\n<li>导出的文本文件会落到:</li>\n</ol>\n<pre><code class=\"language-text\">/sdcard/Android/data/com.junge.algorithmAidePro/files/Log/&lt;yyyy-MM-dd_HH_mm_ss&gt;.log\n</code></pre>\n<p>第一轮里, 借用已有的经验, 先沿 &quot;已验证路径&quot; 查找:</p>\n<ol>\n<li>已经验证过的 <code>content://algorithmAidePro/...</code></li>\n<li>已经落到外置目录的日志或配置文件\n第一, <code>content://algorithmAidePro/...</code> 能稳定读到: </li>\n<li><code>projection=config</code></li>\n<li><code>projection=AppSwitch</code>\n但没看到任何 &quot;导出日志&quot; 相关 projection, 也没看到稳定可用的 <code>insert/update/call</code> 写入口.</li>\n</ol>\n<p>第二, Frida 日志这条链路是独立成立的. </p>\n<pre><code class=\"language-text\">/sdcard/Android/data/com.junge.algorithmAidePro/files/files/fridaLog.html\n</code></pre>\n<p>这个能直接拉, 但它只对应 Frida 脚本日志, 不是原生 hook 日志. \n而且 <code>fridaLog.html</code> 的确存在, 但它偏向 UI/导出面, 不一定是最底层运行时写入面. 后面继续实机推进时, <code>com.example.app</code> 又看到了一个更直接的文件: </p>\n<pre><code class=\"language-text\">/sdcard/Android/media/com.example.app/database/frida.log\n</code></pre>\n<p>这个文件会直接记录 Frida 运行时日志, 做 smoke test 比 <code>fridaLog.html</code> 更直接. </p>\n<h4>第二轮：开始怀疑 UI 文本导出不是最优目标</h4>\n<p>到这里警觉了,  &quot;保存所有日志&quot; 本身就是一个面向人看的导出动作. 它本质上是: </p>\n<ul>\n<li>从内部真实数据源读数据</li>\n<li>再拼成文本</li>\n<li>最后才写到 <code>files/Log/*.log</code>\n继续研究 &quot;怎么替代按钮点击&quot; , 应该是走远了, 且绕不过那个触发 UI 动作.</li>\n</ul>\n<p>更该问的是: &quot;日志页面展示的数据, 最原始的存储到底在哪？&quot; \n思路从 &quot;模拟 UI 导出&quot; 变成了 &quot;直接找日志源&quot; . </p>\n<h4>第三轮：从私有目录和系统侧配置继续反查</h4>\n<p>接下来先排了几处看起来最像“会放日志”的地方：</p>\n<ol>\n<li><code>/data/user/0/com.junge.algorithmAidePro/files</code></li>\n<li><code>/data/user/0/com.junge.algorithmAidePro/databases</code></li>\n<li><code>/data/system/junge/</code></li>\n</ol>\n<p><code>/data/system/junge/</code> 里面确实东西很多，而且看起来很像“算法助手系统侧仓库”：</p>\n<ul>\n<li><code>AppSwitch.json</code></li>\n<li><code>logList.json</code></li>\n<li><code>com.example.app/config.json</code></li>\n<li><code>com.example.app/script_data.json</code></li>\n</ul>\n<p>但再往里看就会发现，这里主要是：</p>\n<ul>\n<li>开关状态</li>\n<li>hook 配置</li>\n<li>script 配置\n不是日志明细本身.</li>\n</ul>\n<p>以 <code>com.example.app/config.json</code> 为例，里面已经能直接看到：</p>\n<ul>\n<li><code>hookList</code></li>\n<li><code>printLog</code></li>\n<li><code>enableScript</code></li>\n</ul>\n<p>但这里后来踩了一个很典型的坑。</p>\n<p>一开始很容易顺着笔记继续默认：</p>\n<ol>\n<li><code>files/config/&lt;pkg&gt;.json</code> 是当前生效配置</li>\n<li><code>enableScript</code> 在这份 JSON 里</li>\n<li>那改外部脚本文件内容再重启，应该就会生效</li>\n</ol>\n<p>第一句是对的，后两句不完整。</p>\n<p>后面实机和反编译一起对账后，边界变成了：</p>\n<ul>\n<li><code>files/config/&lt;pkg&gt;.json</code> 决定当前“选中了哪个脚本名”</li>\n<li>实际执行的脚本内容，落在 <code>/data/system/junge/&lt;pkg&gt;/frida/&lt;script&gt;.js</code></li>\n<li>脚本列表元数据，落在 <code>/data/system/junge/&lt;pkg&gt;/script_data.json</code></li>\n</ul>\n<p>以 <code>com.example.app</code> 为例，当前能直接对上的就是：</p>\n<ul>\n<li><code>/data/system/junge/com.example.app/config.json</code></li>\n<li><code>/data/system/junge/com.example.app/script_data.json</code></li>\n<li><code>/data/system/junge/com.example.app/frida/bezierzhixian.js</code>\n<code>enableScript</code> 管的是脚本选择, 不是脚本内容本身.</li>\n</ul>\n<p>这时候已经能排掉几类常见误判了:</p>\n<ul>\n<li><code>/data/system/junge/</code> 更偏向配置仓库, 不是日志明细仓库</li>\n<li><code>files/Log/*.log</code> 更偏向导出结果, 不是长期主存储</li>\n<li>真日志源更可能是目标包维度的独立库</li>\n</ul>\n<p>按这个思路继续往下找, <code>Android/media/&lt;pkg&gt;/database</code> 这条线就变得很顺了. 想想也是, 对这类“一套宿主管多个目标”的形态, 配置统一放宿主侧, 日志按目标包落地, 本来就很合理.</p>\n<p>第四轮：开始按数据库名全局反查\n既然 UI 页面背后大概率是结构化数据，就应该反过来找数据库，而不是继续盯着文本文件。\n转折点不在代码, 而在设备全局搜索：</p>\n<pre><code class=\"language-text\">/data/media/0/Android/media/com.example.app/database/algorithmAidePro.db\n</code></pre>\n<p>这个路径一出现，很多事情就串起来了：</p>\n<ol>\n<li>路径按目标包分目录，符合“每个目标 App 单独存日志”的直觉</li>\n<li>名字直接叫 <code>algorithmAidePro.db</code>，和产品本身高度相关</li>\n<li>它不在 UI 导出目录，而在目标 App 的外置媒体目录\n这时候就不应该再猜了，直接拉下来开 SQLite。</li>\n</ol>\n<p>第五轮：验证这个库是不是日志源\n把库拉下来后，看 <code>sqlite_master</code>，结果非常干净：</p>\n<pre><code class=\"language-sql\">table|LOG_DATA_V2|LOG_DATA_V2\ntable|android_metadata|android_metadata\ntable|sqlite_sequence|sqlite_sequence\n</code></pre>\n<p>表结构也直接指向日志用途：</p>\n<pre><code class=\"language-sql\">CREATE TABLE IF NOT EXISTS &quot;LOG_DATA_V2&quot; (\n  &quot;_id&quot; INTEGER PRIMARY KEY AUTOINCREMENT,\n  &quot;GROUP&quot; INTEGER NOT NULL,\n  &quot;TYPE&quot; INTEGER NOT NULL,\n  &quot;OBJ_NAME&quot; TEXT,\n  &quot;CLASS_NAME&quot; TEXT,\n  &quot;LOG_NAME&quot; TEXT,\n  &quot;TIME&quot; INTEGER NOT NULL,\n  &quot;IS_READ&quot; INTEGER NOT NULL,\n  &quot;LOG_DETAILS_RAW&quot; BLOB,\n  &quot;CALL_STACK&quot; TEXT\n);\n</code></pre>\n<p>到这里基本已经坐实了：</p>\n<ul>\n<li>这不是 UI 导出文件</li>\n<li>这是日志页面背后的原始结构化仓库</li>\n</ul>\n<p>再继续查最近几条：</p>\n<ul>\n<li><code>com.example.app.MainActivity | unregisterPluginTestReceiver()</code></li>\n<li><code>com.example.app.MainActivity | onDestroy()</code></li>\n<li><code>com.example.app.MainActivity | lambda$setupTestButtons$3$com-example-app-MainActivity()</code></li>\n</ul>\n<p>而且当前设备上行数是实打实的：</p>\n<pre><code class=\"language-sql\">124\n</code></pre>\n<p>做到这里, 结论就很清楚了.</p>\n<p>最初设想是：</p>\n<ol>\n<li>想办法不用 UI 点击“保存所有日志”</li>\n<li>让 app 生成一个 <code>.log</code></li>\n<li>再 <code>adb pull</code>\n而现在找到的路径是：</li>\n<li>直接 <code>adb pull /sdcard/Android/media/&lt;pkg&gt;/database/algorithmAidePro.db</code></li>\n<li>用 <code>sqlite3</code> / GUI 工具直接查询</li>\n</ol>\n<p>后者明显更适合后续 MCP 化:</p>\n<ol>\n<li><p>结构化</p>\n</li>\n<li><p>可筛选</p>\n</li>\n<li><p>可排序</p>\n</li>\n<li><p>可增量导出</p>\n</li>\n<li><p>可直接转 TSV / CSV / JSON</p>\n</li>\n<li><p>UI 文本导出链路已经逆出来了，但它不是最优自动化目标</p>\n</li>\n<li><p><code>content://algorithmAidePro/...</code> 仍然是可靠读校验面，不是日志导出面</p>\n</li>\n<li><p>Frida 日志仍然独立落在 <code>fridaLog.html</code></p>\n</li>\n<li><p>原生 hook 日志已经找到更好的非 UI 主路径：<code>Android/media/&lt;pkg&gt;/database/algorithmAidePro.db</code></p>\n</li>\n</ol>\n<p>这轮跑通的小闭环不是“自动触发 UI 保存日志”, 而是“直接通过 CLI 导出结构化 hook 日志数据库并用 SQL 查询”.</p>\n<p>示例：</p>\n<pre><code class=\"language-bash\">adb pull /sdcard/Android/media/com.example.app/database/algorithmAidePro.db .\nsqlite3 algorithmAidePro.db &#39;select count(*) from LOG_DATA_V2;&#39;\nsqlite3 -header -column algorithmAidePro.db &quot;\nselect\n  _id,\n  \\&quot;GROUP\\&quot;,\n  TYPE,\n  ifnull(OBJ_NAME,&#39;&#39;) as obj_name,\n  ifnull(CLASS_NAME,&#39;&#39;) as class_name,\n  ifnull(LOG_NAME,&#39;&#39;) as log_name,\n  TIME,\n  length(LOG_DETAILS_RAW) as raw_len\nfrom LOG_DATA_V2\norder by TIME desc\nlimit 10;\n&quot;\n</code></pre>\n<h4>方法提示 1：Provider 是极强的验证面</h4>\n<p>只要目标自己暴露了 Provider，就不要只做静态分析\n因为 Provider 能直接回答：</p>\n<ul>\n<li>这个键有没有</li>\n<li>当前值是什么</li>\n<li>业务代码到底读的是哪一份配置\n这比靠猜文件格式效率高得多。</li>\n</ul>\n<h4>方法提示 2：Xposed 模块要特别警惕 <code>/data/misc/.../prefs</code></h4>\n<p>很多人会一直盯着：</p>\n<ul>\n<li><code>/data/user/0/&lt;pkg&gt;/shared_prefs</code>\n但带 <code>xposedsharedprefs</code> 的模块，配置可能根本不落在应用私有目录，而是 Xposed 可共享读取的位置</li>\n</ul>\n<h3>2. MCP 化拆解：先把三层状态分开</h3>\n<p>如果后面要把算法助手做成一个通用 Java Hook MCP，动作至少要拆成三层：</p>\n<ol>\n<li>写目标包 Hook 配置<ul>\n<li>落点：<code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config/&lt;targetPackage&gt;.json</code></li>\n</ul>\n</li>\n<li>打开算法助手 UI 勾选<ul>\n<li>当前主持久化落点：<code>/data/system/junge/AppSwitch.json</code></li>\n<li>校验要拆成两层：<ul>\n<li>存储层回读 <code>AppSwitch.json</code></li>\n<li>运行期层回读 <code>logList.json</code></li>\n<li>Provider 层回读 <code>projection=AppSwitch</code>（仅作参考）</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>同步 LSPosed 作用域<ul>\n<li>优先接口：<code>/data/adb/lspd/bin/cli</code></li>\n<li>落盘位置：<code>/data/adb/lspd/config/modules_config.db</code></li>\n</ul>\n</li>\n</ol>\n<p>这三层不拆开，后面做 CLI 和 MCP 很容易把状态混在一起。</p>\n<p>这次主要拆开的就是：</p>\n<ul>\n<li>算法助手自身配置</li>\n<li>算法助手 UI 的应用启用状态</li>\n<li>LSPosed 作用域配置</li>\n</ul>\n<h3>3. 实机验证：包级 JSON</h3>\n<p>当所有默认开关都打开时，实机里这份包级 JSON 大致会长这样：</p>\n<pre><code class=\"language-json\">{\n  &quot;ApplicationSwitch&quot;: true,\n  &quot;ExceptionSwitch&quot;: true,\n  &quot;SharedPreferencesPutSwitch&quot;: true,\n  &quot;activitySwitch&quot;: true,\n  &quot;assetsSwitch&quot;: true,\n  &quot;cameraHookSwitch&quot;: true,\n  &quot;checkRootSwitch&quot;: true,\n  &quot;cipherSwitch&quot;: true,\n  &quot;closeDialogSwitch&quot;: true,\n  &quot;dialogKeyword&quot;: &quot;注册码,机器码,激活码&quot;,\n  &quot;dialogSwitch&quot;: true,\n  &quot;digestSwitch&quot;: true,\n  &quot;exitSwitch&quot;: true,\n  &quot;fileDeleteSwitch&quot;: true,\n  &quot;fileSwitch&quot;: true,\n  &quot;fileWriteSwitch&quot;: true,\n  &quot;getSharedPreferencesSwitch&quot;: true,\n  &quot;hiddenVpnSwitch&quot;: true,\n  &quot;hiddenWifiProxySwitch&quot;: true,\n  &quot;hiddenXposedSwitch&quot;: true,\n  &quot;justTrustMePlushSwitch&quot;: true,\n  &quot;logSwitch&quot;: true,\n  &quot;macSwitch&quot;: true,\n  &quot;onClickSwitch&quot;: true,\n  &quot;reqableSwitch&quot;: true,\n  &quot;reqableSwitch_native&quot;: true,\n  &quot;screenSwitch&quot;: true,\n  &quot;shellSwitch&quot;: true,\n  &quot;signSwitch&quot;: true,\n  &quot;sqliteDeleteSwitch&quot;: true,\n  &quot;sqliteExecSQLSwitch&quot;: true,\n  &quot;sqliteInsertSwitch&quot;: true,\n  &quot;sqliteOpenSwitch&quot;: true,\n  &quot;sqliteQuerySwitch&quot;: true,\n  &quot;sqliteUpdateSwitch&quot;: true,\n  &quot;textViewSwitch&quot;: true,\n  &quot;webCryptSwitch&quot;: true,\n  &quot;webViewDebugSwitch&quot;: true,\n  &quot;webViewLoadUrlSwitch&quot;: true\n}\n</code></pre>\n<h4>验证 1：<code>projection=config</code> 不是 <code>config.xml</code> 说了算</h4>\n<p>一开始很容易觉得：</p>\n<ul>\n<li><code>AppSwitch.json</code> 管 UI 勾选</li>\n<li><code>config.xml</code> 管功能配置</li>\n<li><code>files/config/&lt;pkg&gt;.json</code> 只是导出副本\n但实机验证下来，至少对 <code>projection=config</code> 不是这样。</li>\n</ul>\n<p>做了两组对照实验：</p>\n<ol>\n<li><code>pkg.json.digestSwitch=true</code>，<code>config.xml.digestSwitch=false</code><ul>\n<li><code>adb shell content query --uri content://algorithmAidePro/com.example.app --projection config</code></li>\n<li>返回 <code>digestSwitch=true</code></li>\n</ul>\n</li>\n<li><code>pkg.json.digestSwitch=false</code>，<code>config.xml.digestSwitch=false</code><ul>\n<li>同样查询</li>\n<li>返回 <code>digestSwitch=false</code></li>\n</ul>\n</li>\n</ol>\n<p>这已经足够说明：</p>\n<pre><code class=\"language-text\">/sdcard/Android/data/com.junge.algorithmAidePro/files/config/&lt;pkg&gt;.json\n</code></pre>\n<p>才是 <code>projection=config</code> 的主控制源，优先级高于：</p>\n<pre><code class=\"language-text\">/data/misc/&lt;uuid&gt;/prefs/com.junge.algorithmAidePro/config.xml\n</code></pre>\n<p>所以按字段改目标包配置时，直接改包级 JSON 就行，不用碰 <code>config.xml</code>。</p>\n<h4>验证 2：Root 也不等于无条件可写</h4>\n<p>这台机器上 <code>su</code> 的上下文是：</p>\n<pre><code class=\"language-text\">uid=0(root) gid=0(root) context=u:r:magisk:s0\n</code></pre>\n<p>SELinux 仍然是 <code>Enforcing</code>。而且：</p>\n<ul>\n<li><code>setenforce 0</code> 会直接失败</li>\n<li>shell 重定向覆盖 <code>config.xml</code> 会报 <code>Permission denied</code>\n这说明“有 root”不代表“随便哪条写法都能写成功”。尤其是带 <code>/data/misc/.../prefs</code> 的路径，要考虑 <code>magisk su</code> 的上下文限制，不能靠想当然。</li>\n</ul>\n<h4>验证 3：纯文件覆盖的推荐 CLI 顺序</h4>\n<p>如果目标是“不要点 UI，只靠命令行改成功”，当前最稳定的闭环已经收敛成下面这 4 步：</p>\n<ol>\n<li><code>force-stop</code> 算法助手</li>\n<li>覆盖包级 JSON</li>\n<li>启动算法助手</li>\n<li>用 Provider 回读校验</li>\n</ol>\n<p>命令顺序如下：</p>\n<pre><code class=\"language-bash\">adb shell am force-stop com.junge.algorithmAidePro\nadb push com.example.app.json /data/local/tmp/com.example.app.json\nadb shell su -c &#39;cp /data/local/tmp/com.example.app.json /sdcard/Android/data/com.junge.algorithmAidePro/files/config/com.example.app.json&#39;\nadb shell monkey -p com.junge.algorithmAidePro -c android.intent.category.LAUNCHER 1\nadb shell content query --uri content://algorithmAidePro/com.example.app --projection config\n</code></pre>\n<h3>4. 自定义 Hook 导出：已经能看清原生 DSL 的轮廓</h3>\n<p>这次顺手把“算法助手里手工快速添加的自定义 hook 方法”也导出看了一眼。\n以 <code>com.example.app</code> 为例，设备上能看到两份同内容文件：</p>\n<ul>\n<li><code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config/com.example.app.json</code></li>\n<li><code>/sdcard/Android/data/com.junge.algorithmAidePro/files/exportConfig/com.example.app.json</code></li>\n</ul>\n<ol>\n<li><code>config/&lt;pkg&gt;.json</code> 是当前生效配置</li>\n<li><code>exportConfig/&lt;pkg&gt;.json</code> 是导出快照</li>\n<li>导出文件本身就是算法助手当前认可的原生配置语法，不需要先自己猜 schema</li>\n</ol>\n<p><code>com.example.app</code> 当前导出的核心内容大致如下：</p>\n<pre><code class=\"language-json\">{\n  &quot;enableScript&quot;: &quot;bezierzhixian.js&quot;,\n  &quot;hookList&quot;: [\n    {\n      &quot;argsValues&quot;: [],\n      &quot;className&quot;: &quot;com.example.app.DemoTarget&quot;,\n      &quot;constructor&quot;: true,\n      &quot;description&quot;: &quot;来自快速添加的Hook&quot;,\n      &quot;enable&quot;: true,\n      &quot;intercept&quot;: false,\n      &quot;methodName&quot;: &quot;&lt;init&gt;&quot;,\n      &quot;parameterSign&quot;: &quot;&quot;,\n      &quot;printLog&quot;: true,\n      &quot;results&quot;: &quot;&quot;\n    },\n    {\n      &quot;argsValues&quot;: [],\n      &quot;className&quot;: &quot;com.example.app.DemoTarget&quot;,\n      &quot;constructor&quot;: false,\n      &quot;description&quot;: &quot;来自快速添加的Hook&quot;,\n      &quot;enable&quot;: true,\n      &quot;intercept&quot;: false,\n      &quot;methodName&quot;: &quot;a&quot;,\n      &quot;parameterSign&quot;: &quot;Landroid/content/Context;&quot;,\n      &quot;printLog&quot;: true,\n      &quot;results&quot;: &quot;&quot;\n    }\n  ]\n}\n</code></pre>\n<h4>结构 1：自定义 Hook 不只是一个方法名列表</h4>\n<p>每条 <code>hookList</code> 至少包含：</p>\n<ul>\n<li><code>className</code></li>\n<li><code>methodName</code></li>\n<li><code>constructor</code></li>\n<li><code>parameterSign</code></li>\n<li><code>enable</code></li>\n<li><code>printLog</code></li>\n<li><code>intercept</code></li>\n<li><code>results</code></li>\n<li><code>argsValues</code></li>\n</ul>\n<h4>结构 2：参数签名不是 Java 写法, 而是描述符写法</h4>\n<p>例如：</p>\n<ul>\n<li><code>Landroid/content/Context;</code></li>\n<li><code>Ljava/lang/String;</code></li>\n<li><code>Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;</code></li>\n</ul>\n<p>这类写法更接近 JNI / Smali 描述符，不是 Java 源码签名。</p>\n<h4>结构 3：构造函数是单独编码的</h4>\n<p>这次导出里，构造函数同时具备两个特征：</p>\n<ul>\n<li><code>constructor=true</code></li>\n<li><code>methodName=&quot;&lt;init&gt;&quot;</code></li>\n</ul>\n<p>后面如果要抽 DSL，<code>constructor</code> 和 <code>&lt;init&gt;</code> 最好别让调用方重复写。</p>\n<h4>结构 4：<code>enableScript</code> 说明脚本和 <code>hookList</code> 可以并存</h4>\n<p>这份 JSON 不只是 <code>hookList</code>，还包含：</p>\n<pre><code class=\"language-json\">&quot;enableScript&quot;: &quot;bezierzhixian.js&quot;\n</code></pre>\n<p>并且这次设备上也确实找到了对应脚本文件。</p>\n<p>也就是说，结构化 Hook 和额外脚本本来就能并存。</p>\n<h3>5. Frida 自动化：上层应抽象成统一 Hook DSL</h3>\n<p>后面不应该只盯着“把算法助手 JSON 原样搬来搬去”，而应该往更高一层抽象：</p>\n<ol>\n<li>上层统一描述“我要观察哪个类/哪个方法/是否拦截/是否替换参数/是否挂额外脚本”</li>\n<li>中间层根据目标后端，分别编译成：<ul>\n<li>算法助手 <code>hookList</code></li>\n<li>算法助手 <code>enableScript</code></li>\n<li>Frida 脚本</li>\n</ul>\n</li>\n<li>底层再负责把产物落到：<ul>\n<li><code>files/config/&lt;pkg&gt;.json</code></li>\n<li><code>files/exportConfig/&lt;pkg&gt;.json</code></li>\n<li>或 Frida 对应的运行入口</li>\n</ul>\n</li>\n</ol>\n<ul>\n<li>算法助手原生 hook</li>\n<li>附加脚本</li>\n<li>Frida 脚本</li>\n</ul>\n<p>这三种东西后面完全可以统一进一个更高阶的 Hook DSL.</p>\n<h3>6. Frida 实机验证：这轮踩过的 5 个坑</h3>\n<h4>坑 1：不要把 <code>enableScript</code> 误当成脚本内容落点</h4>\n<p>实测里先改了：</p>\n<ul>\n<li><code>/sdcard/Android/data/com.junge.algorithmAidePro/files/config/com.example.app.json</code></li>\n<li><code>/sdcard/bezierzhixian.js</code></li>\n</ul>\n<p>然后重启算法助手去验证。\n这个动作本身不算错, 但它只能证明 &quot;脚本名选择生效&quot; , 不能证明 &quot;运行时读到的是这份外部脚本内容&quot;. </p>\n<p>原因是：</p>\n<ul>\n<li>内部执行副本还在 <code>/data/system/junge/com.example.app/frida/bezierzhixian.js</code></li>\n<li>它的修改时间还停在更早的日期</li>\n<li>所以外部脚本内容变了，内部缓存没变，运行结果自然不会跟着变</li>\n</ul>\n<h4>坑 2：不要默认 <code>su -c</code> 一定已经切到 root</h4>\n<p>这台设备上一个很隐蔽的问题是：</p>\n<ul>\n<li><code>adb shell su -c &#39;...&#39;</code></li>\n</ul>\n<p>有时候实际上仍在 <code>shell</code> 身份跑。</p>\n<p>直到显式改成：</p>\n<pre><code class=\"language-bash\">adb shell &#39;su 0 sh -c &quot;...&#39;&quot; \n</code></pre>\n<p>才拿到 <code>uid=0(root)</code>，并成功覆盖 <code>/data/system/junge/com.example.app/frida/bezierzhixian.js</code>。</p>\n<p>否则很容易误以为“目录有缓存”或者“文件不可写”，实际只是 root 没真的切成功。</p>\n<h4>坑 3：先验证“脚本确实被执行”, 再纠结 Hook 点</h4>\n<p>后面直接改了真实执行脚本之后，<code>com.example.app</code> 对应目录下的：</p>\n<pre><code class=\"language-text\">/sdcard/Android/media/com.example.app/database/frida.log\n</code></pre>\n<p>已经能看到我们主动写入的：</p>\n<pre><code class=\"language-text\">[smoke-probe] script loaded pid=...\n</code></pre>\n<ul>\n<li>实际执行脚本路径已经找对</li>\n<li>修改真实脚本再重启的链路已经成立</li>\n</ul>\n<p>这之后如果 <code>onCreate</code>、<code>onResume</code>、<code>showMessage</code> 这类 hook 没看到，不应再回头怀疑“脚本没生效”，而应该优先怀疑：</p>\n<ul>\n<li>注入时机晚于目标方法</li>\n<li>方法签名/重载不对</li>\n<li>选择的验证点不够硬</li>\n</ul>\n<p>后面要解决的就不是“脚本存哪”, 而是“选什么 hook 点才一定会触发”。</p>\n<h4>坑 4：这轮最终成立的最短路径</h4>\n<p>对现有算法助手而言，当前更可靠的 Frida 改脚本闭环是：</p>\n<ol>\n<li><code>force-stop com.junge.algorithmAidePro</code></li>\n<li>直接改 <code>/data/system/junge/&lt;pkg&gt;/frida/&lt;script&gt;.js</code></li>\n<li>启动算法助手</li>\n<li>等待几秒，给它完成注入准备时间</li>\n<li>启动目标 App</li>\n<li>先看 <code>/sdcard/Android/media/&lt;pkg&gt;/database/frida.log</code></li>\n<li>确认脚本已执行后，再迭代 hook 点</li>\n</ol>\n<p>这条链已经够短, 也适合后面继续做 CLI/MCP.</p>\n<h4>坑 5：<code>onResume</code> 不是不能用, 前提是 Hook 点要选得更硬, 时序也要对</h4>\n<p>前面一度会怀疑：</p>\n<ul>\n<li><code>MainActivity.onResume()</code> 这类点到底会不会触发</li>\n</ul>\n<p>单独盯某个 Activity 自己声明的方法，确实可能踩到两个问题：</p>\n<ol>\n<li>方法本身不是目标类直接声明</li>\n<li>算法助手注入完成时，目标方法已经跑过去了</li>\n</ol>\n<p>这轮后面换了个更稳的 smoke hook：</p>\n<ul>\n<li>直接 hook <code>android.app.Activity.onResume()</code></li>\n<li>再按类名前缀筛掉，只打印 <code>com.example.app*</code></li>\n</ul>\n<p>然后按下面时序跑：</p>\n<ol>\n<li>先启动算法助手</li>\n<li>等几秒</li>\n<li>再启动 <code>com.example.app</code></li>\n</ol>\n<p>最后 <code>frida.log</code> 里就稳定拿到了：</p>\n<ul>\n<li><code>com.example.app.DemoCamera2Activity</code></li>\n<li><code>com.example.app.MainActivity</code></li>\n</ul>\n<p>的 <code>onResume</code> 日志。</p>\n<p>现在在 <code>algorithmaide-mcp</code> 里，Frida 这条链已经额外做了一层受控适配：脚本写入时统一预置 <code>__aaLog</code> / <code>__aaLogHit</code> 结构化 logger，并强制要求脚本按契约打点。读取侧则按真机实际格式解包 <code>frida.log</code> 外层 <code>{&quot;type&quot;:&quot;log&quot;,&quot;payload&quot;:&quot;...&quot;}</code> envelope，再回收到统一查询视图里。</p>\n<ol>\n<li>smoke hook 应优先选系统层、一定存在、且偏晚触发的方法</li>\n<li>算法助手先启动并等待几秒，这个时序在实机上确实会影响是否能稳定 hook 上</li>\n</ol>\n<h3>7. 目标再往前推一步：它已经在逼近 Java 层动态分析逆向</h3>\n<p>上面这套东西如果再往前推一步, 已经是在做一个可重复迭代的动态分析闭环：</p>\n<ol>\n<li>先从人工经验或静态分析里拿到候选类、候选方法、疑似参数点</li>\n<li>自动生成算法助手原生 hook、附加脚本或 Frida 脚本</li>\n<li>跑一轮目标流程</li>\n<li>把运行日志抽出来</li>\n<li>统一格式化成结构化事件</li>\n<li>再根据日志决定下一轮该 hook 什么、该拦截什么、该追加什么脚本</li>\n<li>更新配置继续重跑</li>\n</ol>\n<p>这里核心在于“日志格式统一”。</p>\n<p>因为不管接的是：</p>\n<ul>\n<li>算法助手原生日志</li>\n<li><code>enableScript</code> 对应的附加脚本日志</li>\n<li>外部 Frida 项目的行内日志</li>\n</ul>\n<p>最后都不能停留在“原始文本打印”这一层。\n更合适的是把它们都收敛成统一事件结构，例如：</p>\n<ul>\n<li>时间</li>\n<li>来源后端</li>\n<li>目标类</li>\n<li>目标方法</li>\n<li>参数</li>\n<li>返回值</li>\n<li>调用栈</li>\n<li>标签</li>\n<li>原始文本</li>\n</ul>\n<h3>8. 为什么它天然应该和 jadx-ai-mcp 协同</h3>\n<p>如果只靠动态侧自己盲打，效率会很差。\n所以它和 <code>jadx-ai-mcp</code> 很适合协同：</p>\n<ol>\n<li><code>jadx-ai-mcp</code> 提供静态分析结果<ul>\n<li>例如关键类</li>\n<li>关键方法</li>\n<li>可疑调用链</li>\n<li>值得观察的参数传播点</li>\n</ul>\n</li>\n<li>动态 Hook MCP 根据这些静态线索生成一轮最小 hook/script 配置</li>\n<li>运行后把日志结构化</li>\n<li>再把结构化结果回给上层分析，继续收敛候选点</li>\n</ol>\n<ul>\n<li><code>jadx-ai-mcp</code> 偏静态分析前端</li>\n<li>这个项目偏动态执行后端</li>\n</ul>\n<h2>五、工程化落地：构建 <code>algorithmaide-mcp</code></h2>\n<p>有了这些底层 API, 接下来就是把它们封装成大模型能直接调用的 MCP Tool. \n<code>algorithmaide-mcp</code> 的架构被有意设计为分层结构</p>\n<p><img src=\"/blog/260314_algorithm-mcp/architecture.png\" alt=\"架构图\"></p>\n<p>我们把这些底层动作, 例如包级 JSON 写入, <code>AppSwitch</code> 同步校验, <code>LSPosed</code> scope CLI 接管, 封装成了类似 <code>apply_algorithm_aide_config</code> 这样的高级工具. </p>\n<p>现在, 你只需要对 Cursor 说一句: &quot;帮我用算法助手 Hook <code>com.example.app</code>, 开启网络抓包和加解密打印&quot;, AI 就会自动调用 MCP, 在真机上完成所有的配置、强杀应用、重启并抓取日志.</p>\n<p>当然, 写 SKILL.md 时候加上 &quot;不确定的时候, 先用 jadx-ai-mcp 自己分析下&quot; 会更好. </p>\n<p><strong>项目开源地址：</strong>\n<a href=\"https://github.com/vwww-droid/algorithmaide-mcp\">algorithmaide-mcp</a></p>\n",
            "url": "https://vw2x.vercel.app/blog/260314_algorithm-mcp",
            "title": "算法助手 MCP: 从 GUI 到 API 的思考",
            "summary": "探索如何将面向人的逆向 GUI 工具逐步剥离成 agent 向的 API 与 CLI.",
            "date_modified": "2026-03-14T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/en/blog/20250125_beam-cross-device-copy-cli",
            "content_html": "<h2>Background</h2>\n<p>When I work on two computers and want to move copied text, configs, commands, or snippets to the other machine, the UI flow is always tedious, like using WeChat, DingTalk, or AirDrop.</p>\n<p>I also do not like installing clients. <code>localsend</code> still needs the same LAN, and I am used to my current input method setup, so I do not want to switch to something like WeChat Input Method.</p>\n<p>Web clipboards were much faster, but I still had to open the same URL, paste, save, and exit. And those sites usually transmit in plain text, so anyone who knows the URL can see the content.</p>\n<p>So I wrapped a simple command-line tool: <strong>two commands for cross-device copy and paste, with optional encrypted transfer</strong>.</p>\n<pre><code class=\"language-bash\"># On device A\n# Copy clipboard contents\nbm c\n# Or copy a specific string\n# bm c &quot;hello world&quot;\n\n# On device B\nbm p\n</code></pre>\n<p>That is all. No pairing, no shared LAN, and no tedious UI flow. As long as the devices can reach the internet, it works.</p>\n<h2>Key Features</h2>\n<p><strong>Low memory cost</strong> - only two commands to remember: <code>bm c</code> copies and <code>bm p</code> pastes. No complex configuration, no learning curve.</p>\n<p><strong>Cross-platform</strong> - works directly in Mac, Linux, and Windows terminals; on mobile, just open the URL in a browser.</p>\n<p><strong>Encrypted by default</strong> - all content is compressed and encrypted before upload, so the server only sees gibberish. Custom passwords and private deployment are supported.</p>\n<p><strong>Ready to use</strong> - install with <code>pip install beam-clipboard</code> and start using it right away. First run will guide you through setting a personal key.</p>\n<h2>Use Cases</h2>\n<p><strong>Sync between multiple computers</strong><br>Copy a snippet while coding, switch to another machine, and paste immediately. Transferring a token or a config file becomes a second-level operation.</p>\n<p><strong>Computer-to-phone transfer</strong><br>Run <code>bm c --plain &quot;content&quot;</code> on your computer, then open your personal URL in a phone browser to see it. Type text in the phone web page, and <code>bm p</code> on the computer can fetch it.</p>\n<p><strong>Temporary text relay</strong><br>Faster than WeChat File Transfer and simpler than emailing yourself. Good for commands, code snippets, and temporary notes.</p>\n<h2>Technical Highlights</h2>\n<ul>\n<li>Pure Python, no extra dependencies</li>\n<li>Built on the free TextDB API, with <strong>private deployment supported</strong></li>\n<li>Compression reduces transfer size by 60%</li>\n<li>Lightweight XOR + SHA256 encryption</li>\n<li>MIT licensed</li>\n</ul>\n<h2>One-Line Summary</h2>\n<p><strong>Beam makes cross-device copy and paste as simple as local copy and paste.</strong></p>\n<p>If you are also tired of all the friction involved in moving text between devices, give Beam a try.</p>\n<h2>Support</h2>\n<p>Beam is still mainly something I use myself, and the features are evolving from my own needs. If you think it is useful, feel free to give it a star on <a href=\"https://github.com/vwww-droid/Beam\">GitHub</a> ⭐️</p>\n<p>Issues and PRs are also welcome if you have ideas or needs. More feedback and participation will make the tool better.</p>\n<hr>\n<p>Install: <code>pip install &#39;beam-clipboard[clipboard]&#39;</code></p>\n<p>More usage: <a href=\"https://github.com/vwww-droid/Beam\">https://github.com/vwww-droid/Beam</a></p>\n",
            "url": "https://vw2x.vercel.app/en/blog/20250125_beam-cross-device-copy-cli",
            "title": "Beam: A Clean CLI for Cross-Device Copy and Paste",
            "summary": "Two commands for cross-device copy and paste, with optional encrypted transfer.",
            "date_modified": "2026-01-25T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        },
        {
            "id": "https://vw2x.vercel.app/blog/20250125_beam-intro",
            "content_html": "<h2>背景</h2>\n<p>两台电脑开发, 想复制文本传个配置,命令或文本到另一台电脑上时, 都涉及比较繁琐的 UI 操作, 如微信/钉钉/AirDrop这种</p>\n<p>个人习惯也不喜欢装客户端, localsend 这种还需要同个局域网, 微信输入法这种我百度输入法各种配置习惯了也懒得换</p>\n<p>后面用网页粘贴板快了很多, 但还是要打开同一个网址, 输入保存退出, 但这些网站都是明文传输, 知道网址就能看见内容</p>\n<p>就封装了个简单命令行工具, <strong>两个命令搞定跨设备复制粘贴, 而且可选解密传输</strong></p>\n<pre><code class=\"language-bash\"># 在设备 A\n# 复制剪切板\nbm c\n# 或复制指定文字\n# bm c &quot;hello world&quot;\n\n# 在设备 B  \nbm p\n</code></pre>\n<p>就这么简单. 不需要配对, 不需要同一局域网, 也没有繁琐的 UI 操作. 只要能联网就行.</p>\n<h2>核心特性</h2>\n<p><strong>记忆成本低</strong> - 只需记住两个命令: <code>bm c</code> 复制, <code>bm p</code> 粘贴. 没有复杂的配置, 没有学习成本.</p>\n<p><strong>跨平台</strong> - Mac, Linux, Windows 命令行直接用; 手机没命令行就用浏览器打开网址.</p>\n<p><strong>默认加密</strong> - 所有内容自动压缩加密后再上传, 服务器只能看到乱码. 支持自定义密码或私有部署.</p>\n<p><strong>开箱即用</strong> - <code>pip install beam-clipboard</code> 装完直接用, 首次使用会自动引导设置个人 key.</p>\n<h2>使用场景</h2>\n<p><strong>多台电脑间同步</strong><br>写代码时复制个 snippet, 切到另一台机器直接粘贴; 传个 token, 传个配置文件内容, 都是秒级操作.</p>\n<p><strong>电脑手机互传</strong><br>电脑上 <code>bm c --plain &quot;内容&quot;</code>, 手机浏览器访问你的专属网址就能看到; 手机网页输入文本, 电脑 <code>bm p</code> 就能获取.</p>\n<p><strong>临时文本中转</strong><br>比微信文件助手更快, 比邮件发给自己更简洁. 适合传命令、代码片段、临时笔记等.</p>\n<h2>技术亮点</h2>\n<ul>\n<li>纯 python 实现, 无额外依赖</li>\n<li>基于 TextDB 免费 API, <strong>支持私有部署</strong></li>\n<li>压缩算法让代码传输体积减少 60%</li>\n<li>XOR + SHA256 轻量级加密</li>\n<li>MIT 开源协议</li>\n</ul>\n<h2>一句话总结</h2>\n<p><strong>Beam 让跨设备复制粘贴, 和本地复制粘贴一样简单.</strong></p>\n<p>如果你也厌倦了在设备间传文本的各种繁琐操作, 试试 Beam 吧.</p>\n<h2>求个支持</h2>\n<p>Beam 目前主要是我个人在使用, 迭代的功能也都是根据自己的需求来做的. 如果你觉得这个工具还不错, 欢迎去 <a href=\"https://github.com/vwww-droid/Beam\">GitHub</a> 给个 Star ⭐️</p>\n<p>也很欢迎你提 Issue 说说你的想法和需求, 或者提 PR 一起完善它. 有更多人的反馈和参与, 才能让工具变得更好用.</p>\n<hr>\n<p>安装命令: <code>pip install &#39;beam-clipboard[clipboard]&#39;</code></p>\n<p>更多使用方式: <a href=\"https://github.com/vwww-droid/Beam\">https://github.com/vwww-droid/Beam</a></p>\n",
            "url": "https://vw2x.vercel.app/blog/20250125_beam-intro",
            "title": "Beam: 优雅的跨设备复制命令行方案",
            "summary": "两个命令搞定跨设备复制粘贴, 而且可选加密传输.",
            "date_modified": "2026-01-25T00:00:00.000Z",
            "author": {
                "name": "vw2x",
                "url": "https://vw2x.vercel.app"
            }
        }
    ]
}