Files
yml2213 d21b06a311 新增商品类目(限量返场/精英专享)支持与版本号 0.2.1
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 补记结论
2026-10-15 18:23:38 +08:00

242 lines
9.0 KiB
Markdown
Raw Permalink 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.
# 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 编解码单测。
- 限量返场商品是「预告上架」,兑换时的服务端文案(如「未开放」)与开抢瞬间的表现需真号验证。