v0.4.0:窗口调度/限流分类/可观测性,抢购改每账号独立循环,移除 CLI 只保留 GUI

This commit is contained in:
yml2213
2026-10-16 18:14:30 +08:00
parent 29aed15e4e
commit 8b3b14654f
20 changed files with 2309 additions and 664 deletions
+56 -85
View File
@@ -1,22 +1,14 @@
# huya-go
虎牙活动积分兑换/抢购的 Go 复刻版。支持两种传输层,CLI 与 GUI 默认都走 `wss`:
虎牙活动积分兑换/抢购的 Go 复刻版(Wails 桌面应用)。支持两种传输层,默认走 `wss`:
- `wss`(默认):浏览器实际使用的通道。先调用 `launch.queryHttpDns` 解析可用 host,再走 TAF `WebSocketCommand` 外层与 WSS 初始化流程(`launch.wsLaunch` → `mobileui.getConfig`)。host 留空即自动解析,解析失败会回退内置默认 host。
- `http`:走 `https://cdnws.api.huya.com/`,作为可回退通道;CLI 用 `-transport http`,GUI 在设置里切换。
- `http`:走 `https://cdnws.api.huya.com/`,作为可回退通道,在设置里切换。
## 构建
CLI:
```bash
go test ./...
go build -o huya-go ./cmd/huya-go
```
GUI:
```bash
cd frontend
npm install
npm run typecheck
@@ -45,43 +37,15 @@ yyuid=...; udb_uid=...; guid=...; ...
不要把 Cookie、token、JWT 提交到仓库或写进文档。
## 常用命令
## 常用操作
GUI 界面提供账号预览、奖品列表、积分查询、兑换记录、直接兑换、监控抢购、实时日志。
界面提供账号预览、奖品列表、积分查询、兑换记录、直接兑换、监控抢购、实时日志:
CLI 示例:
查询奖品列表:
```bash
./huya-go -cmd list -sid 2203 -cookie-file cookie.txt
```
查询积分:
```bash
./huya-go -cmd score -sid 2203 -cookie-file cookie.txt
```
直接兑换指定 PID:
```bash
./huya-go -cmd exchange -sid 2203 -pid 12889 -cookie-file cookie.txt
```
按奖品名轮询并抢购:
```bash
./huya-go -cmd monitor -sid 2203 -name 小黄鸭 -cookie-file cookie.txt -loops 20 -delay 120 -concurrency 5
```
定时开始:
```bash
./huya-go -cmd monitor -sid 2203 -pid 12889 -cookie-file cookie.txt -at "2026-07-06 20:00:00"
```
以上默认都走 `wss`。需要回退到 http 传输时,给任意命令加上 `-transport http`。
- **查奖品列表**:填好 SID 与账号后点「获取商品」,列表按类目分页签(全部 / 限量返场 / 精英专享,带商品数),未上架商品显示上架时间与倒计时。
- **查积分 / 兑换记录**:在商品列表里按行查询;行内还能直接对单个 PID 发起兑换。
- **抢购**:选中账号 + 填写目标(PID 或奖品名,可配合「最高积分」「最少库存」「自动选品」),点「监控抢购」进入窗口循环。
- **定时抢购**:填写「开始时间」即可,支持 `2026-07-06 20:00:00`、`20:00:00`、RFC3339;开启「时间校准」时按服务端时间理解。
- **传输**:默认 `wss`;需要回退到 http 时在设置里把「传输」切成 `http`。
## 多开(多实例)
@@ -95,64 +59,78 @@ CLI 示例:
## 日志
GUI 与 CLI 共用同一套结构化日志(分类 + 等级 + 账号标签),同时输出到界面与磁盘:
结构化日志(分类 + 等级 + 账号标签)同时输出到界面与磁盘:
- 路径:`<数据目录>/logs/huya-go_<实例标识>_<时间戳>.log`。macOS 数据目录为 `~/Library/Application Support/huya-go`,Windows 为 `%AppData%\huya-go`。
- 每次启动创建独立文件(`O_EXCL` 抢占,重名自动加 `-2` 后缀),所以多开或同秒重启都不会写进同一个文件;文件开头写明实例名、数据目录与格式说明。
- 单文件上限 5MB,超出后滚动为 `.1` ~ `.5`,最旧的被丢弃。
- 每行格式:`时间 [分类][等级][账号] 正文`。逐条网络请求是 `debug` 级,默认不记录,可在界面「日志」页签打开「调试」开关,或 CLI 使用 `-log-level debug`。
- 每行格式:`时间 [分类][等级][账号] 正文`。逐条网络请求是 `debug` 级,默认不记录,可在「日志」页签用「调试日志」开关打开(等级也可由环境变量 `HUYA_LOG_LEVEL` 覆盖)。
- 写盘是异步且队列有界:队列满时丢弃日志并在文件尾部与界面提示丢弃条数,避免磁盘 I/O 拖慢抢购链路。
- 界面「日志」页签支持按等级筛选、按 ID 增量拉取、一键打开日志目录或复制路径。
- GUI 启动失败(例如 WebView2 建不出环境)时,`<用户缓存目录>/huya-go/startup.log` 会记录原因。
## 参数说明
## 设置项说明
- `-cmd`:`list`、`score`、`records`、`exchange`、`monitor`。
- `-sid`:活动 SID,必填。
- `-pid`:奖品 PID。`exchange` 必填,`monitor` 可用 `-pid` 或 `-name`。
- `-max-price`:目标奖品最高积分,`0` 表示不限制(CLI 与 GUI 设置都支持)。
- `-min-left`:目标奖品最少剩余库存,`0` 表示不限制(CLI 与 GUI 设置都支持)。
- `-category`:类目过滤,支持类目名(如 `限量返场`、`精英专享`)或类目 id(如 `99`);留空表示不限。
- `-category-only`:`list` 只输出该类目的商品(配合 `-category` 使用)。
- `-cookie`:单账号 Cookie。
- `-cookie-file`:多账号 Cookie 文件。
- `-transport`:`wss` 或 `http`,默认 `wss`。
- `-wss-host`:WebSocket host。默认留空,自动调用 `launch.queryHttpDns` 解析(带 TTL 缓存、多 host 轮询),解析失败时回退内置默认 host;需要固定时可显式指定。
- `-timeout`:单次调用超时秒数。
- `-loops`、`-delay`、`-concurrency`:轮询次数、轮询间隔、并发额度。`-concurrency` 限制的是**同时在途的请求数**(不是同时运行的账号数),账号在退避等待期间不占用额度。
- `-stop-after-success`:`monitor` 中任一账号成功后是否停止。
- `-at`:定时开始时间,支持 `RFC3339`、`2006-01-02 15:04:05`、`HH:MM:SS`;开启时间校准时按**服务端时间**理解。
- `-lead`:相对 `-at` 的提前启动量,默认 `2500ms`(用于覆盖网络往返,让请求在开抢瞬间已经在途)。
- `-time-sync`:默认开启。定时前用 `launch.pingSignal` 校准服务端与本机的钟差,并把触发时刻对齐到服务端时钟(详见「时间校准」)。
- `-log-dir`:日志目录,默认使用应用数据目录下的 `logs`。
- `-log-level`:日志最低等级,`debug`/`info`/`warn`/`error`,默认 `info`。
- `-v`:输出调试日志(等价于 `-log-level debug`,并把网络日志同时打到 stderr)。
界面设置对应 `internal/gui` 的 `Config`:
- **传输**:`wss`(默认)或 `http`。
- **WSS Host**:默认留空,自动调用 `launch.queryHttpDns` 解析(带 TTL 缓存、多 host 轮询),解析失败时回退内置默认 host;需要固定时可显式指定。
- **SID**:活动 SID,必填。
- **PID / 目标**:奖品 PID;监控时也可以用奖品名(支持模糊匹配)。
- **最高积分 / 最少库存**:目标奖品的价格上限与剩余库存下限,`0` 表示不限制。
- **次数上限**:`0`(默认)表示按窗口运行,由「窗口秒」兜底;填了就作为单个账号的轮次上限。
- **窗口秒**:抢购窗口长度,默认 `30`。窗口模式下跑到「开抢 + 窗口」才收工(详见「窗口调度」)。
- **延时**:单个账号两轮之间的间隔(毫秒)。账号之间互不等待,所以它只决定单个账号的节奏。
- **线程**:限制**同时在途的请求数**(不是同时运行的账号数);账号在退避等待期间不占用额度。
- **提前**:相对开始时间的提前量,`0` = 按服务端时钟校准误差动态计算(`误差上界 + RTT/2`,下限 50ms);填了就用固定提前量。
- **预热 / 直兑 / 共享列表 / 自动选品 / 账号成功停 / 成功刷新**:见「抢购调度」。
- **时间校准**:默认开启。定时前用 `launch.pingSignal` 校准服务端与本机的钟差,并把触发时刻对齐到服务端时钟(详见「时间校准」)。
- **自适应降速**:默认开启。连续十几个请求都拿不到业务响应时整体降速(最多 4 倍),一旦恢复进展立刻回退。
- **开始时间**:定时开始时间,支持 `RFC3339`、`2006-01-02 15:04:05`、`HH:MM:SS`;开启时间校准时按**服务端时间**理解。
- **超时**:单次调用超时秒数。
## 抢购调度与退避(2026-10-16 调整)
参考 `dygou_go` 的抢购调度经验(该项目的实测复盘结论)做了以下调整:
- **每个账号独立循环**:账号各自在自己的 goroutine 里按「延时」节奏重试,互不等待、互不阻塞。旧实现按「一轮打完等所有人」(`wg.Wait`)同步,某个账号卡在慢请求上时,其余账号这一轮只能干等——账号少的实例尤其吃亏(pc-3 instance-4 因此 0 成功)。请求速率由请求级并发闸门兜底。
- **并发额度按单次请求计**:额度只覆盖一次在途请求,账号在退避等待期间释放额度。旧实现把额度给整个账号循环,退避中的账号会一直霸占额度,账号多时互相饿死。
- **售罄不再默认终止**:服务端回「已抢完 / 库存不足」时继续按退避节奏探测到轮询结束——别人锁单未支付或取消会让库存回流。需要「售罄即停」时显式设置 `Config.StopOnSoldOut`。
- **下架仍然终止**:`已下架 / 奖品不存在` 视为本窗口内的终态,直接停止该奖品(与售罄区别对待)。
- **积分不足仍然终止**:积分不会自己变多,默认 `StopOnNotEnough`,避免无意义的重复请求。
- **退避带抖动**:所有退避(限频 / 失败 / 售罄探测)在配置值基础上叠加 0-25% 随机抖动,避免多个账号在同一时刻整批重试形成周期性尖峰;配置值始终是退避下限。
- **定时精确触发**:定时开抢改用一次性定时器(旧实现按 200ms 轮询,醒来时刻会随机晚 0-200ms,正好错过开抢窗口)。`-lead` 仍用于按 RTT 提前起跑。
- **定时精确触发**:定时开抢改用一次性定时器(旧实现按 200ms 轮询,醒来时刻会随机晚 0-200ms,正好错过开抢窗口)。「提前」仍用于按 RTT 提前起跑。
- **任务结束汇总**:每次监控结束输出一行汇总(轮次 / 账号 / 目标 / 成功账号 / 耗时 / 请求次数)与按次数排序的结局分布(`success`、`sold_out`、`too_frequent`、`not_found` 等),便于复盘。
- **HTTP 传输连接池**:`http` 传输补上 `MaxConnsPerHost=64`、`MaxIdleConnsPerHost=64`(Go 默认每主机只有 2 个空闲连接),多账号并发时不再反复重建连接。
### 0.4.0:窗口调度 / 限流分类 / 可观测性(2026-10-16 三机复盘)
依据 2026-10-16 三台机器同一场活动的真实日志(247 账号、2000 次兑换请求):
- **每账号独立循环**:正式窗口里每个账号一个 goroutine 跑自己的循环,直到窗口结束或自己打完(所有目标成功 / 进入终态)。旧实现是「一轮打完等所有人」,慢账号会拖住全部账号的节奏;「延时」从此只决定**单个账号**的节奏。
- **共享列表 TTL + 单飞**:「共享列表」打开时,同一时刻只有一个账号去查奖品列表,其余账号在 1 秒 TTL 内复用同一份成功快照(失败不缓存,避免一次抖动让所有账号一起失败)。pc-1 的 152 个账号从每轮 152 次查询降到 1 次。
- **窗口调度取代轮次预算**:终止条件从「轮次 × 间隔」改为「开抢 + 窗口秒数」(默认 30s)。旧实现里 `Delay=150ms` 时 1 个账号的 20 轮只有 3.1 秒,会把预算全部烧在开抢之前(当天 pc-3 instance-4 一轮都没打进窗口,0 成功)。GUI 的「次数上限」退化为可选上限,留空即按窗口运行。
- **开抢前只预打一轮**:开抢前若还没到点,先补一轮探活请求,随后静默等到「开抢 − 60ms」再进入窗口循环。旧实现把 2.5s 提前量整段用来循环重试,实测 1421 次请求里 82% 被限流挡回。
- **动态提前量 + 临近复校**:触发提前量 = `误差上界 + RTT/2`(下限 50ms),并在开抢前 3s 复校一次时钟、2s 前预热连接(顺序与 `sniper.PlanTrigger` 统一)。
- **首发错峰**:第 0 个账号立即发,其余按 `min(300ms/(n-1), 8ms)` 依次错开,避免同机上百账号在同一毫秒齐射。
- **退避阶梯**:基础退避随时间分档放大(<2s ×1、<5s ×2、≥5s ×3),同一账号连续失败第 3 次起翻倍(上限 ×4),再乘全局降速倍率并叠加 0-25% 抖动;任何一次等待都不超过 1.5s(超过就可能错过库存回流)。
- **全局自适应降速(默认开)**:连续 12 个以上请求都拿不到业务响应且持续 800ms 时,整体退避按 1.5 倍逐级放大(上限 4 倍),一旦有进展立即回退。触发信号刻意选「无进展」而不是「限流占比」——实测即使成功率 97.6% 的那次也有 67% 请求被限流,用占比阈值会把正常运行的机器一起拖慢。GUI 设置里的「自适应降速」可关。
- **限流分类补全**:「活动太火爆 / 领取频率限制 / 网络繁忙 / 点击太快 / 排队…」统一归入 `too_frequent`,并按「未开放 → 售罄 → 货币不足 → 会话失效 → 下架 → 限流」的固定顺序判定(过去这些文案全落到 `failed`,看不出被限流)。新增 `session_lost`(「账号未绑定!」,瞬时错误,连续 3 次才停止该账号)与 `protocol`(回包缺 `tRsp/tResp`)两个分类。
- **协议错误可观测**:`wup.Response.Diagnose()` 把 `iRet / servant / status / context / keys / payload` 一并带进报错;debug 级日志打印原始帧前缀(32B HEX)。此前只有一句「响应缺少 tRsp/tResp」,同一账号连续 13 次命中也无法定性。
- **复盘日志**:任务结束输出「一行总量(含限流占比与降速倍率)+ 结局分布(带中文标签)+ 接口统计(次数/失败/平均/最慢/最后错误)+ 每个账号一行最终结局」;`启动监控` 带上目标 `pid(名称)`(此前只有 `targets=1`,无法判断三台机器打的是否同一个奖品)。
## 时间校准(2026-10-16 新增)
定时抢购默认先做一次服务端时间校准(CLI `-time-sync`,GUI 设置里的「时间校准」开关,默认开):
定时抢购默认先做一次服务端时间校准(GUI 设置里的「时间校准」开关,默认开):
- **数据来源**:`launch.pingSignal` 响应里的 `signal_ap_start` / `signal_ap_end`。2026-10-13 抓包确认这两个字段是 Unix 毫秒时间戳(`start` 是服务端收到请求的时刻,`end` 是写出响应的时刻,实测相差 13ms);真实字节夹具见 `internal/huya/launch_test.go` 的 `realPingSignalRspHex`。
- **钟差估计**:`偏移 = signal_ap_start − (发送时刻 + RTT/2)`,误差上界取 `RTT/2`——服务端打时间戳的具体位置落在发送与接收之间,无法再精确,按最坏情况估计。
- **多轮取优**:默认 3 轮采样,取「误差上界最小、其次 RTT 最低」的样本;再对最近 3 次偏移取中位数平滑,压掉单轮网络抖动。
- **生效方式**:`-at` / `startAt` 按服务端时间理解,触发时刻 = `目标时刻 − 提前量 − 偏移`。本机时钟比服务端慢 300ms 就自动提前 300ms,反之推后。
- **生效方式**:「开始时间」(`startAt`)按服务端时间理解,触发时刻 = `目标时刻 − 提前量 − 偏移`。本机时钟比服务端慢 300ms 就自动提前 300ms,反之推后。
- **失败降级**:一次样本都拿不到时只告警,仍按本机时钟触发;时间戳明显不合理(解析出来不是这个世纪的毫秒值)会被丢弃,避免按错误偏移整体挪走抢购窗口。
- **日志**:`时间校准完成: 服务端=2026-10-16 20:00:01.234 本机偏移=+123ms 误差<=45ms RTT=90ms 样本=3 已平滑`。
校准每次只多花几百毫秒(3 轮 pingSignal),发生在预热之后、等待定时之前,因此不会挤占开抢窗口。
校准每次只多花几百毫秒(3 轮 pingSignal),发生在**开抢前 3 秒**(预热在开抢前 2 秒,触发在开抢前 `advance`),因此既不会挤占开抢窗口,也不会因为启动过早让偏移过期。
## 已确认的小黄鸭参数
@@ -175,7 +153,7 @@ GUI 与 CLI 共用同一套结构化日志(分类 + 等级 + 账号标签)
- 消耗积分:`20`
- 订单号格式:`sOrderId_2277_13409_<uid>_<时间戳>_<随机>`
该活动的列表/积分/兑换接口与 `sid=2203` 走同一套 `webActUI`,可直接用 `-cmd list -sid 2277` 验证;同一次抓包里「开通精英宝典 / 购买精英币」属于商城付费链路(`shopMiddleUI.*` + 支付宝),本项目未实现,详见 `WSS_NOTES.md`。
该活动的列表/积分/兑换接口与 `sid=2203` 走同一套 `webActUI`,把 SID 改成 `2277` 即可在界面里验证;同一次抓包里「开通精英宝典 / 购买精英币」属于商城付费链路(`shopMiddleUI.*` + 支付宝),本项目未实现,详见 `WSS_NOTES.md`。
## 商品类目:限量返场 / 精英专享(2026-10-15 抓包)
@@ -187,18 +165,11 @@ GUI 与 CLI 共用同一套结构化日志(分类 + 等级 + 账号标签)
- 限量返场是「预告上架」类商品:`start_time`(TAF tag25,实测 2026-10-16 17:00:00)与 `end_time`(tag26)决定
可兑换时间窗,页面上的「xx:xx:xx 后上架」倒计时就来自它;`daily_start/daily_end`(tag27/28)是每日可兑时段。
用法:
用法(界面「商品列表」):
```bash
# 全量列表(日志里会打出「类目=精英专享(101)=18项/限量返场(99)=12项」)
./huya-go -cmd list -sid 2277 -cookie-file cookies.txt
- **全量列表**:SID 填 `2277` 点「获取商品」,日志里会打出「类目=精英专享(101)=18项/限量返场(99)=12项」。
- **只看某个类目**:在商品列表上方切换类目页签,或点某个类目标签过滤(也支持类目 id 或模糊名,如 `99` / `返场`)。
- **限定类目抢购**:选中类目并打开「自动选品」,当前类目会一起提交给监控,作为自动选品的范围。
# 只看限量返场(也支持类目 id 或模糊名,如 -category 99 / -category 返场)
./huya-go -cmd list -sid 2277 -cookie-file cookies.txt -category 限量返场 -category-only
# 抢购时限定只在该类目里选品(配合 -auto)
./huya-go -cmd monitor -sid 2277 -cookie-file cookies.txt -category 限量返场 -auto
```
界面「商品列表」同样按类目分了页签(全部 / 限量返场 / 精英专享,带商品数),每行标注类目,
界面「商品列表」按类目分了页签(全部 / 限量返场 / 精英专享,带商品数),每行标注类目,
未上架的商品显示上架时间与倒计时;当前选中的类目会一起提交给监控,用作自动选品的范围。