逆向代码该重写还是忍受浏览器桥接:落地形态的三级阶梯

最后更新: 2026-07-30 11:04:40

逆向工作做到一半,完成机械拆分、算法族识别和差分验证后,通常会得出一个结论:这段代码到底在算什么,已经弄清楚了。但这还不能直接上线。逻辑依然困在原始运行时里,接下来必须决定它如何落地:是进 CI 的纯函数,还是一个需要长期照看的外部进程。这个选择一旦错了,前面省下的时间,最后都会在运维阶段连本带利还回去。

四阶段总览里,这一步只用一句话带过:“第 4 阶段,按传输层降级。”本文把它展开说清楚。核心原则很简单:每往下一级,依赖面、故障模式和部署成本都会增加一个数量级,所以默认策略应当是尽力往上走。

三种落地方式,优先级不能倒置

逆向逻辑最终只有以下三种落地形态,从高到低的顺序应该写进团队规范,而不是临时按“哪个先跑通就用哪个”来决定:

  1. 原生重写。用目标语言重新实现,彻底脱离原始运行时,只依赖标准库。前提是算法族识别正确:一旦指纹识别阶段通过,90% 的内容可以直接参考公开实现,剩下的偏差点单独处理,最终落成一个纯函数。

  2. 本地 JS 引擎运行最小片段。有些逻辑短期内不值得彻底纯化,可以保留原始 JS 的一小段,用本地 Node/V8 跑必要的几十行代码,但绝不是把整个页面都搬过来。

  3. 被动浏览器桥接。有一类状态只存在于真实且已登录的页面运行时中,例如运行时下发的签名、与会话绑定的动态标识。静态重建无法复现它们,当前只能在浏览器内读取。这只能是临时方案,必须在接口说明中明确标注,绝不能成为默认落地方式。

层级之间,成本不是线性增长

这套顺序之所以不能随意调整,是因为三层的成本并非逐步增加,而是每降一级就上升一个数量级。

层级

依赖面

故障模式

能否进入 CI?

原生重写

仅标准库,不需要外部进程

输出不一致,一条 assert 就能定位

可以——它是纯函数

本地 JS 引擎

额外引入一个 Node 运行时;V8 context 不具备线程安全性,并发时需要加锁

引擎版本不匹配、片段依赖的全局变量缺失

勉强可以——前提是安装好引擎

被动浏览器桥接

真实 Chrome + 扩展程序 + 人工维护的会话 + 本地回环通信通道

页面没开、会话过期、页面结构变了、标签页被关掉

不行——它需要真实用户在场

第一层出问题,单元测试就能抓到;第三层出问题,可能只是“用户今天把那个标签页关掉了”。把原本可以写成纯函数的东西降到第三层,等于让每一次调用都绑定一个人。作为参考:算法族一旦确认,一个经过混淆的签名 SDK 可以被落成不足 600 行的独立实现,只依赖内置 crypto,运行在第一层。你以为只能通过浏览器桥接完成的事情,往往只是还有算法没有识别透。

被动桥接的边界:只取一次快照,绝不主动操作

被动桥接失控,通常只有一种起点:它开始“帮忙”了——自动刷新、自动登录、自动等待页面加载。每多加一个“自动”,它就从转发器往爬虫滑一步。因此边界必须划得极窄。以下约束都是踩过坑之后留下来的:

只做一次快照探测,不修改页面。对已打开页面的 Cookie、会话状态和页面运行时各探测一次,仅此一次。不要新建标签页,不要刷新、跳转、聚焦,也不要轮询等待。页面不存在就是不存在,不要替用户打开它。

缺什么就立刻返回明确错误。没有匹配标签页时返回 tab_unavailable;页面存在但未登录时返回 not_logged_in;已经登录但运行时尚未就绪时返回 runtime_unavailable。三个错误码分别对应一种现实状态和下一步动作:等待页面、登录,或切换目标。不要只丢给调用方一个模糊的“失败了”,让它自己猜原因。

敏感状态绝不离开浏览器。扩展不申请 cookies / webRequest 权限;它只查询一个已打开且匹配的标签页,在该页面上下文中完成请求,并在返回前清理字段。Cookie 和页面签名状态不会有任何一步离开 Chrome,通信通道默认连接本地回环地址。桥接传递的是结果,不是凭证。

为什么只有少数平台,却要拆出 15 个 scope

被动桥接转发的是经过白名单约束的请求:路径、参数和 referer 都由 adapter 限制。最容易让人意外的是 scope 的切分粒度:总共 15 个 scope,却只覆盖少数几个平台。因为scope 是按“页面上下文”切分,而不是按“平台”切分。同样是 TikTok,Creative Center、Top Ads、Creator platform、influencer library 和 Ads Manager 是五个独立 scope,对应五套独立的会话状态与页面运行时。登录广告后台,并不意味着你能拿到 Creator platform 的运行时。如果每个平台只切一块,第一次碰到“已登录子站 A,却无法服务子站 B”时,方案就得推倒重来。小红书主站、类 App 路径和创作者市场,同样应当拆成三个 scope。

调度粒度也应随之确定:锁只加在平台家族这一层。同一平台家族内的请求,例如抖音家族,需要串行执行,因为它们复用同一个真实标签页;并发向同一页面上下文发请求会互相踩踏。不同家族,例如抖音和小红书,则可以并行,因为它们是两个互不相关的标签页。同一家族内还要增加最小请求间隔。锁的粒度太粗,会把本可并行的工作强行串行;粒度太细,共用标签页的请求又会发生冲突。平台家族恰好就是“共享同一页面运行时”的自然边界。

显式登录接管:唯一允许的人为介入

