Files
huya-go/internal/huya/activity.go
T
yml2213 36aadae90a 按 10.13 抓包对齐 WSS:host 自动解析、exp 回传、pingSignal 与兑换记录字段
- 新增 launch.queryHttpDns:类型定义 + HTTPClient.QueryHttpDns + WSSClient.resolveHost(TTL 缓存、多 host 轮询、失败回退默认 host);Host 留空或等于历史默认值即视为自动解析
- WSS 握手 baseinfo 补 tag5=exp 与 tag10 空 customHeaders;UpdateUserExpsReq(cmd23) 改为回传 getConfig 的版本 map(缓存供重连复用)
- 新增 launch.pingSignal:HTTP 预热改走 pingSignal,WSS 预热在建连后探测业务通道(失败仅记日志)
- UserPrizeInfo 补 tag12 orderId/tag14 图标/tag16 prizeId/tag17 shopItemId/tag19 itemId/tag20 ext/tag21 exchangeDate,前端复盘表加订单号列
- taf 补 WriteListString/ReadListString/ReadMapStringInt64/ReadMapStringStruct
- 新增 internal/huya/launch_test.go:用抓包真实报文做字节级夹具(baseinfo 122B、queryHttpDns 请求 93B、记录 308B 等)
- CLI -wss-host 默认改为留空;GUI 设置支持留空并给出占位提示;README/WSS_NOTES 同步协议细节
2026-10-13 20:19:14 +08:00

