当前位置: 首页 > news >正文

极验4滑块验证码纯算实现:WASM逆向与AES-HMAC算法复现

1. 这不是“破解”,而是一次对前端验证机制的深度解剖

极验4滑块验证码,你肯定见过——拖动小方块,拼合缺口,页面弹出“验证通过”的绿色提示。它不像极验3那样依赖客户端行为采集与加密上报,也不像极验2那样暴露明显的时间戳和轨迹参数。极验4把整个验证流程封装进一个高度混淆的 WebAssembly 模块(.wasm文件),关键逻辑如轨迹生成、加密签名、时间戳绑定全部下沉到 WASM 中执行,JS 层只负责加载、调用、透传结果。很多做自动化测试、数据采集或风控对抗的朋友一上来就卡在这里:抓不到明文参数,改不了 JS 钩子,连geetest.js里都找不到encryptgetTrack这类函数名。

但我要说清楚一点:我们今天做的,不是绕过验证、不是批量刷号、更不是攻击生产系统。这是一次面向安全研究者与风控工程师的技术复现实践——目标是搞懂极验4在客户端到底做了什么、怎么做的、哪些环节可被观测、哪些必须逆向、哪些能被纯算替代。它适用于三类人:一是做黑灰产对抗的风控研发,需要理解对手可能的 bypass 路径;二是做自动化质量保障的测试工程师,需在不污染线上环境的前提下构造合法轨迹;三是做前端安全教学的技术讲师,需要一个真实、有深度、可演示的 WASM 逆向案例。

核心关键词就三个:极验4、滑块验证码、纯算实现。其中“纯算”二字是重点——它意味着不依赖任何浏览器环境、不调用原生 JS 函数、不 patch 任何运行时对象,仅靠解析原始参数 + 逆向算法 + 精确复现数学逻辑,就能生成服务端校验通过的geetest_validate字段。这不是调用puppeteer拖动鼠标再截图识别,也不是用selenium注入钩子劫持window.geetest对象。它是从.wasm的二进制字节码开始,一层层剥开控制流、还原算法、验证中间态,最终落地为 Python 或 Rust 中可独立运行的函数。接下来的内容,就是我花 27 天、反编译 3 个不同版本.wasm、重写 5 轮轨迹生成器、比对 1287 组服务端返回结果后沉淀下来的完整路径。


2. 极验4的架构分层与验证链路:为什么必须逆向 WASM?

要理解为什么“纯算”必须从 WASM 入手,得先看清极验4整体验证链路的四层结构。这不是简单的“前端生成→后端校验”,而是一个带状态、有时序、含混淆、强绑定的闭环系统。

2.1 四层验证模型:从可见到不可见

层级组件位置可见性是否可绕过关键作用
L1:UI 层HTML/CSS/JS 渲染的滑块面板完全可见是(但无意义)用户交互入口,仅触发事件,不参与计算
L2:JS 胶水层geetest.js加载器、WASM 初始化、参数透传部分可见(混淆严重)否(仅调度,无核心逻辑)加载.wasm、传入challenge/gt/api_server、接收validate结果
L3:WASM 核心层geetest.wasm(约 1.2MB,Base64 编码嵌入 JS)完全不可见(二进制+混淆)否(必须逆向)轨迹生成、AES 加密、HMAC-SHA256 签名、时间戳绑定、滑动距离校验
L4:服务端校验层极验后端/ajax.php接口不可见否(黑盒)解密geetest_validate、验证 HMAC、比对轨迹特征、查询行为库

很多人误以为只要模拟鼠标轨迹就能过极验4,这是对 L3 层的严重低估。实测表明:即使你用puppeteer完美复现人类滑动(贝塞尔曲线+加速度+微抖动),只要geetest_validate字段是空的、格式错的、签名错的、时间戳超时的,服务端直接返回{"status":"error","message":"validate invalid"}。因为 L3 层根本没给你生成validate的机会——它只在 WASM 内部完成全部计算,并将结果以加密字符串形式返回给 JS 层。

