Files

176 lines
16 KiB
Markdown
Raw Permalink 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.
# huya-go
虎牙活动积分兑换/抢购的 Go 复刻版(Wails 桌面应用)。支持两种传输层,默认走 `wss`:
- `wss`(默认):浏览器实际使用的通道。先调用 `launch.queryHttpDns` 解析可用 host,再走 TAF `WebSocketCommand` 外层与 WSS 初始化流程(`launch.wsLaunch` → `mobileui.getConfig`)。host 留空即自动解析,解析失败会回退内置默认 host。
- `http`:走 `https://cdnws.api.huya.com/`,作为可回退通道,在设置里切换。
## 构建
```bash
go test ./...
cd frontend
npm install
npm run typecheck
cd ..
wails build -debug -nopackage
```
GUI 二进制输出到 `build/bin/huya-gui`。开发时可以使用 `wails dev` 启动 Wails + React + Ant Design 界面。
Windows exe(文件名带版本号):
```bash
./scripts/build-windows.sh # 默认 windows/amd64
./scripts/build-windows.sh arm64 # 如需 windows/arm64
```
产物为 `build/bin/huya-gui-v<版本>.exe`(PE32+ GUI)。版本号的单一来源是 `internal/version/version.go`:脚本从这里读取版本号,既拼进 exe 文件名,也通过 `-ldflags "-X huya-go/internal/version.Version=..."` 编译进程序,界面左下角的版本标签来自同一个值。发布新版本时改这一处,并同步 `wails.json` 的 `info.productVersion`(只影响 exe 文件属性)。在 macOS 上交叉编译不会生成 NSIS 安装包(`-nopackage`),只产出 exe。
## Cookie
推荐把账号 Cookie 放到本地文件,一行一个账号:
```text
yyuid=...; udb_uid=...; guid=...; ...
```
不要把 Cookie、token、JWT 提交到仓库或写进文档。
## 常用操作
界面提供账号预览、奖品列表、积分查询、兑换记录、直接兑换、监控抢购、实时日志:
- **查奖品列表**:填好 SID 与账号后点「获取商品」,列表按类目分页签(全部 / 限量返场 / 精英专享,带商品数),未上架商品显示上架时间与倒计时。
- **查积分 / 兑换记录**:在商品列表里按行查询;行内还能直接对单个 PID 发起兑换。
- **抢购**:选中账号 + 填写目标(PID 或奖品名,可配合「最高积分」「最少库存」「自动选品」),点「监控抢购」进入窗口循环。
- **定时抢购**:填写「开始时间」即可,支持 `2026-07-06 20:00:00`、`20:00:00`、RFC3339;开启「时间校准」时按服务端时间理解。
- **传输**:默认 `wss`;需要回退到 http 时在设置里把「传输」切成 `http`。
## 多开(多实例)
同一台机器可以同时启动多个实例,互不干扰:
- 启动时按顺序占用「实例槽位」并加独占锁:第 1 个进程是**默认实例**(沿用历史数据目录),第 2 个及以后分别是 `实例 2`、`实例 3`…,数据目录为 `<数据目录>/instances/instance-N`。
- 每个实例独占自己的日志目录与 WebView2 用户数据目录,所以多开时日志不会交错,Windows 上也不会出现「第二个窗口开不出来」。
- 界面本地存储(账号、设置、商品缓存)按实例分片,每个实例各存各的账号;标题栏会显示当前实例名。
- 槽位锁在进程退出时自动释放;若锁目录不可写,会退回到「进程唯一」的临时实例目录(日志降级为仅内存),**绝不退回默认目录**,以免两个进程互相覆盖。
- 环境变量:`HUYA_DATA_DIR` 覆盖数据根目录,`HUYA_LOG_DIR` 覆盖日志目录,`HUYA_LOG_LEVEL` 覆盖默认日志等级。
## 日志
结构化日志(分类 + 等级 + 账号标签)同时输出到界面与磁盘:
- 路径:`<数据目录>/logs/huya-go_<实例标识>_<时间戳>.log`。macOS 数据目录为 `~/Library/Application Support/huya-go`,Windows 为 `%AppData%\huya-go`。
- 每次启动创建独立文件(`O_EXCL` 抢占,重名自动加 `-2` 后缀),所以多开或同秒重启都不会写进同一个文件;文件开头写明实例名、数据目录与格式说明。
- 单文件上限 5MB,超出后滚动为 `.1` ~ `.5`,最旧的被丢弃。
- 每行格式:`时间 [分类][等级][账号] 正文`。逐条网络请求是 `debug` 级,默认不记录,可在「日志」页签用「调试日志」开关打开(等级也可由环境变量 `HUYA_LOG_LEVEL` 覆盖)。
- 写盘是异步且队列有界:队列满时丢弃日志并在文件尾部与界面提示丢弃条数,避免磁盘 I/O 拖慢抢购链路。
- 界面「日志」页签支持按等级筛选、按 ID 增量拉取、一键打开日志目录或复制路径。
- GUI 启动失败(例如 WebView2 建不出环境)时,`<用户缓存目录>/huya-go/startup.log` 会记录原因。
## 设置项说明
界面设置对应 `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,正好错过开抢窗口)。「提前」仍用于按 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 新增)
定时抢购默认先做一次服务端时间校准(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 次偏移取中位数平滑,压掉单轮网络抖动。
- **生效方式**:「开始时间」(`startAt`)按服务端时间理解,触发时刻 = `目标时刻 − 提前量 − 偏移`。本机时钟比服务端慢 300ms 就自动提前 300ms,反之推后。
- **失败降级**:一次样本都拿不到时只告警,仍按本机时钟触发;时间戳明显不合理(解析出来不是这个世纪的毫秒值)会被丢弃,避免按错误偏移整体挪走抢购窗口。
- **日志**:`时间校准完成: 服务端=2026-10-16 20:00:01.234 本机偏移=+123ms 误差<=45ms RTT=90ms 样本=3 已平滑`。
校准每次只多花几百毫秒(3 轮 pingSignal),发生在**开抢前 3 秒**(预热在开抢前 2 秒,触发在开抢前 `advance`),因此既不会挤占开抢窗口,也不会因为启动过早让偏移过期。
## 已确认的小黄鸭参数
2026-07-06 浏览器手动抢购抓包确认:
- `sid=2203`
- `pid=12889`
- 奖品名:`套装-潮萌小黄鸭`
- 消耗积分:`988`
远端配置里的子奖品代码不能直接当兑换 PID,实际兑换以 `scoreExchangePrize` 请求里的 `pid` 为准。
## 精英宝典/幸运币(2026-10-13 抓包)
`m-shop.huya.com` 积分中心的兑换记录里确认到:
- `sid=2277`
- `pid=13409`
- 奖品名:`幸运币礼包(小)`
- 消耗积分:`20`
- 订单号格式:`sOrderId_2277_13409_<uid>_<时间戳>_<随机>`
该活动的列表/积分/兑换接口与 `sid=2203` 走同一套 `webActUI`,把 SID 改成 `2277` 即可在界面里验证;同一次抓包里「开通精英宝典 / 购买精英币」属于商城付费链路(`shopMiddleUI.*` + 支付宝),本项目未实现,详见 `WSS_NOTES.md`。
## 商品类目:限量返场 / 精英专享(2026-10-15 抓包)
`webActUI.getActPrizeList` 的响应除商品外还下发类目字典(`tRsp.4`),每件商品用 `tags`(TAF tag14)
标注归属:`101=精英专享`、`99=限量返场`。2026-10-15 真号实测 `sid=2277`:
- 共 30 件商品:限量返场(99) 12 件(怪诞乔克服装礼包 8000 积分、天网终结者大礼包 4800 积分、潮萌小黄鸭系列…)+ 精英专享(101) 18 件(幸运币/荣耀勋章礼包、套装…)。
- `sid=2203`(上一期活动)已停止下发商品,列表为空,需要把 SID 切到 `2277`。
- 限量返场是「预告上架」类商品:`start_time`(TAF tag25,实测 2026-10-16 17:00:00)与 `end_time`(tag26)决定
可兑换时间窗,页面上的「xx:xx:xx 后上架」倒计时就来自它;`daily_start/daily_end`(tag27/28)是每日可兑时段。
用法(界面「商品列表」):
- **全量列表**:SID 填 `2277` 点「获取商品」,日志里会打出「类目=精英专享(101)=18项/限量返场(99)=12项」。
- **只看某个类目**:在商品列表上方切换类目页签,或点某个类目标签过滤(也支持类目 id 或模糊名,如 `99` / `返场`)。
- **限定类目抢购**:选中类目并打开「自动选品」,当前类目会一起提交给监控,作为自动选品的范围。
界面「商品列表」按类目分了页签(全部 / 限量返场 / 精英专享,带商品数),每行标注类目,
未上架的商品显示上架时间与倒计时;当前选中的类目会一起提交给监控,用作自动选品的范围。