- 日志落盘:新增 internal/gui/logfile.go,写到 <用户配置目录>/huya-go/logs/huya-go.log,单文件 5MB 后滚动保留 5 个备份;「日志」页签顶部显示路径并支持打开目录/复制路径;写盘失败自动降级为仅界面输出 - 修复重复兑换:界面选品会同时提交 pid、名称与 targetText 里的同一 pid,normalizeTargets 现按 pid/名称去重;runState 再按「账号+奖品」记账,多个 target 命中同一奖品时只兑换一次(新增 skip_duplicate 结果) - 修复 use of closed network connection:成功后的异步刷新改为 runState.refresh 等待,RunTargets 返回前不再提前关闭连接 - queryHttpDns 增加 single-flight 与失败冷却,多账号并发建连只解析一次 - 新增 internal/gui/logfile_test.go 与 sniper/huya 回归测试;README 补日志说明
270 lines
6.9 KiB
Go
270 lines
6.9 KiB
Go
package huya
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"huya-go/internal/taf"
|
||
)
|
||
|
||
const (
|
||
ServantLaunch = "launch"
|
||
|
||
FuncWsLaunch = "wsLaunch"
|
||
FuncQueryHttpDns = "queryHttpDns"
|
||
FuncPingSignal = "pingSignal"
|
||
FuncGetConfig = "getConfig"
|
||
|
||
// HttpDnsDomain 是 cdnws 入口域名,launch.queryHttpDns 用它换取 WSS host 列表。
|
||
HttpDnsDomain = "cdnws.api.huya.com"
|
||
|
||
// DefaultDNSTTL 是响应没有 ttl 时的兜底缓存时间。
|
||
DefaultDNSTTL = 10 * time.Minute
|
||
// dnsFailureCooldown 是 queryHttpDns 失败后的冷却时间。
|
||
dnsFailureCooldown = 30 * time.Second
|
||
)
|
||
|
||
// QueryHttpDnsReq 对应浏览器 launch.queryHttpDns 的 tReq。
|
||
//
|
||
// 实测字段(2026-10-13 HAR):0=uid, 1=ua, 2=域名列表, 3=appSrc,
|
||
// 4="", 5=0, 6=1,iRequestId 固定 -1。
|
||
type QueryHttpDnsReq struct {
|
||
UID int64
|
||
UA string
|
||
Domains []string
|
||
AppSrc string
|
||
Extra string
|
||
FlagType int32
|
||
FlagVer int32
|
||
}
|
||
|
||
func NewQueryHttpDnsReq(uid int64, ua string, domains []string) *QueryHttpDnsReq {
|
||
if len(domains) == 0 {
|
||
domains = []string{HttpDnsDomain}
|
||
}
|
||
return &QueryHttpDnsReq{
|
||
UID: uid,
|
||
UA: ua,
|
||
Domains: domains,
|
||
AppSrc: "HUYA&ZH&2052",
|
||
FlagType: 0,
|
||
FlagVer: 1,
|
||
}
|
||
}
|
||
|
||
func (q *QueryHttpDnsReq) WriteTo(w *taf.Writer) error {
|
||
if err := w.WriteInt64(0, q.UID); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(1, q.UA); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteListString(2, q.Domains); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(3, q.AppSrc); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(4, q.Extra); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteInt32(5, int64(q.FlagType)); err != nil {
|
||
return err
|
||
}
|
||
return w.WriteInt32(6, int64(q.FlagVer))
|
||
}
|
||
|
||
func (q *QueryHttpDnsReq) ReadFrom(r *taf.Reader) error {
|
||
var err error
|
||
if q.UID, err = r.ReadInt64(0, false, q.UID); err != nil {
|
||
return err
|
||
}
|
||
if q.UA, err = r.ReadString(1, false, q.UA); err != nil {
|
||
return err
|
||
}
|
||
if q.Domains, err = r.ReadListString(2, false); err != nil {
|
||
return err
|
||
}
|
||
if q.AppSrc, err = r.ReadString(3, false, q.AppSrc); err != nil {
|
||
return err
|
||
}
|
||
if q.Extra, err = r.ReadString(4, false, q.Extra); err != nil {
|
||
return err
|
||
}
|
||
if q.FlagType, err = r.ReadInt32(5, false, q.FlagType); err != nil {
|
||
return err
|
||
}
|
||
q.FlagVer, err = r.ReadInt32(6, false, q.FlagVer)
|
||
return err
|
||
}
|
||
|
||
// HttpDnsHosts 是 tRsp tag0 里单个域名的解析结果:host 列表 + ttl 秒。
|
||
type HttpDnsHosts struct {
|
||
Hosts []string
|
||
TTL int32
|
||
Extra []string
|
||
}
|
||
|
||
func (h *HttpDnsHosts) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (h *HttpDnsHosts) ReadFrom(r *taf.Reader) error {
|
||
var err error
|
||
if h.Hosts, err = r.ReadListString(0, false); err != nil {
|
||
return err
|
||
}
|
||
if h.TTL, err = r.ReadInt32(1, false, h.TTL); err != nil {
|
||
return err
|
||
}
|
||
h.Extra, err = r.ReadListString(2, false)
|
||
return err
|
||
}
|
||
|
||
// QueryHttpDnsResp 对应 launch.queryHttpDns 的 tRsp:tag0=域名->host 列表, tag1=状态。
|
||
type QueryHttpDnsResp struct {
|
||
Hosts map[string]HttpDnsHosts
|
||
Status int32
|
||
}
|
||
|
||
func (r *QueryHttpDnsResp) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (r *QueryHttpDnsResp) ReadFrom(rd *taf.Reader) error {
|
||
items, err := rd.ReadMapStringStruct(0, false, func() taf.Unmarshaler { return &HttpDnsHosts{} })
|
||
if err != nil {
|
||
return err
|
||
}
|
||
r.Hosts = make(map[string]HttpDnsHosts, len(items))
|
||
for key, item := range items {
|
||
if hosts, ok := item.(*HttpDnsHosts); ok {
|
||
r.Hosts[key] = *hosts
|
||
}
|
||
}
|
||
r.Status, err = rd.ReadInt32(1, false, r.Status)
|
||
return err
|
||
}
|
||
|
||
// WSSHosts 返回指定域名解析到的 WSS host 列表,保持服务端下发顺序。
|
||
func (r QueryHttpDnsResp) WSSHosts(domain string) []string {
|
||
item, ok := r.Hosts[domain]
|
||
if !ok {
|
||
return nil
|
||
}
|
||
out := make([]string, 0, len(item.Hosts))
|
||
for _, host := range item.Hosts {
|
||
if trimmed := strings.TrimSpace(host); trimmed != "" {
|
||
out = append(out, trimmed)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// TTL 返回指定域名的缓存时间。
|
||
func (r QueryHttpDnsResp) TTL(domain string) time.Duration {
|
||
item, ok := r.Hosts[domain]
|
||
if !ok || item.TTL <= 0 {
|
||
return DefaultDNSTTL
|
||
}
|
||
return time.Duration(item.TTL) * time.Second
|
||
}
|
||
|
||
// PingSignalReq 对应 launch.pingSignal:0="launch", 1="pingSignal", 2=扩展 map。
|
||
type PingSignalReq struct {
|
||
Extra map[string]string
|
||
}
|
||
|
||
func (p *PingSignalReq) WriteTo(w *taf.Writer) error {
|
||
if err := w.WriteString(0, ServantLaunch); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(1, FuncPingSignal); err != nil {
|
||
return err
|
||
}
|
||
return w.WriteMapStringString(2, p.Extra)
|
||
}
|
||
|
||
func (p *PingSignalReq) ReadFrom(r *taf.Reader) error {
|
||
var err error
|
||
if _, err = r.ReadString(0, false, ServantLaunch); err != nil {
|
||
return err
|
||
}
|
||
if _, err = r.ReadString(1, false, FuncPingSignal); err != nil {
|
||
return err
|
||
}
|
||
p.Extra, err = r.ReadMapStringString(2, false)
|
||
return err
|
||
}
|
||
|
||
// PingSignalResp 对应 launch.pingSignal 的 tRsp:tag2 是 signal_ap_start/end 等服务端时间戳。
|
||
type PingSignalResp struct {
|
||
Servant string
|
||
Func string
|
||
Values map[string]int64
|
||
}
|
||
|
||
func (r *PingSignalResp) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (r *PingSignalResp) ReadFrom(rd *taf.Reader) error {
|
||
var err error
|
||
if r.Servant, err = rd.ReadString(0, false, r.Servant); err != nil {
|
||
return err
|
||
}
|
||
if r.Func, err = rd.ReadString(1, false, r.Func); err != nil {
|
||
return err
|
||
}
|
||
r.Values, err = rd.ReadMapStringInt64(2, false)
|
||
return err
|
||
}
|
||
|
||
// ServerElapsed 返回服务端 signal_ap_start/end 的差值(毫秒),不可得时返回 0。
|
||
func (r PingSignalResp) ServerElapsed() time.Duration {
|
||
start, okStart := r.Values["signal_ap_start"]
|
||
end, okEnd := r.Values["signal_ap_end"]
|
||
if !okStart || !okEnd || end < start {
|
||
return 0
|
||
}
|
||
return time.Duration(end-start) * time.Millisecond
|
||
}
|
||
|
||
// GetConfigResp 对应 mobileui.getConfig 的 tRsp:
|
||
// tag0=平台开关 map,tag1=按平台下发的配置版本 map。
|
||
type GetConfigResp struct {
|
||
Flags map[string]string
|
||
Exps map[string]string
|
||
}
|
||
|
||
func (r *GetConfigResp) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (r *GetConfigResp) ReadFrom(rd *taf.Reader) error {
|
||
var err error
|
||
if r.Flags, err = rd.ReadMapStringString(0, false); err != nil {
|
||
return err
|
||
}
|
||
r.Exps, err = rd.ReadMapStringString(1, false)
|
||
return err
|
||
}
|
||
|
||
// Exp 返回握手 baseinfo.tag5 与 UpdateUserExps 使用的版本串。
|
||
func (r GetConfigResp) Exp() string {
|
||
return FormatConfigExps(r.Exps)
|
||
}
|
||
|
||
// FormatConfigExps 把 {"58664":"107263"} 拼成 "58664.107263"。
|
||
//
|
||
// 2026-10-13 HAR 中 WSS 握手 baseinfo.tag5 与 UpdateUserExpsReq 都用这个形式;
|
||
// 多个平台时按 key 升序用 "." 连接(实测样本只有一个平台)。
|
||
func FormatConfigExps(exps map[string]string) string {
|
||
if len(exps) == 0 {
|
||
return ""
|
||
}
|
||
keys := make([]string, 0, len(exps))
|
||
for key := range exps {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
parts := make([]string, 0, len(keys))
|
||
for _, key := range keys {
|
||
parts = append(parts, key+"."+exps[key])
|
||
}
|
||
return strings.Join(parts, ".")
|
||
}
|