From 2a1b1e642c2681be79184a2d4bed67f7f665607b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 2 Sep 2026 12:48:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=99=8E=E7=89=99=E7=B2=BE?= =?UTF-8?q?=E8=8B=B1=E5=AE=9D=E5=85=B8=E5=B7=A5=E4=BD=9C=E5=8F=B0=E4=B8=8E?= =?UTF-8?q?Cookie=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/huya/web_cookie_fields.py | 18 +++--- web/frontend/src/pages/HuyaElitePage.tsx | 80 +++++++++++++++++++++--- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/core/huya/web_cookie_fields.py b/core/huya/web_cookie_fields.py index c8fdae3..7aa30e2 100644 --- a/core/huya/web_cookie_fields.py +++ b/core/huya/web_cookie_fields.py @@ -75,13 +75,13 @@ def _yamid_new_generate32(rng=None) -> str: _generate_bits(c, 0, 31), _generate_bits(c, 32, 47), _generate_bits(c, 48, 59) + "1", - _generate_bits(r.randrange(4096), 0, 7), - _generate_bits(r.randrange(4096), 0, 7), - _generate_bits(r.randrange(8192), 0, 7) - + _generate_bits(r.randrange(8192), 8, 15) - + _generate_bits(r.randrange(8192), 0, 7) - + _generate_bits(r.randrange(8192), 8, 15) - + _generate_bits(r.randrange(8192), 0, 15), + _generate_bits(r.randrange(4095), 0, 7), + _generate_bits(r.randrange(4095), 0, 7), + _generate_bits(r.randrange(8191), 0, 7) + + _generate_bits(r.randrange(8191), 8, 15) + + _generate_bits(r.randrange(8191), 0, 7) + + _generate_bits(r.randrange(8191), 8, 15) + + _generate_bits(r.randrange(8191), 0, 15), )) @@ -131,7 +131,7 @@ def fill_web_cookie_fields( pairs[key] = value.strip() def add(key: str, value: str): - if value and key not in pairs: + if value and (key not in pairs or not pairs[key]): pairs[key] = value uid = int(uid or 0) @@ -209,4 +209,4 @@ __all__ = [ "cookie_fields", "fill_web_cookie_fields", "missing_web_cookie_fields", -] \ No newline at end of file +] diff --git a/web/frontend/src/pages/HuyaElitePage.tsx b/web/frontend/src/pages/HuyaElitePage.tsx index 2e2e1ab..78ab8fb 100644 --- a/web/frontend/src/pages/HuyaElitePage.tsx +++ b/web/frontend/src/pages/HuyaElitePage.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import type { MouseEvent } from 'react'; import { Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography, } from 'antd'; @@ -13,6 +14,7 @@ import { usePermissions } from '../hooks/usePermissions'; import { useWebSocketLogs } from '../hooks/useWebSocketLogs'; import { getErrorMessage } from '../utils/error'; import { message } from '../utils/antdMessage'; +import { formatTime } from '../utils/time'; const { Text } = Typography; const SCOPE = 'elite' as const; @@ -35,6 +37,16 @@ const TASK_STATUS_LABELS: Record = { failed: '失败', error: '异常', stopped: '已停止', timeout: '支付超时', }; +const HUYA_QUICK_ACTIONS = [ + { key: 'get_bind_qr', icon: }, + { key: 'query_game_name', icon: }, + { key: 'confirm_bind', icon: }, + { key: 'query_points', icon: }, + { key: 'query_act_tasks', icon: }, + { key: 'refresh_goods', icon: }, + { key: 'query_exchange_records', icon: }, +]; + function savedInterval(): number { const value = Number(localStorage.getItem('huya_elite_refresh_interval')); return Number.isInteger(value) && value >= 5 && value <= 60 ? value : 15; @@ -49,6 +61,11 @@ function latestTask(tasks: HuyaTaskItem[], accountId: number): HuyaTaskItem | un return tasks.filter((task) => task.account_id === accountId).sort((a, b) => b.id - a.id)[0]; } +function taskQrUrl(task: HuyaTaskItem | undefined): string { + if (!task) return ''; + return resultText(task, 'bind_redirect_url') || resultText(task, 'pay_url') || resultText(task, 'url'); +} + export default function HuyaElitePage() { const { can } = usePermissions(); const canConfig = can('huya:config'); @@ -78,6 +95,7 @@ export default function HuyaElitePage() { const [configOpen, setConfigOpen] = useState(false); const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); + const [contextMenu, setContextMenu] = useState<{ x: number; y: number; accountIds: number[] } | null>(null); const tasksLoading = useRef(false); const logs = useWebSocketLogs(); @@ -115,6 +133,21 @@ export default function HuyaElitePage() { const timer = window.setInterval(() => { void loadTasks(); }, (active ? 3 : refreshInterval) * 1000); return () => window.clearInterval(timer); }, [loadTasks, refreshInterval, tasks]); + useEffect(() => { + if (!contextMenu) return undefined; + const close = () => setContextMenu(null); + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') close(); + }; + window.addEventListener('click', close); + window.addEventListener('resize', close); + window.addEventListener('keydown', onKeyDown); + return () => { + window.removeEventListener('click', close); + window.removeEventListener('resize', close); + window.removeEventListener('keydown', onKeyDown); + }; + }, [contextMenu]); const saveAccounts = async (ids: number[]) => { const saved = await huyaApi.updateWorkbenchAccounts(ids, SCOPE); @@ -124,8 +157,8 @@ export default function HuyaElitePage() { setSelectedIds((prev) => prev.filter((id) => saved.account_ids.includes(id))); }; - const startTask = async (taskType: string) => { - if (!selectedIds.length) { message.warning('请先勾选账号'); return; } + const startTask = async (taskType: string, accountIds: number[] = selectedIds) => { + if (!accountIds.length) { message.warning('请先勾选账号'); return; } if (taskType === 'exchange_goods' && !selectedGoodsId) { message.warning('请先选择兑换商品'); return; } if (taskType === 'create_recharge_order' && !selectedRechargeSpu) { message.warning('请先选择宝典商品'); return; } setStarting(true); @@ -137,7 +170,7 @@ export default function HuyaElitePage() { const selected = rechargeGoods.find((item) => item.spu_id === selectedRechargeSpu); return { spu_id: selectedRechargeSpu, sku_id: Number(selected?.sku_id || 0), product_name: selected?.name || '精英宝典' }; })() : {}; - const created = await huyaApi.createTasks({ account_ids: selectedIds, task_type: taskType, handbook_scope: SCOPE, concurrency, payload }); + const created = await huyaApi.createTasks({ account_ids: accountIds, task_type: taskType, handbook_scope: SCOPE, concurrency, payload }); setActiveBatches((prev) => [...new Set([...prev, created.batch_id])]); logs.connectBatch(created.batch_id, `/api/huya/ws/${created.batch_id}`, { clear: false, @@ -177,6 +210,23 @@ export default function HuyaElitePage() { catch (error) { message.error(getErrorMessage(error)); } }; + const handleRowContextMenu = (row: HuyaAccountItem, event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const accountIds = selectedIds.includes(row.id) && selectedIds.length > 1 + ? selectedIds + : [row.id]; + setSelectedIds(accountIds); + setContextMenu({ x: event.clientX, y: event.clientY, accountIds }); + }; + + const runContextAction = (taskType: string) => { + if (!contextMenu) return; + const ids = contextMenu.accountIds; + setContextMenu(null); + void startTask(taskType, ids); + }; + const openQrTask = async (task: HuyaTaskItem) => { setQrTask(task); try { setQrTask(await huyaApi.getTask(task.id)); } catch { /* 使用列表结果 */ } @@ -192,10 +242,11 @@ export default function HuyaElitePage() { const accountColumns: TableProps['columns'] = [ { title: '#', width: 45, render: (_value, _row, index) => {index + 1} }, - { title: '账号', width: 190, render: (_value, row) => {row.nickname || row.username || row.uid}UID {row.uid || '-'} }, - { title: '游戏名', width: 180, render: (_value, row) => row.game_name || row.game_channel || 未查询 }, - { title: '积分', dataIndex: 'points', width: 80, render: (value: number | null) => value ?? - }, - { title: '最近操作', ellipsis: true, render: (_value, row) => { const task = latestTask(tasks, row.id); return task ? {TASK_LABELS[task.task_type] || task.task_type}{task.message || '-'} : 暂无; } }, + { title: '账号', width: 180, render: (_value, row) => {row.nickname || row.username || row.uid}UID {row.uid || '-'} }, + { title: '游戏名', width: 160, render: (_value, row) => {row.game_name || 未查询}{row.game_channel && {row.game_channel}} }, + { title: '积分', dataIndex: 'points', width: 75, align: 'right', render: (value: number | null) => value ?? - }, + { title: '最近操作', width: 250, render: (_value, row) => { const task = latestTask(tasks, row.id); return task ? {TASK_STATUS_LABELS[task.status] || task.status}{TASK_LABELS[task.task_type] || task.task_type}{task.message || '-'}{formatTime(task.finished_at || task.created_at)} : 暂无操作; } }, + { title: '二维码', width: 80, align: 'center', render: (_value, row) => { const task = latestTask(tasks, row.id); const url = taskQrUrl(task); const image = resultText(task, 'mini_qrcode_image'); if (!url && !image) return -; return
{ event.stopPropagation(); if (task) void openQrTask(task); }} style={{ cursor: 'pointer', display: 'inline-block', lineHeight: 0 }} title="点击查看二维码">{image ? 二维码 : }
; } }, { title: '状态', width: 90, render: (_value, row) => { const task = latestTask(tasks, row.id); const status = task?.status || row.status || ''; return {TASK_STATUS_LABELS[status] || status || '待操作'}; } }, ]; @@ -211,14 +262,23 @@ export default function HuyaElitePage() { ); - const accountsBody = <>} placeholder="搜索账号/昵称/游戏名" value={search} onChange={(event) => setSearch(event.target.value)} style={{ width: 240 }} />{selectedIds.length > 0 && }
setSelectedIds(keys.map(Number)) }} columns={accountColumns} dataSource={filteredAccounts} pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 760, y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (task && ['get_bind_qr', 'create_recharge_order'].includes(task.task_type) && task.result) void openQrTask(task); } })} />; + const accountsBody = <>} placeholder="搜索账号/昵称/游戏名" value={search} onChange={(event) => setSearch(event.target.value)} style={{ width: 240 }} />{selectedIds.length > 0 && }
setSelectedIds(keys.map(Number)) }} columns={accountColumns} dataSource={filteredAccounts} locale={{ emptyText: '工作台暂无账号,点击右上角「导入账号」添加' }} pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 1040, y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (task && ['get_bind_qr', 'create_recharge_order'].includes(task.task_type) && task.result) void openQrTask(task); }, onContextMenu: (event) => handleRowContextMenu(row, event), style: { cursor: 'context-menu' }, title: '右键打开账号动作' })} />; const configField = (key: Exclude, label: string) => configDraft && {label} setConfigDraft({ ...configDraft, [key]: event.target.value })} style={{ width: 300 }} />; + const contextMenuStyle = contextMenu ? { + position: 'fixed' as const, + zIndex: 1000, + left: Math.max(8, Math.min(contextMenu.x, window.innerWidth - 228)), + top: Math.max(8, Math.min(contextMenu.y, window.innerHeight - 330)), + } : undefined; - return
+ const tableStyle = `.huya-operation-section{border:1px solid rgba(128,128,128,.22);border-radius:6px;padding:8px}.huya-operation-title{display:flex;gap:6px;align-items:center;font-weight:600;margin-bottom:8px}.huya-action-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.huya-account-table{flex:1;min-height:0;overflow:hidden}.huya-account-table .ant-table-wrapper,.huya-account-table .ant-spin-nested-loading,.huya-account-table .ant-spin-container{height:100%}.huya-account-table .ant-table{height:100%;display:flex;flex-direction:column}.huya-account-table .ant-table-container{flex:1;min-height:0;display:flex;flex-direction:column}.huya-account-table .ant-table-header{flex-shrink:0}.huya-account-table .ant-table-body{flex:1;min-height:0;overflow:auto !important}.huya-account-table .ant-table-thead > tr > th,.huya-account-table .ant-table-tbody > tr > td{padding:4px 8px;line-height:20px}.huya-account-table .ant-typography{font-size:12px}.huya-account-table .ant-tag{font-size:11px;line-height:18px;margin-inline-end:0;padding-inline:5px}.huya-account-table .ant-pagination{flex-shrink:0;margin:0;padding:6px 4px 2px;border-top:1px solid rgba(128,128,128,.22)}`; + + return

