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:
+54
-29
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user