diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1465170..8a7225a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -29,6 +29,7 @@ import { Plus, RefreshCw, Save, + ShieldCheck, ShoppingCart, Square, Settings, @@ -36,6 +37,7 @@ import { Zap, } from 'lucide-react' import { + CheckCookies, DefaultConfig, Exchange, GetInstanceInfo, @@ -58,6 +60,9 @@ import { ClipboardGetText, ClipboardSetText, EventsOn } from '../wailsjs/runtime 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,与现在的动态提前设计冲突;只迁移一次。 +const legacyDefaultLeadMs = 2500 // 界面本地存储保留的日志上限,与后端的增量拉取配合使用。 const maxClientLogs = 1000 @@ -123,11 +128,26 @@ type UiConfig = { disableAdaptiveThrottle: boolean } +type CookieCheckStatus = 'valid' | 'expired' | 'unknown' | 'invalid' + +type CookieCheckResult = { + line: number + uid: number + status: CookieCheckStatus + message?: string + statusCode?: number + checkedAt: number +} + type AccountPreview = { line: number uid: number valid: boolean error?: string + // checkStatus 是服务端登录态检查结果;valid 仅表示本地能解析出 uid。 + checkStatus?: CookieCheckStatus + checkMessage?: string + checkedAt?: number } type CookieEntry = { @@ -136,6 +156,9 @@ type CookieEntry = { uid: number valid: boolean error?: string + checkStatus?: CookieCheckStatus + checkMessage?: string + checkedAt?: number updatedAt: number } @@ -483,9 +506,45 @@ function categoryTag(category?: string): ReactNode { return {item.label} } +function normalizeCookieCheckStatus(value: unknown): CookieCheckStatus | undefined { + const status = String(value || '').trim() + if (status === 'valid' || status === 'expired' || status === 'unknown' || status === 'invalid') { + return status + } + return undefined +} + +function isCookieCheckUnusable(status?: CookieCheckStatus): boolean { + return status === 'expired' || status === 'unknown' || status === 'invalid' +} + +function cookieCheckTag(status?: CookieCheckStatus, message?: string): ReactNode { + const config: Record = { + valid: { color: 'success', label: '可用' }, + expired: { color: 'error', label: '失效' }, + unknown: { color: 'error', label: '失效' }, + invalid: { color: 'error', label: '无效' }, + unchecked: { color: 'default', label: '未检查' }, + } + const item = config[status || 'unchecked'] + const tag = {item.label} + return message ? {tag} : tag +} + function readSavedConfig(): Partial { try { - return JSON.parse(localStorage.getItem(scopedKey(configStorageBase)) || '{}') as Partial + const configKey = scopedKey(configStorageBase) + const saved = JSON.parse(localStorage.getItem(configKey) || '{}') as Partial + const migrationKey = scopedKey(leadDefaultMigrationBase) + if (!localStorage.getItem(migrationKey)) { + if (Number(saved.leadMs || 0) === legacyDefaultLeadMs) { + saved.leadMs = 0 + // 迁移必须立即落盘,否则下一次 readSavedConfig 又会读回旧值。 + localStorage.setItem(configKey, JSON.stringify(saved)) + } + localStorage.setItem(migrationKey, '1') + } + return saved } catch { return {} } @@ -505,6 +564,9 @@ function readSavedCookies(): CookieEntry[] { uid: Number(item.uid || 0), valid: Boolean(item.valid), error: item.error, + checkStatus: normalizeCookieCheckStatus(item.checkStatus), + checkMessage: String(item.checkMessage || '').trim() || undefined, + checkedAt: Number(item.checkedAt || 0) || undefined, updatedAt: Number(item.updatedAt || Date.now()), })) .filter((item) => item.text) @@ -706,11 +768,23 @@ export default function Dashboard() { } }) setCookieEntries(enriched) - setAccounts(previews || []) - setSelectedAccountLines((previews || []).filter((item) => item.valid).map((item) => item.line)) + const enrichedPreviews = (previews || []).map((preview) => { + const entry = enriched[preview.line - 1] + return { + ...preview, + checkStatus: entry?.checkStatus, + checkMessage: entry?.checkMessage, + checkedAt: entry?.checkedAt, + } + }) + setAccounts(enrichedPreviews) + const selectableLines = enrichedPreviews + .filter((item) => item.valid && !isCookieCheckUnusable(item.checkStatus)) + .map((item) => item.line) + setSelectedAccountLines(selectableLines) if (persist) writeSavedCookies(enriched) - if (refreshLatest && enriched.some((item) => item.valid)) { - window.setTimeout(() => queryPrizes(true), 0) + if (refreshLatest && selectableLines.length > 0) { + window.setTimeout(() => queryPrizes(true, selectableLines), 0) } return enriched } @@ -798,7 +872,22 @@ export default function Dashboard() { writeSavedConfigPatch({ startAt: String(form.getFieldValue('startAt') || '') }) }, [configLoaded, form, watchedStartAt]) - const validAccounts = useMemo(() => accounts.filter((item) => item.valid), [accounts]) + const validAccounts = useMemo( + () => accounts.filter((item) => item.valid && !isCookieCheckUnusable(item.checkStatus)), + [accounts], + ) + const cookieCheckStats = useMemo(() => { + const stats = { valid: 0, expired: 0, unknown: 0, invalid: 0, unchecked: 0 } + cookieEntries.forEach((entry) => { + if (entry.checkStatus === 'valid') stats.valid += 1 + else if (entry.checkStatus === 'expired') stats.expired += 1 + else if (entry.checkStatus === 'unknown') stats.unknown += 1 + else if (entry.checkStatus === 'invalid') stats.invalid += 1 + else stats.unchecked += 1 + }) + return stats + }, [cookieEntries]) + const failedCookieCount = cookieCheckStats.expired + cookieCheckStats.unknown + cookieCheckStats.invalid const importedCookieCount = useMemo( () => normalizeCookieText(watchedCookieText || '').split('\n').filter(Boolean).length, [watchedCookieText], @@ -918,10 +1007,10 @@ export default function Dashboard() { [logs, logFilter], ) - const config = () => ({ + const config = (selectedLines: Key[] = selectedAccountLines) => ({ ...pickConfig(form.getFieldsValue(true)), category: activeCategory, - selectedAccountLines: selectedAccountLines.map(Number).filter((line) => line > 0), + selectedAccountLines: selectedLines.map(Number).filter((line) => line > 0), }) const runAction = async ( @@ -1021,7 +1110,16 @@ export default function Dashboard() { if (editingCookie) { const text = normalizedText.split('\n')[0] const nextEntries = cookieEntries.map((entry) => - entry.id === editingCookie.id ? { ...entry, text, updatedAt: Date.now() } : entry, + entry.id === editingCookie.id + ? { + ...entry, + text, + checkStatus: undefined, + checkMessage: undefined, + checkedAt: undefined, + updatedAt: Date.now(), + } + : entry, ) await applyCookieEntries(nextEntries, true, true) message.success('CK 已更新') @@ -1064,13 +1162,110 @@ export default function Dashboard() { message.success('已重新载入本地 CK') } - function queryPrizes(quiet = false) { + // 检查走服务端 getUserScore;失效结果连续确认 3 次,避免偶发「账号未绑定」误判。 + const checkCookies = async () => { + if (running) { + message.warning('抢购运行中,请先停止再检查 CK') + return + } + if (cookieEntries.length === 0) { + message.warning('没有可检查的 CK') + return + } + setLoading('cookie-check') + try { + const snapshot = cookieEntries + const results = (await CheckCookies(config())) as CookieCheckResult[] + const byLine = new Map() + const byUID = new Map() + results.forEach((item) => { + byLine.set(Number(item.line || 0), item) + if (Number(item.uid || 0) > 0) byUID.set(Number(item.uid), item) + }) + + const nextEntries = snapshot.map((entry, index) => { + const result = byLine.get(index + 1) || (entry.uid > 0 ? byUID.get(entry.uid) : undefined) + if (!result) return entry + return { + ...entry, + uid: Number(result.uid || entry.uid || 0), + checkStatus: normalizeCookieCheckStatus(result.status), + checkMessage: result.status === 'valid' ? '' : String(result.message || '').trim(), + checkedAt: Number(result.checkedAt || Date.now()), + } + }) + + const stats = { valid: 0, expired: 0, unknown: 0, invalid: 0 } + nextEntries.forEach((entry) => { + if (entry.checkStatus === 'valid') stats.valid += 1 + else if (entry.checkStatus === 'expired') stats.expired += 1 + else if (entry.checkStatus === 'unknown') stats.unknown += 1 + else if (entry.checkStatus === 'invalid') stats.invalid += 1 + }) + const failed = stats.expired + stats.unknown + stats.invalid + + setCookieEntries(nextEntries) + writeSavedCookies(nextEntries) + setAccounts((prev) => + prev.map((account) => { + const result = byLine.get(account.line) || (account.uid > 0 ? byUID.get(account.uid) : undefined) + if (!result) return account + return { + ...account, + checkStatus: normalizeCookieCheckStatus(result.status), + checkMessage: result.status === 'valid' ? '' : String(result.message || '').trim(), + checkedAt: Number(result.checkedAt || Date.now()), + } + }), + ) + + const failedLines = new Set( + nextEntries + .map((entry, index) => (isCookieCheckUnusable(entry.checkStatus) ? index + 1 : 0)) + .filter((line) => line > 0), + ) + setSelectedAccountLines((prev) => prev.filter((line) => !failedLines.has(Number(line)))) + + const summary = `检查完成:可用 ${stats.valid},失效 ${failed}` + if (failed > 0) { + message.warning(summary) + } else { + message.success(summary) + } + } catch (err: unknown) { + message.error(`检查 CK 失败: ${String(err)}`) + } finally { + setLoading('') + } + } + + const clearExpiredCookies = () => { + if (failedCookieCount === 0) return + modal.confirm({ + title: '清理失效 CK', + content: `确认删除 ${failedCookieCount} 条失效 CK?未检查的 CK 会保留。`, + okText: '清理', + cancelText: '取消', + okButtonProps: { danger: true }, + onOk: async () => { + const remaining = cookieEntries.filter((entry) => !isCookieCheckUnusable(entry.checkStatus)) + await applyCookieEntries(remaining, true, false) + message.success(`已清理 ${failedCookieCount} 条失效 CK`) + }, + }) + } + + function queryPrizes(quiet = false, selectedLines: Key[] = selectedAccountLines) { if (!String(form.getFieldValue('cookieText') || '').trim()) { if (!quiet) message.warning('Cookie 不能为空') return } + if (selectedLines.length === 0) { + if (!quiet) message.warning('请先选择账号') + return + } setLoading('list') - ListPrizes(config()) + ListPrizes(config(selectedLines)) .then((data) => { const result = data as { prizes?: PrizeInfo[] @@ -1187,6 +1382,10 @@ export default function Dashboard() { const startMonitor = (scheduled: boolean) => { const values = config() + if (values.selectedAccountLines.length === 0) { + message.warning('请先选择账号') + return undefined + } const countdown = scheduled ? buildScheduleCountdown(values.startAt, values.leadMs, now) : null if (scheduled && !countdown) { message.warning('请先填写定时目标时间(HH:mm:ss)') @@ -1205,6 +1404,10 @@ export default function Dashboard() { const directExchange = () => { const values = config() + if (values.selectedAccountLines.length === 0) { + message.warning('请先选择账号') + return undefined + } if (Number(values.pid || 0) <= 0) { message.warning('请先在左侧商品列表中选择要兑换的商品') return undefined @@ -1262,14 +1465,29 @@ export default function Dashboard() { render: (_, row) => (
{row.uid || `第${row.line}行`} -
{row.valid ? '已加入任务' : row.error || '未选择'}
+
+ {isCookieCheckUnusable(row.checkStatus) + ? row.checkMessage || 'CK 已失效' + : row.valid + ? row.checkStatus === 'valid' + ? 'CK 可用' + : '已加入任务' + : row.error || '未选择'} +
), }, { title: '凭据', - width: 90, - render: (_, row) => (row.valid ? 已加载 : 无效), + width: 110, + render: (_, row) => + row.checkStatus ? ( + cookieCheckTag(row.checkStatus, row.checkMessage) + ) : row.valid ? ( + 已加载 + ) : ( + 无效 + ), }, { title: '商品', @@ -1368,18 +1586,37 @@ export default function Dashboard() { () => [ { title: '账号', - width: 150, + width: 140, render: (_, row) => (
{row.uid || '-'} -
{row.valid ? '有效 CK' : row.error || '待校验'}
+
+ {isCookieCheckUnusable(row.checkStatus) ? 'CK 已失效' : row.valid ? '可解析 UID' : row.error || '待校验'} +
), }, { - title: '状态', - width: 86, - render: (_, row) => (row.valid ? 有效 : 无效), + title: '格式', + width: 78, + render: (_, row) => (row.valid ? 有效 : 无效), + }, + { + title: 'CK 状态', + width: 190, + render: (_, row) => ( +
+ {cookieCheckTag(row.checkStatus, row.checkMessage)} + {row.checkStatus && row.checkStatus !== 'valid' && row.checkMessage ? ( +
+ {row.checkMessage} +
+ ) : null} + {row.checkedAt ? ( +
检查于 {new Date(row.checkedAt).toLocaleString('zh-CN', { hour12: false })}
+ ) : null} +
+ ), }, { title: 'CK', @@ -1595,7 +1832,7 @@ export default function Dashboard() { block icon={} loading={loading === 'list'} - disabled={validAccounts.length === 0} + disabled={selectedAccountCount === 0} onClick={() => queryPrizes()} > 用 CK 刷新商品 @@ -1719,7 +1956,7 @@ export default function Dashboard() { type="primary" icon={} loading={loading === 'monitor'} - disabled={running} + disabled={running || selectedAccountCount === 0} onClick={() => startMonitor(false)} > 立即抢购 @@ -1729,7 +1966,7 @@ export default function Dashboard() { type="primary" icon={} loading={loading === 'monitor'} - disabled={running} + disabled={running || selectedAccountCount === 0} onClick={() => startMonitor(true)} > 开启定时 @@ -1737,7 +1974,7 @@ export default function Dashboard() { + @@ -1815,7 +2072,9 @@ export default function Dashboard() { rowSelection={{ selectedRowKeys: selectedAccountLines, onChange: setSelectedAccountLines, - getCheckboxProps: (row) => ({ disabled: !row.valid }), + getCheckboxProps: (row) => ({ + disabled: !row.valid || isCookieCheckUnusable(row.checkStatus), + }), }} columns={accountColumns} dataSource={accounts} @@ -1848,7 +2107,15 @@ export default function Dashboard() { setCookieManagerOpen(false)} footer={null} > @@ -1868,10 +2135,28 @@ export default function Dashboard() { + {cookieEntries.length} 条 CK - {validAccounts.length} 个有效 + {cookieCheckStats.valid} 可用 + {failedCookieCount > 0 ? {failedCookieCount} 失效 : null} + {cookieCheckStats.unchecked > 0 ? {cookieCheckStats.unchecked} 未检查 : null} +