虎牙精英宝典工作台

绑定、开通、积分与兑换任务
{canConfig && }{!!activeBatches.length && }
{layoutMode === 'split' ?
已选 {selectedIds.length}/{accounts.length}} style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}并发 setConcurrency(value || 1)} style={{ width: 58 }} />} style={{ width: 360, flexShrink: 0, minHeight: 0, overflow: 'hidden' }} styles={{ body: { padding: 8, overflowY: 'auto', height: 'calc(100% - 38px)' } }}>{operationBody}
: <>并发 setConcurrency(value || 1)} style={{ width: 58 }} />} style={{ flexShrink: 0, marginBottom: 12 }} styles={{ body: { padding: 8 } }}>{operationBody}已选 {selectedIds.length}/{accounts.length}} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}} - 兑换任务 {exchangeCount} 条;勾选账号后可批量执行。 + 兑换任务 {exchangeCount} 条;勾选账号后可批量执行,账号行支持右键操作。 + {contextMenu &&
event.stopPropagation()}> 1 ? '批量动作' : '账号动作'} styles={{ body: { padding: 4 } }} style={{ width: 220, boxShadow: '0 4px 12px rgba(0,0,0,0.14)' }}>{HUYA_QUICK_ACTIONS.map((item) => )}
} setQrTask(null)} centered>{qrTask &&
{resultText(qrTask, 'mini_qrcode_image') ? 绑定二维码 : }

{qrTask.message || qrTask.status}

{qrTask.task_type === 'create_recharge_order' && 扫码后订单状态会自动轮询更新}
}
setImportOpen(false)} onOk={() => { void saveAccounts([...new Set([...accounts.map((item) => item.id), ...importSelected])]).then(() => { setImportSelected([]); setImportOpen(false); }).catch((error) => message.error(getErrorMessage(error))); }} okText="导入" cancelText="取消"> setImportSearch(event.target.value)} style={{ marginBottom: 8 }} />
!accounts.some((current) => current.id === item.id) && (!importSearch || [item.uid, item.nickname, item.username].some((value) => String(value || '').includes(importSearch))) && (!importTag || item.tag === importTag))} columns={[{ title: '账号', render: (_value, row) => row.nickname || row.username || row.uid }, { title: '标签', dataIndex: 'tag' }]} pagination={{ pageSize: 8 }} rowSelection={{ selectedRowKeys: importSelected, onChange: (keys) => setImportSelected(keys.map(Number)) }} /> setConfigOpen(false)} onOk={() => void saveConfig()} confirmLoading={configSaving} okText="保存" cancelText="取消" width={500}>{configLoading ?
加载中...
: configDraft && {configField('sid', '活动 SID')}{configField('bind_act_id', '绑定活动 ID')}{configField('outer_act_id', '外部活动 ID')}{configField('room_pid', '直播间 PID')}{configField('pay_channel', '支付渠道')}空闲轮询间隔(秒) { const next = Math.min(60, Math.max(5, value || 15)); setRefreshInterval(next); localStorage.setItem('huya_elite_refresh_interval', String(next)); }} style={{ width: 300 }} />}