diff --git a/.gitignore b/.gitignore index 4bd2770..7df8257 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /frontend/node_modules/ *.test build/ -frontend/dist/ \ No newline at end of file +frontend/dist/ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 849ece8..116295c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # huya-go -虎牙活动积分兑换/抢购的 Go 复刻版。当前支持两种传输层: +虎牙活动积分兑换/抢购的 Go 复刻版。支持两种传输层,CLI 与 GUI 默认都走 `http`: -- `http`:走 `https://cdnws.api.huya.com/`,兼容原有命令。 -- `wss`:实验实现。已按浏览器 HAR 改为 TAF `WebSocketCommand` 外层和 WSS 初始化流程,仍建议先用真实 Cookie 小流量验证。 +- `http`(默认):走 `https://cdnws.api.huya.com/`,兼容原有命令。 +- `wss`:实验实现。CLI 需要显式指定 `-transport wss`,GUI 在设置里切换;已按浏览器 HAR 改为 TAF `WebSocketCommand` 外层和 WSS 初始化流程,仍建议先用真实 Cookie 小流量验证。 ## 构建 @@ -77,6 +77,8 @@ CLI 示例: - `-cmd`:`list`、`score`、`records`、`exchange`、`monitor`。 - `-sid`:活动 SID,必填。 - `-pid`:奖品 PID。`exchange` 必填,`monitor` 可用 `-pid` 或 `-name`。 +- `-max-price`:目标奖品最高积分,`0` 表示不限制(CLI 与 GUI 设置都支持)。 +- `-min-left`:目标奖品最少剩余库存,`0` 表示不限制(CLI 与 GUI 设置都支持)。 - `-cookie`:单账号 Cookie。 - `-cookie-file`:多账号 Cookie 文件。 - `-transport`:`http` 或 `wss`,默认 `http`;WSS 仍为实验通道。 diff --git a/cmd/huya-go/main.go b/cmd/huya-go/main.go index b0f0863..4f78fad 100644 --- a/cmd/huya-go/main.go +++ b/cmd/huya-go/main.go @@ -27,6 +27,8 @@ func main() { pids = flag.String("pids", "", "多个奖品 PID,逗号分隔") name = flag.String("name", "", "按奖品名包含匹配") names = flag.String("names", "", "多个奖品名,逗号分隔") + maxPrice = flag.Int64("max-price", 0, "目标奖品最高积分,0 表示不限制") + minLeft = flag.Int64("min-left", 0, "目标奖品最少剩余库存,0 表示不限制") autoSelect = flag.Bool("auto", false, "monitor 自动选择列表里的可兑奖品") loops = flag.Int("loops", 1, "monitor 循环次数") delayMS = flag.Int("delay", 150, "monitor 每轮间隔毫秒") @@ -126,7 +128,7 @@ func main() { waitForCommand(ctx, startAt) runExchange(ctx, client, accounts, int32(*sid), int32(*pid), *concurrency) case "monitor": - targets := parseTargets(*pid, *pids, *name, *names, *autoSelect) + targets := parseTargets(*pid, *pids, *name, *names, *autoSelect, *maxPrice, *minLeft) if len(targets) == 0 { exitErr(errors.New("monitor 必须提供 -pid 或 -name")) } @@ -237,7 +239,7 @@ func loadCookies(cookie, file string) ([]string, error) { return out, nil } -func parseTargets(pid int, pids, name, names string, autoSelect bool) []sniper.Target { +func parseTargets(pid int, pids, name, names string, autoSelect bool, maxPrice, minLeft int64) []sniper.Target { targets := make([]sniper.Target, 0) name = strings.TrimSpace(name) if pid > 0 || name != "" { @@ -259,6 +261,11 @@ func parseTargets(pid int, pids, name, names string, autoSelect bool) []sniper.T } targets = append(targets, sniper.Target{Name: text}) } + // 价格/库存过滤对所有目标统一生效。 + for i := range targets { + targets[i].MaxPrice = maxPrice + targets[i].MinLeft = minLeft + } return targets } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b0c3e87..ba9fd51 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,9 +31,11 @@ import { Square, Settings, Trash2, + Zap, } from 'lucide-react' import { DefaultConfig, + Exchange, GetLogs, GetRecords, IsRunning, @@ -56,6 +58,8 @@ type UiConfig = { pid: number name: string targetText: string + maxPrice: number + minLeft: number cookieText: string selectedAccountLines: number[] loops: number @@ -167,6 +171,14 @@ type ExchangeResp = { actPreCondition?: ActPreCondition } +type ExchangeResult = { + uid: number + resp?: ExchangeResp + category?: string + message?: string + error?: string +} + type MonitorResult = { key?: string loop?: number @@ -300,6 +312,7 @@ function categoryTag(category?: string): ReactNode { already_done: { color: 'purple', label: '已兑' }, off_shelf: { color: 'error', label: '下架' }, sold_out: { color: 'error', label: '抢完' }, + condition: { color: 'geekblue', label: '条件' }, failed: { color: 'warning', label: '失败' }, error: { color: 'error', label: '异常' }, } @@ -399,12 +412,14 @@ function newCookieID(): string { function pickConfig(values: Partial): UiConfig { return { - transport: values.transport || 'wss', + transport: values.transport || 'http', wssHost: values.wssHost || '', sid: Number(values.sid || 0), pid: Number(values.pid || 0), name: values.name || '', targetText: values.targetText || '', + maxPrice: Number(values.maxPrice || 0), + minLeft: Number(values.minLeft || 0), cookieText: values.cookieText || '', selectedAccountLines: values.selectedAccountLines || [], loops: Number(values.loops || 1), @@ -596,7 +611,9 @@ export default function Dashboard() { const errorCount = useMemo( () => results.filter((item) => - ['error', 'failed', 'not_enough', 'off_shelf', 'sold_out'].includes(item.category || ''), + ['error', 'failed', 'not_enough', 'off_shelf', 'sold_out', 'condition'].includes( + item.category || '', + ), ).length, [results], ) @@ -827,6 +844,8 @@ export default function Dashboard() { pid: values.pid, name: values.name, targetText: values.targetText, + maxPrice: values.maxPrice, + minLeft: values.minLeft, loops: values.loops, delayMs: values.delayMs, concurrency: values.concurrency, @@ -881,6 +900,33 @@ export default function Dashboard() { }) } + const directExchange = () => { + const values = config() + if (Number(values.pid || 0) <= 0) { + message.warning('请先在左侧商品列表中选择要兑换的商品') + return undefined + } + return runAction( + 'exchange', + () => Exchange(values), + (data) => { + const items = (data || []) as ExchangeResult[] + setResults( + items.map((item, index) => ({ + key: `exchange-${item.uid}-${index}-${Date.now()}`, + uid: item.uid, + action: 'exchange', + category: item.category || (item.error ? 'error' : 'failed'), + message: item.message || item.error || item.resp?.msg || '', + exchange: item.resp, + })), + ) + const success = items.filter((item) => item.category === 'success').length + message.info(`直接兑换:成功 ${success}/${items.length}`) + }, + ) + } + const selectAllAccounts = () => { setSelectedAccountLines(validAccounts.map((item) => item.line)) } @@ -1180,7 +1226,7 @@ export default function Dashboard() { - {String(watchedTransport || 'wss').toUpperCase()} + {String(watchedTransport || 'http').toUpperCase()} @@ -1225,6 +1271,14 @@ export default function Dashboard() { > 开启定时 +