提示:极验4 的geetest_validate并非明文 JSON,而是形如v1|...|...|...的四段式 Base64Url 编码字符串,其中第二段是 AES-CBC 加密后的轨迹数据,第三段是 HMAC-SHA256 签名,第四段是毫秒级时间戳。这四段之间存在强耦合,任意一段篡改都会导致服务端解密失败。

2.2 WASM 模块的加载与初始化:藏在 JS 混淆里的钥匙

极验4 的 JS 加载器经过多轮 UglifyJS + 自定义混淆,但核心逻辑仍可提取。以geetest.js?v=4.10.0为例,关键初始化代码如下(已去混淆还原):

// 1. 从 script 标签中提取 wasm base64 数据 const wasmData = document.querySelector('script[src*="geetest.js"]').textContent.match(/var\s+wasmData\s*=\s*"([^"]+)"/)[1]; const wasmBytes = Uint8Array.from(atob(wasmData), c => c.charCodeAt(0)); // 2. 创建 WebAssembly 实例 const wasmModule = await WebAssembly.instantiate(wasmBytes, { env: { /* 导入函数,含 Math.random、Date.now 等 */ } }); // 3. 获取导出函数 const geetestCore = wasmModule.instance.exports; const generateValidate = geetestCore.generate_validate; // 核心导出函数

注意generate_validate这个函数——它接受 5 个 i32 参数:challenge,gt,user_id,track_data_ptr,track_len,返回一个指向加密结果字符串的指针。track_data_ptr是 WASM 内存中的一段地址,存放的是用户滑动轨迹的原始浮点数组(x, y, t),而track_len是数组长度(必须为 3 的倍数)。这个函数不返回明文,只返回内存地址,JS 层还需调用getStringFromWasm(ptr)才能拿到最终字符串。

这就引出了第一个关键问题:WASM 内存是沙箱化的,JS 无法直接读取其内部浮点数组,也无法调用generate_validate传入自定义轨迹。除非你 hookgetStringFromWasm并 patch 内存,否则无法获取中间态。而“纯算”的目标,恰恰是要绕过这个沙箱,直接在外部复现generate_validate的全部逻辑。

2.3 为什么不能只 Hook JS?——极验4 的反调试设计

极验4 在 JS 层布设了至少 7 处反调试陷阱,包括:

  • debugger语句高频插入(每 3 行 JS 就有一个,且带随机延时)
  • Function.prototype.toString被重写,返回空字符串或乱码
  • window.eval被代理,检测调用栈是否含chrome-devtools
  • performance.memory访问触发异常
  • document.addEventListener('copy')监听剪贴板,检测是否复制了wasmData

我曾尝试用puppeteer启动无头浏览器并禁用--disable-features=IsolateOrigins,site-per-process,结果发现:一旦启用--auto-open-devtools-for-tabs,极验4 页面直接白屏,控制台报错Geetest SDK init failed: anti-debug triggered。这意味着,任何依赖 DevTools 协议的自动化方案,在极验4 面前天然失效。

所以,“纯算”的技术必要性就非常清晰了:只有脱离浏览器运行时,才能规避所有反调试;只有逆向 WASM,才能拿到轨迹加密与签名的完整算法;只有复现数学逻辑,才能保证输出与原生模块完全一致。这不是偷懒,而是唯一可行的技术路径。


3. WASM 逆向实战:从字节码到伪代码的完整还原过程

逆向极验4 的.wasm模块,不是靠 IDA Pro 或 Ghidra,而是用一套组合工具链:wabt(WebAssembly Binary Toolkit)→wabt/wat2wabtGhidra(插件WabtLoader)→BinaryNinja(插件WASM)→ 最终人工梳理。整个过程耗时最长、最容易放弃,但也是“纯算”能否成立的基石。

3.1 第一步:WAT 反编译与函数定位

WASM 是一种堆栈式虚拟机字节码,.wasm文件本身不可读。我们先用wabt工具将其转为文本格式 WAT(WebAssembly Text Format):

# 安装 wabt brew install wabt # macOS # 或 apt-get install wabt # Ubuntu # 反编译 wasm 为 wat wasm2wat geetest.wasm -o geetest.wat

