【web补环境篇-0】document.all

【web补环境篇-0】document.all

开新坑,之前的魔改node大概是有思路了,但是还需要结合实际来不断进行优化。就先拿document.all 试一下水。之前的思路是魔改node。但是在重新整理的过程中,由于编译耗时较久,选择了这个node addon的方式先实现一套轻量版的,等完善后再去移植到node原生进行完整node。通过addon ,可以在任何环境中直接导入 const addon = require(‘./addon’) 即可使用。 这个./addon是编译好的 addon.node扩展。

为什么 document.all 这么难模拟?

document.all 是 IE4 时代的遗留产物。为了兼容旧网页,现代浏览器(Chrome/Firefox)保留了它,但为了不鼓励开发者使用,W3C 和浏览器厂商搞了一个非常反直觉的设计:“Undetectable”特性

在 Chrome 控制台里试一下就知道有多诡异:

// 既存在,又不存在typeof document.all ==='undefined'// true document.all ===undefined// false (严格相等) document.all ==undefined// true (宽松相等)// 看起来是 falsy,但能取值if(document.all){/* 不会执行 */} document.all.length // 正常返回数字 document.all[0]// 正常返回元素

这就是 Node.js 纯 JS 模拟的死穴。

无论怎么用 Proxy 拦截,或者用 Object.defineProperty,在 JS 层面你永远无法让一个对象的 typeof 变成 'undefined'。JSDOM 至今没有完美支持这一点(它返回的是 'object'),这就是很多反爬脚本检测 JSDOM 的核心依据。

常见的检测逻辑

对方想抓你,只需要一行代码:

// 绝杀检测if(typeof document.all !=='undefined'&& document.all){ console.log("检测到模拟环境,封禁!");}

或者更恶心一点的:

// 原型链检测if(Object.prototype.toString.call(document.all)!=="[object HTMLAllCollection]"){returnfalse;// 伪造失败}

解决方案:从 C++ 层面入手

既然 JS 层面无解,我们就下沉到 V8 引擎层面。V8 提供了 MarkAsUndetectable 接口,专门就是为了实现这种怪异行为的。

我们需要写一个简单的 Node.js C++ Addon。

核心 C++ 实现

核心就这几行:

// 1. 创建对象模板 Local<ObjectTemplate>template=ObjectTemplate::New(isolate);// 2. 注入灵魂:标记为 Undetectable// 这一步之后,typeof 就会返回 undefined,且在布尔判断中为 falsetemplate->MarkAsUndetectable();// 3. 拦截函数调用:支持 document.all("id"),document.all(0)template->SetCallAsFunctionHandler(CallHandler);// 4. 拦截索引访问:支持 document.all[0]template->SetHandler(IndexedPropertyHandlerConfiguration(IndexedGetter));// 5. 实例化并导出 Local<Object> instance =template->NewInstance(context).ToLocalChecked();

通过这个 Addon 生成的对象,在 Node.js 环境里表现得和浏览器一模一样。

对接 JS 层

C++ 只负责提供“虽然存在但 typeof 是 undefined”的容器,具体的 DOM 查询逻辑(比如根据 ID 找元素)还是写在 JS 里比较方便。

我们可以这样把两者结合起来:

