29 KiB
真实性保护、Proxy 风险与 native-like 行为
每次进入 Node.js 补环境阶段、准备编写 env 模块、或修改任何 WebAPI 对象模型时读取本文件。真实性保护不是等目标 JS 检测到再补,而是补环境默认基线;除非用户明确要求不做保护或不使用 addon,否则必须从补环境初始化开始执行。
新版 addon API 的详细调用方式见 references/addon-api.md。进入补环境阶段、修改 addon helper、创建构造函数 / 原型链 / 集合对象 / document.all / navigator.plugins / navigator.mimeTypes 时必须同时读取该文件。
如果用户明确选择 isolated-vm 补环境框架,则必须同时读取 references/runtime-frameworks.md 与 references/xbs-isolated-vm-api.md。该模式下不要把旧 addon.node 桥接进 isolated-vm;补环境代码运行在 isolated-vm Context 内,addon-first 应体现为 window.xbs / globalThis.xbs native-first。xbs.createProtoChains、xbs.createNativeFunction、xbs.createGetter、xbs.createSetter、xbs.createNativeCollection、xbs.getMimeTypesAndPlugins、xbs.createUndetectable 等 API 与 addon-first 同等作为优先主路径。
补环境初始化硬性基线(不等待检测)
进入补环境阶段后,先执行以下基线,再运行目标 JS:
- 先加载 / 检测 native 能力:普通 Node runtime 运行
scripts/load_native_addon.js --json或等价加载随包addon.node;如果用户选择isolated-vm,运行scripts/check_xbs_isolated_vm.js --markdown并在 Context 内自检window.xbs。记录 available、path、exports / API、Node ABI、平台和失败原因。 - 从第一版 env 骨架开始就使用
assets/env-modules/native-protect.js或等价 addon-first helper;不要先用普通函数 / 普通赋值跑通,等检测到toString、descriptor、原型链问题后再补保护。 - 所有新增 WebAPI 默认使用
Object.defineProperty/defineProperties,并显式设置writable、enumerable、configurable。 - 所有方法、构造函数、getter、setter、实例对象都默认做 native-like / toString /
Symbol.toStringTag/ 原型链保护。 - native 能力可用时必须优先调用 addon API 或 xbs API;只有用户明确要求不使用 native 能力、addon / xbs 缺失、ABI 不兼容或 API 调用失败时,才允许
NativeProtect/ JS fallback,并记录豁免或降级原因。
该基线是规范性要求,不以“目标是否已经检测到”为触发条件。
Proxy 只用于探测
JS Proxy 很适合发现缺失环境,但不适合作为最终交付。
目标 JS 可能通过以下方式发现异常:
Object.keys(obj)
Reflect.ownKeys(obj)
Object.getOwnPropertyDescriptor(obj, 'xxx')
Object.getPrototypeOf(obj)
obj instanceof SomeConstructor
Object.prototype.toString.call(obj)
Function.prototype.toString.call(fn)
'xxx' in obj
obj.constructor.name
策略:
| 阶段 | 做法 |
|---|---|
| 初次运行 | 可以使用全量 Proxy 探测访问路径 |
| 中期调试 | 已知对象改为真实结构,只对未知分支继续 Proxy |
| 最终交付 | 尽量无 Proxy,用真实对象、描述符、原型链和 native-like 函数 |
即使 trace 暂时没有出现 ownKeys、getOwnPropertyDescriptor、getPrototypeOf、toString、instanceof 等信号,新增关键 WebAPI 也应按真实对象、描述符、原型链和 native-like 函数实现;当这些信号出现时,应立即把残留 Proxy 迁移到真实对象。
补环境阶段默认真实性清单
从补环境阶段开始,不能只补到“不报错”,也不能只在检测命中后才补保护。凡新增、修改或被 RuyiTrace / Node trace / 目标检测命中的 WebAPI,都必须逐项确认:
- 原型链:补构造函数、
prototype.constructor、Object.create(Constructor.prototype)、必要的Object.setPrototypeOf多级链路,并验证instanceof。 - 属性描述符:所有关键属性都用
Object.defineProperty/defineProperties;明确writable、enumerable、configurable,不要用普通赋值替代。 - 访问器:真实浏览器中是 getter / setter 的属性,必须补成 accessor descriptor;不要为了省事改成 data descriptor。
- 函数 toString 保护:普通方法、构造函数、原型方法在 addon 可用时必须先用
createNativeFunction;addon 不可用或调用失败时才用NativeProtect.setNativeFuncfallback。 - 访问器 toString 保护:getter / setter 本身也是函数;addon 可用时必须先用
createGetter/createSetter,失败时才用NativeProtect.setNativeFunc(getter, "get xxx")/setNativeFunc(setter, "set xxx")fallback。 - 实例对象 toString 保护:对
navigator、document、localStorage、screen、location等实例,优先用 addon 构造函数 /createProtoChains实例工厂创建真实对象和原型链;addon 创建出的实例通常已经具备正确的Object.prototype.toString行为,不要再叠加markObjectType、markObjectToString或手写Symbol.toStringTag伪装。只有 addon 不可用、必须创建普通 JS fallback 对象时,才使用Symbol.toStringTag、NativeProtect.setObjFunc(obj, "Navigator")/markObjectToString,并记录 fallback 原因。 - 集合对象:
HTMLCollection、NodeList、PluginArray、MimeTypeArray、DOMTokenList等集合对象必须优先用 addoncreateNativeCollection或getMimeTypesAndPlugins,不得以普通数组 / 普通对象作为主路径。 - plugins / mimeTypes:
navigator.plugins和navigator.mimeTypes必须优先使用 addongetMimeTypesAndPlugins(config);真实浏览器插件数据不同则传入 config 生成一致数据,不能手写[]或普通对象作为主路径。 - 特殊对象:
document.all这类 HTMLDDA / 不可检测对象必须优先使用 addoncreateUndetectable;JS fallback 只能标记为近似,不得声称完全一致。 - 指纹终端 API:Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何等指纹 API 必须优先回放真实浏览器采样值,同时保持 API 所在对象的原型链、描述符和 native-like
toString;不得因为 Node.js 无法真实渲染就把最终流程改成自动化。
建议交付前运行:
node scripts/check_env_realism.js --case-dir case --markdown
node scripts/check_env_realism.js --case-dir case --require-document-all --require-ruyitrace --require-fingerprint-fixture --markdown
node scripts/check_fingerprint_fixture.js --case-dir case --require canvas,webgl --markdown
指纹值回放真实性
即使 toDataURL、getParameter、getBoundingClientRect 等方法只是返回 fixture 采样值,也要像浏览器 native API:
- 方法挂在正确 prototype 上,例如
HTMLCanvasElement.prototype.toDataURL。 - 函数名、
length、toString()尽量 native-like。 - 实例对象满足
Object.prototype.toString.call(canvas)/instanceof HTMLCanvasElement等目标检测。 - 缺少采样值时抛出明确错误,不能悄悄返回空字符串、空数组或随机值。
- 不使用
node-canvas/headless-gl作为最终指纹一致性方案;这些库可用于离线探索,但不能替代真实浏览器采样值。
toString / native-like 保护优先级
推荐优先级:
- 可选 native addon 或 xbs isolated-vm 创建 native-like 函数、getter、setter。
- JS 层真实对象 + 描述符 + 原型链。
NativeProtect作为 addon 不可用时的 fallback;必须同时覆盖普通函数、访问器 getter / setter 和实例对象Object.prototype.toString。- 现有能力无法覆盖时,再考虑新增 C++ addon API。
native 能力缺口闭环
如果目标行为经过真实浏览器采样后确认:纯 JS fallback 无法可靠表达,当前 addon.node API 也无法覆盖,用户选择 isolated-vm 时当前 window.xbs / xbs.dom API 也无法覆盖,则不要继续硬凑补环境代码。必须读取 references/native-capability-gap.md,并按以下方式处理:
- 先排除实现不完整和已有 API 用法错误。比如没有使用
createProtoChains、createNativeFunction、createGetter、createSetter、createNativeCollection、createUndetectable、createInterceptor或xbs.dom.createDocument()时,先改为 native-first,不得直接判定为能力缺口。 - 确认为能力缺口后,写入
case/notes/native-capability-gap.md,记录阻塞点、真实浏览器基线、纯 JS / addon / xbs 当前结果、无法解决原因、建议新增 native API、最小行为测试用例和通过标准。 - 测试用例必须覆盖导致阻塞的关键行为,例如
typeof、Boolean、== null、descriptor、prototype、Object.prototype.toString、instanceof、DataCloneError 或 Error stack 等,不得只覆盖当前报错消失。 - 用户更新 addon.node 或 xbs isolated-vm 后,先运行能力缺口测试用例。测试通过后才能把该点标记为已解决;测试不通过时继续阻塞。
- 用户选择临时 JS workaround 时,只能写成“仅当前样本路径临时兼容”,不得写成稳定浏览器行为。用户拒绝扩展 native 能力且目标参数生成必须依赖该行为时,标记 case 阻塞。
典型场景包括 HTMLDDA / document.all、内部槽 brand check、跨 Realm 对象一致性、不可检测对象、structuredClone / postMessage DataCloneError、Error stack 与其他必须依赖 V8 / 浏览器引擎能力的行为。
addon-first 门禁
进入补环境阶段第一步必须先检测 native 能力;普通 Node runtime 检测 addon,选择 isolated-vm 时检测 xbs isolated-vm;交付前还要复查 addon-first / xbs native-first 证据:
node scripts/load_native_addon.js --json
node scripts/check_xbs_isolated_vm.js --markdown # 仅用户选择 isolated-vm 时必跑
实现规则:
load_native_addon.js返回{ available, addon, path, exports };env helper 必须同时兼容 raw addon 和这个包装对象,不能因为传入{ addon }而静默跳过 addon。- 创建函数、构造函数、getter、setter、集合对象、
navigator.plugins/mimeTypes、document.all、createNativeObject/createProtoChains支持的对象时,先调用 addon API;如果选择 xbs isolated-vm,则先调用 Context 内的xbsAPI。 - 只有用户明确要求不使用 addon、addon 不可用、ABI 不兼容或 API 抛错时,才降级到
NativeProtect/ JS fallback,并在notes、阶段输出和最终总结中记录豁免或降级原因。 - 交付前运行
check_env_realism.js --case-dir case --markdown;该脚本默认强制 addon-first / xbs native-first。如果源码只出现NativeProtect/markNativeFunction,却没有 addon API、window.xbs/globalThis.xbs、options.addon/options.xbs或加载记录,应视为失败。只有用户明确要求不使用 native 能力时,才可传入--no-require-addon-first并记录原因。
构造函数报错必须与浏览器一致
构造函数行为不只包括“能不能 new”,还包括错误类型、错误对象构造器、错误信息和调用路径。不要把所有不可构造对象都简单写成:
throw new TypeError('Illegal constructor');
要求:
- 对进入补环境范围的每个构造函数,先用已确认取证浏览器采样:
Ctor()直接调用的error.name、error.constructor.name、error.message、String(error)、stack 首行。new Ctor()构造调用的同样字段。- 可构造对象的返回实例、
instanceof、Object.prototype.toString.call(instance)。
- 把采样结果写入
case/fixtures/constructor-errors.fixture.json、case/notes/构造函数行为采样.md或阶段报告,不要只凭记忆猜测。 - 多数 DOM 构造错误是
TypeError,但仍以目标浏览器采样为准;如果采样为DOMException或其他错误类型,必须按样本复现。 - addon 可用时优先使用
addon.throwTypeError(message)或 addon 提供的等价错误抛出能力;addon 不支持目标错误类型时,才用 JS fallback,并记录差异。 - 错误信息要精确到浏览器版本和调用方式。例如 Chrome 中“需要 new 调用”和“非法构造”通常不是同一条 message。
推荐采样模板:
function collectConstructorError(name) {
const Ctor = globalThis[name];
const result = { name, call: null, construct: null };
for (const mode of ['call', 'construct']) {
try {
if (mode === 'call') Ctor();
else Reflect.construct(Ctor, []);
result[mode] = { ok: true };
} catch (error) {
result[mode] = {
ok: false,
errorName: error && error.name,
errorConstructor: error && error.constructor && error.constructor.name,
message: error && error.message,
string: String(error),
stackFirstLine: String(error && error.stack || '').split('\n')[0],
};
}
}
return result;
}
交付前用 check_webapi_addon_coverage.js 检查泛化 Illegal constructor 和错误类型风险;发现问题时先补采样,再修复构造函数。
可选 native addon
本 Skill 支持可选 addon.node,并随包放置在 assets/native-addon/<platform>-<arch>/addon.node。脚本会按当前 process.platform-process.arch 自动加载;如果当前平台缺失、Node ABI 不兼容或加载失败,必须降级到 JS fallback,并在报告中说明。用户也可以通过 --addon 或环境变量 WEB_JS_ENV_PATCHER_ADDON 显式覆盖路径,但不要在 Skill 中写入任何本机绝对路径。addon 可用时,“可选”只表示不是跨平台强依赖,不表示补环境时可以跳过;凡 addon 支持的 native-like 能力都必须优先调用 addon,除非用户明确要求不使用 addon。
已知可用 API:
createNativeObject
createNativeFunction
createProtoChains
getProtoChainRegistry
deleteProtoChainRegistryEntry
clearProtoChainRegistry
getPrivate
setPrivate
hasPrivate
deletePrivate
createInterceptor
createNativeCollection
getMimeTypesAndPlugins
createGetter
createSetter
createUndetectable
throwTypeError
新版调用约束:
- 新代码创建构造函数、原型链、别名和实例工厂时,优先使用
createProtoChains(descriptors)。 createNativeObject(options)仅作为旧式单对象兼容;不要把createNativeObject(tag, proto, properties)写入新代码主路径。- 发现旧式
createProtoChains(name, chain)时要迁移为 descriptors 数组;兼容层只能记录 fallback,不能把旧式形态继续作为 addon 主调用。 createUndetectable(callback, handlers)支持getter、setter、query、deleter、enumerator、definer、descriptor等 handlers;处理document.all时优先使用。createProtoChains支持constructorBehavior、callBehavior、constructorErrorMessage、callErrorMessage、illegalConstructor、prototypeMethods、staticMethods、internalClassName、aliases、aliasOf、instanceFactoryName、instanceInitializer;应一次性规划对象模型,减少后续反复改注册表。getMimeTypesAndPlugins([config])是navigator.plugins/navigator.mimeTypes主路径;createNativeCollection(options)是通用集合对象主路径。
检查方式:
node scripts/load_native_addon.js --json
node scripts/load_native_addon.js --addon <path-to-addon.node> --json
常见用途:
| 问题 | 推荐 API |
|---|---|
document.all / HTMLDDA 特殊行为 |
createUndetectable |
| native-like 函数 | createNativeFunction |
| native-like getter | createGetter |
| native-like setter | createSetter |
| 构造函数、实例、别名和实例工厂 | createProtoChains(descriptors) |
| 旧式单对象兼容 | createNativeObject(options) |
navigator.plugins / navigator.mimeTypes |
getMimeTypesAndPlugins(config) |
HTMLCollection / NodeList / 类数组集合 |
createNativeCollection(options) |
| named / indexed property interceptor | createInterceptor(options) |
| 内部私有状态 | setPrivate / getPrivate / hasPrivate / deletePrivate |
| 注册表查看与测试隔离 | getProtoChainRegistry / deleteProtoChainRegistryEntry / clearProtoChainRegistry |
| 浏览器式 TypeError | throwTypeError |
document.all
document.all 的关键行为:
typeof document.all // 'undefined'
document.all == undefined // true
document.all === undefined // false
Boolean(document.all) // false
'all' in document // true
addon 可用时:
Object.defineProperty(document, 'all', {
value: addon.createUndetectable(function () {
return undefined;
}),
enumerable: false,
configurable: true,
});
addon 不可用时只能近似:
Object.defineProperty(document, 'all', {
value: undefined,
enumerable: false,
configurable: true,
});
近似方案不能满足 document.all !== undefined,必须在报告中说明。
native-like 函数
addon 可用时优先:
const querySelector = addon.createNativeFunction(false, 'querySelector', 1, function (selector) {
return null;
});
querySelector.toString();
// function querySelector() { [native code] }
getter / setter:
const getUserAgent = addon.createGetter('userAgent', 0, function () {
return userAgent;
});
Object.defineProperty(Navigator.prototype, 'userAgent', {
get: getUserAgent,
enumerable: true,
configurable: true,
});
多通道 toString 与 DataCloneError 保护
高强度检测不会只调用 fn.toString()。凡进入补环境范围的函数、构造函数、getter、setter 都要按以下通道验证:
fn.toString()Function.prototype.toString.call(fn)- 目标 JS 先保存
const FTS = Function.prototype.toString后再FTS.call(fn) String(fn)fn + ""fn.toString.toString()structuredClone(fn)抛出的DataCloneErrormessage / stackMessagePort.prototype.postMessage(fn)抛出的DataCloneErrormessage / stack
用户实测 addon 创建的 native-like 函数对上述多通道 toString 检测没有问题,因此 addon / xbs native-first 仍是第一选择。只有 fallback 到 NativeProtect 时,才使用下方带 structuredClone / postMessage DataCloneError 改写的版本;旧版只 patch Function.prototype.toString 的 NativeProtect 不再作为主推荐。
NativeProtect fallback
addon 不可用、ABI 不兼容或调用失败时,可以在目标 JS 所在运行上下文内使用以下 fallback。必须在加载目标 JS 之前执行。
class NativeProtect {
#map = new Map();
#objMap = new Map();
#clonePatched = false;
static #instance = null;
static getInstance() {
if (!NativeProtect.#instance) {
NativeProtect.#instance = new NativeProtect();
const instance = NativeProtect.#instance;
const _toString = Function.prototype.toString;
const patchedToString = {
toString() {
if (instance.#map.has(this)) {
const name = instance.#map.get(this);
return `function ${name || this.name}() { [native code] }`;
}
return _toString.call(this);
}
}.toString;
Object.defineProperty(Function.prototype, "toString", {
value: patchedToString,
writable: true,
enumerable: false,
configurable: true,
});
instance.#map.set(Function.prototype.toString, "toString");
const _objToString = Object.prototype.toString;
const patchedObjToString = {
toString() {
if (instance.#objMap.has(this)) {
const name = instance.#objMap.get(this);
return `[object ${name}]`;
}
return _objToString.call(this);
}
}.toString;
Object.defineProperty(Object.prototype, "toString", {
value: patchedObjToString,
writable: true,
enumerable: false,
configurable: true,
});
instance.#map.set(Object.prototype.toString, "toString");
instance.#patchCloneErrorLeak();
}
return NativeProtect.#instance;
}
constructor() {
if (NativeProtect.#instance) {
throw new Error("NativeProtect类只能实例化一次");
}
}
setNativeFunc(func, name = "") {
this.#map.set(func, name);
}
setObjFunc(obj, name = "") {
this.#objMap.set(obj, name);
}
#patchCloneErrorLeak() {
if (this.#clonePatched) return;
this.#clonePatched = true;
const rawStructuredClone = globalThis.structuredClone;
const rawMessagePortPostMessage =
typeof MessagePort !== "undefined" &&
MessagePort.prototype &&
MessagePort.prototype.postMessage;
if (typeof rawStructuredClone === "function") {
const self = this;
function structuredClone(value, options) {
try {
return rawStructuredClone.apply(this, arguments);
} catch (err) {
self.#rewriteDataCloneError(err, value);
}
}
this.#copyFunctionMeta(structuredClone, rawStructuredClone, "structuredClone");
const desc =
Object.getOwnPropertyDescriptor(globalThis, "structuredClone") || {
writable: true,
enumerable: true,
configurable: true,
};
Object.defineProperty(globalThis, "structuredClone", {
...desc,
value: structuredClone,
});
this.setNativeFunc(structuredClone, "structuredClone");
}
if (typeof rawMessagePortPostMessage === "function") {
const self = this;
function postMessage(value, transferList) {
try {
return rawMessagePortPostMessage.apply(this, arguments);
} catch (err) {
self.#rewriteDataCloneError(err, value);
}
}
this.#copyFunctionMeta(postMessage, rawMessagePortPostMessage, "postMessage");
const desc =
Object.getOwnPropertyDescriptor(MessagePort.prototype, "postMessage") || {
writable: true,
enumerable: true,
configurable: true,
};
Object.defineProperty(MessagePort.prototype, "postMessage", {
...desc,
value: postMessage,
});
this.setNativeFunc(postMessage, "postMessage");
}
}
#rewriteDataCloneError(err, value) {
if (!err || err.name !== "DataCloneError") {
throw err;
}
const fn = this.#findFunction(value);
if (!fn) {
throw err;
}
const fakeSource = this.#getFunctionSource(fn);
const msg = `${fakeSource} could not be cloned.`;
try {
Object.defineProperty(err, "message", {
value: msg,
configurable: true,
});
} catch (_) {}
try {
if (typeof err.stack === "string") {
const lines = err.stack.split(/\r?\n/);
lines[0] = `${err.name}: ${msg}`;
Object.defineProperty(err, "stack", {
value: lines.join("\n"),
configurable: true,
});
}
} catch (_) {}
throw err;
}
#getFunctionSource(fn) {
try {
return Function.prototype.toString.call(fn);
} catch (_) {
return "function () { [native code] }";
}
}
#findFunction(value, seen = new WeakSet()) {
if (typeof value === "function") return value;
if (value === null || typeof value !== "object") return null;
if (seen.has(value)) return null;
seen.add(value);
if (value instanceof Map) {
for (const [k, v] of value) {
const fk = this.#findFunction(k, seen);
if (fk) return fk;
const fv = this.#findFunction(v, seen);
if (fv) return fv;
}
}
if (value instanceof Set) {
for (const v of value) {
const fv = this.#findFunction(v, seen);
if (fv) return fv;
}
}
for (const key of Reflect.ownKeys(value)) {
let desc;
try {
desc = Object.getOwnPropertyDescriptor(value, key);
} catch (_) {
continue;
}
if (desc && "value" in desc) {
const f = this.#findFunction(desc.value, seen);
if (f) return f;
}
}
return null;
}
#copyFunctionMeta(target, source, name) {
try {
Object.defineProperty(target, "name", {
value: name || source.name,
writable: false,
enumerable: false,
configurable: true,
});
} catch (_) {}
try {
Object.defineProperty(target, "length", {
value: source.length,
writable: false,
enumerable: false,
configurable: true,
});
} catch (_) {}
}
}
使用示例:
const nativeProtect = NativeProtect.getInstance();
function getItem(key) {
return storageMap.get(String(key)) ?? null;
}
nativeProtect.setNativeFunc(getItem, 'getItem');
Object.defineProperty(Storage.prototype, 'getItem', {
value: getItem,
writable: true,
enumerable: true,
configurable: true,
});
注意:
- 只在目标 JS 所在运行上下文内 patch,不要污染宿主 Node.js 全局环境;该上下文可以是
vm、独立 Node 进程或显式隔离的全局对象。 NativeProtect是 fallback,不是第一选择;如果 addon 可加载但源码直接手写函数再setNativeFunc,应回退修改为 addon-first。- 使用
NativeProtectfallback 时必须使用带structuredClone/MessagePort.prototype.postMessageDataCloneError 改写的版本;如果目标会通过structuredClone(fn)或postMessage(fn)检测函数源码,旧版 NativeProtect 会暴露非 native 函数。 - 如果目标 JS 提前保存了原始
Function.prototype.toString,后 patch 可能失效。 - 如果使用
vm,要确保 patch 发生在目标 JS 所在 context 内;如果不用vm,要确保入口文件以独立进程 / 隔离 global 初始化并在结束后不污染其他任务。
属性描述符、访问器与原型链
常见检查:
Object.getOwnPropertyDescriptor(navigator, 'userAgent')
Object.getOwnPropertyDescriptor(window, 'navigator')
Object.getPrototypeOf(document)
navigator.constructor.name
处理原则:
- 所有关键属性都用
Object.defineProperty,禁止用普通赋值替代关键 WebAPI。 - 对不可写、不可枚举、只读 getter 要显式设置;真实浏览器是 accessor 的属性不得降级为 data descriptor。
- getter / setter 的
Function.prototype.toString.call(descriptor.get)/descriptor.set也要 native-like。 constructor通常不可枚举,并且构造函数本身也要做 toString 保护。Symbol.toStringTag与NativeProtect.setObjFunc(obj, "Xxx")只能用于 JS fallback 的[object Xxx];addon 创建的实例不要再额外调用markObjectType/markObjectToString。- 发现
instanceof、Object.getPrototypeOf、constructor.name检测时,补构造函数和完整原型链。
网络代理检测
如果用户所说“代理检测”是网络代理 / IP 检测,而不是 JS Proxy:
- 默认回退到用户真实浏览器手动取证。
- 不默认启用网络代理。
- 如果必须使用代理,必须由用户提供授权代理并确认使用范围。
- 代理被检测时,不尝试绕过风控;应暂停并让用户选择:
- 换回用户真实浏览器手动取证。
- 提供 HAR / cURL / JS 文件离线分析。
- 在授权环境中更换合规网络环境。