10.15 抓包确认:积分中心商城 PlayMallUI.queryAllocation 的商品与 webActUI.getActPrizeList(sid=2277) pid 完全一致,属于同一批商品,因此列表无需接商城链路;真号实测 sid=2277 返回 30 项(限量返场 99 共 12 项 + 精英专享 101 共 18 项),sid=2203 已返回空。\n\n- huya:解析商品 tag23/25/26/27/28(发布时间、上架/下架时间、每日时段),新增 PrizeInfo.CategoryIDs/InCategory/StartsAt/NotYetOnSale 与响应级 CategoryStats/CategorySummary/FilterByCategory,列表日志带类目统计\n- sniper:Config.Category 限定自动选品范围,未命中提示带类目\n- CLI:新增 -category / -category-only\n- GUI:Config.Category、ListPrizes 回传类目字典与统计;界面商品列表按类目分页签、每行标注类目、未上架显示上架时间与倒计时,所选类目随配置提交给监控\n- 测试:新增 internal/huya/category_test.go 与 sniper 类目用例;README/WSS_NOTES 补记结论
242 lines
9.0 KiB
Markdown
242 lines
9.0 KiB
Markdown
# WSS 协议要点
|
||
|
||
本文件记录 2026-07-06 手动浏览器 HAR、页面 JS 和 Go 版本当前实现的复核结论。
|
||
|
||
## 抓包结论
|
||
|
||
手动浏览器链路是 HTTP + WSS 混合:
|
||
|
||
- 页面初始化、商品信息、HttpDNS 等有多条 `https://cdnws.api.huya.com/` POST。
|
||
- 浏览器同时建立了 3 条 `wss://77bc035c-ws.va.huya.com/` 连接。
|
||
- `scoreExchangePrize` 出现在 WSS 连接数据和静态 JS 中,没有出现在 `cdnws` POST 请求中。
|
||
- 当前 HTTP 版本可用,是因为 `webActUI` 的 WUP over HTTP 编码已经对齐。
|
||
- Go 版 WSS 已改为浏览器同款 `WebSocketCommand` 外层,仍需真实 Cookie 联调确认服务端返回。
|
||
|
||
## 连接
|
||
|
||
- WebSocket URL:`wss://<host>/?baseinfo=<base64 taf>`
|
||
- 手动 HAR host:`77bc035c-ws.va.huya.com`
|
||
- host 来自 `launch.queryHttpDns` 或浏览器抓包,不应长期写死。
|
||
- 手动 HAR 中 `baseinfo.cookie` 与 `traceId` 为空,cookie 通过后续 `VerifyCookie` 命令发送。
|
||
- 握手启用 `permessage-deflate`。
|
||
- 浏览器请求扩展:`permessage-deflate; client_max_window_bits`
|
||
- 服务端返回扩展:`permessage-deflate;server_no_context_takeover;client_no_context_takeover`
|
||
|
||
## 正确外层结构
|
||
|
||
浏览器 JS 不是 `1B version + 1B command + 4B sequence + body`。
|
||
|
||
页面实际构造 TAF/JCE `WebSocketCommand`:
|
||
|
||
```text
|
||
WebSocketCommand {
|
||
0: iCmdType int32
|
||
1: vData bytes
|
||
2: lRequestId int64
|
||
3: traceId string
|
||
4: iEncryptType int32
|
||
5: lTime int64
|
||
6: sMD5 string
|
||
}
|
||
```
|
||
|
||
WUP 请求发送逻辑:
|
||
|
||
```text
|
||
Wup.setServant(servant)
|
||
Wup.setFunc(func)
|
||
Wup.setRequestId(requestId)
|
||
Wup.writeStruct("tReq", req)
|
||
|
||
WebSocketCommand.iCmdType = EWSCmd_WupReq(3)
|
||
WebSocketCommand.vData = Wup.encode()
|
||
WebSocketCommand.traceId = "<trace>:<trace>:0:<enableTrace>"
|
||
ws.send(WebSocketCommand.writeTo(JceOutputStream))
|
||
```
|
||
|
||
服务端返回 `EWSCmd_WupRsp(4)`,内部 `vData` 才是 WUP 响应。
|
||
|
||
## 命令枚举
|
||
|
||
页面 JS 中可见的关键枚举:
|
||
|
||
- `3`:`EWSCmd_WupReq`
|
||
- `4`:`EWSCmd_WupRsp`
|
||
- `5`:`EWSCmdC2S_HeartBeat`
|
||
- `6`:`EWSCmdS2C_HeartBeatAck`
|
||
- `10`:`EWSCmdC2S_VerifyCookieReq`
|
||
- `11`:`EWSCmdS2C_VerifyCookieRsp`
|
||
- `16`:`EWSCmdC2S_RegisterGroupReq`
|
||
- `17`:`EWSCmdS2C_RegisterGroupRsp`
|
||
- `20`:`EWSCmdC2S_HeartBeatReq`
|
||
- `21`:`EWSCmdS2C_HeartBeatRsp`
|
||
- `23`:`EWSCmdC2S_UpdateUserExpsReq`
|
||
- `24`:`EWSCmdS2C_UpdateUserExpsRsp`
|
||
- `33`:`EWSCmdC2S_UpdateUserInfoReq`
|
||
- `34`:`EWSCmdS2C_UpdateUserInfoRsp`
|
||
|
||
手动 HAR 解出的服务端 WSS 回包包括:
|
||
|
||
- `EWSCmdS2C_HeartBeatRsp`
|
||
- `EWSCmd_WupRsp launch.wsLaunch`
|
||
- `EWSCmd_WupRsp mobileui.getConfig`
|
||
- `EWSCmd_WupRsp webActUI.getActPrizeList`
|
||
- `EWSCmd_WupRsp webActUI.getUserScore`
|
||
- `EWSCmd_WupRsp webActUI.scoreExchangePrize`
|
||
|
||
## 初始化流程
|
||
|
||
浏览器 WSS session 的关键流程:
|
||
|
||
1. 连接 `wss://<host>/?baseinfo=<base64 taf>`。
|
||
2. 发送 `EWSCmdC2S_RegisterGroupReq(16)`,默认 group 为 `live:0`。
|
||
3. 通过 WSS 调用 `launch.wsLaunch`,手动 HAR 中 requestId 为 `-1`。
|
||
4. 发送 `EWSCmdC2S_VerifyCookieReq(10)`,cookie 在这里进入 WSS 通道。
|
||
5. 通过 WSS 调用 `mobileui.getConfig`。
|
||
6. 发送 `EWSCmdC2S_UpdateUserExpsReq(23)`,请求体是 `getConfig` 下发的版本 map(实测 `{"58664":"107263"}`,Go 版已按此回传)。
|
||
7. 再发送一次 `EWSCmdC2S_RegisterGroupReq(16)`。
|
||
8. 发送 `EWSCmdC2S_HeartBeatReq(20)`,后续维持心跳。
|
||
9. `launch.queryHttpDns` 仍通过 HTTP `cdnws` 调用。
|
||
10. 后续 WUP 业务在连接可用时走 `signalSdk.sendWup`,连接不可用时可 fallback 到 `sendWupHttp`。
|
||
|
||
握手 `baseinfo`(11 个字段)实测带 `exp`(tag5),值就是 getConfig 的版本串;cookie(tag8)/traceId(tag9) 为空,tag10 是空的 `customHeaders` map。
|
||
|
||
响应应按 WUP `iRequestId` 匹配回调,不能只用 FIFO。
|
||
|
||
## 活动接口
|
||
|
||
当前 Go HTTP 版本已实现:
|
||
|
||
- `webActUI.getActPrizeList`
|
||
- `webActUI.getUserScore`
|
||
- `webActUI.getUserPrizeRecords`
|
||
- `webActUI.scoreExchangePrize`
|
||
|
||
WSS 版本仍是实验状态,但外层 `WebSocketCommand`、初始化帧顺序和 requestId 匹配已按手动 HAR 修正。
|
||
|
||
抢购兑换请求结构:
|
||
|
||
```text
|
||
tReq.user
|
||
tReq.sid
|
||
tReq.pid
|
||
tReq.ip = ""
|
||
tReq.clientEnv = {}
|
||
tReq.source = ""
|
||
tReq.isRole = 1
|
||
```
|
||
|
||
浏览器手动抢购小黄鸭成功回包确认:
|
||
|
||
- `webActUI.scoreExchangePrize`
|
||
- `sid=2203`
|
||
- `pid=12889`
|
||
- `status=200`
|
||
- `msg=请求成功`
|
||
- `exchangeInfo.pid=12889`
|
||
- `exchangeInfo.exchangeScore=988`
|
||
- `exchangeInfo.desc=兑换奖品:套装-潮萌小黄鸭`
|
||
|
||
## launch.queryHttpDns(已实现)
|
||
|
||
2026-10-13「精英宝典」抓包确认:`m-shop.huya.com` 页面用 HTTP `cdnws` 调它换取 WSS host,`iRequestId` 固定 `-1`。
|
||
|
||
请求 `tReq`:
|
||
|
||
```text
|
||
tReq.0 uid int64
|
||
tReq.1 ua string # webh5&0.0.1&websocket&&h5_/points.html
|
||
tReq.2 domains list<string> # ["cdnws.api.huya.com"]
|
||
tReq.3 appSrc string # HUYA&ZH&2052
|
||
tReq.4 "" string
|
||
tReq.5 0 int32
|
||
tReq.6 1 int32
|
||
```
|
||
|
||
响应 `tRsp`:
|
||
|
||
```text
|
||
tRsp.0 map<domain, {0: list<string> hosts, 1: int32 ttl秒, 2: list<string>}> # 实测 ttl=1296000
|
||
tRsp.1 int32
|
||
```
|
||
|
||
Go 版:`HTTPClient.QueryHttpDns` + `WSSClient.resolveHost`(TTL 缓存、多 host 轮询、失败回退默认 host)。
|
||
|
||
## launch.pingSignal(已实现)
|
||
|
||
页面在 HTTP `cdnws` 上调用它做链路探测,`iRequestId=0`:
|
||
|
||
```text
|
||
tReq.0 "launch" tReq.1 "pingSignal" tReq.2 map<string,string> 空
|
||
tRsp.0 "launch" tRsp.1 "pingSignal" tRsp.2 map<string,int64>{signal_ap_start, signal_ap_end}
|
||
```
|
||
|
||
Go 版:`HTTPClient.PingSignal`(HTTP 预热)与 `WSSClient.PingSignal`(WSS 预热后探测业务通道),预热结果只记日志、不影响抢购。
|
||
|
||
## 商城链路(仅记录,未实现)
|
||
|
||
同一抓包中「开通精英宝典 / 购买幸运币」属于商城付费链路,全部走 WSS 上的 `shopMiddleUI.*`:
|
||
|
||
```text
|
||
getGoodsInfoV5 → getSupplierInfoV5 → listPayChannelV5 → checkHyProtocolV5 → acceptHyProtocolV5
|
||
→ calcOrderPromotion → checkUserBuyAuth → createOrderV5 → payOrderSubmitV5(支付宝二维码) → orderDetailV5(轮询)
|
||
另有 createOrderV6(另一条下单通道)与 revenueWebUI.queryUserOrderList
|
||
```
|
||
|
||
注意函数名带版本后缀(V5/V6)。`webActUI` 在 WSS 上同样可用(实测 `checkUserBindGameAccount`、`getUserPrizeRecords`)。
|
||
|
||
### 积分中心商城的商品列表(`PlayMallUI.queryAllocation`)
|
||
|
||
2026-10-15 HAR(`m-shop.huya.com/points.html?pagecode=heping_baodian_credit_center_page`)确认:
|
||
|
||
```text
|
||
tReq.0 user {uid, guid, token, ua=web&1.0.0&huya, cookie, tokenType, deviceInfo}
|
||
tReq.1 {0:"huya", 1:1001, 3:"", 5:7, 6:"yellowcarlist", 7:""}
|
||
tReq.2 pageId string # "heping_baodian_credit_center_page"
|
||
tReq.3 "" tReq.4 0 tReq.5 1 tReq.6 24
|
||
tReq.7 extraJson string # 切换类目时是 {"jumpTabId":433}
|
||
tReq.11 list<int> # [6, 19]
|
||
tRsp.0 {0:200(status), 1:"成功"}
|
||
tRsp.1 {0:"和平精英宝典积分中心", 1:pageId}
|
||
tRsp.2 list<tab>{0:tabId(343/345), 4:int, 5:bytes} # 5 是内嵌 TAF 负载
|
||
tRsp.3 list<string> # CDN 域名
|
||
```
|
||
|
||
内嵌负载解码后是页面区块(tabId 433=限量返场、431=精英专享),每个区块的 `6: list<商品>` 里带
|
||
`tag9=2277(sid)`、`tag10="13433"(pid)`、价格是活动积分的 10 倍。
|
||
|
||
结论:**商城 grid 与 `webActUI.getActPrizeList(sid=2277)` 是同一批商品**(pid 完全一致),
|
||
本项目不需要走商城链路取列表;10.13 该负载只有 431(精英专享) 一个区块,10.15 新增 433(限量返场)。
|
||
|
||
## 商品类目(限量返场 / 精英专享)
|
||
|
||
`webActUI.getActPrizeList` 响应除商品外还下发类目字典,商品侧用 `tag14` 标注归属:
|
||
|
||
```text
|
||
tRsp.3 moduleId int32
|
||
tRsp.4 list<PrizeTag>{0:id, 1:name, 2:weight} # 2026-10-15 实测:101=精英专享、99=限量返场
|
||
PrizeInfo.14 tags string # 类目 id,多个用逗号分隔
|
||
```
|
||
|
||
2026-10-15 实测 `sid=2277`:30 项商品 = 限量返场(99) 12 项(怪诞乔克服装礼包 8000 积分等,
|
||
页面显示「xx:xx:xx 后上架」)+ 精英专享(101) 18 项。`sid=2203` 的旧活动已停止返回内容。
|
||
Go 版:`GetActPrizeListResp.CategorySummary/CategoryStats/FilterByCategory` 与
|
||
`PrizeInfo.CategoryIDs/InCategory/StartsAt/NotYetOnSale`,CLI `-category` / `-category-only`,
|
||
界面商品列表按类目筛选并标注类目。
|
||
|
||
商品侧还有一组时间字段(此前未解析):
|
||
|
||
```text
|
||
PrizeInfo.23 publish_time int64 # 商品发布时间(实测 2026-10-15 11:04:57)
|
||
PrizeInfo.25 start_time int64 # 上架时间,页面倒计时来源(实测 2026-10-16 17:00:00)
|
||
PrizeInfo.26 end_time int64 # 下架时间(实测 2026-10-23 23:59:59)
|
||
PrizeInfo.27/28 daily_start/daily_end string # 每日可兑时段 "00:00:00"/"23:59:59"
|
||
```
|
||
|
||
## 待完善
|
||
|
||
- 使用真实 Cookie 联调 `list/score/exchange`,确认服务端不再超时。
|
||
- WSS 侧对 `launch.pingSignal` 的实际可用性需真号验证(失败已降级为日志)。
|
||
- 补更多基于真实抓包摘要的 WSS 编解码单测。
|
||
- 限量返场商品是「预告上架」,兑换时的服务端文案(如「未开放」)与开抢瞬间的表现需真号验证。
|