# 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__<时间戳>_<随机>` 该活动的列表/积分/兑换接口与 `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` / `返场`)。 - **限定类目抢购**:选中类目并打开「自动选品」,当前类目会一起提交给监控,作为自动选品的范围。 界面「商品列表」按类目分了页签(全部 / 限量返场 / 精英专享,带商品数),每行标注类目, 未上架的商品显示上架时间与倒计时;当前选中的类目会一起提交给监控,用作自动选品的范围。