[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80244":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":8,"htmlUrl":8,"language":9,"languages":8,"totalLinesOfCode":8,"stars":10,"forks":11,"watchers":12,"openIssues":13,"contributorsCount":13,"subscribersCount":13,"size":13,"stars1d":14,"stars7d":14,"stars30d":15,"stars90d":13,"forks30d":13,"starsTrendScore":16,"compositeScore":17,"rankGlobal":8,"rankLanguage":8,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":8,"pushedAt":8,"updatedAt":23,"readmeContent":24,"aiSummary":25,"trendingCount":13,"starSnapshotCount":13,"syncStatus":26,"lastSyncTime":27,"discoverSource":28},80244,"fast-spider-skills-kit","alei-xi\u002Ffast-spider-skills-kit","alei-xi",null,"JavaScript",59,14,55,0,3,4,9,48.43,"MIT License",false,"main",true,[],"2026-06-12 04:01:27","# fast-spider-skills-kit · 快蛛补环境工具箱\r\n\r\n[![License: MIT](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FLicense-MIT-green.svg)](LICENSE)\r\n[![Node.js](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fnode-%3E%3D18-brightgreen)](https:\u002F\u002Fnodejs.org)\r\n[![Python](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fpython-%3E%3D3.10-blue)](https:\u002F\u002Fpython.org)\r\n\r\n> **面向爬虫逆向工程师的补环境框架。** 在 Node.js `vm` 沙箱中构建高仿真浏览器环境，将目标站点原样的 JSVMP \u002F 混淆加签 SDK 直接离线执行，产出有效签名——全程无需 Headless Browser。\r\n\r\n### 你面临的问题 → 本框架的解法\r\n\r\n| 场景 | 传统方案痛点 | 本框架解法 |\r\n|------|-------------|-----------|\r\n| JSVMP 签名 SDK 无法纯算还原 | 手动补环境 3-5 轮，每轮改几十个属性 | **五层防御体系** + 原型链自动构造，一次配置永久复用 |\r\n| SDK 隔周更新，签名失效 | 重抓 SDK → 重写 `sign_xxx.js` → 调试 | **平台化配置**：新增目标只需复制 JSON 模板，不改核心代码 |\r\n| 多站点同时维护 | 每个站点维护一套独立脚本，改一处漏十处 | **统一 Toolkit 入口**：`createBrowser({ platform: '\u003Cname>' })` 一招通吃 |\r\n| Canvas\u002FWebGL\u002FAudio 指纹被风控识别 | 硬编码固定值，多次调用完全一致 | **种子确定性随机化**：同种子同指纹，异种子异指纹，消除固定特征 |\r\n| 签名 403 查不出原因 | 猜测式调试，改代码→重跑→再猜 | **`document.all` 监控**：精确记录 SDK 触碰的每个浏览器属性 + 调用栈 |\r\n\r\n> **声明**：本仓库提供通用方法论与模板代码，不包含任何第三方站点的加签 SDK 或敏感数据。仅供合法授权测试使用；禁止用于未授权的爬取或绕过保护措施。\r\n\r\n### 导航\r\n\r\n| 你想做什么 | 跳转 |\r\n|-----------|------|\r\n| 了解整体架构与核心概念 | [架构总览](#架构总览) |\r\n| 快速上手，跑通第一个签名 | [快速开始](#快速开始) |\r\n| 接入新站点（平台开发） | [平台开发指南](#平台开发指南) |\r\n| 排查签名 403 \u002F 风控拦截 | [实战排错](#实战排错) |\r\n| 处理高并发 + SDK 轮换 | [实战场景](#实战场景) |\r\n| 避免常见翻车操作 | [避坑指南](#避坑指南) |\r\n\r\n## Quick Start\r\n\r\n```bash\r\ngit clone https:\u002F\u002Fgithub.com\u002Falei-xi\u002Ffast-spider-skills-kit.git\r\ncd fast-spider-skills-kit\r\n\r\n# 0. 安装 Playwright 浏览器（仅首次）\r\nnpx playwright install chromium\r\n\r\n# 1. 自动抓取 + 自动搭建环境（两步即可出签名）\r\nnode core\u002Fcapture_sdk.js --url \"https:\u002F\u002Fwww.\u003Ctarget>.com\u002F\"\r\nnode core\u002Ftrace_env.js bundles\u002Fsigner.js > bundles\u002Ffake_env.js\r\n\r\n# 2. 编写 sign.js 加载 fake_env.js + 你的 SDK，启动签名服务\r\n\r\n# 3. 从 Python 调用\r\npip install curl_cffi\r\npython core\u002Fpersistent_signer.py\r\n```\r\n\r\n## 架构总览\r\n\r\n```mermaid\r\nflowchart TB\r\n    subgraph Phase1_2[\"阶段 1-2：SDK 获取与环境追踪\"]\r\n        A[\"capture_sdk.js\u003Cbr\u002F>有头浏览器自动抓取\"] --> B[\"trace_env.js\u003Cbr\u002F>自愈 Proxy 追踪\u003Cbr\u002F>(已归档为 v1 遗留)\"]\r\n    end\r\n\r\n    subgraph Engine[\"阶段 3-6：v2.2 原型引擎（自动化）\"]\r\n        C[\"Toolkit.createBrowser()\"]\r\n        C --> D[\"五层防御\"]\r\n        D --> D1[\"L1: 原型链 (instanceof)\"]\r\n        D --> D2[\"L2: 原生标记 ([native code])\"]\r\n        D --> D3[\"L3: 属性锁定 (lockAll)\"]\r\n        D --> D4[\"L4: 访问监控 (getAccessLog)\"]\r\n        D --> D5[\"L5: 配置隔离 (platforms\u002F)\"]\r\n        C --> E[\"指纹引擎\"]\r\n        E --> E1[\"Canvas (种子 PNG)\"]\r\n        E --> E2[\"WebGL (可配置 GPU)\"]\r\n        E --> E3[\"Audio (AnalyserNode)\"]\r\n        C --> F[\"beforeParse 生命周期钩子\"]\r\n    end\r\n\r\n    subgraph Phase7[\"阶段 7：验证\"]\r\n        G[\"curl_cffi\u003Cbr\u002F>chrome120 TLS 指纹\"]\r\n    end\r\n\r\n    Phase1_2 --> Engine --> Phase7\r\n```\r\n\r\n本框架的核心思路：**不在最终爬虫中跑浏览器**。浏览器仅用于阶段 1（抓 SDK）和阶段 7（验证签名）。阶段 2-6 全部在 Node.js `vm` 沙箱中离线完成，单次签名耗时 \u003C 20ms。\r\n\r\n## 前置依赖\r\n\r\n| 组件 | 版本要求 | 用途 |\r\n|------|----------|------|\r\n| Node.js | >= 18 | 运行 `vm` 沙箱、JS 模板 |\r\n| Playwright | latest | `capture_sdk.js` 自动抓取 SDK |\r\n| Python | >= 3.10 | `persistent_signer.py` 子进程管理 |\r\n| curl_cffi | latest | 端到端验证时的 TLS 指纹模拟 |\r\n\r\n## 决策树：纯算法复现 vs 补环境\r\n\r\n```\r\n签名 SDK 是否为 JSVMP \u002F 重度虚拟化？\r\n├── 否，普通压缩 JS\r\n│   ├── \u003C 5 KB → 直接翻译到 Python\r\n│   └── 5-50 KB → 纯算法翻译，几天内交付\r\n└── 是，JSVMP \u002F 栈式虚拟机解释器模式\r\n    ├── 单一目标、低流量、接受 Node 依赖 → 补环境（本仓库方案）\r\n    ├── 多目标 \u002F 大规模 \u002F 禁止 Node 运行时 → 先补环境验证，再规划移植\r\n    └── 延迟关键（\u003C5ms p99）→ 硬着头皮翻译，或用预计算签名\r\n```\r\n\r\n## 七阶段工作流\r\n\r\n```\r\n- [ ] Phase 1: 从目标页面抓取 SDK 文件\r\n- [ ] Phase 2: 通过 Proxy 追踪 SDK 实际触碰的浏览器属性\r\n- [ ] Phase 3: 搭建假浏览器环境\r\n- [ ] Phase 4: 拦截签名触发点（XHR \u002F fetch）\r\n- [ ] Phase 5: 找到正确的 SDK 初始化配置（最容易静默出错的阶段）\r\n- [ ] Phase 6: 锁定随机性实现确定性加签（可选）\r\n- [ ] Phase 7: 端到端验证（真实 HTTP 请求）\r\n```\r\n\r\n### Phase 1: 抓取 SDK\r\n\r\n打开目标页面，在 DevTools \u002F Playwright 中抓取签名链涉及的**每一个** JS 文件的精确 URL 和内容。**不要**从 GitHub 镜像或博客复制 —— 版本每周都在变化。\r\n\r\n**自动抓取**（推荐）：\r\n\r\n```bash\r\n# 基本用法\r\nnode core\u002Fcapture_sdk.js --url \"https:\u002F\u002Fwww.\u003Ctarget>.com\u002F\"\r\n\r\n# 完整参数\r\nnode core\u002Fcapture_sdk.js \\\r\n  --url \"https:\u002F\u002Fwww.\u003Ctarget>.com\u002F\" \\\r\n  --out bundles\u002F \\\r\n  --timeout 15000 \\\r\n  --ua \"Mozilla\u002F5.0 ... Chrome\u002F146.0.0.0 ...\" \\\r\n  --cookie \"\u003Cyour-cookie-string>\" \\\r\n  --min-size 30000 \\\r\n  --pattern \"signer|vmp|sdk\" \\\r\n  --screenshot\r\n```\r\n\r\n脚本会自动：打开 Chromium → 加载目标页面 → 按大小和文件名正则匹配 JS → 保存到 `bundles\u002F` → 输出 `manifest.json`（含来源 URL、文件大小、sha256、抓取时间、cookie）。\r\n\r\nSDK 链通常由 4 个角色构成（文件名因站点而异）：\r\n\r\n| 角色 | 作用 | 识别方式 |\r\n|------|------|----------|\r\n| 核心运行时 | Web 工具函数、polyfill、环境检测 | 最早加载，20-50 KB |\r\n| 反爬框架 | 探测浏览器环境、指纹采集、决定是否加签 | 中等大小；引用 `navigator.webdriver`、canvas、webgl |\r\n| **签名生成器** | 实际的 JSVMP 打包体，输出签名字段 | 最大（100-200 KB）；含栈式虚拟机解释器、opcode 表 |\r\n| 路由胶水层 | 决定哪些路径触发签名、签名如何附加到请求 | 小巧；读取 `XMLHttpRequest.prototype.open` 参数 |\r\n\r\n保存到 `bundles\u002F` 并记录来源 URL、文件大小、sha256、抓取时间到 `bundles\u002Fmanifest.json`。\r\n\r\n#### 抓取验证\r\n\r\n```js\r\nconst text = fs.readFileSync('bundles\u002F\u003Ccandidate>.js', 'utf8');\r\nconsole.log({\r\n    has_jsvmp_pattern: \u002F_vc_actionList|__vmp_|opCode|opcodeTable|stack\\.push\u002F.test(text),\r\n    has_init_export: \u002F\\.init\\s*=\\s*function|\\.init\\s*:\\s*function\u002F.test(text),\r\n    has_custom_alphabet: \u002F['\"][A-Za-z0-9+\u002F=_\\-]{60,68}['\"]\u002F.test(text),\r\n    size: text.length,\r\n});\r\n```\r\n\r\n没有任何一项命中说明抓错了文件。\r\n\r\n### Phase 2: Proxy 追踪（全自动自愈）\r\n\r\n**一键运行，零手动迭代。** 使用自愈 Proxy：当 SDK 访问不存在的属性时，Proxy 自动创建合理的 stub。如果仍然抛错，脚本会解析错误、注入缺失 stub、自动重跑，直到成功或达上限。\r\n\r\n```bash\r\n# 默认 8 轮自愈，输出可直接 require 的 env stub\r\nnode core\u002Ftrace_env.js bundles\u002Fsigner.js > bundles\u002Ffake_env.js\r\n\r\n# 更多轮次 + 触发 XHR 签名探测\r\nnode core\u002Ftrace_env.js --max-rounds 12 --init bundles\u002Fsigner.js\r\n```\r\n\r\n脚本输出 stderr 显示每轮的自愈过程：\r\n```\r\nSDK: bundles\u002Fsigner.js (145822 bytes)\r\n\r\n── Round 1\u002F8 ──\r\nERROR: Cannot read properties of undefined (reading 'userAgent')\r\n  → last access before crash: navigator.userAgent\r\n  → injected: navigator.userAgent\r\n\r\n── Round 2\u002F8 ──\r\nERROR: Cannot read properties of undefined (reading 'getContext')\r\n  → last access before crash: document.createElement\r\n  → injected: document.createElement\r\n\r\n── Round 3\u002F8 ──\r\nOK — SDK loaded without throwing.\r\n\r\nTotal unique accesses: 87\r\nAuto-stubbed: 71\r\n\r\n──────────────────────────────────────────\r\n\u002F\u002F 最终 env stub 直接写入 stdout →\r\n```\r\n\r\n生成的 `bundles\u002Ffake_env.js` 是可直接 `require` 的模块，覆盖了 SDK 实际触碰的所有属性。**无需像传统流程那样 3-5 轮手动补属性。**\r\n\r\n完整的手动入门模板仍保留在 [core\u002Ffake_env.js](core\u002Ffake_env.js)，如果你偏好从头手工搭建。\r\n\r\n### Phase 3: 搭建假浏览器环境\r\n\r\n把 Proxy 追踪日志翻译成有类型的 stub，按浏览器表面分组：\r\n\r\n| 表面 | 签名关键属性 | 备注 |\r\n|------|-------------|------|\r\n| `navigator` | UA \u002F platform \u002F hardwareConcurrency \u002F deviceMemory \u002F webdriver=false \u002F connection.effectiveType | UA 必须和 URL 参数（如有）一致 |\r\n| `document` | createElement \u002F cookie \u002F characterSet \u002F addEventListener | `createElement('canvas')` 必须返回可调用 `getContext` 的对象 |\r\n| Canvas 2D 上下文 | fillText \u002F measureText \u002F getImageData \u002F toDataURL | **返回值必须稳定**——随机像素会破坏签名可复现性 |\r\n| WebGL 上下文 | getParameter(VENDOR\u002FRENDERER) \u002F getExtension \u002F getSupportedExtensions | 固定字符串 |\r\n| `screen` | width \u002F height \u002F colorDepth | 匹配 URL 参数 |\r\n| `location` | href \u002F origin \u002F hostname \u002F pathname | 必须和目标域名一致 |\r\n| Web 平台类 | Request \u002F Response \u002F Headers \u002F FormData \u002F Blob \u002F WebSocket \u002F *Observer \u002F AbortController | 全部 `typeof === 'function'`；方法体可为空 |\r\n| Storage \u002F History | localStorage \u002F sessionStorage \u002F history.pushState | 通过 getter\u002Fsetter 实现类真实行为 |\r\n| 定时器 | setTimeout \u002F setInterval \u002F requestAnimationFrame \u002F requestIdleCallback | 必须真实地回调 |\r\n| Crypto | crypto.getRandomValues | 如需确定性加签，锁死随机源 |\r\n\r\n完整 stub（~400 行，可直接 `require`）见 [core\u002Ffake_env.js](core\u002Ffake_env.js)。\r\n\r\n### Phase 4: 拦截签名触发点\r\n\r\n大多数签名 SDK 不暴露 `sign(url)` 这样的公开 API——它们**透明地 hook** `XMLHttpRequest.prototype.send` 或 `window.fetch`，在请求发出前改写 URL。为了离线捕获签名，你需要构建一个假 XHR，做到：\r\n\r\n1. `open()` 时记录 URL\r\n2. `send()` 时将 `readyState` 设为 4、`status` 设为 200，**同步**触发\r\n3. `responseText` 返回一个**看起来合理的假 token 响应**（签名 SDK 在初始化时经常 POST 到一个 token 接口）\r\n4. 同步调用 `onreadystatechange` \u002F `onload`\r\n\r\n然后触发一次签名：\r\n\r\n```js\r\nvm.runInContext(`(function(u){\r\n    const x = new XMLHttpRequest();\r\n    x.open('GET', u);\r\n    x.setRequestHeader('content-type', 'application\u002Fx-www-form-urlencoded');\r\n    x.send(null);\r\n    return x._url;  \u002F\u002F 此时 SDK hook 已经将原始 URL 改写为带签名的 URL\r\n})(${JSON.stringify(targetUrl)})`, ctx);\r\n```\r\n\r\n如果 SDK hook 的是 `fetch`，同样需要 stub `window.fetch` 返回 `Promise\u003CFakeResponse>`。\r\n\r\n**假响应内容很重要**——如果 SDK 初始化时 POST 获取运行时 token，你的 fake 必须返回它能解析的 JSON。通用最小结构：\r\n\r\n```js\r\n'{\"data\":{\"d\":\"\",\"e\":\"\",\"f\":\"\"},\"message\":\"success\",\"status_code\":0}'\r\n'{\"data\":\"\",\"msg\":\"success\",\"status_code\":0}'\r\n'{\"code\":0,\"data\":null,\"msg\":\"ok\"}'\r\n```\r\n\r\n常见坑：有些 SDK 要求 `readyState` 必须经历 1→2→3→4 的完整过渡；`Promise` 身份敏感（`fetch(...).constructor.name === 'Promise'` 必须为 true）；FakeXHR 绝不要真实发出网络请求。\r\n\r\n### Phase 5: 找到正确的 SDK 初始化配置\r\n\r\n**整个流程中最容易静默出错的阶段。** 配置错误时，`init()` 不抛异常，`sign(url)` 返回长度正确、字母表正确的字符串，LENGTH \u002F FORMAT 检查全绿——但服务端拒绝。\r\n\r\n#### 方法 1：读线上调用\r\n\r\n在 DevTools Sources 面板全文搜索 `.init(`，找到页面启动时的初始化调用，把配置字面量**原样**复制下来。\r\n\r\n#### 方法 2：Proxy 探测配置\r\n\r\n```js\r\nconst accessed = new Set();\r\nconst probe = new Proxy(yourConfig, {\r\n    get(t, p) { accessed.add(String(p)); return Reflect.get(t, p); },\r\n});\r\nrealWindow.\u003CsignerGlobal>.init(probe);\r\nconsole.log('SDK reads:', [...accessed].sort());\r\n```\r\n\r\n#### 常见配置结构（字段名因 vendor 而异）\r\n\r\n```js\r\n{\r\n    appId:     \u003Cnumeric>,\r\n    pageId:    \u003Cnumeric>,\r\n    appKey:    '\u003Cvendor-string>',\r\n    paths:     ['^\u002Fapi\u002Fv1\u002F', ...],  \u002F\u002F 通常是正则前缀，不是字面量路径\r\n    debug:     false,\r\n    staging:   false,\r\n    versionMajor: \u003Cfloat>,\r\n    versionMinor: \u003Cfloat>,\r\n}\r\n```\r\n\r\n#### 常见坑\r\n\r\n- **`paths` 是正则前缀不是字面量** —— 最常见的错误。`'\u002Fapi\u002Fv1\u002Ffeed\u002Flist\u002F'` 在某些匹配器中是字面量前缀，在另一些中是正则。看 SDK 的匹配代码再判断。\r\n- **appId \u002F pageId 不匹配** —— 错误 ID = 错误签名分支。\r\n- **staging \u002F debug 标志反转** —— `debug`、`staging` 这类字段在省略时通常**默认为 true**，导致 SDK 运行在生产环境会拒绝的签名模式。始终显式设为 `false`。\r\n- **版本号字段是精确浮点数** —— `8.5` ≠ `8` ≠ `8.50`，从线上原样复制。\r\n\r\n### Phase 6: 锁定随机性（可选）\r\n\r\n签名 SDK 通常混入 `Date.now()` 和 `Math.random()`，每次输出不同。为了调试、回归测试和版本对比，锁死两者：\r\n\r\n```js\r\nconst fixedNow = 1778048871000;\r\nrealWindow.Date.now = () => fixedNow;\r\nrealWindow.performance.now = () => fixedNow - 1000;\r\n\r\n\u002F\u002F 用 mulberry32 替代 Math.random\r\nlet state = 0xdeadbeef >>> 0;\r\nrealWindow.Math.random = function () {\r\n    state = Math.imul(state ^ (state >>> 15), state | 1);\r\n    state ^= state + Math.imul(state ^ (state >>> 7), state | 61);\r\n    return ((state ^ (state >>> 14)) >>> 0) \u002F 4294967296;\r\n};\r\n\r\n\u002F\u002F 部分 SDK 也使用 crypto.getRandomValues —— 同样锁死\r\nrealWindow.crypto.getRandomValues = function (arr) {\r\n    for (let i = 0; i \u003C arr.length; i++) arr[i] = Math.floor(Math.random() * 256) & 0xff;\r\n    return arr;\r\n};\r\n```\r\n\r\n**锁定必须在加载 SDK 之前完成**，否则 SDK 内部会缓存旧引用。验证：同一 URL 签名两次 → 输出必须逐字节一致。\r\n\r\n**不要在线上环境锁死**——服务端可能校验时间戳新鲜度。仅在测试和回归中使用。\r\n\r\n### Phase 7: 端到端验证\r\n\r\n**这是唯一能证明签名正确的步骤。** 长度\u002F格式\u002F固定字节检查只是必要条件，不是充分条件。\r\n\r\n```python\r\nimport curl_cffi.requests as cr\r\nsigned = signer.sign(url)\r\nr = cr.get(signed,\r\n           headers={'User-Agent': UA, 'Cookie': fresh_cookie},\r\n           impersonate='chrome120')\r\nassert r.status_code == 200\r\ndata = r.json()\r\nassert data.get('data') and len(data['data'].get('items', [])) > 0\r\n```\r\n\r\n**TLS 指纹模拟是必须的**——同样的签名 URL、同样的 cookie：\r\n\r\n| 客户端 | 结果 |\r\n|--------|------|\r\n| 浏览器 fetch | 200 + JSON |\r\n| `curl_cffi` `impersonate=chrome120` | 200 + JSON |\r\n| Python `requests` | 200 + 空 body |\r\n| Node `https.request` | 200 + 空 body |\r\n\r\n反爬系统读到的是 TLS ClientHello、JA3、HTTP\u002F2 settings——它们和 Chrome 的不一致。\r\n\r\n#### 验证失败时的排查顺序\r\n\r\n```\r\nstatus=200 + 空 body\r\n├── 先试：同一个 URL 不带签名参数 → 如果成功，这个接口不需要签名\r\n├── 再试：用浏览器抓到的签名在 30 秒内重放\r\n│   ├── 成功 → 你的本地签名器有 bug\r\n│   └── 失败 → cookie 或一次性 token 过期，刷新再试\r\n├── 排查：刷新一次性 token → 如果解决，只是 token 过期\r\n└── 切换：chrome120 → chrome116 → chrome124 → 如果某个版本成功，是 TLS 指纹差异\r\n```\r\n\r\n## 从 Python 调用\r\n\r\n启动一次 Node 进程然后复用——首次签名约 1.5s（SDK 加载），后续每次约 10-50ms。线程安全的 Python wrapper 见 [core\u002Fpersistent_signer.py](core\u002Fpersistent_signer.py)。\r\n\r\n## 常见坑\r\n\r\n### \"签名生成了，但服务端返回 200 + 空 body\"\r\n\r\n按可能性排序：\r\n1. **TLS 指纹** — 必须用 `curl_cffi --impersonate chrome120`\r\n2. **Cookie \u002F 一次性 token 过期** — 很多 vendor 签发有效期 \u003C 10 分钟的 token，刷新即可\r\n3. **签名与发送之间 URL 发生偏移** — 任何参数变化都需要重新签名\r\n4. **init 配置错误**（Phase 5）\r\n5. **SDK 包过期**（Phase 1）— 从线上重新抓取\r\n\r\n### \"签名长度在不同调用间不一致\"\r\n\r\n对于许多 SDK 来说这是正常的——输出长度和输入 URL 长度以及内部 nonce 有关。没有确凿证据前，不要把短\u002F长输出过滤为\"错误分支\"。\r\n\r\n### \"vm.runInContext 报 `process is not defined`\"\r\n\r\nSDK 探测了 Node 全局变量来检测非浏览器环境。在 `buildContext` 中显式清除：\r\n\r\n```js\r\nrealWindow.process = undefined;\r\nrealWindow.Deno = undefined;\r\nrealWindow.require = undefined;\r\nrealWindow.global = undefined;\r\nrealWindow.module = undefined;\r\n```\r\n\r\n### \"Node 进程在 CLI 运行后不退出\"\r\n\r\nSDK 安装了内部定时器（心跳、token 刷新）来维持事件循环。在捕获签名后显式调用 `process.exit(0)`。\r\n\r\n## 实战场景\r\n\r\n### 场景一：高并发签名任务\r\n\r\n**背景**：日均百万级请求，签名延迟必须控制在 20ms 以内，且不能因为 SDK 内部定时器导致 Node 进程内存泄漏。\r\n\r\n**方案**：\r\n\r\n```mermaid\r\nflowchart LR\r\n    subgraph Master[\"主进程 (Python)\"]\r\n        P[\"persistent_signer.py\u003Cbr\u002F>进程池管理\"]\r\n    end\r\n\r\n    subgraph Workers[\"Node.js Worker 进程\"]\r\n        W1[\"Worker 1: Platform A\"]\r\n        W2[\"Worker 2: Platform B\"]\r\n        W3[\"Worker 3: Platform C\"]\r\n    end\r\n\r\n    subgraph Pool[\"签名请求队列\"]\r\n        Q[\"JSONL stdin\u002Fstdout\u003Cbr\u002F>线程安全 Lock\"]\r\n    end\r\n\r\n    Master --> Q --> W1\r\n    Q --> W2\r\n    Q --> W3\r\n```\r\n\r\n**关键配置**：\r\n\r\n```js\r\n\u002F\u002F config\u002Ftargets\u002Fhigh-throughput.json\r\n{\r\n  \"performance\": {\r\n    \"cache_sandbox\": true,           \u002F\u002F 复用 vm 上下文，避免重复加载 SDK\r\n    \"sdk_init_timeout_ms\": 15000,   \u002F\u002F SDK 初始化超时\r\n    \"signature_timeout_ms\": 5000    \u002F\u002F 单次签名超时\r\n  },\r\n  \"anti_detection\": {\r\n    \"prototype_locking\": false       \u002F\u002F 生产环境不锁定原型，允许 SDK 正常运行\r\n  },\r\n  \"fingerprint\": {\r\n    \"seed\": \"random\"                 \u002F\u002F 每次启动使用新种子，避免指纹固定\r\n  }\r\n}\r\n```\r\n\r\n**性能基准**（参考值）：\r\n\r\n| 阶段 | 耗时 | 说明 |\r\n|------|------|------|\r\n| 首次签名（含 SDK 加载） | ~1500ms | `vm.createContext` + `vm.runInContext(sdk)` |\r\n| 后续签名（复用上下文） | ~15ms 中位数 | 仅执行 `sign(url)` 逻辑 |\r\n| Python 子进程管理 | ~17ms 中位数 | JSONL 协议 + `threading.Lock` |\r\n\r\n**注意事项**：\r\n- `cache_sandbox: true` 时，SDK 内部定时器会累积，定期（如每小时）重启 Worker 进程释放内存\r\n- 每个平台独立 Worker 进程，避免 SDK 全局变量冲突\r\n- 生产环境必须使用实时时间戳（`seed: \"random\"`），服务端会校验签名新鲜度\r\n\r\n### 场景二：SDK 动态轮换\r\n\r\n**背景**：部分站点的 JSVMP 签名 SDK 会定期更新（间隔数天到数周不等）。更新后旧的环境 stub 可能失效，导致签名 403。\r\n\r\n**检测与响应流程**：\r\n\r\n```mermaid\r\nflowchart TB\r\n    A[\"定时健康检查\u003Cbr\u002F>(每 30 分钟)\"] --> B[\"用当前 SDK 签名测试 URL\"]\r\n    B --> C{\"curl_cffi 验证\"}\r\n    C --> |\"200 OK\"| D[\"记录指标，继续\"]\r\n    C --> |\"403\"| E[\"累计失败计数\"]\r\n    E --> F{\"失败率 > 阈值?\"}\r\n    F --> |\"否\"| D\r\n    F --> |\"是\"| G[\"触发 SDK 重抓\"]\r\n    G --> H[\"capture_sdk.js 自动抓取\"]\r\n    H --> I[\"对比 manifest.json sha256\"]\r\n    I --> J{\"SDK 是否变化?\"}\r\n    J --> |\"否\"| K[\"告警：可能是 Cookie 过期\"]\r\n    J --> |\"是\"| L[\"运行 trace_env.js 重建 env stub\"]\r\n    L --> M[\"Phase 7 验证新 stub\"]\r\n    M --> N{\"验证通过?\"}\r\n    N --> |\"是\"| O[\"自动上线新配置\"]\r\n    N --> |\"否\"| P[\"回滚旧配置 + 告警\"]\r\n```\r\n\r\n**SDK 快照版本管理**：\r\n\r\n```bash\r\n# 每次捕获自动保存版本快照\r\nbundles\u002F\r\n  jd\u002F\r\n    v20260518_1430\u002F          # 2026-05-18 14:30 捕获\r\n      manifest.json           # {sha256, captured_at, source_url}\r\n      signer.js\r\n      fake_env.js             # 对应版本的 env stub\r\n    v20260515_0920\u002F          # 上一版本（回滚用）\r\n    latest -> v20260518_1430  # 软链接指向当前版本\r\n```\r\n\r\n**自动轮换检查逻辑**：\r\n\r\n```js\r\n\u002F\u002F tools\u002Frotation-watch.js\r\nconst Toolkit = require('..\u002Fsrc');\r\n\r\nasync function healthCheck(platformName) {\r\n    const config = Toolkit.loadConfig(`.\u002Fconfig\u002Ftargets\u002F${platformName}.json`);\r\n    const testUrl = config.target.test_url;\r\n\r\n    \u002F\u002F 1. 用当前 SDK 签名测试 URL\r\n    const win = Toolkit.createBrowser({ platform: platformName });\r\n    \u002F\u002F ... 加载 SDK，执行 sign(testUrl)，获取 signedUrl ...\r\n\r\n    \u002F\u002F 2. Phase 7 验证：用 curl_cffi 发送签名请求\r\n    \u002F\u002F ... 如果返回码非 200 或响应体为空 → 触发 SDK 重抓\r\n\r\n    \u002F\u002F 3. 比较 manifest.json 中的 sha256 判断 SDK 是否更新\r\n    \u002F\u002F ... 如果更新 → 运行 trace_env.js → 验证 → 自动上线或回滚\r\n}\r\n```\r\n\r\n> 完整的轮换检测工具链（含自动重抓、版本对比、灰度上线）暂未开源——这是本框架的未来方向之一。当前可通过手动触发 `capture_sdk.js` + Phase 7 验证实现。\r\n\r\n## 反模式 (Anti-Patterns)\r\n\r\n以下是国内逆向工程师最常见的翻车操作，每一条背后都有血泪教训：\r\n\r\n### ❌ 致命级\r\n\r\n1. **在生产爬虫中使用 Playwright\u002FPuppeteer 驱动签名**\r\n   - 浏览器仅用于 Phase 1（SDK 抓取）和 Phase 7（获取新鲜 Cookie）。\r\n   - 在签名链路里跑 Headless Browser → 单次签名 500ms+ → 内存泄漏 → 进程崩溃。\r\n   - 正确做法：签名全部在 `vm` 沙箱中离线完成，\u003C 20ms\u002F次。\r\n\r\n2. **跳过 Phase 2 凭记忆手写 stub**\r\n   - \"我知道浏览器有哪些属性\" → SDK 实际触碰了 `document.characterSet`、`navigator.connection.effectiveType`、`window.chrome.runtime` 等冷门属性。\r\n   - 正确做法：始终先运行 `trace_env.js`（或使用 v2.2 自带原型引擎），让 Proxy 追踪 SDK 的真实访问。\r\n\r\n3. **修改 `node_modules` 下的 SDK 代码来绕过风控**\r\n   - 有工程师试图直接改混淆后的 SDK 文件来\"禁用\"风控检测——这会导致签名逻辑也被破坏。\r\n   - JSVMP 的字节码和执行逻辑是耦合的，改一行可能毁掉整个虚拟机的执行流程。\r\n   - 正确做法：通过 `beforeParse` 钩子注入环境状态，**永远不改 SDK 源码**。\r\n\r\n### ❌ 高危级\r\n\r\n4. **用 `URLSearchParams.toString()` \"整理\"待签名 URL**\r\n   - `URLSearchParams` 会按字母顺序重排参数，而签名 SDK 对原始 query string 做哈希。\r\n   - 正确做法：保持参数顺序与浏览器原始请求完全一致。\r\n\r\n5. **从博客 \u002F GitHub 镜像复制 SDK 文件**\r\n   - JSVMP 签名文件通常是动态生成的，每隔一段时间更新，且不同 CDN 节点返回的版本可能不同。\r\n   - 正确做法：始终从目标页面实时抓取（`capture_sdk.js`），并对比 `manifest.json` 中的 sha256。\r\n\r\n6. **仅凭签名长度\u002F格式检查就宣布成功**\r\n   - 错误的 `init` 配置会产出长度正确、字母表正确、base64 解码干净的签名——但服务端拒绝。\r\n   - 正确做法：必须通过 Phase 7 的 `curl_cffi` 端到端 HTTP 验证，返回真实业务数据才算通过。\r\n\r\n### ❌ 常见踩坑\r\n\r\n7. **线上环境锁死随机数种子**\r\n   - `canvasSeed: 42` 在调试\u002F回归时有用，但生产使用固定种子会导致所有请求的指纹完全相同——反而成为风控特征。\r\n   - 正确做法：生产环境使用 `\"seed\": \"random\"`。\r\n\r\n8. **多平台共用同一个 Node 进程**\r\n   - 不同平台的 SDK 可能存在全局变量冲突（如都使用 `window.__signer` 或同名全局对象）。\r\n   - 正确做法：每个平台独立 Worker 进程。\r\n\r\n## 平台开发指南\r\n\r\n> **新站点接入只需三步：复制模板 → 改配置 → 跑验证。** 永远不需要修改 `src\u002Fenv\u002F` 或 `core\u002F`。\r\n\r\n### 快速查找\r\n\r\n| 我想... | 看这里 |\r\n|---------|--------|\r\n| 接入一个新站点 | [快速迁移](#快速迁移) |\r\n| 注入 Cookie \u002F 全局状态 | [注入站点状态](#注入站点状态-beforeparse-生命周期钩子) |\r\n| 调试 Canvas 指纹 | [文档指纹引擎](#文档指纹引擎种子确定性) |\r\n| 换 GPU 型号 | [自定义 GPU 硬件池](#自定义-gpu-硬件池) |\r\n| 看 SDK 到底检测了什么 | [document.all 监控](#documentall-监控) |\r\n| 防止 SDK 篡改原型 | [定向锁定原型](#定向锁定原型) |\r\n\r\n## 高级用法：v2.2 原型引擎\r\n\r\nv2.2 将补环境引擎从扁平对象升级为**浏览器规范的原型链架构**。`src\u002Findex.js` 导出了一个统一的 `Toolkit` 对象。\r\n\r\n### 快速迁移\r\n\r\n```js\r\n\u002F\u002F v1 (core\u002Ffake_env.js)                     \u002F\u002F v2 (src\u002F)\r\nconst { buildFakeBrowser } =                  const Toolkit = require('.\u002Fsrc');\r\n  require('.\u002Fcore\u002Ffake_env');                 const win = Toolkit.createBrowser({\r\nconst win = buildFakeBrowser({                  userAgent: UA,\r\n  userAgent: UA,                                href: targetUrl,\r\n  href: targetUrl,                              canvasSeed: Date.now(),\r\n});                                            });\r\n\u002F\u002F → 完全向下兼容\r\n```\r\n\r\n### 注入站点状态（beforeParse 生命周期钩子）\r\n\r\n在 `vm.createContext` 之前将 cookie、运行时 token、自定义状态写入窗口：\r\n\r\n```js\r\nconst Toolkit = require('.\u002Fsrc');\r\n\r\nconst win = Toolkit.createBrowser({\r\n    userAgent: '\u003Cyour-chrome-ua>',\r\n    href: '\u003Ctarget-page-url>',\r\n    beforeParse(win) {\r\n        \u002F\u002F 注入环境状态 (Cookies, LocalStorage, 全局变量等)\r\n        win.document.cookie = '\u003Ckey>=\u003Cvalue>';\r\n        win.localStorage.setItem('\u003Ckey>', '\u003Cvalue>');\r\n\r\n        \u002F\u002F 注入 SDK 期望的自定义全局状态\r\n        win.__customState = { \u002F* 按目标站点需求填充 *\u002F };\r\n\r\n        \u002F\u002F 预挂生命周期事件监听器\r\n        win.addEventListener('platform:exit', (e) => {\r\n            \u002F\u002F e.detail 包含 { url, eventId }\r\n        });\r\n    },\r\n});\r\n```\r\n\r\n`beforeParse` 回调在窗口组装完成后、`vm.createContext` 冻结前调用。可注入的状态类型：\r\n\r\n| 注入目标 | 方式 | 典型用途 |\r\n|---------|------|---------|\r\n| `document.cookie` | getter\u002Fsetter 属性 | 会话维持、CSRF Token |\r\n| `localStorage` \u002F `sessionStorage` | `setItem(key, value)` | 持久化风控 Token |\r\n| `window.__customState` | 直接赋值 | 站点特有的全局变量（如挑战状态对象） |\r\n| 事件监听器 | `addEventListener(type, fn)` | 导航拦截、签名生命周期回调 |\r\n\r\n### 文档指纹引擎（种子确定性）\r\n\r\n指纹引擎为每次调用提供**一致的、可复现的** Canvas \u002F WebGL \u002F Audio 指纹。相同的种子 → 相同的指纹。不同的种子 → 产生的指纹不同，但仍然有效。\r\n\r\n```js\r\nconst Toolkit = require('.\u002Fsrc');\r\n\r\n\u002F\u002F 使用固定种子进行确定性测试\r\nconst win = Toolkit.createBrowser({ canvasSeed: 42 });\r\n\r\nconst canvas = win.document.createElement('canvas');\r\nconst url1 = canvas.toDataURL();  \u002F\u002F 种子为 42 的 PNG\r\nconst url2 = canvas.toDataURL();  \u002F\u002F 相同的 PNG — 可复现\r\nconsole.assert(url1 === url2, '相同种子 → 相同指纹');\r\n\r\n\u002F\u002F 使用 Date.now() 生产，每次运行指纹都不同：\r\nconst win2 = Toolkit.createBrowser({ canvasSeed: Date.now() });\r\n```\r\n\r\n### 自定义 GPU 硬件池\r\n\r\n`Fingerprint.GPU_POOL` 是一个可变数组，可在创建浏览器窗口之前增删 GPU 配置文件：\r\n\r\n```js\r\nconst Toolkit = require('.\u002Fsrc');\r\n\r\n\u002F\u002F 追加自定义 GPU 配置\r\nToolkit.Fingerprint.GPU_POOL.push({\r\n    vendor: '\u003CGPU Vendor String>',\r\n    renderer: '\u003CGPU Renderer String>',\r\n    version: '\u003CWebGL Version String>',\r\n    slVersion: '\u003CGLSL Version String>',\r\n});\r\n\r\n\u002F\u002F 或完全替换为自定义池\r\nToolkit.Fingerprint.GPU_POOL.splice(0, Toolkit.Fingerprint.GPU_POOL.length, ...customProfiles);\r\n\r\n\u002F\u002F 或使用预设名称切换\r\nToolkit.Fingerprint.setProfile('nvidia_desktop');  \u002F\u002F 可用: intel_integrated | nvidia_desktop | amd_desktop | apple_silicon | angle_intel\r\n```\r\n\r\n| 操作 | API | 说明 |\r\n|------|-----|------|\r\n| 追加配置 | `GPU_POOL.push({vendor, renderer, version, slVersion})` | 增加候选 GPU 型号 |\r\n| 替换全部 | `GPU_POOL.splice(0, length, ...profiles)` | 完全自定义硬件池 |\r\n| 使用预设 | `Fingerprint.setProfile('name')` | 内置 5 个常用 GPU 配置 |\r\n| 列出预设 | `Fingerprint.listProfiles()` | 返回可用预设名称数组 |\r\n\r\n### 独立使用指纹生成器\r\n\r\n`Toolkit.Fingerprint` 可脱离完整浏览器窗口单独使用：\r\n\r\n| API | 参数 | 返回值 |\r\n|-----|------|--------|\r\n| `Fingerprint.createCanvas(seed, width?, height?)` | `seed`: 数字种子，`width\u002Fheight`: 默认 300×150 | Canvas 元素模拟对象（含 `toDataURL`, `toBlob`, `getContext`） |\r\n| `Fingerprint.createWebGLContext(seed, kind)` | `seed`: 数字种子，`kind`: `'webgl'` 或 `'webgl2'` | WebGL 上下文模拟对象（含 `getParameter`, `getSupportedExtensions` 等 100+ 方法） |\r\n| `Fingerprint.createAudioContext(seed, opts?)` | `seed`: 数字种子，`opts.sampleRate`: 采样率（默认 44100） | AudioContext 模拟对象（含 `createOscillator`, `createAnalyser` 等） |\r\n| `Fingerprint.createRNG(seed)` | `seed`: 数字种子 | mulberry32 伪随机数生成器函数 `() => number` |\r\n\r\n### document.all 监控\r\n\r\n`document.all` 在纯 JS 中无法完美模拟——`typeof document.all === 'undefined'` 和 `document.all == undefined` 都依赖 V8 引擎级 `MarkAsUndetectable` C++ 标志。本框架采用\"监控替代模拟\"策略：\r\n\r\n| 操作 | API | 说明 |\r\n|------|-----|------|\r\n| 启用监控 | `createBrowser({ debugAll: true })` | 全局开启 `document.all` 访问追踪 |\r\n| 读取日志 | `win.document.all.getAccessLog()` | 返回 `[{prop, time, stack}, ...]` 数组 |\r\n| 清空日志 | `win.document.all.clearAccessLog()` | 重置监控缓冲区 |\r\n| 注册元素 | `win.document.all._register(id, element)` | 向集合中动态添加元素 |\r\n\r\n当 SDK 访问 `document.all` 时，监控层捕获：\r\n- `prop` — 被访问的属性名或 `.call()`（作为函数调用时）\r\n- `time` — Unix 毫秒时间戳\r\n- `stack` — 完整调用栈（前 4 帧）\r\n\r\n如果监控日志显示 SDK 触碰了 `document.all` 且 `typeof` \u002F `== undefined` 检查被触发，则需要为目标编译 C++ 原生插件以获得完美保真度。\r\n\r\n### 定向锁定原型\r\n\r\n基础原型在构造后可以进行防篡改锁定，阻止 SDK 通过 `Object.defineProperty` 重新定义原型属性：\r\n\r\n```js\r\nconst { lockPrototypes } = require('.\u002Fsrc\u002Fenv\u002Fcore');\r\n\r\n\u002F\u002F 在将环境传递给 vm.createContext 之前锁定所有 8 个原型\r\nlockPrototypes();\r\n\r\n\u002F\u002F 之后，SDK 对 Navigator.prototype.userAgent 重新定义的尝试将抛出 TypeError\r\n```\r\n\r\n### 引擎内部兼容性\r\n\r\n旧版的 `src\u002Fenv\u002Fbrowser.js` 导出仍然有效——所有现有代码无需修改即可继续工作：\r\n\r\n```js\r\n\u002F\u002F 仍然有效 — src\u002Fenv\u002Findex.js 向后兼容\r\nconst { buildFakeBrowser } = require('.\u002Fsrc\u002Fenv');\r\nconst win = buildFakeBrowser({ userAgent: '...', href: '...' });\r\n```\r\n\r\n## 目录结构\r\n\r\n| 路径 | 说明 |\r\n|------|------|\r\n| `src\u002Findex.js` | **v2.0 统一入口** — 导出 `Toolkit` |\r\n| `src\u002Fenv\u002Futils.js` | `safefunction` — Function.prototype.toString 劫持 |\r\n| `src\u002Fenv\u002Fprototype.js` | `PrototypeBuilder` — 浏览器规范属性定义 |\r\n| `src\u002Fenv\u002Fcore.js` | 8 个构造函数 + 原型链 + `lockPrototypes()` |\r\n| `src\u002Fenv\u002Fnavigator.js` | Navigator + Plugin\u002FMimeType 组装器 |\r\n| `src\u002Fenv\u002Fdocument-all.js` | `document.all` 仿真 + 访问监控 |\r\n| `src\u002Fenv\u002Fbrowser.js` | `buildFakeBrowser()` — 完整窗口组装 + `beforeParse` |\r\n| `src\u002Fenv\u002Ffingerprint\u002F` | 种子确定性 Canvas \u002F WebGL \u002F Audio 生成器 |\r\n| `core\u002F` | v1 可复用模板（`capture_sdk.js`、`trace_env.js`、`fake_env.js`） |\r\n| `bundles\u002F` | 本地存放抓到的 SDK 文件；**勿将第三方 SDK 提交到公开仓库** |\r\n| `SKILLS.md` | 技能能力参考与设计原理 |\r\n| `CLAUDE.md` | Claude Code 上下文指引 |\r\n\r\n## 实战排错\r\n\r\n### 签名生成但服务端返回 403 \u002F 空响应\r\n\r\n按概率从高到低排查：\r\n\r\n| 优先级 | 排查项 | 检查方法 |\r\n|--------|--------|---------|\r\n| P0 | TLS 指纹不匹配 | 必须使用 `curl_cffi` 的 `impersonate='chrome120'`——原生 `requests`\u002F`https` 库的 JA3 指纹会被识别 |\r\n| P0 | Cookie \u002F 一次性 Token 过期 | 刷新目标页面 Cookie，部分站点 Token 有效期 \u003C 10 分钟 |\r\n| P1 | SDK 初始化配置错误 | 从 DevTools Sources 面板搜索 `.init(`，将配置**原样复制**到平台的 `beforeParse` 钩子 |\r\n| P1 | 签名与发送之间 URL 参数偏移 | 任何参数变化都需要重新签名——不要用 `URLSearchParams` 重排参数 |\r\n| P2 | SDK 文件版本过期 | 重新运行 `capture_sdk.js`，对比 `manifest.json` 中的 sha256 |\r\n| P2 | `document.all` 被风控检测 | 开启 `debugAll: true`，查看 `getAccessLog()` 确认 SDK 是否触碰了此项 |\r\n\r\n### JSVMP SDK 常见特征\r\n\r\n- SDK 文件大小通常在 80-200 KB 之间\r\n- 代码中包含栈式虚拟机解释器模式：`for` 循环 + `switch` 分发器结构\r\n- 关键搜索词：`_vc_actionList`、`opCode`、`opcodeTable`\r\n- 签名长度随输入 URL 长度变化——不要以长度差异判断\"错误分支\"\r\n\r\n### Node 进程不退出\r\n\r\nSDK 内部可能安装了心跳定时器或 Token 刷新定时器来维持事件循环。在一次性签名模式下，捕获结果后显式调用 `process.exit(0)` 强制退出。\r\n\r\n## 许可证\r\n\r\n见 [LICENSE](LICENSE)。\r\n","fast-spider-skills-kit 是一个面向爬虫逆向工程师的补环境框架，旨在通过在 Node.js `vm` 沙箱中构建高仿真浏览器环境来离线执行目标站点的 JSVMP 或混淆加签 SDK，从而产出有效签名。其核心功能包括五层防御体系与原型链自动构造、平台化配置管理、统一 Toolkit 入口以及种子确定性随机化等技术特点，能够有效解决手动补环境繁琐、SDK 更新频繁导致签名失效等问题。此外，该工具还提供了详细的监控机制以帮助开发者排查签名 403 错误。适用于需要频繁维护多站点签名且希望减少重复劳动、提高开发效率的场景。项目遵循 MIT 许可证开放源代码。",2,"2026-06-11 03:59:49","CREATED_QUERY"]