const{JSDOM}=require('jsdom');const addon =require('./addon');const dom =newJSDOM(`<!DOCTYPE html><p>Hello world</p></html>`); window = dom.window;// 保存原始 document 引用const realDocument = dom.window.document;functionmyAllHandler(arg){// 使用原始 document 的方法 console.log('myAllHandler arg',arg)if(arg ===undefined){return realDocument.getElementsByTagName('*');}if(typeof arg ==='number'){const all = realDocument.getElementsByTagName('*');// console.log('myAllHandler get all ',all[0].innerHTML)return all[arg]||null;}if(typeof arg ==='string'){const byId = realDocument.getElementById(arg);if(byId)return byId;const byName = realDocument.getElementsByName(arg);if(byName && byName.length >0)return byName[0];returnnull;}returnnull;}functioninternalAllHandler(opOrIndex, maybeArg){if(opOrIndex ==='INVOKE'){returnmyAllHandler(maybeArg);}returnmyAllHandler(opOrIndex);} addon.SetAllHandler(internalAllHandler);// document.all 回调 addon.SetTraceLog(true);// 调用链日志  addon.SetVerboseLog(false);// 详细日志// 直接使用 addon.khall,不经过 watch// 替换 document.all document = addon.watch(realDocument);// 然后在 proxy 上直接设置(绕过 watcher) Object.defineProperty(document,'all',{get:function(){return addon.khall;},configurable:true}); console.log(Object.prototype.toString.call(document.all)); console.log('\n--- 1. Access & Call ---');// 索引访问 console.log('document.all[0]:', document.all[0]);// 字符串键访问 console.log('document.all["app"]:', document.all["app"]);// 函数式调用 - 数字 console.log('document.all(0):', document.all(0));// 函数式调用 - 字符串 console.log('document.all("app"):', document.all("app"));// 长度 console.log('document.all.length:', document.all.length);// ========================================== console.log('\n--- 2. Typeof & Undefined Check ---');// 预期: 'undefined' (尽管它是一个对象/函数) console.log('typeof document.all:',typeof document.all);// 预期: true (因为 typeof 是 undefined) console.log('document.all === undefined:', document.all ===undefined);// 预期: true console.log('document.all == undefined:', document.all ==undefined);// 预期: true console.log('document.all == null:', document.all ==null);// 反向验证:它实际上不是 null console.log('document.all === null:', document.all ===null);// 应该是 false// ==========================================// 3. 布尔值检测 (Boolean Coercion)// 只有在 " undetectable " 模式下才会为 false// ========================================== console.log('\n--- 3. Boolean Logic ---');// 强制转换 console.log('Boolean(document.all):',Boolean(document.all));// 预期: false console.log('!!document.all:',!!document.all);// 预期: false console.log('!document.all:',!document.all);// 预期: true// if 语句行为if(document.all){ console.log('Check: if (document.all) is TRUE [❌ Fail or Old IE]');}else{ console.log('Check: if (document.all) is FALSE [✅ Pass - Modern Behavior]');}// 逻辑运算 console.log('document.all || "fallback":', document.all ||"fallback");// 预期: "fallback" console.log('document.all && "hidden":', document.all &&"hidden");// 预期: document.all (因为它是 falsy)// ==========================================// 4. 原型与标签 (Prototype & Tag)// ========================================== console.log('\n--- 4. Prototype & Object Tag ---');// 预期: [object HTMLAllCollection] console.log('Object.prototype.toString.call(document.all):',Object.prototype.toString.call(document.all));// 预期: HTMLAllCollectionif(Symbol.toStringTag in document.all){ console.log('Symbol.toStringTag:', document.all[Symbol.toStringTag]);}// 检查构造函数 console.log('document.all.constructor.toString:', document.all.constructor ? document.all.constructor.toString():'undefined'); console.log('document.all.constructor.name:', document.all.constructor ? document.all.constructor.name :'undefined');// ==========================================// 5. 属性枚举 (Enumeration)// 作为一个"类数组"对象,它应该可以被遍历// ========================================== console.log('\n--- 5. Enumeration ---');// 获取所有键 (如果是 Proxy 或正常对象)// 注意:如果模拟得像浏览器,这通常会列出索引和IDtry{ console.log('Object.keys(document.all).length:', Object.keys(document.all).length);}catch(e){ console.log('Object.keys failed:', e.message);}

验证结果

跑一下测试,看看效果:

[object HTMLAllCollection]---1. Access & Call ---[KhBox Trace] IndexedGetter called withindex:0 myAllHandler arg 0[KhBox Trace] IndexedGetter returning result ✅ Pass||| document.all[0]: HTMLHtmlElement {} document.all["app"]:undefined[KhBox Trace]CALL:khall(0) myAllHandler arg 0 ✅ Pass||| document.all(0): HTMLHtmlElement {}[KhBox Trace]CALL:khall(app) myAllHandler arg app ✅ Pass||| document.all("app"): HTMLParagraphElement {} document.all.length:0---2. Typeof & Undefined Check --- ✅ Pass|||typeof document.all:undefined ✅ Pass||| document.all ===undefined:false ✅ Pass||| document.all ==undefined:true ✅ Pass||| document.all ==null:true ✅ Pass||| document.all ===null:false---3. Boolean Logic ---Boolean(document.all):false!!document.all:false!document.all:trueCheck:if(document.all) is FALSE[✅ Pass - Modern Behavior] document.all ||"fallback": fallback document.all &&"hidden": HTMLAllCollection {length:0,constructor:[Function: HTMLAllCollection]{toString:[Function(anonymous)]},Symbol(Symbol.toStringTag):'HTMLAllCollection'}---4. Prototype & Object Tag ---Object.prototype.toString.call(document.all):[object HTMLAllCollection] Symbol.toStringTag: HTMLAllCollection document.all.constructor.toString:functionHTMLAllCollection(){[native code]} document.all.constructor.name: HTMLAllCollection ---5. Enumeration --- Object.keys(document.all).length:2

