refactor(sniper): 对齐 dygou_go 调度架构,增加双时间日志并解耦预热

- 抢购调度重构:参考 dygou_go 将 sniper 拆分为独立子模块(runner/run_control/retry/exchange/classify/config/summary/category/helpers);移除全局自适应降速与轮次上限,采用账号独立循环、统一账号级退避、单请求并发闸门与首发错峰
- 预热与建连优化:默认关闭预热(避免单账号超时连坐整机开抢),开启时转为后台非阻塞协程;WebSocket 握手超时从 10s 降至 1500ms
- 双时间日志:logger 增加 SetServerOffset,校准后日志前缀自动渲染「本地时间(H虎牙估算时间)」,实现跨机器绝对毫秒对齐
- 默认配置调优:版本提升至 v0.5.0,默认线程调为 2(缓解多实例并发限流),前端自动平滑迁移
- 验证方式:go test ./... -count=1 全通,npm run build 构建正常,单测覆盖双时间与预热配置
This commit is contained in:
yml2213
2026-10-17 18:29:17 +08:00
parent 2451466bed
commit 6e5beac151
21 changed files with 2142 additions and 2078 deletions
+54 -29
View File
@@ -61,8 +61,12 @@ const configStorageBase = 'huya-go.gui.config.v2'
const cookieStorageBase = 'huya-go.gui.cookies.v1'
const prizeCacheStorageBase = 'huya-go.gui.prizes.v1'
const leadDefaultMigrationBase = 'huya-go.gui.lead-default.v1'
// 旧版本默认把提前量写成 2500ms,与现在的动态提前设计冲突;只迁移一次。
// 旧版本默认把提前量写成 2500ms、线程写成 5。轮次上限已从产品移除;
// 线程 5 在多开时会放大请求洪峰,旧默认只迁移一次。
const legacyDefaultLeadMs = 2500
const concurrencyDefaultMigrationBase = 'huya-go.gui.concurrency-pressure.v1'
const legacyDefaultConcurrency = 5
const defaultConcurrency = 2
// 界面本地存储保留的日志上限,与后端的增量拉取配合使用。
const maxClientLogs = 1000
@@ -102,8 +106,6 @@ type UiConfig = {
minLeft: number
cookieText: string
selectedAccountLines: number[]
// loops 是轮次上限:0(留空)表示按窗口运行到窗口结束。
loops: number
delayMs: number
concurrency: number
// windowSeconds 是抢购窗口秒数(窗口模式下跑到「开抢+窗口」才收工)。
@@ -122,10 +124,6 @@ type UiConfig = {
errorBackoffMs: number
timeoutSec: number
startAt: string
// adaptiveThrottle 是「全局自适应降速」开关(表单用正语义)。
adaptiveThrottle: boolean
// disableAdaptiveThrottle 只发给后端(后端零值=开启,所以用反语义字段),不落本地存储。
disableAdaptiveThrottle: boolean
}
type CookieCheckStatus = 'valid' | 'expired' | 'unknown' | 'invalid'
@@ -331,7 +329,7 @@ function exchangeOrderID(exchange?: ExchangeResp): string {
return exchange?.order_id || exchange?.orderId || ''
}
type SavedConfig = Omit<UiConfig, 'cookieText' | 'selectedAccountLines' | 'disableAdaptiveThrottle'>
type SavedConfig = Omit<UiConfig, 'cookieText' | 'selectedAccountLines'>
type ScheduleCountdown = {
targetAt: number
@@ -535,14 +533,47 @@ function readSavedConfig(): Partial<SavedConfig> {
try {
const configKey = scopedKey(configStorageBase)
const saved = JSON.parse(localStorage.getItem(configKey) || '{}') as Partial<SavedConfig>
const migrationKey = scopedKey(leadDefaultMigrationBase)
if (!localStorage.getItem(migrationKey)) {
let dirty = false
// v0.5.0 移除了轮次和全局降速器配置,顺手清掉本地旧字段。
const legacyFields = saved as Record<string, unknown>
for (const key of ['loops', 'adaptiveThrottle', 'disableAdaptiveThrottle']) {
if (key in legacyFields) {
delete legacyFields[key]
dirty = true
}
}
const leadMigrationKey = scopedKey(leadDefaultMigrationBase)
if (!localStorage.getItem(leadMigrationKey)) {
if (Number(saved.leadMs || 0) === legacyDefaultLeadMs) {
saved.leadMs = 0
// 迁移必须立即落盘,否则下一次 readSavedConfig 又会读回旧值。
localStorage.setItem(configKey, JSON.stringify(saved))
dirty = true
}
localStorage.setItem(migrationKey, '1')
localStorage.setItem(leadMigrationKey, '1')
}
const concurrencyMigrationKey = scopedKey(concurrencyDefaultMigrationBase)
if (!localStorage.getItem(concurrencyMigrationKey)) {
if (Number(saved.concurrency || 0) === legacyDefaultConcurrency) {
saved.concurrency = defaultConcurrency
dirty = true
}
localStorage.setItem(concurrencyMigrationKey, '1')
}
const prewarmMigrationKey = scopedKey('huya_gui_prewarm_default_migration_v050')
if (!localStorage.getItem(prewarmMigrationKey)) {
if (saved.prewarm !== false) {
saved.prewarm = false
dirty = true
}
localStorage.setItem(prewarmMigrationKey, '1')
}
if (dirty) {
// 迁移必须立即落盘,否则下一次 readSavedConfig 又会读回旧值。
localStorage.setItem(configKey, JSON.stringify(saved))
}
return saved
} catch {
@@ -656,13 +687,12 @@ function pickConfig(values: Partial<UiConfig>): UiConfig {
minLeft: Number(values.minLeft || 0),
cookieText: values.cookieText || '',
selectedAccountLines: values.selectedAccountLines || [],
loops: Number(values.loops || 0),
delayMs: Number(values.delayMs || 150),
concurrency: Number(values.concurrency || 1),
concurrency: Number(values.concurrency || defaultConcurrency),
windowSeconds: Number(values.windowSeconds || 30),
stopAfterSuccess: Boolean(values.stopAfterSuccess),
stopAccountAfterSuccess: values.stopAccountAfterSuccess !== false,
prewarm: values.prewarm !== false,
prewarm: Boolean(values.prewarm),
directExchange: values.directExchange !== false,
sharePrizeList: values.sharePrizeList !== false,
autoSelect: Boolean(values.autoSelect),
@@ -673,8 +703,6 @@ function pickConfig(values: Partial<UiConfig>): UiConfig {
errorBackoffMs: Number(values.errorBackoffMs || 300),
timeoutSec: Number(values.timeoutSec || 15),
startAt: values.startAt || '',
adaptiveThrottle: values.adaptiveThrottle !== false,
disableAdaptiveThrottle: values.adaptiveThrottle === false,
}
}
@@ -1340,7 +1368,6 @@ export default function Dashboard() {
targetText: values.targetText,
maxPrice: values.maxPrice,
minLeft: values.minLeft,
loops: values.loops,
delayMs: values.delayMs,
concurrency: values.concurrency,
windowSeconds: values.windowSeconds,
@@ -1357,7 +1384,6 @@ export default function Dashboard() {
errorBackoffMs: values.errorBackoffMs,
timeoutSec: values.timeoutSec,
startAt: values.startAt,
adaptiveThrottle: values.adaptiveThrottle,
}
localStorage.setItem(scopedKey(configStorageBase), JSON.stringify(saved))
message.success('已保存')
@@ -2267,16 +2293,13 @@ export default function Dashboard() {
<Form.Item name="minLeft" label="最少库存">
<InputNumber min={0} />
</Form.Item>
<Form.Item name="loops" label="次数上限" tooltip="留空或 0 = 按窗口运行到窗口结束;填了就作为轮次上限">
<InputNumber min={0} max={9999} placeholder="0=按窗口" />
</Form.Item>
<Form.Item name="windowSeconds" label="窗口秒" tooltip="窗口模式下跑到「开抢 + 窗口秒数」才收工;开抢后约 9 秒内库存就会见底,默认 30 秒留足库存回流余量">
<Form.Item name="windowSeconds" label="窗口秒" tooltip="跑到「开抢 + 窗口秒数」才收工;账号会在整个窗口内独立重试,直到成功、窗口结束或收到停止指令">
<InputNumber min={1} max={600} addonAfter="s" />
</Form.Item>
<Form.Item name="delayMs" label="延时">
<InputNumber min={10} max={60000} addonAfter="ms" />
</Form.Item>
<Form.Item name="concurrency" label="线程">
<Form.Item name="concurrency" label="线程" tooltip="同时在途请求上限,不是账号槽位;多开时同机总速率约为线程×实例数,限流严重时建议 1~2">
<InputNumber min={1} max={100} />
</Form.Item>
<Form.Item name="leadMs" label="提前" tooltip="留空或 0 = 按服务端时钟校准误差动态计算(通常几十毫秒);填了就用固定提前量">
@@ -2284,7 +2307,12 @@ export default function Dashboard() {
</Form.Item>
</div>
<div className="settings-switch-grid">
<Form.Item name="prewarm" label="预热" valuePropName="checked">
<Form.Item
name="prewarm"
label="预热"
valuePropName="checked"
tooltip="默认关闭。开启时在后台异步建连,绝不阻塞主发令枪;不预热时首发自动建连并配合提前量放行"
>
<Switch />
</Form.Item>
<Form.Item name="directExchange" label="直兑" valuePropName="checked">
@@ -2305,9 +2333,6 @@ export default function Dashboard() {
<Form.Item name="timeSync" label="时间校准" valuePropName="checked" tooltip="定时前用 pingSignal 校准服务端时间,按钟差对齐触发时刻">
<Switch />
</Form.Item>
<Form.Item name="adaptiveThrottle" label="自适应降速" valuePropName="checked" tooltip="连续十几个请求都拿不到业务响应时整体降速,缓解同机同 IP 的限流风暴">
<Switch />
</Form.Item>
</div>
</div>
</Modal>