被动桥接不会操作页面,但会话终究会过期。解决办法是把人工操作收敛为一次明确、单次触发的动作:交互式命令进入接管流程后,程序通过操作系统打开对应业务页面,等待你手动登录、等待页面就绪,再重放原始请求。整个过程中,扩展不会点击按钮、填写表单或导出 Cookie。登录发生在真实浏览器中,程序只会在你完成后继续处理原来的请求。

最关键的限制是:它绝不能隐式触发。非交互式命令,例如 CI 或定时任务,绝不弹出浏览器;它只应清晰返回会话错误,由上层决定后续动作。接管流程还要防止误判:登录成功后,运行时最多只能再等待一小段时间——当前实现为 20 秒——之后必须给出结论。如果业务页已跳转到账户页,而那里没有目标接口所需的上下文,就应立即结束当前阶段,不能把一个确定的“无法到达”误认为“还在加载”,然后无意义地继续等待。允许降级的流程应将此阶段记为 unavailable 并继续执行,而不是让整个流程失败。

阶段状态必须可读:完成、主动跳过、需要会话

前面所有设计都有一个共同基础:任何阶段都不能只返回“成功”或“失败”。在编排流水线中,每个阶段的结果应有六种状态:completed(已完成)、empty(已执行但无数据)、ready(已就绪,等待提交)、skipped(按规则主动跳过)、unavailable(当前不可用,通常意味着需要会话)、blocked(前置条件未满足)。

“主动跳过”“需要会话”和“确实没有数据”是三种完全不同的信号。如果只返回一个不透明结果,你无法判断空结果究竟是“本来就该为空”,还是“会话早就失效却没人发现”,流水线也就无法运维。把阶段状态建模为有限枚举后,编排层无论是脚本还是模型,都可以据此决定降级、重新认证还是中止。这与被动桥接的三个错误码遵循同一原则,只是提升到了流程层面。

用模型判断能力该落在哪一层

模型只应在这里有限介入:它不替你完成纯化,而是帮你判断是否值得纯化、该纯化到什么程度。这是一项架构判断,不是破解签名。核心问题只有一个:它依赖的状态能否静态重建,还是只能在运行时获得?再据此权衡纯化成本与变更频率。不同步骤对模型能力的要求不同:

步骤

所需能力

选择

model id

通读整个模块,梳理依赖面

长上下文,一次读懂调用图

Kimi K3

kimi-k3

双向论证落地层级,反驳“先跑通再说”

强推理能力,愿意说明为什么值得多花一天完成纯化

Claude Opus 5

claude-opus-5

批量初筛数十到数百项能力

成本低、并发高

Claude Sonnet 5

claude-sonnet-5

降级后的归因分析

中等推理能力,能结合失败日志解释问题

GPT-5.6 Sol

gpt-5.6-sol

最重要的是第二层判断。落地分层时最常犯的错,是模型顺着你“先跑通再说”的语气,给出“浏览器桥接最简单”的答案。它是在附和你,而不是替你计算长期成本。强推理层应该会反驳:“这一段是标准哈希加上一处常量扰动,值得花一天做成纯函数,不该放进桥接层。”别只听我说,自己测一下:挑 3 段逻辑,其中至少 1 段是你已知落地层级正确的对照组。将同一条“给出落地层级、说明理由、并反驳过早使用浏览器桥接”的提示,分别交给 claude-opus-5gpt-5.6-sol。只看一件事:它会努力把你往上推一层,还是懒得判断、默认落到第三层。

真正的阻力,是切换模型的成本

来自三家供应商的四个层级,意味着三套 SDK、三种认证方式、三种错误格式。为了在不同步骤间切模型而重写客户端并不划算,所以多数人最后会用同一个模型处理所有事情,并在最需要推理能力的落地判断上,使用一个只会附和的模型。

AIReiter把这一层抹平了:一个 key、一个兼容 OpenAI 的接口,四个层级都在后面。切换模型只需要修改请求体中的 model 字段。

# Placement argument: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<placement prompt + the reversed logic fragment + dependency list>"}]
  }'

# Bulk first-pass triage: change one field
#   "model": "claude-sonnet-5"
# Degradation attribution:
#   "model": "gpt-5.6-sol"

已经在使用 OpenAI SDK?将 base_url 指向 https://aireiter.com/api/v1 即可。使用 Anthropic SDK?用同一个 key 请求 POST /api/v1/messages。价格方面,Claude 模型按标价七折计费,GPT 模型半价。这个工作流的成本主要集中在两处:一是数十到数百项能力的批量初筛,使用 Sonnet,调用量很高;二是为梳理整个模块依赖面而进行的长上下文单次输入,使用 Kimi,单次调用 token 较多。批量初筛跑在 Claude 模型上,折扣正好覆盖调用最密集的环节;推理层的落地论证同样使用 Claude 模型,享受七折。Kimi K3 的长上下文层也可使用同一个 key 调用。

  • 获取 API key

  • 免注册试用——先手动输入几段逻辑,观察两个模型面对“这段该不该放到浏览器桥接层”时,是在附和你,还是会提出反驳。

结语

逆向逻辑如何落地,本质不是技术问题,而是成本问题。原生重写 > 本地 JS 引擎 > 被动浏览器桥接,这条三级阶梯不能倒置,因为每降一级,纯函数就会变成一个带有外部依赖、人工介入和真实标签页的进程。被动浏览器桥接并非禁区,但只能是一个边界严格的临时组件:只取一次快照,绝不主动操作;页面缺失立即返回明确错误;敏感状态绝不离开浏览器;登录只能通过显式接管;阶段状态始终可读。守住这些约束,它就是可靠的权宜之计;漏掉其中任何一项,它都会变成没人敢维护的黑盒。模型帮助你判断能力应落在哪一层,并抵抗“先跑通再说”的惯性;至于每一次重写是否正确,裁判应当是差分测试,而不是模型。