This commit is contained in:
yml2213
2026-07-06 18:08:23 +08:00
commit 4235cce4f5
49 changed files with 9880 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
package main
import (
"bufio"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
"huya-go/internal/huya"
"huya-go/internal/sniper"
)
func main() {
var (
cmd = flag.String("cmd", "list", "命令:list/score/records/exchange/monitor")
cookie = flag.String("cookie", "", "虎牙 Cookie")
cookieFile = flag.String("cookie-file", "", "Cookie 文件,一行一个账号")
sid = flag.Int("sid", 0, "活动 SID")
pid = flag.Int("pid", 0, "奖品 PID")
name = flag.String("name", "", "按奖品名包含匹配")
loops = flag.Int("loops", 1, "monitor 循环次数")
delayMS = flag.Int("delay", 150, "monitor 每轮间隔毫秒")
concurrency = flag.Int("concurrency", 5, "monitor/exchange 并发账号数")
stopAfterSuccess = flag.Bool("stop-after-success", true, "monitor 成功后停止")
at = flag.String("at", "", "定时开始时间,支持 RFC3339 或 2006-01-02 15:04:05")
timeoutSec = flag.Int("timeout", 15, "单次请求超时秒数")
transport = flag.String("transport", "http", "传输层:http/wss")
wssHost = flag.String("wss-host", huya.DefaultWSSHost, "WSS host,来自 launch.queryHttpDns 或抓包")
verbose = flag.Bool("v", false, "输出调试日志")
)
flag.Parse()
if *sid <= 0 {
exitErr(errors.New("必须提供有效 -sid"))
}
cookies, err := loadCookies(*cookie, *cookieFile)
if err != nil {
exitErr(err)
}
accounts := make([]sniper.Account, 0, len(cookies))
for _, ck := range cookies {
uid := huya.ResolveUID(ck)
if uid <= 0 {
exitErr(errors.New("无法从 Cookie 解析 yyuid/udb_uid"))
}
accounts = append(accounts, sniper.Account{UID: uid, Cookie: ck})
}
logger := func(string) {}
if *verbose {
logger = func(msg string) { fmt.Fprintln(os.Stderr, msg) }
}
client, closeClient, err := huya.NewActivityClient(*transport, *wssHost, time.Duration(*timeoutSec)*time.Second, logger)
if err != nil {
exitErr(err)
}
defer closeClient()
ctx := context.Background()
if *at != "" {
when, err := parseTime(*at)
if err != nil {
exitErr(err)
}
waitUntil(ctx, when)
}
switch *cmd {
case "list":
resp, err := client.GetActPrizeList(ctx, accounts[0].UID, accounts[0].Cookie, int32(*sid))
if err != nil {
exitErr(err)
}
printJSON(resp)
case "score":
results := make([]map[string]any, 0, len(accounts))
for _, account := range accounts {
resp, err := client.QueryUserScore(ctx, account.UID, account.Cookie, int32(*sid))
results = append(results, map[string]any{
"uid": account.UID,
"resp": resp,
"score": availableScore(resp),
"error": errString(err),
})
}
printJSON(results)
case "records":
results := make([]map[string]any, 0, len(accounts))
for _, account := range accounts {
resp, err := client.GetUserPrizeRecords(ctx, account.UID, account.Cookie, int32(*sid))
results = append(results, map[string]any{
"uid": account.UID,
"resp": resp,
"error": errString(err),
})
}
printJSON(results)
case "exchange":
if *pid <= 0 {
exitErr(errors.New("exchange 必须提供 -pid"))
}
runExchange(ctx, client, accounts, int32(*sid), int32(*pid), *concurrency)
case "monitor":
if *pid <= 0 && strings.TrimSpace(*name) == "" {
exitErr(errors.New("monitor 必须提供 -pid 或 -name"))
}
runner := sniper.Runner{Client: client}
var outMu sync.Mutex
err := runner.Run(ctx, accounts, sniper.Target{PID: int32(*pid), Name: strings.TrimSpace(*name)}, sniper.Config{
SID: int32(*sid),
Loops: *loops,
Delay: time.Duration(*delayMS) * time.Millisecond,
Concurrency: *concurrency,
StopAfterSuccess: *stopAfterSuccess,
}, func(result sniper.Result) {
outMu.Lock()
defer outMu.Unlock()
_ = json.NewEncoder(os.Stdout).Encode(result)
})
if err != nil && !errors.Is(err, context.Canceled) {
exitErr(err)
}
default:
exitErr(fmt.Errorf("未知 -cmd: %s", *cmd))
}
}
func runExchange(ctx context.Context, client huya.ActivityClient, accounts []sniper.Account, sid, pid int32, concurrency int) {
if concurrency <= 0 {
concurrency = 1
}
type item struct {
UID int64 `json:"uid"`
Resp *huya.ScoreExchangePrizeResp `json:"resp,omitempty"`
Error string `json:"error,omitempty"`
}
results := make([]item, len(accounts))
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
for i, account := range accounts {
i, account := i, account
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
resp, err := client.ScoreExchangePrize(ctx, account.UID, account.Cookie, sid, pid)
results[i] = item{UID: account.UID, Resp: resp, Error: errString(err)}
}()
}
wg.Wait()
printJSON(results)
}
func loadCookies(cookie, file string) ([]string, error) {
var out []string
if strings.TrimSpace(cookie) != "" {
out = append(out, strings.TrimSpace(cookie))
}
if file != "" {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out = append(out, line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
}
if len(out) == 0 {
return nil, errors.New("必须提供 -cookie 或 -cookie-file")
}
return out, nil
}
func parseTime(text string) (time.Time, error) {
text = strings.TrimSpace(text)
if text == "" {
return time.Time{}, errors.New("时间为空")
}
if ts, err := strconv.ParseInt(text, 10, 64); err == nil {
return time.Unix(ts, 0), nil
}
for _, layout := range []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02 15:04"} {
if t, err := time.ParseInLocation(layout, text, time.Local); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("无法解析 -at 时间: %s", text)
}
func waitUntil(ctx context.Context, when time.Time) {
for {
remaining := time.Until(when)
if remaining <= 0 {
return
}
timer := time.NewTimer(minDuration(remaining, 200*time.Millisecond))
select {
case <-timer.C:
case <-ctx.Done():
timer.Stop()
return
}
}
}
func printJSON(value any) {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(value); err != nil {
exitErr(err)
}
}
func errString(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func availableScore(resp *huya.GetUserScoreResp) int64 {
if resp == nil {
return 0
}
return resp.AvailableScore()
}
func exitErr(err error) {
fmt.Fprintln(os.Stderr, "错误:", err)
os.Exit(1)
}
func minDuration(a, b time.Duration) time.Duration {
if a < b {
return a
}
return b
}