Files
huya-go/internal/huya/launch.go
T
yml2213 41565566c5 GUI 日志落盘,修复同一奖品重复兑换与监控结束竞态
- 日志落盘:新增 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 补日志说明
2026-10-13 20:34:43 +08:00

270 lines
6.9 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 (
"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, ".")
}