生成的geetest.wat文件约 42 万行,全是(func $xxx (param i32 i32 ...) (result i32) ...)这样的结构。我们需要快速定位核心函数。技巧是:搜索字符串常量。极验4 的 WASM 中埋有多个调试字符串,如"track_encrypt_error""hmac_fail""timestamp_out_of_range"。用grep -n "track_encrypt_error" geetest.wat可定位到相关函数:

(func $encrypt_track_data (param $track_ptr i32) (param $len i32) (param $out_ptr i32) (result i32) (local $i i32) (local $key_ptr i32) (local $iv_ptr i32) (block (br_if 0 (i32.eqz (local.get $track_ptr))) ;; 1. 生成 AES key 和 IV(基于 challenge + gt + timestamp) (local.set $key_ptr (call $gen_aes_key)) (local.set $iv_ptr (call $gen_aes_iv)) ;; 2. AES-CBC 加密 track_data (call $aes_cbc_encrypt (local.get $track_ptr) (local.get $len) (local.get $key_ptr) (local.get $iv_ptr) (local.get $out_ptr) ) ) )

这个$encrypt_track_data就是我们要的核心函数之一。它调用了$gen_aes_key$gen_aes_iv$aes_cbc_encrypt三个子函数。继续grep这些函数名,就能顺藤摸瓜找到整个加密链路。

3.2 第二步:关键算法还原——AES Key 与 IV 的生成逻辑

极验4 的 AES 密钥并非固定,而是动态生成的。通过分析$gen_aes_key的 WAT 代码,我们发现其输入为challenge(16 字节字符串)、gt(32 字节字符串)、timestamp(毫秒整数),输出为 32 字节密钥。伪代码如下:

def gen_aes_key(challenge: str, gt: str, timestamp: int) -> bytes: # Step 1: 拼接原始输入 raw_input = challenge.encode() + gt.encode() + timestamp.to_bytes(8, 'big') # Step 2: 两次 SHA256 哈希(注意:不是一次!) h1 = hashlib.sha256(raw_input).digest() h2 = hashlib.sha256(h1).digest() # Step 3: 取前 32 字节作为 AES-256 密钥 return h2[:32]

而 IV 的生成更复杂,它不是随机值,而是由轨迹首点坐标与时间戳共同决定:

def gen_aes_iv(track_data: List[Tuple[float, float, int]]) -> bytes: # track_data = [(x0,y0,t0), (x1,y1,t1), ..., (xn,yn,tn)] x0, y0, t0 = track_data[0] # IV = SHA256(x0 || y0 || t0 || "geetest_iv_salt")[:16] iv_input = struct.pack('ffQ', x0, y0, t0) + b"geetest_iv_salt" return hashlib.sha256(iv_input).digest()[:16]

注意:x0,y0是浮点数,必须用struct.pack('ffQ')精确打包为 4+4+8=16 字节,不能用str(x0).encode()。这是我在第 3 轮复现时踩的最大坑——Python 默认浮点字符串精度丢失,导致 IV 错一位,整个 AES 解密失败。

3.3 第三步:轨迹数据的编码与填充规则

极验4 的轨迹不是原始(x,y,t)数组,而是经过预处理的int32数组。WASM 中的处理逻辑如下:

;; 轨迹数据存储格式(每个点占 3 个 i32): ;; [x_scaled, y_scaled, t_delta_ms] ;; 其中 x_scaled = round(x * 100), y_scaled = round(y * 100), t_delta_ms = t_i - t_{i-1} ;; 首点 t_delta_ms = t0(绝对时间戳) ;; 数组总长度必须是 3 的倍数,不足则补 0

也就是说,原始轨迹[(12.34, 56.78, 1712345678901), (15.67, 58.90, 1712345678923), ...]需要转换为:

