Files
huya-go/purchase_logic_analysis.md
T
2026-07-06 18:47:10 +08:00

1355 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 抢购/抢兑逻辑静态分析记录
## 分析范围
- 项目目录:`/Users/yml/Downloads/多功能计算器2.7.unpacked.exe_export_for_ai`
- 样本形态:Windows EXE 的逆向导出结果,不是源码仓库。
- 主要依据:
- `strings.txt`
- `memory/00586000--005CD000.txt`
- `pointers.txt`
- `function_index.txt`
- `decompile/*.c`
- `disassembly/42FC3C.asm`
- 追加依据:
- `imgs/1.png`
- `imgs/2.png`
- `imgs/3.png`
- `/Users/yml/codes/douyu_login_py/core/huya/http_client.py`
- `/Users/yml/codes/douyu_login_py/core/huya/wss_client.py`
- `/Users/yml/codes/douyu_login_py/core/huya/wup_protocol.py`
- `/Users/yml/codes/douyu_login_py/core/huya/taf_protocol.py`
- `/Users/yml/codes/douyu_login_py/core/huya/activity_structs.py`
- 本记录为静态分析,没有运行程序,也没有打开浏览器。
## 总体结论
当前样本里确实存在“抢购/抢兑/自动兑换”相关逻辑。
这套逻辑不是普通计算器功能,而是一组虎牙活动自动监控与积分兑换流程:
1. 登录/连接后维护 WUP/WebSocketCommand 通道。
2. 周期性查询活动奖品列表。
3. 根据界面配置的目标奖品或自定义 pid 匹配奖品。
4. 命中后构造 `scoreExchangePrize(cookie, pid, sid)` 兑换请求。
5. 通过通用 HTTP 封装发送编码后的 WUP/WebSocketCommand 数据。
6. 根据返回的成功、频控、下架等状态继续重试、记录或重启监控。
## 核心接口
嵌入 JS/WUP 协议代码在 `strings.txt` 中,关键函数如下:
- `scoreExchangePrize(cookie, pid, sid)`
- 位置:`strings.txt:617`
- servant/func:`webActUI` / `scoreExchangePrize`
- 请求体:`ExchangePrizeReq`
- 关键字段:
- `pid`
- `sid`
- `userId.lUid`
- `userId.sCookie`
- `userId.sHuYaUA = "web&1.0&huya"`
- `getActPrizeList(cookie, sid)`
- 位置:`strings.txt:622`
- servant/func:`webActUI` / `getActPrizeList`
- 用于查询活动奖品列表。
- `getUserPrizeRecords(cookie, sid)`
- 位置:`strings.txt:622`
- servant/func:`webActUI` / `getUserPrizeRecords`
- 用于查询用户兑换/中奖记录。
- `getUserScore(cookie, sid)`
- 位置:`strings.txt:617`
- 用于查询剩余积分。
- `checkUserBindGameAccount(cookie, outerActId)`
- 位置:`strings.txt:625`
- 用于检查绑定角色/区服信息。
## WUP 封装逻辑
`_sendWup` 在 JS 中构造 `w.Wup`:
- 设置 servant:`setServant(t)`
- 设置 func:`setFunc(e)`
- 设置 request id:`setRequestId(r)`
- 写入请求结构:`writeStruct("tReq", i)`
- 外层再包成 `E.WebSocketCommand`
- `iCmdType = EWSCmd_WupReq`
- 最后编码成 hex/base64 样式字符串返回给 C 层发送。
响应解码通过 `decodeTaf`:
- 先解 `WebSocketCommand`
- 如果是 `EWSCmd_WupRsp`,再解 WUP
- 根据 `sFuncName` 查 `WupMapping`
- 映射包括:
- `scoreExchangePrize -> ExchangePrizeResp`
- `getActPrizeList -> GetActPrizeListResp`
- `getUserScore -> GetUserScoreResp`
- `checkUserBindGameAccount -> CheckUserBindGameAccountResp`
- `getUserPrizeRecords -> GetUserPrizeRecordsResp`
## 自动监控入口
### `sub_43597D`
位置:`decompile/43597D.c`
这是自动监控的外层调度器:
- 初始化账号/任务列表相关容器。
- 等待 `dword_69EE3C` 连接对象可用:
- `decompile/43597D.c:133`
- 打印/记录线程创建成功:
- `decompile/43597D.c:152`
- 设置自动监控标志:
- `dword_69EE38 = 1`
- `decompile/43597D.c:156`
- 遍历账号和活动/任务组合。
- 通过连接对象的 `+24` 方法投递任务:
- `decompile/43597D.c:221`
- 投递入口为:
- `sub_436737 -> sub_4363A4`
- 循环间隔走 `sub_4276C9`:
- `decompile/43597D.c:243`
- 结束时清理并关闭监控:
- `dword_69EE38 = 0`
- `decompile/43597D.c:248`
### `sub_436756`
位置:`decompile/436756.c`
这是继续监控线程入口:
```c
int sub_436756()
{
return sub_43597D();
}
```
汇编中可见在未命中奖品后会投递该入口继续监控:
- `disassembly/42FC3C.asm:2872`
## 请求构造函数对应关系
这些函数本质上都是从 ScriptControl/JScript 调用指定函数名,拿到编码后的 WUP/WebSocketCommand 数据,再转成二进制/缓冲区。
从 `pointers.txt` 可确认:
- `sub_435001`
- 字符串指针:`getActPrizeList`
- `pointers.txt:2376`
- 用于构造奖品列表查询请求。
- `sub_436610`
- 字符串指针:`scoreExchangePrize`
- `pointers.txt:2479`
- 用于构造兑换请求。
- `sub_4362B2`
- 字符串指针:`getUserPrizeRecords`
- `pointers.txt:2463`
- 用于构造兑换记录查询请求。
- `sub_42FA78`
- 多个账号/槽位回调中使用。
- 具体函数名需结合 `sub_4968A7(5)` 的运行时字符串表确认。
- `sub_42FB54`
- 多个账号/槽位回调中使用。
- 具体函数名需结合 `sub_4968A7(5)` 的运行时字符串表确认。
## 核心响应处理
### `sub_43E81F`
位置:`decompile/43E81F.c`
这是一个槽位回调模板,其他如 `436B5A.c`、`4394F1.c`、`43BE88.c`、`443B4D.c`、`458805.c`、`4604CA.c` 等逻辑高度同构,只是开头取不同账号/任务槽位。
关键解析字段:
- `status`
- `decompile/43E81F.c:213`
- `msg`
- 实际变量名由反编译器表现为 `off_58DDE9`
- `decompile/43E81F.c:221`
- `iRequestId`
- `decompile/43E81F.c:230`
- `score`
- `decompile/43E81F.c:239`
- `roleName`
- `decompile/43E81F.c:248`
- `areaName`
- `decompile/43E81F.c:260`
- `platName`
- `decompile/43E81F.c:272`
## iRequestId 分支
已确认的分支含义:
- `iRequestId == "1"`
- 奖品列表/兑换响应主分支。
- C 侧在该分支解析 `prizeList.value`,也处理 `scoreExchangePrize` 返回。
- 位置:`decompile/43E81F.c:330`
- `iRequestId == "2"`
- `getUserScore` 积分分支。
- 位置:`decompile/43E81F.c:288`
- `iRequestId == "6"`
- `getUserPrizeRecords` 兑换记录分支。
- 位置:`decompile/43E81F.c:286` 和 `decompile/43E81F.c:673`
- `iRequestId == "17"`
- `checkUserBindGameAccount` 绑定信息分支。
- 位置:`decompile/43E81F.c:307`
- `iRequestId == "-1"`
- `doLaunch/wsLaunch`,读取 `sGuid`。
- 位置:`decompile/43E81F.c:766`
## 奖品列表解析
`iRequestId == "1"` 时会解析:
- `prizeList.value`
- `decompile/43E81F.c:337`
- `prizeList.value[i].id`
- 汇编确认:`disassembly/42FC3C.asm:2051`
- `prizeList.value[i].name`
- 汇编确认:`disassembly/42FC3C.asm:2106`
- `prizeList.value[i].num`
- 汇编确认:`disassembly/42FC3C.asm:2161`
- `prizeList.value[i].percent`
- 汇编确认:`disassembly/42FC3C.asm:2226`
- `prizeList.value[i].newScore`
- 汇编确认:`disassembly/42FC3C.asm:2298`
同时有 `num * percent / 100` 一类计算,用于展示或估算剩余可兑数量:
- `disassembly/42FC3C.asm:2276`
- `disassembly/42FC3C.asm:2350`
## 命中策略
命中逻辑有两类信号:
1. 自定义 pid
- 常量:`自定义pid`
- 地址:`0x5A479F`
- 汇编引用:`disassembly/42FC3C.asm:1950`
2. 自动选择奖品
- 常量:`自动选择奖品:`
- 地址:`0x5A47EB`
- 汇编引用:`disassembly/42FC3C.asm:2775`
当没有在 `prizeList.value` 中查到目标时:
- 输出:`未查询到所需物品,继续监控`
- 地址:`0x5A47FD`
- 汇编引用:`disassembly/42FC3C.asm:2886`
- 会等待约 100ms:
- `sub_4276C9(100, 1, 0, 0)`
- `disassembly/42FC3C.asm:2847`
- 再投递 `sub_436756` 继续监控:
- `disassembly/42FC3C.asm:2872`
## 兑换请求发送
兑换请求由 `scoreExchangePrize(cookie, pid, sid)` 生成 WUP/WebSocketCommand 数据。
C 层最终通过 `sub_428C41` 发送:
- 第一处发送:
- `decompile/43E81F.c:512`
- 汇编:`disassembly/42FC3C.asm:1376`
- 第二处发送:
- `decompile/43E81F.c:620`
- 汇编:`disassembly/42FC3C.asm:1749`
这两处发送均使用:
- URL/目标地址:`unk_69EE18`
- 方法参数:`a2 = 1`
- 在 `sub_428C41` 中 `a2 = 1` 对应第 2 个方法,即 `POST`
- `decompile/428C41.c:159`
## HTTP 封装函数
### `sub_428C41`
位置:`decompile/428C41.c`
这是通用 HTTP 请求封装,不是业务函数。
功能特征:
- 支持多种 HTTP 方法:
- GET
- POST
- HEAD
- PUT
- OPTIONS
- DELETE
- TRACE
- CONNECT
- 方法分支位置:
- `decompile/428C41.c:159`
- 检测 `https://`:
- `decompile/428C41.c:212`
- 默认 User-Agent:
- `Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)`
- `decompile/428C41.c:225`
- 默认超时约 5000ms:
- `decompile/428C41.c:524`
## 响应处理
### 请求成功/兑换成功
相关常量:
- `请求成功`
- 地址:`0x5A4756`
- 在 `decompile/43E81F.c:453` 比较。
- `:兑换成功`
- 地址:`0x5A475F`
- 汇编引用:`disassembly/42FC3C.asm:1092`
逻辑含义:
- 当 `status == "200"` 且 `msg == "请求成功"` 时,会进入成功处理路径。
- 成功输出中会把当前奖品名与 `:兑换成功` 拼接。
注意:
- `byte_5A476B` 不是“兑换成功”,而是“操作频繁,请稍后再试”。
- `:兑换成功` 实际从 `0x5A475F` 开始。
### 操作频繁
相关常量:
- `操作频繁,请稍后再试`
- 地址:`0x5A476B`
- 比较位置:`decompile/43E81F.c:569`
- `操作频繁数据#`
- 地址:`0x5A477F`
- 汇编引用:`disassembly/42FC3C.asm:1561`
逻辑含义:
- 如果返回频控提示,会记录频控数据。
- 之后仍会进入后续请求/重试相关路径。
### 奖品已下架
相关常量:
- `奖品已下架`
- 地址:`0x5A4827`
- 汇编引用:`disassembly/42FC3C.asm:3888`
关联逻辑:
- 检测到下架后,会调用连接对象停止/关闭方法:
- `disassembly/42FC3C.asm:3908`
- 打印 `线程销毁`
- 地址:`0x5A4832`
- 汇编引用:`disassembly/42FC3C.asm:3914`
- 清除监控标志:
- `dword_69EE38 = 0`
- `disassembly/42FC3C.asm:3933`
- 短暂等待后打印 `执行自动监控`
- 地址:`0x5A483B`
- 汇编引用:`disassembly/42FC3C.asm:3943`
- 调用 `sub_4349E2` 尝试重启/恢复监控:
- `disassembly/42FC3C.asm:3954`
## 已解码关键中文/字符串常量
从 `memory/00586000--005CD000.txt` GBK 解码:
| 地址 | 内容 |
| --- | --- |
| `0x5A3C72` | `1` |
| `0x5A471F` | `2` |
| `0x5A472E` | `17` |
| `0x5A4756` | `请求成功` |
| `0x5A475F` | `:兑换成功` |
| `0x5A476B` | `操作频繁,请稍后再试` |
| `0x5A477F` | `操作频繁数据#` |
| `0x5A478F` | `prizeList.value` |
| `0x5A479F` | `自定义pid` |
| `0x5A47A9` | `].id` |
| `0x5A47AE` | `prizeList.value[` |
| `0x5A47BF` | `].num` |
| `0x5A47C5` | `].percent` |
| `0x5A47D7` | `].newScore` |
| `0x5A47E2` | `开始监控` |
| `0x5A47EB` | `自动选择奖品:` |
| `0x5A47FD` | `未查询到所需物品,继续监控` |
| `0x5A4818` | `-1` |
| `0x5A481B` | `sGuid` |
| `0x5A4821` | `guid=` |
| `0x5A4827` | `奖品已下架` |
| `0x5A4832` | `线程销毁` |
| `0x5A483B` | `执行自动监控` |
| `0x5A4C14` | `线程创建成功...` |
| `0x5A4C24` | `getUserPrizeRecords` |
| `0x5A4C38` | `scoreExchangePrize` |
| `0x5A4C5F` | `prize:` |
## 同构槽位回调
下面这些文件中的主体逻辑基本相同,差异主要是开头从 `dword_69EDD8 + 4 * slot` 读取不同账号/任务槽位:
- `decompile/436B5A.c`
- `decompile/4394F1.c`
- `decompile/43BE88.c`
- `decompile/43E81F.c`
- `decompile/4411B6.c`
- `decompile/443B4D.c`
- `decompile/4464E4.c`
- `decompile/448E7B.c`
- `decompile/44B812.c`
- `decompile/44E1A9.c`
- `decompile/4534D7.c`
- `decompile/455E6E.c`
- `decompile/458805.c`
- `decompile/45B19C.c`
- `decompile/45DB33.c`
- `decompile/4604CA.c`
- `decompile/462E61.c`
- `decompile/4657F8.c`
这些函数均引用同一批常量:
- `byte_5A3C72`
- `unk_5A471F`
- `unk_5A472E`
- `byte_5A4756`
- `byte_5A476B`
- `byte_5A4818`
- `unk_5A4C5F`
可见它们是一套多账号/多槽位并发模板。
## 流程伪代码
```text
启动自动监控:
初始化账号列表和活动/任务列表
等待连接对象 dword_69EE3C 可用
dword_69EE38 = 1
while 未停止 且 未超过循环次数:
for 每个账号:
for 每个活动/任务:
投递请求任务 sub_436737 -> sub_4363A4
sleep(配置间隔)
收到 WUP 响应:
json = decodeTaf(response)
status = json.status
msg = json.msg
requestId = json.iRequestId
if requestId == 2:
读取 score 并输出剩余积分
if requestId == 17:
读取 roleName/areaName/platName
输出绑定信息或未获取到绑定信息
if requestId == 1:
if 响应里无目标 pid/目标奖品:
输出 未查询到所需物品,继续监控
sleep(约 100ms)
投递 sub_436756 继续监控
else:
遍历 prizeList.value
读取 id/name/num/percent/newScore
记录/展示奖品信息
构造 scoreExchangePrize(cookie, pid, sid)
通过 sub_428C41 POST 发出
if status == 200 且 msg == 请求成功:
输出 当前奖品 + :兑换成功
if msg == 操作频繁,请稍后再试:
记录操作频繁数据
进入后续重试/刷新路径
if 奖品已下架:
停止连接/销毁线程
dword_69EE38 = 0
再次执行自动监控
if requestId == 6:
遍历 userPrizeList.value
输出/记录兑换记录
if requestId == -1:
读取 sGuid
```
## 待继续确认点
1. `unk_69EE18` 的运行时 URL 来源还未完全定位。
- 静态字符串里没有直接看到固定初始化。
- 更像是连接对象、配置或运行时脚本中保存的地址。
2. `sub_4968A7(index)` 的运行时字符串表需要进一步命名。
- 目前已通过 `pointers.txt` 和上下文确认一部分业务函数。
- 仍有若干 `sub_4968A7(5/6/7/9)` 取出的值需要结合更多调用点还原。
3. GUI 控件字段可以继续命名。
- 已知存在自定义 pid、自动选择奖品、循环/间隔相关配置。
- 但每个控件 ID 到语义字段的映射还可以继续细分。
4. `getActPrizeList` 的 request id 在 JS 中传入 `null`,但 C 侧奖品列表解析落在 `iRequestId == "1"` 分支。
- 可能是 WUP/脚本层默认值或反编译导出表现导致。
- 这一点如需完全确认,需要继续追 `setRequestId(null)` 的编码结果或运行时响应样本。
## 一句话总结
程序的抢兑链路是:
```text
查活动奖品列表 -> 匹配自定义 pid/目标奖品 -> 选中奖品 -> 构造 scoreExchangePrize(cookie,pid,sid)
-> POST 发送 WUP/WebSocketCommand -> 根据成功/频控/下架结果继续记录、重试或重启监控
```
## 2026-07-06 追加:截图与参考项目对照
### 截图里的运行态信息
`imgs/1.png`、`imgs/2.png`、`imgs/3.png` 能把 GUI 字段和逆向逻辑对齐:
- 主页面按钮:
- `查询`
- `积分查询`
- `绑定查询`
- `兑换记录`
- `兑换1次`
- `停止兑换`
- `取消定时`
- 设置页面参数:
- `线程 = 5`
- `次数 = 300`
- `延时 = 150`
- `Cookie`
- `网页登录`
- 定时时间选择器
- 已登录账号页面:
- 多账号表列:`num / cookie / name / 状态 / 首个兑换`
- 每个账号可勾选。
- 下方共用同一份奖品表。
- 奖品表列:
- `num`
- `name`
- `剩余`
- `总量`
- `价格`
- 日志示例:
- `登录成功`
- `你选择了:套装-朝萌小黄鸭`
与反编译对应关系:
- `次数` 基本对应 `sub_43597D` 中的 `v51`,循环条件为 `v46 < v51`:
- `decompile/43597D.c:73-95`
- `decompile/43597D.c:243-245`
- `延时` 基本对应 `sub_43597D` 中的 `v50`,外层循环 sleep:
- `decompile/43597D.c:86-90`
- `decompile/43597D.c:243`
- `线程` 与连接对象/worker 数量相关,`sub_43597D` 会通过连接对象方法读取可用数量并投递任务:
- `decompile/43597D.c:160`
- `decompile/43597D.c:221`
- 具体 UI 控件到线程池大小的赋值点还未完全命名。
- 多账号、多目标组合会形成笛卡尔积投递:
- 外层账号列表:`v53`
- 内层目标/活动列表:`v52`
- 投递入口:`sub_436737 -> sub_4363A4`
- `decompile/43597D.c:163-227`
### JS 请求构造的精确字段
从 `strings.txt` 里抽出的 JS 可以确认:
- `_sendWup(servant, func, req, requestId, ..., options)`
- 构造 `w.Wup`
- `setServant(servant)`
- `setFunc(func)`
- `setRequestId(requestId)`
- `writeStruct("tReq", req)`
- 再包进 `E.WebSocketCommand`
- `iCmdType = EWSCmd_WupReq`
- `traceId = hex16:hex16:0:0`
- 返回 base64 编码后的 `WebSocketCommand` 字节。
- `scoreExchangePrize(cookie, pid, sid)`
- `ExchangePrizeReq.pid = pid`
- `ExchangePrizeReq.sid = sid`
- `userId.iTokenType = 0`
- `userId.lUid = parseInt(cookie["yyuid"])`
- `userId.sCookie = cookie`
- `userId.sHuYaUA = "web&1.0&huya"`
- `requestId = 1`
- `getActPrizeList(cookie, sid)`
- `GetActPrizeListReq.userId.lUid = yyuid`
- `GetActPrizeListReq.userId.sCookie = cookie`
- `GetActPrizeListReq.userId.sHuYaUA = "web&1.0&huya"`
- `GetActPrizeListReq.sid = sid`
- JS 里 `requestId = null`
- 但 C 侧奖品列表和兑换响应都落在 `iRequestId == "1"` 分支,需要按实际响应确认。
- `getUserPrizeRecords(cookie, sid)`
- `GetUserPrizeRecordsReq.sid = sid`
- `userId.lUid = yyuid`
- `userId.sGuid = guid`
- `userId.sCookie = cookie`
- `userId.sHuYaUA = "webh5&1.0.0&huya"`
- `requestId = 6`
- `getUserScore(cookie, sid)`
- `requestId = 2`
- `userId.sHuYaUA = "web&1.0&huya"`
- `checkUserBindGameAccount(cookie, outerActId)`
- `requestId = 17`
- `userId.sHuYaUA = "pc&1.0.0&huya"`
- `gid = "3203"`
- `doLaunch(cookie)`
- servant/func:`launch` / `wsLaunch`
- `requestId = -1`
- `sUA = "webh5&0.0.1&activity&other"`
- `sAppSrc = "HUYA&ZH&2052"`
### 奖品结构字段
样本内 JS 定义的 `TActPrizeInfo` 字段:
| tag | 字段 | 类型/含义 |
| --- | --- | --- |
| 0 | `id` | 奖品 pid |
| 1 | `sid` | 活动 sid |
| 2 | `name` | 奖品名 |
| 3 | `score` | 旧积分价格 |
| 4 | `type` | 类型 |
| 5 | `img` | 图片 |
| 6 | `sort` | 排序 |
| 7 | `frequency` | 频次 |
| 8 | `frequencyLimit` | 频次限制 |
| 9 | `createTime` | 创建时间 |
| 10 | `num` | 总量 |
| 11 | `status` | 状态 |
| 12 | `percent` | 剩余百分比文本 |
| 13 | `newScore` | 新积分价格 |
| 14 | `tags` | 标签/分类 |
| 15 | `leftNum` | 剩余数量字段 |
| 16 | `isShowNum` | 是否展示数量 |
当前 C 回调实际重点读取:
- `id`
- `name`
- `num`
- `percent`
- `newScore`
截图中的展示大概率为:
- `总量 = num`
- `价格 = newScore`
- `剩余 = floor(num * percent / 100)` 或近似取整
例子:截图里 `套装-朝萌小黄鸭` 行显示 `剩余 6000 / 总量 24001 / 价格 988`,和 `num * percent / 100` 这类计算路径吻合:
- `decompile/43E81F.c:401-419`
- `disassembly/42FC3C.asm:2276`
- `disassembly/42FC3C.asm:2350`
## 参考项目可复用结论
参考项目:`/Users/yml/codes/douyu_login_py`
### HTTP RPC 通道
文件:`core/huya/http_client.py`
该项目已经实现了一条可直接迁移到 Go 的 HTTP POST RPC 链路:
- URL:
- `https://cdnws.api.huya.com/?baseinfo=<base64url(WSConnectParaInfo)>`
- Body:
- 完整 WUP 包,带 4 字节大端长度前缀。
- Header:
- `Content-Type: application/octet-stream`
- `Origin: https://zt.huya.com`
- `Referer: https://zt.huya.com/`
- PC Chrome UA
- `WSConnectParaInfo` baseinfo 字段:
| tag | 字段 | 说明 |
| --- | --- | --- |
| 0 | `lUid` | yyuid |
| 1 | `sGuid` | guid,可为空 |
| 2 | `sUA` | `webh5&0.0.1&websocket&&diypc_52775` |
| 3 | `sAppSrc` | `HUYA&ZH&2052` |
| 4 | `sMid` | 空 |
| 5 | `sExp` | 空 |
| 6 | `iTokenType` | 0 |
| 7 | `sToken` | 空 |
| 8 | `sCookie` | 完整 Cookie |
| 9 | `sTraceId` | `hex16:hex16:0:0` |
| 10 | `mCustomHeaders` | 空 Map |
便捷方法:
- `query_user_score`
- `get_act_prize_list`
- `get_user_prize_records`
- `score_exchange_prize`
- `check_user_bind_game_account`
注意:
- 参考项目的 HTTP `call_rpc` 统一 `setRequestId(1)`。
- 这和样本 JS 的 `getUserScore=2`、`getUserPrizeRecords=6`、`check=17` 不完全一致。
- 对 HTTP cdnws 通道来说,服务端主要靠 servant/func 和 `tReq` 解析,requestId 更像客户端响应关联字段。
- 如果目标是“尽量复刻样本”,Go 版应允许每个接口显式指定 requestId。
### TAF/WUP 编解码
文件:
- `core/huya/taf_protocol.py`
- `core/huya/wup_protocol.py`
可直接翻译为 Go:
- TAF type:
- `INT8=0`
- `INT16=1`
- `INT32=2`
- `INT64=3`
- `FLOAT=4`
- `DOUBLE=5`
- `STRING1=6`
- `STRING4=7`
- `MAP=8`
- `LIST=9`
- `STRUCT_BEGIN=10`
- `STRUCT_END=11`
- `ZERO=12`
- `SIMPLE_LIST=13`
- 头部:
- `tag < 15`:单字节 `(tag << 4) | type`
- `tag >= 15`:两字节 `0xF0 | type, tag`
- 整数写入要做压缩:
- 0 写 `ZERO`
- int8 范围写 `INT8`
- int16 范围写 `INT16`
- int32 范围写 `INT32`
- 否则写 `INT64`
- WUP v3:
- tag1 `iVersion`
- tag2 `cPacketType`
- tag3 `iMessageType`
- tag4 `iRequestId`
- tag5 `sServantName`
- tag6 `sFuncName`
- tag7 `sBuffer`
- tag8 `iTimeout`
- tag9 `context`
- tag10 `status`
- `sBuffer = Map<string, bytes>{"tReq": encodedStruct}`
- 最外层加 4 字节大端总长度。
### 活动结构体
文件:`core/huya/activity_structs.py`
Go 版最小需要先实现:
- `ActivityUserId`
- tag0 `lUid`
- tag1 `sGuid`
- tag2 `sToken`
- tag3 `sHuYaUA`
- tag4 `sCookie`
- tag5 `iTokenType`
- tag6 `sDeviceInfo`
- `GetActPrizeListReq`
- tag0 `userId`
- tag1 `sid`
- `GetActPrizeListResp`
- tag0 `status`
- tag1 `msg`
- tag2 `prizeList`
- tag4 `tagList`
- `ScoreExchangePrizeReq`
- tag0 `userId`
- tag1 `sid`
- tag2 `pid`
- tag3 `ip`
- tag4 `clientEnv`
- tag5 `source`
- tag6 `isRole`
- `ScoreExchangePrizeResp`
- tag0 `status`
- tag1 `msg`
- tag3 `orderId`
- tag4 `exchangeInfo`
- tag5 `actPreCondition`
- `GetUserPrizeRecordsReq/Resp`
- `GetUserScoreReq/Resp`
### WSS 参考项目和当前样本的差异
文件:`core/huya/wss_client.py`
参考项目的 WSS 是商城通道:
- host:`77bc035c-ws.va.huya.com`
- frame header:`>BBI`
- 1B version
- 1B command
- 4B sequence
- 初始化:
- heartbeat
- `launch.wsLaunch`
- `mobileui.getConfig`
- register
- confirm
- business RPC
- 响应匹配:
- 服务端响应 seq 不一定等于请求 seq
- 项目用 FIFO 队列匹配。
当前样本 JS 的业务外层是 `E.WebSocketCommand` TAF 结构:
- tag0 `iCmdType`
- tag1 `vData`
- tag2 `lRequestId`
- tag3 `traceId`
- tag4 `iEncryptType`
- tag5 `lTime`
- tag6 `sMD5`
样本里明文 WSS host:
- `wss://65cecb21-ws.va.huya.com/`
- 位置:
- `strings.txt:702`
- `decompile/46DEE1.c:147`
因此 Go 复刻时应把通道拆成两个可替换实现:
1. `cdnws HTTP POST WUP`
- 参考项目已有完整实现。
- 最适合先做最小可用版本。
2. `va.huya WSS + WebSocketCommand`
- 更贴近样本 JS 的外层 `WebSocketCommand`。
- 需要继续确认握手、verifyCookie、registerGroup、heartbeat 与业务发送封装。
## Go 复刻建议蓝图
### 包结构
```text
huya-go/
taf/
type.go
writer.go
reader.go
wup/
request.go
response.go
huya/
cookie.go
baseinfo.go
activity/
structs.go
client.go
transport/
http_rpc.go
wss_rpc.go
sniper/
config.go
scheduler.go
matcher.go
result.go
```
### 最小可用链路
先做 HTTP cdnws 版本:
```text
读取账号 Cookie
-> 解析 yyuid/guid
-> 构造 ActivityUserId
-> 构造 GetActPrizeListReq
-> WUP encode
-> POST cdnws
-> WUP decode GetActPrizeListResp
-> 匹配目标 pid/name
-> 构造 ScoreExchangePrizeReq
-> POST cdnws
-> WUP decode ScoreExchangePrizeResp
-> 记录 status/msg/orderId
```
### 抢购调度逻辑
建议按样本行为复刻,但加上明确的停止条件和退避:
```text
config:
accounts: []Account
targets: []Target
sid: int
maxLoops: int # 截图里的 次数
delayMs: int # 截图里的 延时
concurrency: int # 截图里的 线程
scheduledAt: time
stopAfterFirstSuccess: bool
run:
等到 scheduledAt
for loop in 1..maxLoops:
并发遍历勾选账号
查询奖品列表
过滤目标:
自定义 pid 优先
否则按 name/选中行匹配
如果未命中:
记录 未查询到所需物品,继续监控
return
如果命中:
如果剩余估算 <= 0:
可选择跳过或继续尝试
调 scoreExchangePrize
status == 200 且 msg == 请求成功:
记录兑换成功
如果 stopAfterFirstSuccess: 标记账号停止
msg == 操作频繁:
记录频控,按账号退避
msg == 奖品已下架:
刷新列表/重启监控
sleep(delayMs)
```
### Go 实现时的关键细节
- Cookie:
- 去重保留最后一次。
- 从 `yyuid` 或 `udb_uid` 解析 uid。
- `guid` 可从 Cookie 取;HTTP cdnws 可为空,但 WSS/launch 分支需要处理返回 `sGuid`。
- UA:
- 样本活动兑换/列表:`web&1.0&huya`
- 样本兑换记录:`webh5&1.0.0&huya`
- 样本绑定查询:`pc&1.0.0&huya`
- 参考 HTTP 客户端:`webh5&0.0.1&websocket&&diypc_52775`
- 建议 Go 结构允许按接口覆盖,不要写死一个全局值。
- requestId:
- 样本兑换:`1`
- 样本积分:`2`
- 样本记录:`6`
- 样本绑定:`17`
- 样本 launch:`-1`
- HTTP cdnws 可先统一 `1`,但保留参数。
- 奖品剩余:
- 样本 C 侧使用 `num`、`percent`、`newScore` 展示。
- 如果响应有 `leftNum`,Go 版可以同时记录直接字段和估算字段,避免误判。
- 频控:
- 样本识别 `操作频繁,请稍后再试` 后仍会继续后续重试路径。
- Go 版建议记录频控并按账号退避,避免无意义高频请求。
## 新增待确认点
1. `unk_69EE18` 仍未定位到静态写入点。
- 目前只看到它作为 `sub_428C41` 的 URL 参数被读取:
- `decompile/43E81F.c:513`
- `decompile/43E81F.c:621`
- 明文有 `wss://65cecb21-ws.va.huya.com/`:
- `strings.txt:702`
- `decompile/46DEE1.c:147`
- 但参考项目 HTTP 版使用 `https://cdnws.api.huya.com/?baseinfo=...`。
- 需要实际抓一次样本运行请求,或继续追连接对象初始化,才能最终确认业务发送 URL。
2. `线程` 控件的赋值点还未完全映射。
- 样本有 18 个同构槽位回调。
- 截图中设置为 5。
- `sub_43597D` 通过连接对象方法读取可用数量和投递任务,但 UI 控件到连接池容量的路径还需继续找。
3. `getActPrizeList` 的 requestId。
- JS 传 `null`。
- `Wup.prototype.setRequestId=function(t){this.iRequestId=t||++this.iRequestId}`。
- 所以 `null` 会触发自增,初始值为 0 时会变成 1。
- 这解释了 C 侧奖品列表落入 `iRequestId == "1"` 分支。
4. HTTP cdnws 与样本 WSS 外层差异。
- 用 Go 先做 HTTP 版本可更快验证结构体和兑换接口。
- 如果要完全复刻样本,需要再补 `WebSocketCommand` 外层、verifyCookie、doLaunch、registerGroup、heartbeat。
## 2026-07-06 运行日志与 HAR 新证据
### 输入文件
- `/Users/yml/Desktop/抓包/hy/抢购/7.6-软件抓包.har`
- `/Users/yml/Desktop/抓包/hy/抢购/软件日志.txt`
- `/Users/yml/Desktop/抓包/hy/抢购/软件日志2.txt`
注意:HAR 中包含真实账号 Cookie、uid、guid、token 类字段。下面只记录结构化结论,不保存明文敏感值。
### HAR 总览
- HAR 共 702 条请求。
- 其中 661 条是 `HEAD http://www.baidu.com/`,应为软件或代理的联网检测/保活噪声。
- 真正与本软件相关的主要请求:
- `api3.1wxyun.com`:5 条软件授权/登录相关请求,`type=1/11/8/24/21`。
- `129.204.148.26:7381/huya/heping.html`:1 条远端配置/公告页。
- `i.huya.com`:3 条账号页面加载。
- `65cecb21-ws.va.huya.com`:3 条 WebSocket 握手,状态码 `101`。
- `diy-assets.msstatic.com/cqhfolder/NewDefault.png`:3 条默认头像/资源图加载。
- `82.156.166.142:1822/upload`:3 条兑换成功后的远端上报。
HAR 只保存了 WebSocket 握手,没有保存 WebSocket 帧内容,所以无法直接从这份 HAR 还原 `scoreExchangePrize` 的二进制 WUP 请求体。它能确认真实运行使用 WSS 通道,但请求体仍需要依赖反编译和参考项目里的 WUP/TAF 编码继续复刻。
### 远端配置页
`http://129.204.148.26:7381/huya/heping.html` 返回了软件配置:
- 版本号:`2.7`
- 软件开关:`1`
- 兑换代码:`2203`
- 子奖品代码 A:`10329`
- 子奖品代码 B:`10329`
- livelink 活动 id:`17096`
- 上传地址:
- `http://129.204.148.26:1822/upload`
- `http://115.159.215.151:1822/upload`
- `http://82.156.166.142:1822/upload`
- 登录开关、cookie 开关、切换活动开关、任务开关均为 `1`。
这说明软件并不只依赖本地静态常量,还会启动时拉取作者远端配置。对 Go 复刻来说,`兑换代码=2203`、`子奖品代码=10329`、`活动 id=17096` 是这次“套装-潮萌小黄鸭”的直接运行参数;远端开关/上传地址不需要复刻。
### WSS 连接时序
HAR 时间换算成本地时间后:
- `16:53:58.071`:3 条 `GET https://i.huya.com/`
- `16:53:58.109` 到 `16:53:58.114`:3 条 `GET wss://65cecb21-ws.va.huya.com/` 握手成功
- `16:53:59`:日志显示 3 个账号登录成功
- `16:59:57`:日志显示线程创建成功并开始兑换
这和前面的反编译结论可以对上:软件在登录阶段为每个账号建立或准备连接,抢购触发时复用已有连接,而不是到点后再创建 WSS。Go 版如果要贴近样本,应改成“账号初始化 -> 建立持久 WSS -> launch/register/heartbeat -> 到点并发发送兑换 WUP”。
WebSocket 握手头特征:
- Host:`65cecb21-ws.va.huya.com`
- User-Agent:`WebSocket++/0.8.2`
- Upgrade:`websocket`
- 未看到浏览器常见的 `Origin`。
### 成功上报行为
HAR 中 3 条 `POST http://82.156.166.142:1822/upload` 与 `软件日志.txt` 的 3 个成功账号对应:
- 本地时间约 `17:00:01.946`、`17:00:04.751`、`17:00:05.508` 上报。
- 上报体使用 GBK 文本拼接,字段以 `#` 分隔。
- 上报内容包括账号昵称、成功时间、奖品名、完整 Cookie、活动/软件标识、程序名 `多功能计算器2.7.vmp.exe` 等。
这是明显的数据外传逻辑。Go 复刻不应实现这个上传接口;如果保留日志,只应写本地脱敏日志。
### `软件日志.txt` 统计
该日志对应 3 个账号,和 HAR 中的 3 条 `i.huya.com`、3 条 WSS、3 条上传最匹配。
- 登录账号数:3
- 选择奖品:`套装-潮萌小黄鸭`
- 兑换动作日志:270 条
- 成功:3 条
- 操作频繁:4 条
- 兑换货币不足:263 条
- 成功时间:全部在 `16:59:57`
- 成功后查询到剩余积分:3 个账号均为 `2`
首批结果:
| 时间 | 账号结果 |
| --- | --- |
| 16:59:57 | 堂上不仁也:兑换成功 |
| 16:59:57 | 里村路老:兑换成功 |
| 16:59:57 | 颁赏胡同兰博:先操作频繁,随后同秒兑换成功 |
后续从 `17:00:01` 起继续大量重试,但返回基本变成“兑换货币不足”。这说明样本成功后没有立刻按账号停止兑换循环,或停止标记只影响了部分路径;复刻时应加“账号成功后停止该奖品重试”,否则会制造无意义请求并触发频控。
### `软件日志2.txt` 统计
该日志对应 10 个账号,HAR 未捕获到对应的 10 条 WSS/上传,因此它应是另一轮运行日志或抓包缺失了这部分流量。
- 登录账号数:10
- 选择奖品:`套装-潮萌小黄鸭`
- 兑换动作日志:1190 条
- 奖品暂未开放兑换:70 条
- 成功:10 条
- 操作频繁:6 条
- 兑换货币不足:1104 条
- 成功时间:全部在 `17:00:00`
- 成功后查询到剩余积分:10 个账号均为 `2`
关键节奏:
| 秒级时间 | 请求结果分布 |
| --- | --- |
| 16:59:57 | 13 条,全部“奖品暂未开放兑换” |
| 16:59:58 | 37 条,全部“奖品暂未开放兑换” |
| 16:59:59 | 20 条,全部“奖品暂未开放兑换” |
| 17:00:00 | 10 条,全部“兑换成功” |
| 17:00:02 起 | 大量“兑换货币不足”,夹杂少量“操作频繁” |
这说明软件会在开放前约 3 秒开始高频探测,不等官方开放时间才发第一波。开放瞬间,每个账号至少成功提交一次;成功之后仍继续跑循环,造成 1100+ 条无效的“兑换货币不足”。
### 对抢购逻辑的更新判断
1. 远端配置确认了这次奖品参数:
- 活动 id:`17096`
- 兑换代码/父奖品或活动奖品标识:`2203`
- 子奖品 id:`10329`
2. 实际样本运行确认使用 WSS:
- 静态字符串里的 `wss://65cecb21-ws.va.huya.com/` 不是备用字符串。
- 登录阶段按账号建立 WSS,抢购阶段复用连接发请求。
3. HAR 未保存 WSS 帧:
- 不能从当前 HAR 直接提取 `WebSocketCommand`、`iRequestId` 或兑换请求 WUP body。
- 仍需从反编译、JS/WUP 定义、参考 Python 项目或重新抓 WebSocket frames 来补齐。
4. 样本抢购策略:
- 提前约 3 秒开始轮询/尝试。
- 多账号并发。
- 成功后继续重试,直到后续查询/循环结束。
- 频控文案不是致命错误,样本会继续尝试。
5. Go 复刻建议:
- 账号维度维护持久 WSS 连接,而不是每次兑换新建 HTTP 请求。
- 抢购开始时间可设置为官方时间前 2-3 秒,允许“暂未开放”继续重试。
- 每个账号成功一次后立即停止该账号对同一奖品的兑换重试。
- “操作频繁”做短退避;“兑换货币不足”可视为该账号已成功或积分不足,停止重试。
- 禁止实现样本的远端 `upload` 上报;只保留本地脱敏日志。
### 仍需补证
- 重新抓包时需要确认工具能保存 WebSocket binary frames;当前 HAR 只有 WSS handshake。
- 如果抓不到帧,可以在 Go 版用已实现的 WUP 编码对照参考项目,先构造:
- `webActUI.getActPrizeList`
- `webActUI.getUserScore`
- `webActUI.scoreExchangePrize`
- 然后补样本 WSS 外层:
- `WebSocketCommand`
- `verifyCookie`
- `doLaunch`
- `registerGroup`
- heartbeat
## 2026-07-06 浏览器手动抢购小黄鸭 HAR 新证据
### 输入文件
- `/Users/yml/Desktop/抓包/hy/抢购/7.6-手动抢购小黄鸭`
该文件是 Charles Proxy 5.2 导出的 HAR,文件无扩展名,实际是单行 JSON。HAR 内含真实 Cookie、uid、guid、token、账号名等敏感字段;这里只记录脱敏后的协议结论。
### HAR 总览
- 条目数:228
- 主要 host:
- `cdnws.api.huya.com`:12 条 HTTP WUP 请求。
- `77bc035c-ws.va.huya.com`:3 条 WebSocket 握手,状态码 `101`。
- `zt.huya.com`:活动页 `b02faae1/pc/index.html`。
- `dataservice.cdn.huya.com`、`l.huya.com`:配置/登录态检查。
- `metric.huya.com`、`ylog.huya.com`、`statwup.huya.com`:埋点/统计噪声。
- 活动页 URL:
- `https://zt.huya.com/b02faae1/pc/index.html?sourceId=yellowcarlist&pid=1199650619883&anchorUid=1199650619883&roomid=30596253`
这份 HAR 比软件 HAR 更完整:Charles 保存了 WebSocket binary frames,能还原手动浏览器实际发送的 WUP 请求。
### Charles HAR 编码特征
Charles 在 HAR 中不是直接保存二进制 body,而是保存 base64 文本:
1. `cdnws.api.huya.com` 的 `request.postData.text` 和 `response.content.text` 是 WUP 二进制包的 base64。
2. `wss://77bc035c-ws.va.huya.com/` 的 `request.postData.text` / `response.content.text` 是原始 WebSocket wire bytes 的 base64。
3. WebSocket client -> server 帧:
- `opcode=2`
- `masked=1`
- `RSV1=1`
- 启用了 `permessage-deflate`
4. WebSocket 解码顺序:
- base64 decode
- 解析 WebSocket frame
- client 帧按 mask key XOR 解 mask
- 若 `RSV1=1`,使用 raw deflate 解压;Python 可用 `zlib.decompressobj(wbits=-15)`,并在 payload 后补 `00 00 ff ff`
- 解压后 payload 中从 offset `6` 或 `7` 开始是 WUP packet
- WUP packet 后还有尾部追踪字段:client 侧约 44 bytes,server 侧约 25 bytes
Go 复刻 WSS 时需要支持:
- WebSocket binary frame
- permessage-deflate
- client mask 由 WebSocket 库自动处理
- 从 `WebSocketCommand` 外层取出/填入内部 WUP packet
### HTTP cdnws 初始化请求
`cdnws.api.huya.com` 中可见的主要 WUP:
| 时间 | servant.func | 作用 |
| --- | --- | --- |
| 16:59:45.467 | `launch.wsLaunch` | HTTP 版启动/获取连接信息 |
| 16:59:45.467 | `launch.pingSignal` | 信令 ping |
| 16:59:45.518 | `webActUI.checkUserBindGameAccount` | 查询游戏账号绑定 |
| 16:59:45.518 | `webActUI.getLiveLinkParam` | 获取活动参数 |
| 16:59:45.519 | `huyauserui.getUserProfileBatch` | 查询用户资料 |
| 16:59:45.551 / 16:59:45.553 | `shopMiddleUI.getGoodsInfoV5` | 黄车商品信息 |
| 16:59:45.670/672/679 | `launch.queryHttpDns` | 查询可用 WSS host |
| 16:59:46.064 | `webLiveUI.getWebUserBaseInfo` | 直播用户信息 |
| 16:59:47.079 | `launch.pingSignal` | 后续 ping |
`webActUI.getLiveLinkParam` 回包中确认:
- `actId=17096`
- `gameIdList=cjm`
- `livePlatId=huya`
- `code`、`sig` 为动态签名字段,不在文档中保存明文。
`launch.queryHttpDns` 返回多个可用 WSS host,其中本次实际连接的是:
- `77bc035c-ws.va.huya.com`
这说明 `65cecb21-ws.va.huya.com` 不是唯一固定地址;Go 版不要写死单个 WSS host,应该先走 `launch.queryHttpDns`,或至少允许配置/轮换 host。
### 手动 WSS 抢购函数顺序
主要业务发生在 `77bc035c-ws.va.huya.com` 的第 2 条 WSS 连接中。解 mask + inflate 后,client -> server 函数顺序如下:
| frame | requestId | servant.func | 关键参数 |
| --- | ---: | --- | --- |
| 6 | 7 | `webActUI.getActPrizeList` | `sid=2203` |
| 7 | 8 | `webActUI.getActInfo` | 活动信息 |
| 8 | 9 | `webActUI.getActTaskDetail` | 任务列表 |
| 9 | 10 | `webActUI.getActUserTaskDetail` | 用户任务状态 |
| 10 | 11 | `webActUI.getUserScore` | `sid=2203` |
| 12 | 12 | `webActUI.getActPrizeDetail` | `sid=2203` |
| 13 | 13 | `webActUI.getActPrizeDetail` | `sid=2203` |
| 14 | 14 | `webActUI.scoreExchangePrize` | `sid=2203`, `pid=12889`, `ip=""`, `clientEnv={}`, `source=""`, `isRole=1` |
| 15 | 15 | `webActUI.getActPrizeList` | `sid=2203` |
| 16 | 16 | `webActUI.getUserScore` | `sid=2203` |
这条链路和之前逆向出的 `webActUI.scoreExchangePrize(cookie,pid,sid)` 完全对上。这里可以把 `sid=2203` 理解为活动/兑换场次 id,把 `pid=12889` 理解为本次“套装-潮萌小黄鸭”的奖品 id。
### 手动兑换成功回包
`scoreExchangePrize` 的 server -> client 回包:
- `requestId=14`
- `servant=webActUI`
- `func=scoreExchangePrize`
- `status=200`
- `msg=请求成功`
- `orderId=sOrderId_2203_12889_<uid>_1783328404_957`
- `exchangeInfo.pid=12889`
- `exchangeInfo.exchangeScore=988`
- `exchangeInfo.desc=兑换奖品:套装-潮萌小黄鸭`
`1783328404` 转成本地时间为 `2026-07-06 17:00:04`,说明这次浏览器手动兑换成功发生在 17:00:04 左右。
该回包进一步确认:
- 本次手动小黄鸭兑换参数是 `sid=2203`、`pid=12889`。
- 小黄鸭套装消耗积分是 `988`。
- 兑换成功后页面会继续请求 `getActPrizeList` 和 `getUserScore` 刷新列表与积分。
### 奖品列表信息
`getActPrizeList` 回包包含“小黄鸭”系列多项奖品,例如:
- `套装-潮萌小黄鸭`
- `潮萌小黄鸭头盔`
- `潮萌小黄鸭背包`
- `潮萌小黄鸭降落伞`
- `平底锅-潮萌小黄鸭`
- `破片手榴弹-潮萌小黄鸭`
- `背包挂件-潮萌小黄鸭`
`getActPrizeDetail` 在兑换前后各请求了一次,回包都指向 `套装-潮萌小黄鸭`;这解释了页面手动点击前会先刷详情,再发兑换。
### 与软件 HAR 的对比
1. 软件 HAR:
- 只保存 WSS handshake,没有保存 frames。
- host 是 `65cecb21-ws.va.huya.com`。
- 3 个账号登录阶段各建一条 WSS。
- 成功后会向作者服务器 `82.156.166.142:1822/upload` 外传 Cookie 和结果。
2. 浏览器手动 HAR:
- 保存了完整 WebSocket frames。
- host 是 `77bc035c-ws.va.huya.com`,由 `launch.queryHttpDns` 返回。
- 真实兑换函数和参数已经确认:`webActUI.scoreExchangePrize(sid=2203,pid=12889)`。
- 没有软件作者的 `upload` 外传行为。
3. 共同点:
- 都使用 `*-ws.va.huya.com` WSS 通道。
- 都依赖 WUP/TAF。
- 都围绕 `webActUI` 的活动接口:列表、积分、详情、兑换。
### 对 Go 复刻的更新建议
1. 参数修正:
- 当前“套装-潮萌小黄鸭”手动兑换的 `pid` 应使用 `12889`。
- 远端配置里的 `子奖品代码=10329` 可能是软件作者配置中的另一层子奖品/旧字段;从浏览器成功回包看,实际 `scoreExchangePrize` 使用的是 `pid=12889`。
- `sid=2203` 明确用于 `getActPrizeList`、`getUserScore`、`getActPrizeDetail`、`scoreExchangePrize`。
2. 传输层:
- HTTP `cdnws.api.huya.com` 可用于 launch、queryHttpDns、部分初始化请求。
- 真正贴近浏览器/软件的抢购应走 WSS。
- WSS 要支持 `permessage-deflate`;建议 Go 版使用成熟 WebSocket 库,并确认是否能开启压缩。
3. WSS 外层:
- 每个 WebSocket message 不是裸 WUP,前面有约 6/7 字节外层,后面有追踪尾部。
- 内部 WUP 包可以按 4 字节大端长度切出。
- Go 版需要补 `WebSocketCommand` 外层编码/解码,至少要能构造出和浏览器一致的 client frame payload。
4. 请求顺序:
- 初始化:`wsLaunch`、`getConfig`、`getLiveLinkParam`、`queryHttpDns`
- 抢购前:`getActPrizeList`、`getUserScore`、`getActPrizeDetail`
- 抢购:`scoreExchangePrize`
- 成功后:`getActPrizeList`、`getUserScore`
5. UA/环境:
- WUP 元数据中常见 `webh5&0.0.1&websocket&&diypc_52775`。
- Cookie 中可见 `huya_ua=webh5&0.1.0&websocket`。
- Go 版不要写死单一 UA,应按接口/阶段可配置。