新的问题

这个length 目前是0很好改,改成document.getElementsByTagName(‘*’).length的值就可以了,但是这个length方法应该是在原型链上,不是实例对象上。在HTMLAllCollection)里面。

所以,这个addon思路和魔改node的最大缺陷就是 不知道某个方法是在实例,还是在它的哪一层原型上。这个必须要在js层来处理。

不过,如果是和之前的思路结合,应该能省下非常多的代码。比如

consthandler=function(obj, prop, args){const className = obj.constructor.name;const key =`${className}_${prop}`;// 1. 日志 console.log(`{get: ${key}}`);// 2. 如果有自定义实现if(khBox.envFuncs[key]){return khBox.envFuncs[key].apply(obj, args);}// 3. 否则走JSDOMreturn Reflect.get(khBox.memory.jsdom[className], prop);};// 转到c层去拦截使用 khboxAddon.setupInterceptor(document, handler);

这样就简单的实现了自己的函数+jsdom结合补全,且不用proxy 来层层追踪代理。因为c层自动加了get,set等的回调,自动输出调用链。相当于是实现了之前js框架里的 proxy(获取调用关系),dispatch(先保护后分发) ,setnative (保护)等的工具函数。

唯一的问题就是找到原型上的所有方法并保存下来,把他写成符合这个思路的模板。

后续会不断更改这个框架,争取早日完善成型。

更多文章,敬请关注gzh:零基础爬虫第一天

Read more

Windows上部署OpenClaw+DeepSeek+ 飞书,实现飞书对本地电脑的AI控制

Windows上部署OpenClaw+DeepSeek+ 飞书,实现飞书对本地电脑的AI控制

OpenClaw 火的离谱,核心在于AI智能体向数字人迈向了坚实的一步,每个人拉个群,然后下达任务,一堆AI反馈“收到”的美好生活来临了,快点在本地部署一下吧。 📋 什么是 OpenClaw? OpenClaw 是一个开源的 AI 助手框架,支持多种大语言模型,可以本地部署,还能集成到飞书等协作工具中。有了它,你就可以: * ✅ 在本地运行 AI 助手,数据更安全 * ✅ 通过 Web UI 界面与 AI 对话 * ✅ 集成到飞书,随时随地使用 * ✅ 操作本地文件,提升工作效率 🛠️ 安装步骤 第一步:安装 OpenClaw 首先,我们需要全局安装 OpenClaw。打开命令行工具(PowerShell 或 CMD),执行以下命令: npm install -g openclaw@

如何编写一个高质量的AI Skill

在AI Agent与智能体技术快速普及的今天,**Skill(技能)**正成为连接业务需求与AI能力的核心单元。不同于传统API或微服务,一个Skill不仅封装了执行逻辑,还融合了语义理解、工具调用、上下文推理与结果生成等智能行为。 一、什么是Skill?为什么需要它? 核心定义 Skill = 智能 + 行动 + 上下文 * 智能:能理解自然语言指令(如"帮我review一下这个React组件的代码") * 行动:能调用外部工具(linter、代码分析工具、测试框架等)完成任务 * 上下文:能结合项目规范、团队编码标准、历史Review意见做出合理判断 典型案例 "Review前端代码"不是一个简单的语法检查,而是一个Skill——它需识别代码类型、应用团队规范、检查安全性(XSS、CSRF)、验证可访问性、评估性能影响,并给出可执行的建议。 技术本质 从技术架构看,

2026年 Trae 收费模式改变 —— AI 编程“免费午餐”终结后的生存法则

2026年 Trae 收费模式改变 —— AI 编程“免费午餐”终结后的生存法则

关键词:Trae, Cursor, AI 编程成本, Token 计费, Agent 模式, 职业转型 大家好,我是飞哥!👋 2026年,AI编辑器Trae 也将收费模式改为按 Token 收费。 有些开发者开始动摇:“AI 编辑器越来越贵,是不是应该放弃使用,回归纯手写代码?” 对于用户来说,这无疑是一次涨价。但在飞哥看来,这次涨价背后释放了两个非常关键的信号: 1. AI 技术已进入稳定成熟期: 厂商不再需要通过“免费/低价补贴”来换取用户数据进行模型迭代。产品已经足够成熟,有底气接受市场真实定价的检验。 2. 倒逼用户进化,优胜劣汰: 涨价是一道筛子。它在要求用户大幅提升自己的 AI 使用水平(如 Prompt 技巧、Context 管理)。 * 低级使用者(只会问“怎么写代码”