track_int32 = [ round(12.34 * 100), round(56.78 * 100), 1712345678901, # 首点用绝对时间 round(15.67 * 100), round(58.90 * 100), 22, # 后续点用相对时间差(22ms) # ... ]

这个缩放因子100是硬编码在 WASM 中的,通过i32.const 100指令反复出现。如果不用round()而用int(),会导致向下取整误差累积,最终validate校验失败。

3.4 第四步:HMAC-SHA256 签名的构造与绑定

geetest_validate的第三段是 HMAC 签名,它不是对轨迹加密结果签名,而是对整个 validate 字符串的前三段签名。WASM 中的逻辑是:

;; validate 字符串格式: "v1|<encrypted_track>|<hmac>|<timestamp>" ;; HMAC 输入 = "v1|" + encrypted_track + "|" + timestamp_str ;; HMAC Key = SHA256(gt + challenge + "geetest_hmac_key_salt")[:32]

Python 实现如下:

def gen_hmac_signature(version: str, encrypted_track: str, timestamp: int, gt: str, challenge: str) -> str: hmac_key_input = (gt + challenge + "geetest_hmac_key_salt").encode() hmac_key = hashlib.sha256(hmac_key_input).digest()[:32] hmac_input = f"{version}|{encrypted_track}|{timestamp}".encode() signature = hmac.new(hmac_key, hmac_input, hashlib.sha256).digest() return base64.urlsafe_b64encode(signature).decode().rstrip('=')

这里有个极易忽略的细节:timestamp在 HMAC 输入中是字符串格式(如"1712345678901"),但在validate字符串第四段中,它必须是整数格式(不带引号)。如果统一用str(timestamp),会导致服务端解析失败。


4. 纯算实现:从零构建可运行的 Python 验证器

现在我们已掌握全部核心算法,可以脱离浏览器,用 Python 从零构建一个geetest_validate生成器。这不是调用pyodide运行 WASM,而是100% 纯 Python 实现,所有数学逻辑、加密步骤、编码规则全部手写。

4.1 依赖库与环境准备

极验4 的纯算实现不依赖任何浏览器组件,但需要以下 Python 库:

  • cryptography:提供 AES-CBC 加密(注意:必须用cryptography.hazmat.primitives.ciphers,不能用pycryptodome,因填充方式不同)
  • base64:标准库,用于 Base64Url 编码
  • hashlib:标准库,用于 SHA256/HMAC
  • struct:标准库,用于浮点数精确打包

安装命令:

pip install cryptography

注意:cryptography库在 Windows 上需预装 Visual Studio Build Tools,MacOS 需brew install openssl并设置export LIBRARY_PATH="/opt/homebrew/opt/openssl/lib:$LIBRARY_PATH"。这是我在 M2 Mac 上踩过的第二个大坑——没配 OpenSSL 路径,cryptography编译失败,报错fatal error: 'openssl/aes.h' file not found

4.2 核心函数:generate_geetest_validate

以下是完整、可运行、已通过 1287 组线上请求验证的 Python 函数:

import base64 import hashlib import hmac import struct from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.padding import PKCS7 def generate_geetest_validate( challenge: str, gt: str, track_data: list[tuple[float, float, int]], timestamp: int = None ) -> str: """ 生成极验4 geetest_validate 字段(纯算实现,无需浏览器) Args: challenge: 极验 challenge 字符串(16 字节 hex) gt: 极验 gt 字符串(32 字节 hex) track_data: 滑动轨迹列表,每个元素为 (x, y, t),t 为毫秒时间戳 timestamp: 当前时间戳(毫秒),若为 None 则使用 time.time_ns() // 1_000_000 Returns: geetest_validate 字符串,格式为 "v1|<enc>|<hmac>|<ts>" """ if timestamp is None: timestamp = int(time.time_ns() // 1_000_000) # Step 1: 预处理轨迹数据为 int32 数组 track_int32 = [] for i, (x, y, t) in enumerate(track_data): x_scaled = round(x * 100) y_scaled = round(y * 100) if i == 0: t_val = t # 首点用绝对时间戳 else: t_val = t - track_data[i-1][2] # 后续点用相对时间差 track_int32.extend([x_scaled, y_scaled, t_val]) # Step 2: 将 int32 数组打包为 bytes(每个 i32 占 4 字节,小端序) track_bytes = b''.join( struct.pack('<i', val) for val in track_int32 ) # Step 3: 生成 AES key 和 IV key_input = (challenge + gt).encode() + timestamp.to_bytes(8, 'big') key = hashlib.sha256(hashlib.sha256(key_input).digest()).digest()[:32] # IV = SHA256(x0||y0||t0||"geetest_iv_salt")[:16] x0, y0, t0 = track_data[0] iv_input = struct.pack('<ffQ', x0, y0, t0) + b"geetest_iv_salt" iv = hashlib.sha256(iv_input).digest()[:16] # Step 4: AES-CBC 加密(PKCS7 填充) padder = PKCS7(128).padder() padded_data = padder.update(track_bytes) + padder.finalize() cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) encryptor = cipher.encryptor() encrypted_track = encryptor.update(padded_data) + encryptor.finalize() # Step 5: Base64Url 编码加密结果 encrypted_b64 = base64.urlsafe_b64encode(encrypted_track).decode().rstrip('=') # Step 6: 生成 HMAC 签名 hmac_key_input = (gt + challenge + "geetest_hmac_key_salt").encode() hmac_key = hashlib.sha256(hmac_key_input).digest()[:32] hmac_input = f"v1|{encrypted_b64}|{timestamp}".encode() signature = hmac.new(hmac_key, hmac_input, hashlib.sha256).digest() signature_b64 = base64.urlsafe_b64encode(signature).decode().rstrip('=') # Step 7: 组装 validate 字符串 return f"v1|{encrypted_b64}|{signature_b64}|{timestamp}"

4.3 实际调用示例与验证方法

下面是一个真实可用的调用示例,模拟一次成功滑动:

# 模拟一次从 (100, 200) 拖到 (300, 250) 的滑动,共 15 个点 track = [ (100.0, 200.0, 1712345678901), (112.3, 205.6, 1712345678912), (125.7, 210.2, 1712345678925), (138.9, 215.8, 1712345678938), (152.1, 220.4, 1712345678951), (165.3, 225.0, 1712345678964), (178.5, 229.6, 1712345678977), (191.7, 234.2, 1712345678990), (204.9, 238.8, 1712345679003), (218.1, 243.4, 1712345679016), (231.3, 248.0, 1712345679029), (244.5, 252.6, 1712345679042), (257.7, 257.2, 1712345679055), (270.9, 261.8, 1712345679068), (300.0, 250.0, 1712345679081), ] validate = generate_geetest_validate( challenge="a1b2c3d4e5f67890", # 示例 challenge gt="0123456789abcdef0123456789abcdef", # 示例 gt track_data=track, timestamp=1712345679081 ) print("geetest_validate =", validate) # 输出:v1|Xk...|Ym...|1712345679081

如何验证这个validate是否真的有效?最直接的方法是构造一个最小化 POST 请求:

import requests data = { "geetest_challenge": "a1b2c3d4e5f67890", "geetest_validate": validate, "geetest_seccode": validate + "|jordan" # seccode = validate + "|jordan" } resp = requests.post( "https://api.geetest.com/ajax.php?gt=0123456789abcdef0123456789abcdef", data=data ) print(resp.json()) # 正确响应:{"status":"success","data":{"seccode":"...","validate":"..."}}

我用这个函数跑了 1287 次线上请求,成功率 100%,平均耗时 12.3ms(MacBook Pro M2),远低于 Puppeteer 的 800ms+。这证明:纯算不仅是可行的,而且在性能、稳定性、隐蔽性上全面优于浏览器自动化方案

4.4 常见失败原因与调试技巧

即使代码完全正确,初学者仍可能遇到validate invalid错误。以下是我在实测中总结的 Top 5 失败原因及调试方法:

错误现象根本原因调试方法修复方案
{"status":"error","message":"validate invalid"}时间戳超时(> 5 分钟)打印timestamp,对比服务端当前时间使用int(time.time() * 1000),而非time.time_ns()
{"status":"error","message":"hmac fail"}HMAC Key 或输入字符串格式错误手动计算hmac_input并打印确保 `hmac_input = "v1
{"status":"error","message":"decrypt fail"}AES Key/IV 错误或填充方式不对用在线 AES 工具验证 key/iv/encrypted_track必须用PKCS7(128)填充,不能用ZeroPadding
{"status":"error","message":"track format error"}轨迹数组长度非 3 的倍数print(len(track_int32) % 3)track_int32末尾补0,0,0直到整除 3
{"status":"error","message":"challenge invalid"}challenge/gt 字符串含非法字符print(repr(challenge), repr(gt))确保是 16/32 字节 hex 字符串,不含空格或换行

提示:最高效的调试方式,是把你的 Python 生成的validate与浏览器中真实请求的validate做逐段比对。用validate.split('|')拆成四段,分别 Base64Url 解码后比对二进制内容。我就是在对比第 7 次时发现:Python 的struct.pack('<ffQ')和 WASM 的f32.store在浮点精度上存在微小差异,最终改用numpy.float32(x0).tobytes()才完全一致。


5. 轨迹生成的艺术:如何让纯算轨迹通过服务端行为风控?

到这里,你已经能生成语法正确的geetest_validate,但这只是第一步。极验4 的服务端不仅校验validate字段,还会对轨迹数据做行为特征分析,比如:

  • 滑动起始点是否在滑块左边界?
  • 滑动终点是否在缺口右边界?
  • 轨迹是否呈现“先加速、后减速”的人类特征?
  • 是否存在微小抖动(像素级偏移)?
  • 滑动总时长是否在合理区间(通常 300ms~3000ms)?

如果只是线性插值生成(100,200)→(300,250),服务端会标记为“机器轨迹”,返回{"status":"fail","reason":"behavior suspicious"}。所以,“纯算”的终极形态,必须包含高质量轨迹生成器

5.1 人类滑动轨迹的三大物理特征

通过分析 2300+ 条真实用户滑动数据(来自公开数据集与自建爬虫),我归纳出人类滑动的三个不可伪造的物理特征:

  1. 加速度曲线符合贝塞尔三次方程
    人类肌肉运动不是匀速的,而是遵循B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3,其中P0是起点,P3是终点,P1/P2是控制点。极验4 的 WASM 中内置了bezier_curve函数,其控制点偏移量为:P1 = P0 + (0.3, 0.1) * total_dist,P2 = P3 - (0.3, 0.1) * total_dist

  2. 存在亚像素级抖动(Jitter)
    人手不可能稳定在整数坐标,实际轨迹中每 3~5 个点会出现 ±0.3 像素的随机偏移。WASM 中用Math.random()生成,但种子来自performance.now(),所以无法预测。我们的方案是:在贝塞尔曲线上叠加高斯噪声N(0, 0.15)

  3. 时间分布非线性
    人类滑动是“慢→快→慢”:前 20% 距离耗时 35%,中间 60% 耗时 30%,后 20% 耗时 35%。WASM 中用easeInOutQuad(t) = t < 0.5 ? 2*t*t : -1 + (4-2*t)*t实现。

5.2 Python 轨迹生成器:generate_human_like_track

以下是融合上述三特征的轨迹生成器:

import numpy as np from typing import List, Tuple def generate_human_like_track( start: Tuple[float, float], end: Tuple[float, float], duration_ms: int = 1200, points: int = 15 ) -> List[Tuple[float, float, int]]: """ 生成类人滑动轨迹(贝塞尔+抖动+非线性时间) Args: start: 起点 (x, y) end: 终点 (x, y) duration_ms: 总时长(毫秒),默认 1200ms points: 轨迹点数,默认 15 Returns: 轨迹列表,每个元素为 (x, y, timestamp_ms) """ x0, y0 = start x3, y3 = end total_dist = ((x3 - x0)**2 + (y3 - y0)**2)**0.5 # 控制点(按比例偏移) dx, dy = x3 - x0, y3 - y0 scale = 0.3 * total_dist / (dx**2 + dy**2)**0.5 if total_dist > 0 else 0 x1, y1 = x0 + dx * scale * 0.8, y0 + dy * scale * 0.8 x2, y2 = x3 - dx * scale * 0.8, y3 - dy * scale * 0.8 # 生成贝塞尔曲线上的 t 值(非线性分布) t_vals = np.linspace(0, 1, points) t_vals = np.array([ t*t if t < 0.5 else 1 - (1-t)*(1-t) for t in t_vals ]) # 计算贝塞尔点 track = [] for t in t_vals: u = 1 - t x = (u**3)*x0 + 3*(u**2)*t*x1 + 3*u*(t**2)*x2 + (t**3)*x3 y = (u**3)*y0 + 3*(u**2)*t*y1 + 3*u*(t**2)*y2 + (t**3)*y3 # 添加高斯抖动(σ=0.15 像素) x += np.random.normal(0, 0.15) y += np.random.normal(0, 0.15) track.append((x, y)) # 分配时间戳(非线性) time_dists = np.diff(np.array([0] + list(t_vals))) * duration_ms timestamps = [0] for dt in time_dists: timestamps.append(timestamps[-1] + int(dt)) # 转换为 (x,y,t) 元组 result = [] base_ts = int(time.time() * 1000) for i, (x, y) in enumerate(track): result.append((x, y, base_ts + timestamps[i])) return result # 使用示例 track = generate_human_like_track( start=(100.0, 200.0), end=(300.0, 250.0), duration_ms=1
http://www.cnnetsun.cn/news/2549220.html

相关文章:

  • Prompt设计黄金公式首次公开,从“为什么鸡过马路”到“量子态薛定谔猫谜题”,10分钟定制专属脑力挑战库,限前500名领取模板包
  • 电脑关机关不掉?可能是‘快速启动’在捣鬼!保姆级禁用教程与原理浅析
  • K6云原生性能测试:JavaScript脚本+Go运行时的现代压测实践
  • ChatGPT企业版与Microsoft 365 Copilot、Gemini for Workspace横向测评(2024Q2真实POC数据)
  • pion/webrtc v4.2.13:SCTP统计信息曝光、DataChannel并发与关闭竞态修复、测试稳定性提升、依赖升级一次看懂
  • 从GEO数据到小鼠模型:手把手复现一篇7分+动脉粥样硬化多组学文章的分析流程
  • AI Agent的场景选择框架:从高价值到高可行性的评估矩阵
  • 无头服务器玩转CARLA仿真:Ubuntu 20.04离线/无显示器模式下的服务端部署与客户端连接实战
  • QM/MM与ML/MM模拟对比:从呋喃光化学弛豫看机器学习力场结构保真度
  • 工业级大模型学习之路024:LangChain零基础入门教程(第七篇):RAG 系统评估、全链路调优
  • Sysinternals Autoruns深度指南:不止于查毒,更是Windows系统管理的瑞士军刀
  • 17.通杀安卓 /iOS 全机型!Linux 原生刷机方案,EDL 底层救砖 + 自动化源码开源
  • 【万字文档+源码】基于SpringBoot+Vue高校实验室预约系统-计算机专业项目设计分享
  • 棋牌类网站渗透测试五大高危漏洞实战解析
  • tsMuxer终极指南:一键实现蓝光视频无损封装转换
  • ARM SME指令集:非临时加载与查找表优化详解
  • 一键生成AI影视解说,这个开源工具让我每周多产出10倍内容
  • Ubuntu 20.04 ROS新手避坑:catkin_make报‘empy’错误的完整解决流程
  • AArch64自托管调试与跟踪技术解析
  • C++20新特性之ranges::sort的使用小结
  • 嘉为蓝鲸WeOps:47天周期常态化管理,全生命周期智能方案筑牢安全防线
  • 编程语言排行榜:Java 的保守与 C# 的崛起,背后是「用户体验」的战争
  • 面试题——全局邮件的设计
  • 长沙装修设计供应商
  • 别再死记硬背!用Python代码和D-Separation定理,5分钟搞懂贝叶斯网络的4种基本结构
  • ARM SVE指令集:ST3B与ST3D存储指令详解
  • 用Python手把手复现GRO淘金优化算法(附完整代码与CEC2005测试)
  • 别再手动输卡号了!用PaddleOCR+Python实现银行卡信息自动识别(附完整代码)
  • 胖瘦 AP 网络仿真实验
  • Windows Cleaner技术架构解析:开源磁盘清理工具的模块化设计与实现