v3.3.0: 核心层回归扩容 + 案例层强化

SKILL.md:
- Phase 1-5 详细动作从 references/ 拉回核心层
- 10 个场景速查回迁
- 经验法则扩到 24 条(含环境伪装编码纪律)
- 工具名全部对齐 MCP v1.0.0 API
- 版本记录移至 README.md

cases/jsvmp-xhr-interceptor-env-emulation.md:
- 追加禁动清单(5 个禁动对象)
- 追加 UA 分支矩阵(Firefox/Chrome 必补/禁补)
- 追加 6 条踩坑记录 + env-patch 最小化原则

cases/_template.md:
- 追加禁动清单 + UA 分支矩阵可选段骨架

README.md:
- MCP 工具表对齐 v1.0.0 API
- 项目结构补充 cases/ 和完整 references/
- 追加版本记录
- 删除 skill不保存什么 段

references/:
- phase-details.md / mcp-cookbook.md / experience-rules-full.md 顶部加以SKILL.md为准提示
This commit is contained in:
白夜
2026-04-19 23:23:31 +08:00
parent 4f84657647
commit 9477d076f2
7 changed files with 1173 additions and 137 deletions
+16
View File
@@ -101,6 +101,22 @@
---
## 🚫 禁动清单(实战踩过的"不要碰",可选段)
> 如果本案例有"AI 演绎偏差导致踩坑"的经验,在此列出。每条格式:
>
> ### ❌ 禁动对象 N:___具体对象___
> - **理由**:___为什么禁动(要有实测证据或技术原理)___
> - **正确做法**:___如果真需要某种效果,替代方案是什么___
## 🌐 UA 分支矩阵(如涉及环境伪装,可选段)
> 如果本案例涉及 jsdom 环境伪装,列出 UA 分支的必补/禁补清单。
>
> ### UA 分支 1:___(如 Firefox UA)___
> **必补**:...
> **禁补**:...
## 踩坑记录
| # | 坑 | 现象 | 解决方法 |
@@ -76,6 +76,99 @@
jsdom 环境伪装,喂入-截出策略截获 a_bogus。环境值必须与 UA 自洽。
## 🚫 禁动清单(实战踩过的"不要碰")
> 以下 API / 全局对象 / 原型位置,**未经 `hook_function(mode='trace')` 或 `instrumentation` 证明 JSVMP 真的读了**,严禁在 env-patch 里改动。每一条都源自本案例的真实踩坑——改动后签名正常生成但服务端静默拒绝(HTTP 200 + 空 body)。
### ❌ 禁动对象 1:`win.Error` 构造函数
- **理由**:替换 `win.Error` 会让所有 `instanceof Error` 判断走向不确定,JSVMP 内部多处用 Error 做类型分发,替换会连锁打乱内部状态。**没有任何证据证明 JSVMP 读 Error.stack**(本案例实测)
- **正确做法**:完全不动。如果担心 Error.stack 里的 Node.js 路径泄露,先用 `hook_function(path='Error', mode='trace')` 确认 JSVMP 真的调用了再说
### ❌ 禁动对象 2:任何 `constructor.name` 修改
- **理由**:改 `document.constructor.name = 'HTMLDocument'` 不会让 `document.constructor === window.HTMLDocument` 成立,反而让 `Object.prototype.toString.call(document.constructor)` 返回非标准字符串,得不偿失
- **正确做法**:要让 `Object.prototype.toString.call(document) === '[object HTMLDocument]'`,**只走 `Symbol.toStringTag` 路线**(设到 `HTMLDocument.prototype` 和 `Document.prototype` 上)
### ❌ 禁动对象 3:`Object.prototype` 层的原型链修改
- **理由**:`Object.getPrototypeOf(Object.getPrototypeOf(doc))` 可能越过预期到达 `Object.prototype`,改那里会破坏全局 `Object.prototype.toString`,JSVMP 任何对象的 toString 调用都会跑进你的代码
- **正确做法**:`scanPrototypeChain` 必须有 `if (proto === Object.prototype) break;` 边界
### ❌ 禁动对象 4:`Symbol.toStringTag` 设在实例上
- **理由**:`Object.prototype.toString.call(document)` 的算法是**原型链查找** `Symbol.toStringTag`。设在 `doc` 实例上对 `instanceof` / `constructor.name` 等相关检测不生效
- **正确做法**:统一设到 prototype 上(`HTMLDocument.prototype` / `Document.prototype` / `Screen.prototype` / `Navigator.prototype` / `Performance.prototype`)
### ❌ 禁动对象 5:对 `Function.prototype.toString` 做第二次覆盖
- **理由**:整个 env-patch 只能有**一次** `win.Function.prototype.toString = function() {...}` 赋值。"三层防御"指**单次赋值的函数体内部**有三段检测逻辑(WeakSet + jsdom 源码正则 + 源码 fallback),**不是覆盖三次**。第二次覆盖会让命名函数暴露自身源码给 JSVMP
- **正确做法**:写完第一次赋值后**不要**再写第二次"加固"。`grep -c "win.Function.prototype.toString *=" env-patch.js` 应该 = 1
### 行数自检
| 文件 | 合理范围 | 警报阈值 |
|---|---|---|
| env-patch-firefox.js | ~600-650 行 | > 800 行 |
| env-patch-chrome.js | ~650-700 行 | > 850 行 |
超过警报阈值 → 跑上述 5 条禁动清单的 grep 自检,大概率有一条踩了:
```bash
grep -c "win.Function.prototype.toString *=" env-patch.js # 必须 = 1
grep -c "win.Error *=" env-patch.js # 必须 = 0
grep -c "\.constructor.*name" env-patch.js # 必须 = 0
```
## 🌐 UA 分支矩阵(必须按目标站点 UA 选分支,不跨分支混补)
> 环境值必须与 `navigator.userAgent` 自洽。Firefox UA 下补 Chrome 独有 API = 立即暴露。
### Firefox UA 分支(UA 含 `Firefox/XXX`)
**必补**(jsdom 缺失但真实 Firefox 有):
- ✅ `Notification`(构造函数 + `permission` + 静态 `requestPermission`)
- ✅ `Worker` / `SharedWorker` / `RTCPeerConnection`
- ✅ `AudioContext` / `OfflineAudioContext`(至少 stub `createOscillator` / `createDynamicsCompressor` / `startRendering`)
- ✅ `matchMedia`(返回 `{matches:false, media:query}`)
- ✅ `requestIdleCallback` / `cancelIdleCallback`(`setTimeout` 包装)
- ✅ `indexedDB` / `caches` / `speechSynthesis` / `customElements` / `visualViewport`
- ✅ `isSecureContext = true` / `clientInformation = navigator`
- ✅ `WebSocket`(带静态常量 `CONNECTING / OPEN / CLOSING / CLOSED`)
- ✅ `document.fonts` / `document.timeline`
- ✅ `navigator.permissions.query`(stub) / `navigator.clipboard.readText/writeText`(stub)
- ✅ `IntersectionObserver` / `ResizeObserver` / `PerformanceObserver` / `MessageChannel`(Firefox 有,jsdom 没有)
- ✅ `structuredClone`(Firefox 有,jsdom 没有)
**禁补**(Chrome 独有,Firefox UA 下必须 `undefined`):
- ❌ `navigator.userAgentData`(Chrome 独有 Client Hints)
- ❌ `navigator.connection`(Chrome 独有 Network Information API)
- ❌ `navigator.getBattery`(Firefox 已从 Web 移除)
- ❌ `window.chrome`(名字都带 chrome)
- ❌ `performance.memory`(Chrome 独有)
### Chrome UA 分支(UA 含 `Chrome/XXX`)
**必补**(与 Firefox 大部分重合,额外加上):
- ✅ `navigator.userAgentData`(完整 NavigatorUAData,含 `getHighEntropyValues()`)
- ✅ `navigator.connection`(`NetworkInformation` stub)
- ✅ `window.chrome.{app, runtime, csi, loadTimes}`(完整 Chrome 对象)
**禁补**(Firefox 独有):
- ❌ `navigator.buildID`(Firefox 独有)
- ❌ `InstallTrigger`(Firefox 独有标识符)
### 自检命令(Firefox UA 下)
```bash
grep -c "userAgentData\|navigator\.connection\|getBattery\|window\.chrome\|performance\.memory" env-patch.js
# 必须 = 0(Firefox UA 下)
```
## 各 Phase 加速指引
- **Phase 1**: `search_code(keyword="_SdkGlueInit")` 定位配置;`scripts(action='save')` 保存三件套(webmssdk.es5.js / bdms.js / sdk-glue.js)到 `config/`
@@ -298,6 +391,17 @@ function generateABogus(fullUrl, cookieStr) {
| 11 | `resources: 'usable'` 是 JSVMP 拦截器激活的必要条件 | 不加 `resources: 'usable'` 时 SDK 加载成功、`byted_acrawler.frontierSign()` 能返回 X-Bogus,但 JSVMP 的 XHR 拦截器不触发(a_bogus 不生成) | jsdom 的 `resources: 'usable'` 选项会启用完整的 XHR 实现(含网络层),JSVMP 拦截器依赖这个完整实现才能正确 Hook `XMLHttpRequest.prototype.open`。不加此选项时 XHR 是简化版,拦截器无法正常工作。副作用是会尝试加载外部脚本导致 `SyntaxError: Unexpected token '<'`,但不影响 SDK 功能 |
| 12 | `navigator.permissions` 和 `navigator.clipboard` 缺失 | 环境指纹哈希不匹配 | Firefox 浏览器有这两个 API,jsdom 没有。需要补充存根:`nav.permissions = { query: markNative(function query() { return Promise.resolve({ state: 'prompt' }); }) }` 和 `nav.clipboard = { readText/writeText 存根 }` |
### 踩坑记录(2026-04-19 抖音 a_bogus 二次复用补充)
| # | 坑 | 现象 | 解决方法 |
|---|---|---|---|
| 13 | `Function.prototype.toString` 被覆盖两次("三层防御"被误解为覆盖三段代码)| 签名 192 字符但服务端空 body | 整个 patch 有且仅有一次 `win.Function.prototype.toString = function() {...}` 赋值。三层防御指函数体内部的三段检测逻辑(WeakSet / jsdom 源码正则 / 源码 fallback),不是覆盖三次 |
| 14 | `Symbol.toStringTag` 设在 `doc` 实例上而非 `HTMLDocument.prototype` | 签名 192 字符但服务端空 body | 设到 `win.HTMLDocument.prototype` 和 `win.Document.prototype`。不设实例、不 walk 到 `Object.prototype` |
| 15 | 替换 `win.Error` 构造函数(为"清洗 stack 中 Node.js 路径")| 签名 192 字符但服务端空 body | 完全不动 Error。没有证据证明 JSVMP 读 stack,先证再动手 |
| 16 | 修改 `document.constructor.name = 'HTMLDocument'` | 签名 192 字符但服务端空 body | 不动任何 `constructor.name`。用 `Symbol.toStringTag` 路线 |
| 17 | Firefox UA 下补 Chrome 独有 API(`userAgentData / connection / getBattery / window.chrome / performance.memory`)| 签名 192 字符但服务端空 body | UA 与 API 矩阵必须自洽,见"UA 分支矩阵"段 |
| 18 | env-patch 代码量爆炸(612 行 → 1030 行)| 签名生成正常但服务端静默拒绝 | 严守行数阈值,每加代码走"先证明需要 → 加 → 验证 → 保留;否则回退"循环,禁止"先加上保险" |
---
## 变体说明
@@ -556,3 +660,17 @@ JSVMP 用自定义指令集执行,反编译成本极高且版本迭代频繁
- **document 实例方法**:`createElement`、`createTextNode`、`createDocumentFragment`、`getElementById`、`getElementsByClassName`、`getElementsByTagName`、`querySelector`、`querySelectorAll`
遗漏任何一个都可能导致 JSVMP 检测到非原生环境,生成的 a_bogus 被服务端静默拒绝(HTTP 200 + 空 body)。
### 6. env-patch 最小化原则(2026-04-19 实测补充)
jsdom 环境伪装的 env-patch **行数越多越不稳**。每一行"保险代码"都可能引入新的泄露点。
**加代码的唯一合法循环**:
1. 用 `hook_function(mode='trace')` 或 `instrumentation(action='install')` 证明 JSVMP 确实读了某个 API / 属性
2. 用 `compare_env` 确认 jsdom 中该 API 的值与真实浏览器不同
3. 加最小化 stub(只 stub 真正被读的属性/方法)
4. 跑真实请求验证成功
5. 保留;否则立即回退
**禁止**:"先加上保险"式编码。没有证据的补丁 = 未知泄露点。