656 lines
17 KiB
Go
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.
package huya
import (
"math"
"strconv"
"strings"
"huya-go/internal/taf"
)
const (
ActivityWebUA = "web&1.0&huya"
ActivityRecordUA = "webh5&1.0.0&huya"
ActivityBindUA = "pc&1.0.0&huya"
ServantWebActUI = "webActUI"
FuncGetUserScore = "getUserScore"
FuncPrizeList = "getActPrizeList"
FuncPrizeRecords = "getUserPrizeRecords"
FuncExchangePrize = "scoreExchangePrize"
)
type ActivityUserID struct {
UID int64 `json:"uid"`
GUID string `json:"guid,omitempty"`
Token string `json:"token,omitempty"`
HuyaUA string `json:"huya_ua"`
Cookie string `json:"-"`
TokenType int32 `json:"token_type"`
DeviceInfo string `json:"device_info,omitempty"`
}
func NewActivityUserID(uid int64, cookie, ua string) ActivityUserID {
return ActivityUserID{
UID: uid,
HuyaUA: ua,
Cookie: NormalizeCookie(cookie),
TokenType: 0,
}
}
func (u *ActivityUserID) WriteTo(w *taf.Writer) error {
if err := w.WriteInt64(0, u.UID); err != nil {
return err
}
if err := w.WriteString(1, u.GUID); err != nil {
return err
}
if err := w.WriteString(2, u.Token); err != nil {
return err
}
if err := w.WriteString(3, u.HuyaUA); err != nil {
return err
}
if err := w.WriteString(4, u.Cookie); err != nil {
return err
}
if err := w.WriteInt32(5, int64(u.TokenType)); err != nil {
return err
}
return w.WriteString(6, u.DeviceInfo)
}
func (u *ActivityUserID) ReadFrom(r *taf.Reader) error {
var err error
if u.UID, err = r.ReadInt64(0, false, u.UID); err != nil {
return err
}
if u.GUID, err = r.ReadString(1, false, u.GUID); err != nil {
return err
}
if u.Token, err = r.ReadString(2, false, u.Token); err != nil {
return err
}
if u.HuyaUA, err = r.ReadString(3, false, u.HuyaUA); err != nil {
return err
}
if u.Cookie, err = r.ReadString(4, false, u.Cookie); err != nil {
return err
}
if u.TokenType, err = r.ReadInt32(5, false, u.TokenType); err != nil {
return err
}
u.DeviceInfo, err = r.ReadString(6, false, u.DeviceInfo)
return err
}
type GetUserScoreReq struct {
User ActivityUserID
SID int32
}
func (q *GetUserScoreReq) WriteTo(w *taf.Writer) error {
if err := w.WriteStruct(0, &q.User); err != nil {
return err
}
return w.WriteInt32(1, int64(q.SID))
}
func (q *GetUserScoreReq) ReadFrom(r *taf.Reader) error {
_, err := r.ReadStruct(0, false, &q.User)
if err != nil {
return err
}
q.SID, err = r.ReadInt32(1, false, q.SID)
return err
}
type GetUserScoreResp struct {
Status int32 `json:"status"`
Msg string `json:"msg"`
GainScore int32 `json:"gain_score"`
UsedScore int32 `json:"used_score"`
NewGainScore int64 `json:"new_gain_score"`
NewUsedScore int64 `json:"new_used_score"`
Score int64 `json:"score"`
IsOpenExchangeTip int32 `json:"is_open_exchange_tip"`
}
func (r *GetUserScoreResp) WriteTo(w *taf.Writer) error { return nil }
func (r *GetUserScoreResp) ReadFrom(rd *taf.Reader) error {
var err error
if r.Status, err = rd.ReadInt32(0, false, r.Status); err != nil {
return err
}
if r.Msg, err = rd.ReadString(1, false, r.Msg); err != nil {
return err
}
if r.GainScore, err = rd.ReadInt32(2, false, r.GainScore); err != nil {
return err
}
if r.UsedScore, err = rd.ReadInt32(3, false, r.UsedScore); err != nil {
return err
}
if r.NewGainScore, err = rd.ReadInt64(4, false, r.NewGainScore); err != nil {
return err
}
if r.NewUsedScore, err = rd.ReadInt64(5, false, r.NewUsedScore); err != nil {
return err
}
if r.Score, err = rd.ReadInt64(6, false, r.Score); err != nil {
return err
}
r.IsOpenExchangeTip, err = rd.ReadInt32(7, false, r.IsOpenExchangeTip)
return err
}
func (r GetUserScoreResp) AvailableScore() int64 {
if r.NewGainScore != 0 || r.NewUsedScore != 0 {
score := r.NewGainScore - r.NewUsedScore
if score < 0 {
return 0
}
return score
}
return r.Score
}
type GetActPrizeListReq struct {
User ActivityUserID
SID int32
}
func (q *GetActPrizeListReq) WriteTo(w *taf.Writer) error {
if err := w.WriteStruct(0, &q.User); err != nil {
return err
}
return w.WriteInt32(1, int64(q.SID))
}
func (q *GetActPrizeListReq) ReadFrom(r *taf.Reader) error {
_, err := r.ReadStruct(0, false, &q.User)
if err != nil {
return err
}
q.SID, err = r.ReadInt32(1, false, q.SID)
return err
}
type PrizeInfo struct {
ID int64 `json:"id"`
SID int64 `json:"sid"`
Name string `json:"name"`
Score int64 `json:"score"`
Type int32 `json:"type"`
Image string `json:"img,omitempty"`
Sort int32 `json:"sort"`
Frequency int32 `json:"frequency"`
FrequencyLimit int32 `json:"frequency_limit"`
CreateTime int64 `json:"create_time"`
Num int64 `json:"num"`
Status int32 `json:"status"`
Percent string `json:"percent"`
NewScore int64 `json:"new_score"`
Tags string `json:"tags,omitempty"`
LeftNum int64 `json:"left_num"`
IsShowNum int32 `json:"is_show_num"`
}
func (p *PrizeInfo) WriteTo(w *taf.Writer) error { return nil }
func (p *PrizeInfo) ReadFrom(r *taf.Reader) error {
var err error
if p.ID, err = r.ReadInt64(0, false, p.ID); err != nil {
return err
}
if p.SID, err = r.ReadInt64(1, false, p.SID); err != nil {
return err
}
if p.Name, err = r.ReadString(2, false, p.Name); err != nil {
return err
}
if p.Score, err = r.ReadInt64(3, false, p.Score); err != nil {
return err
}
if p.Type, err = r.ReadInt32(4, false, p.Type); err != nil {
return err
}
if p.Image, err = r.ReadString(5, false, p.Image); err != nil {
return err
}
if p.Sort, err = r.ReadInt32(6, false, p.Sort); err != nil {
return err
}
if p.Frequency, err = r.ReadInt32(7, false, p.Frequency); err != nil {
return err
}
if p.FrequencyLimit, err = r.ReadInt32(8, false, p.FrequencyLimit); err != nil {
return err
}
if p.CreateTime, err = r.ReadInt64(9, false, p.CreateTime); err != nil {
return err
}
if p.Num, err = r.ReadInt64(10, false, p.Num); err != nil {
return err
}
if p.Status, err = r.ReadInt32(11, false, p.Status); err != nil {
return err
}
if p.Percent, err = r.ReadString(12, false, p.Percent); err != nil {
return err
}
if p.NewScore, err = r.ReadInt64(13, false, p.NewScore); err != nil {
return err
}
if p.Tags, err = r.ReadString(14, false, p.Tags); err != nil {
return err
}
if p.LeftNum, err = r.ReadInt64(15, false, p.LeftNum); err != nil {
return err
}
p.IsShowNum, err = r.ReadInt32(16, false, p.IsShowNum)
return err
}
func (p PrizeInfo) Price() int64 {
if p.NewScore != 0 {
return p.NewScore
}
return p.Score
}
func (p PrizeInfo) EstimatedLeft() int64 {
left, ok := p.InventoryLeft()
if ok {
return left
}
return 0
}
func (p PrizeInfo) InventoryLeft() (int64, bool) {
if p.LeftNum > 0 {
return p.LeftNum, true
}
text := strings.TrimSuffix(strings.TrimSpace(p.Percent), "%")
if text == "" || p.Num <= 0 {
return 0, false
}
percent, err := strconv.ParseFloat(text, 64)
if err != nil {
return 0, false
}
return int64(math.Floor(float64(p.Num)*percent/100.0 + 1e-9)), true
}
type PrizeTag struct {
ID int64 `json:"id"`
Name string `json:"name"`
Weight int32 `json:"weight"`
}
func (t *PrizeTag) WriteTo(w *taf.Writer) error { return nil }
func (t *PrizeTag) ReadFrom(r *taf.Reader) error {
var err error
if t.ID, err = r.ReadInt64(0, false, t.ID); err != nil {
return err
}
if t.Name, err = r.ReadString(1, false, t.Name); err != nil {
return err
}
t.Weight, err = r.ReadInt32(2, false, t.Weight)
return err
}
type GetActPrizeListResp struct {
Status int32 `json:"status"`
Msg string `json:"msg"`
Prizes []PrizeInfo `json:"prizes"`
ModuleID int32 `json:"module_id"`
Tags []PrizeTag `json:"tags"`
}
func (r *GetActPrizeListResp) WriteTo(w *taf.Writer) error { return nil }
func (r *GetActPrizeListResp) ReadFrom(rd *taf.Reader) error {
var err error
if r.Status, err = rd.ReadInt32(0, false, r.Status); err != nil {
return err
}
if r.Msg, err = rd.ReadString(1, false, r.Msg); err != nil {
return err
}
items, err := rd.ReadListStruct(2, false, func() taf.Unmarshaler { return &PrizeInfo{} })
if err != nil {
return err
}
r.Prizes = make([]PrizeInfo, 0, len(items))
for _, item := range items {
if prize, ok := item.(*PrizeInfo); ok {
r.Prizes = append(r.Prizes, *prize)
}
}
if r.ModuleID, err = rd.ReadInt32(3, false, r.ModuleID); err != nil {
return err
}
tags, err := rd.ReadListStruct(4, false, func() taf.Unmarshaler { return &PrizeTag{} })
if err != nil {
return err
}
r.Tags = make([]PrizeTag, 0, len(tags))
for _, item := range tags {
if tag, ok := item.(*PrizeTag); ok {
r.Tags = append(r.Tags, *tag)
}
}
return nil
}
type GetUserPrizeRecordsReq struct {
User ActivityUserID
SID int32
}
func (q *GetUserPrizeRecordsReq) WriteTo(w *taf.Writer) error {
if err := w.WriteStruct(0, &q.User); err != nil {
return err
}
return w.WriteInt32(1, int64(q.SID))
}
func (q *GetUserPrizeRecordsReq) ReadFrom(r *taf.Reader) error {
_, err := r.ReadStruct(0, false, &q.User)
if err != nil {
return err
}
q.SID, err = r.ReadInt32(1, false, q.SID)
return err
}
// UserPrizeInfo 对应 getUserPrizeRecords 的一条兑换记录。
//
// 除 tag0~tag10 外,2026-10-13 HAR 实测还有 tag11~tag23:
// 12=orderId、14=奖品图、16=奖品 pid、17=商城 itemId(数量级与 shopMiddleUI
// 的 itemId 一致,语义未在样本中最终确认)、19=prizeItemId、20=扩展 map
// (game/cjm/plat/isRole/liveLinkActId)、21=兑换日期(如 20261013)。
type UserPrizeInfo struct {
ID int64 `json:"id"`
Name string `json:"name"`
Score int64 `json:"score"`
CreateTime int64 `json:"create_time"`
Status int32 `json:"status"`
Type int32 `json:"type"`
IsReal int32 `json:"is_real"`
NewScore int64 `json:"new_score"`
Account string `json:"account,omitempty"`
Code string `json:"code,omitempty"`
CommonPrizeType int32 `json:"common_prize_type"`
OrderID string `json:"order_id,omitempty"`
Image string `json:"img,omitempty"`
PrizeID int64 `json:"prize_id,omitempty"`
ShopItemID int64 `json:"shop_item_id,omitempty"`
ItemID int64 `json:"item_id,omitempty"`
Ext map[string]string `json:"ext,omitempty"`
ExchangeDate int32 `json:"exchange_date,omitempty"`
}
func (p *UserPrizeInfo) WriteTo(w *taf.Writer) error { return nil }
func (p *UserPrizeInfo) ReadFrom(r *taf.Reader) error {
var err error
if p.ID, err = r.ReadInt64(0, false, p.ID); err != nil {
return err
}
if p.Name, err = r.ReadString(1, false, p.Name); err != nil {
return err
}
if p.Score, err = r.ReadInt64(2, false, p.Score); err != nil {
return err
}
if p.CreateTime, err = r.ReadInt64(3, false, p.CreateTime); err != nil {
return err
}
if p.Status, err = r.ReadInt32(4, false, p.Status); err != nil {
return err
}
if p.Type, err = r.ReadInt32(5, false, p.Type); err != nil {
return err
}
if p.IsReal, err = r.ReadInt32(6, false, p.IsReal); err != nil {
return err
}
if p.NewScore, err = r.ReadInt64(7, false, p.NewScore); err != nil {
return err
}
if p.Account, err = r.ReadString(8, false, p.Account); err != nil {
return err
}
if p.Code, err = r.ReadString(9, false, p.Code); err != nil {
return err
}
if p.CommonPrizeType, err = r.ReadInt32(10, false, p.CommonPrizeType); err != nil {
return err
}
if p.OrderID, err = r.ReadString(12, false, p.OrderID); err != nil {
return err
}
if p.Image, err = r.ReadString(14, false, p.Image); err != nil {
return err
}
if p.PrizeID, err = r.ReadInt64(16, false, p.PrizeID); err != nil {
return err
}
if p.ShopItemID, err = r.ReadInt64(17, false, p.ShopItemID); err != nil {
return err
}
if p.ItemID, err = r.ReadInt64(19, false, p.ItemID); err != nil {
return err
}
if p.Ext, err = r.ReadMapStringString(20, false); err != nil {
return err
}
p.ExchangeDate, err = r.ReadInt32(21, false, p.ExchangeDate)
return err
}
type GetUserPrizeRecordsResp struct {
Status int32 `json:"status"`
Msg string `json:"msg"`
Records []UserPrizeInfo `json:"records"`
}
func (r *GetUserPrizeRecordsResp) WriteTo(w *taf.Writer) error { return nil }
func (r *GetUserPrizeRecordsResp) ReadFrom(rd *taf.Reader) error {
var err error
if r.Status, err = rd.ReadInt32(0, false, r.Status); err != nil {
return err
}
if r.Msg, err = rd.ReadString(1, false, r.Msg); err != nil {
return err
}
items, err := rd.ReadListStruct(2, false, func() taf.Unmarshaler { return &UserPrizeInfo{} })
if err != nil {
return err
}
r.Records = make([]UserPrizeInfo, 0, len(items))
for _, item := range items {
if record, ok := item.(*UserPrizeInfo); ok {
r.Records = append(r.Records, *record)
}
}
return nil
}
type ScoreExchangePrizeReq struct {
User ActivityUserID
SID int32
PID int32
IP string
ClientEnv map[string]string
Source string
IsRole int32
}
func (q *ScoreExchangePrizeReq) WriteTo(w *taf.Writer) error {
if err := w.WriteStruct(0, &q.User); err != nil {
return err
}
if err := w.WriteInt32(1, int64(q.SID)); err != nil {
return err
}
if err := w.WriteInt32(2, int64(q.PID)); err != nil {
return err
}
if err := w.WriteString(3, q.IP); err != nil {
return err
}
if err := w.WriteMapStringString(4, q.ClientEnv); err != nil {
return err
}
if err := w.WriteString(5, q.Source); err != nil {
return err
}
if q.IsRole == 0 {
q.IsRole = 1
}
return w.WriteInt32(6, int64(q.IsRole))
}
func (q *ScoreExchangePrizeReq) ReadFrom(r *taf.Reader) error {
_, err := r.ReadStruct(0, false, &q.User)
if err != nil {
return err
}
if q.SID, err = r.ReadInt32(1, false, q.SID); err != nil {
return err
}
if q.PID, err = r.ReadInt32(2, false, q.PID); err != nil {
return err
}
if q.IP, err = r.ReadString(3, false, q.IP); err != nil {
return err
}
if q.ClientEnv, err = r.ReadMapStringString(4, false); err != nil {
return err
}
if q.Source, err = r.ReadString(5, false, q.Source); err != nil {
return err
}
q.IsRole, err = r.ReadInt32(6, false, q.IsRole)
return err
}
type ExchangeInfo struct {
PID int32 `json:"pid"`
ExchangeScore int64 `json:"exchange_score"`
Desc string `json:"desc"`
}
func (e *ExchangeInfo) WriteTo(w *taf.Writer) error { return nil }
func (e *ExchangeInfo) ReadFrom(r *taf.Reader) error {
var err error
if e.PID, err = r.ReadInt32(1, false, e.PID); err != nil {
return err
}
if e.ExchangeScore, err = r.ReadInt64(2, false, e.ExchangeScore); err != nil {
return err
}
e.Desc, err = r.ReadString(3, false, e.Desc)
return err
}
type ActPreCondition struct {
ConditionType int32 `json:"condition_type"`
ConditionParams map[string]string `json:"condition_params,omitempty"`
Tip string `json:"tip,omitempty"`
JumpURL string `json:"jump_url,omitempty"`
ButtonDesc string `json:"button_desc,omitempty"`
}
func (c *ActPreCondition) WriteTo(w *taf.Writer) error { return nil }
func (c *ActPreCondition) ReadFrom(r *taf.Reader) error {
var err error
if c.ConditionType, err = r.ReadInt32(0, false, c.ConditionType); err != nil {
return err
}
if c.ConditionParams, err = r.ReadMapStringString(1, false); err != nil {
return err
}
if c.Tip, err = r.ReadString(2, false, c.Tip); err != nil {
return err
}
if c.JumpURL, err = r.ReadString(3, false, c.JumpURL); err != nil {
return err
}
c.ButtonDesc, err = r.ReadString(4, false, c.ButtonDesc)
return err
}
type ScoreExchangePrizeResp struct {
Status int32 `json:"status"`
Msg string `json:"msg"`
OrderID string `json:"order_id,omitempty"`
ExchangeInfo ExchangeInfo `json:"exchange_info"`
ActPreCondition ActPreCondition `json:"act_pre_condition"`
}
func (r *ScoreExchangePrizeResp) WriteTo(w *taf.Writer) error { return nil }
func (r *ScoreExchangePrizeResp) ReadFrom(rd *taf.Reader) error {
var err error
if r.Status, err = rd.ReadInt32(0, false, r.Status); err != nil {
return err
}
if r.Msg, err = rd.ReadString(1, false, r.Msg); err != nil {
return err
}
if r.OrderID, err = rd.ReadString(3, false, r.OrderID); err != nil {
return err
}
if _, err = rd.ReadStruct(4, false, &r.ExchangeInfo); err != nil {
return err
}
_, err = rd.ReadStruct(5, false, &r.ActPreCondition)
return err
}
func (r ScoreExchangePrizeResp) Success() bool {
return r.Status == 200 && (r.Msg == "" || strings.Contains(r.Msg, "成功"))
}
func prizeListInventorySummary(prizes []PrizeInfo) (left, total int64, leftKnown, totalKnown int) {
for _, prize := range prizes {
if prize.Num > 0 {
total += prize.Num
totalKnown++
}
if itemLeft, ok := prize.InventoryLeft(); ok {
left += itemLeft
leftKnown++
}
}
return left, total, leftKnown, totalKnown
}
func exchangeResponseMessage(resp *ScoreExchangePrizeResp) string {
if resp == nil {
return ""
}
for _, text := range []string{
resp.Msg,
resp.ActPreCondition.Tip,
resp.ActPreCondition.ButtonDesc,
resp.ExchangeInfo.Desc,
resp.OrderID,
} {
if strings.TrimSpace(text) != "" {
return strings.TrimSpace(text)
}
}
return ""
}