新增实例槽位隔离与结构化日志,多开不再共用日志与账号
This commit is contained in:
+136
-20
@@ -38,22 +38,50 @@ import {
|
||||
import {
|
||||
DefaultConfig,
|
||||
Exchange,
|
||||
GetInstanceInfo,
|
||||
GetLogFilePath,
|
||||
GetLogs,
|
||||
GetLogStats,
|
||||
GetRecords,
|
||||
IsRunning,
|
||||
ListPrizes,
|
||||
OpenLogDir,
|
||||
PreviewAccounts,
|
||||
QueryScores,
|
||||
SetLogLevel,
|
||||
StartMonitor,
|
||||
StopMonitor,
|
||||
} from '../wailsjs/go/gui/App'
|
||||
import { ClipboardGetText, ClipboardSetText, EventsOn } from '../wailsjs/runtime/runtime'
|
||||
|
||||
const storageKey = 'huya-go.gui.config.v2'
|
||||
const cookieStorageKey = 'huya-go.gui.cookies.v1'
|
||||
const prizeCacheStorageKey = 'huya-go.gui.prizes.v1'
|
||||
const configStorageBase = 'huya-go.gui.config.v2'
|
||||
const cookieStorageBase = 'huya-go.gui.cookies.v1'
|
||||
const prizeCacheStorageBase = 'huya-go.gui.prizes.v1'
|
||||
|
||||
// 界面本地存储保留的日志上限,与后端的增量拉取配合使用。
|
||||
const maxClientLogs = 1000
|
||||
|
||||
// 多开时按实例给本地存储分片:两个实例各存各的账号与配置,互不覆盖。
|
||||
let storageSuffix = ''
|
||||
|
||||
function scopedKey(base: string): string {
|
||||
return storageSuffix ? `${base}@${storageSuffix}` : base
|
||||
}
|
||||
|
||||
function applyStorageScope(instanceID: string) {
|
||||
const id = (instanceID || '').trim()
|
||||
storageSuffix = !id || id === 'default' ? '' : id
|
||||
}
|
||||
|
||||
// 日志等级筛选项(仅作用于界面显示,不影响后端记录)。
|
||||
const logFilterOptions = [
|
||||
{ value: 'all', label: '全部等级' },
|
||||
{ value: 'debug', label: 'debug' },
|
||||
{ value: 'info', label: 'info' },
|
||||
{ value: 'success', label: 'success' },
|
||||
{ value: 'warn', label: 'warn' },
|
||||
{ value: 'error', label: 'error' },
|
||||
]
|
||||
|
||||
type UiConfig = {
|
||||
transport: string
|
||||
@@ -100,11 +128,30 @@ type CookieEntry = {
|
||||
}
|
||||
|
||||
type LogEntry = {
|
||||
id: number
|
||||
time: string
|
||||
level: string
|
||||
category: string
|
||||
account: string
|
||||
message: string
|
||||
}
|
||||
|
||||
type LogStats = {
|
||||
level: string
|
||||
filePath: string
|
||||
dir: string
|
||||
dropped: number
|
||||
count: number
|
||||
}
|
||||
|
||||
type InstanceInfo = {
|
||||
name: string
|
||||
id: string
|
||||
dataDir: string
|
||||
logDir: string
|
||||
logFile: string
|
||||
}
|
||||
|
||||
type PrizeInfo = {
|
||||
id: number
|
||||
sid?: number
|
||||
@@ -356,19 +403,19 @@ function categoryTag(category?: string): ReactNode {
|
||||
|
||||
function readSavedConfig(): Partial<SavedConfig> {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(storageKey) || '{}') as Partial<SavedConfig>
|
||||
return JSON.parse(localStorage.getItem(scopedKey(configStorageBase)) || '{}') as Partial<SavedConfig>
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function writeSavedConfigPatch(patch: Partial<SavedConfig>) {
|
||||
localStorage.setItem(storageKey, JSON.stringify({ ...readSavedConfig(), ...patch }))
|
||||
localStorage.setItem(scopedKey(configStorageBase), JSON.stringify({ ...readSavedConfig(), ...patch }))
|
||||
}
|
||||
|
||||
function readSavedCookies(): CookieEntry[] {
|
||||
try {
|
||||
const items = JSON.parse(localStorage.getItem(cookieStorageKey) || '[]') as Partial<CookieEntry>[]
|
||||
const items = JSON.parse(localStorage.getItem(scopedKey(cookieStorageBase)) || '[]') as Partial<CookieEntry>[]
|
||||
return items
|
||||
.map((item, index) => ({
|
||||
id: item.id || `ck-${Date.now()}-${index}`,
|
||||
@@ -385,12 +432,12 @@ function readSavedCookies(): CookieEntry[] {
|
||||
}
|
||||
|
||||
function writeSavedCookies(items: CookieEntry[]) {
|
||||
localStorage.setItem(cookieStorageKey, JSON.stringify(items))
|
||||
localStorage.setItem(scopedKey(cookieStorageBase), JSON.stringify(items))
|
||||
}
|
||||
|
||||
function readSavedPrizeCache(): PrizeCache {
|
||||
try {
|
||||
const cache = JSON.parse(localStorage.getItem(prizeCacheStorageKey) || '{}') as Partial<PrizeCache>
|
||||
const cache = JSON.parse(localStorage.getItem(scopedKey(prizeCacheStorageBase)) || '{}') as Partial<PrizeCache>
|
||||
const prizes = Array.isArray(cache.prizes)
|
||||
? cache.prizes.filter((item) => Number(item?.id || 0) > 0 && String(item?.name || '').trim())
|
||||
: []
|
||||
@@ -406,7 +453,7 @@ function readSavedPrizeCache(): PrizeCache {
|
||||
|
||||
function writeSavedPrizeCache(prizes: PrizeInfo[], sid: number) {
|
||||
localStorage.setItem(
|
||||
prizeCacheStorageKey,
|
||||
scopedKey(prizeCacheStorageBase),
|
||||
JSON.stringify({
|
||||
prizes,
|
||||
sid,
|
||||
@@ -480,6 +527,16 @@ function normalizeMonitorResult(row: MonitorResult): MonitorResult {
|
||||
}
|
||||
}
|
||||
|
||||
// mergeLogs 按自增 ID 合并日志:实时事件与初始拉取可能交错,按 ID 去重并保持顺序。
|
||||
function mergeLogs(prev: LogEntry[], incoming: LogEntry[]): LogEntry[] {
|
||||
if (incoming.length === 0) return prev
|
||||
const lastID = prev.length > 0 ? prev[prev.length - 1].id : 0
|
||||
const fresh = incoming.filter((item) => Number(item.id || 0) > lastID)
|
||||
if (fresh.length === 0) return prev
|
||||
fresh.sort((a, b) => a.id - b.id)
|
||||
return [...prev, ...fresh].slice(-maxClientLogs)
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { message, modal } = AntApp.useApp()
|
||||
const [form] = Form.useForm<UiConfig>()
|
||||
@@ -511,6 +568,10 @@ export default function Dashboard() {
|
||||
const [prizeCacheUpdatedAt, setPrizeCacheUpdatedAt] = useState(0)
|
||||
const [configLoaded, setConfigLoaded] = useState(false)
|
||||
const [logPath, setLogPath] = useState('')
|
||||
const [logStats, setLogStats] = useState<LogStats | null>(null)
|
||||
const [logFilter, setLogFilter] = useState('all')
|
||||
const [debugLog, setDebugLog] = useState(false)
|
||||
const [instanceInfo, setInstanceInfo] = useState<InstanceInfo | null>(null)
|
||||
|
||||
const applyCookieEntries = async (entries: CookieEntry[], persist = true, refreshLatest = false) => {
|
||||
const normalizedEntries = entries
|
||||
@@ -555,9 +616,16 @@ export default function Dashboard() {
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
Promise.all([DefaultConfig(), GetLogs(), IsRunning(), GetLogFilePath()])
|
||||
.then(([defaults, logItems, isRunning, savedLogPath]) => {
|
||||
Promise.all([DefaultConfig(), GetLogs(0), IsRunning(), GetLogFilePath(), GetInstanceInfo(), GetLogStats()])
|
||||
.then(([defaults, logItems, isRunning, savedLogPath, info, stats]) => {
|
||||
if (!mounted) return
|
||||
const instance = info as InstanceInfo | null
|
||||
// 本地存储必须按实例分片后再读取,否则多开时两个实例会共用同一份账号与配置。
|
||||
applyStorageScope(instance?.id || '')
|
||||
setInstanceInfo(instance)
|
||||
setLogStats((stats || null) as LogStats | null)
|
||||
setDebugLog(String((stats as LogStats | null)?.level || '') === 'debug')
|
||||
|
||||
const savedCookies = readSavedCookies()
|
||||
const savedPrizeCache = readSavedPrizeCache()
|
||||
form.setFieldsValue({
|
||||
@@ -568,8 +636,8 @@ export default function Dashboard() {
|
||||
setCookieEntries(savedCookies)
|
||||
setPrizes(savedPrizeCache.prizes)
|
||||
setPrizeCacheUpdatedAt(savedPrizeCache.updatedAt)
|
||||
setLogs((logItems || []) as LogEntry[])
|
||||
setLogPath(String(savedLogPath || ''))
|
||||
setLogs((prev) => mergeLogs(prev, (logItems || []) as LogEntry[]))
|
||||
setLogPath(String(savedLogPath || instance?.logFile || ''))
|
||||
setRunning(Boolean(isRunning))
|
||||
setConfigLoaded(true)
|
||||
if (savedCookies.length > 0) {
|
||||
@@ -579,7 +647,7 @@ export default function Dashboard() {
|
||||
.catch((err: unknown) => message.error(String(err)))
|
||||
|
||||
const offLog = EventsOn('gui:log', (entry: LogEntry) => {
|
||||
setLogs((prev) => [...prev.slice(-499), entry])
|
||||
setLogs((prev) => mergeLogs(prev, [entry]))
|
||||
})
|
||||
const offResult = EventsOn('gui:monitor-result', (result: MonitorResult) => {
|
||||
setResults((prev) => [normalizeMonitorResult(result), ...prev].slice(0, 300))
|
||||
@@ -716,6 +784,12 @@ export default function Dashboard() {
|
||||
}, [prizes, productSearch])
|
||||
const inventorySummary = useMemo(() => summarizeInventory(filteredPrizes), [filteredPrizes])
|
||||
|
||||
// 日志筛选只在界面上生效;后端的 debug 开关由「调试日志」按钮控制。
|
||||
const visibleLogs = useMemo(
|
||||
() => (logFilter === 'all' ? logs : logs.filter((item) => item.level === logFilter)),
|
||||
[logs, logFilter],
|
||||
)
|
||||
|
||||
const config = () => ({
|
||||
...pickConfig(form.getFieldsValue(true)),
|
||||
selectedAccountLines: selectedAccountLines.map(Number).filter((line) => line > 0),
|
||||
@@ -904,7 +978,7 @@ export default function Dashboard() {
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
localStorage.removeItem(prizeCacheStorageKey)
|
||||
localStorage.removeItem(scopedKey(prizeCacheStorageBase))
|
||||
setPrizes([])
|
||||
setSelectedPrizeKeys([])
|
||||
setPrizeCacheUpdatedAt(0)
|
||||
@@ -940,7 +1014,7 @@ export default function Dashboard() {
|
||||
timeoutSec: values.timeoutSec,
|
||||
startAt: values.startAt,
|
||||
}
|
||||
localStorage.setItem(storageKey, JSON.stringify(saved))
|
||||
localStorage.setItem(scopedKey(configStorageBase), JSON.stringify(saved))
|
||||
message.success('已保存')
|
||||
}
|
||||
|
||||
@@ -1014,6 +1088,14 @@ export default function Dashboard() {
|
||||
message.success('已复制日志路径')
|
||||
}
|
||||
|
||||
// 调试日志开关:打开后后端会把逐条网络请求也写进日志(debug 级)。
|
||||
const toggleDebugLog = async (checked: boolean) => {
|
||||
const level = await SetLogLevel(checked ? 'debug' : 'info')
|
||||
setDebugLog(level === 'debug')
|
||||
setLogStats((prev) => (prev ? { ...prev, level } : prev))
|
||||
message.success(level === 'debug' ? '已开启 debug 日志(含逐条网络请求)' : '已恢复 info 日志')
|
||||
}
|
||||
|
||||
const selectAllAccounts = () => {
|
||||
setSelectedAccountLines(validAccounts.map((item) => item.line))
|
||||
}
|
||||
@@ -1258,6 +1340,7 @@ export default function Dashboard() {
|
||||
<div className="log-pane">
|
||||
<div className="log-tools">
|
||||
<span className="log-path" title={logPath || '日志文件不可用'}>
|
||||
{instanceInfo ? `${instanceInfo.name} · ` : ''}
|
||||
{logPath || '日志文件不可用'}
|
||||
</span>
|
||||
<Space size={6}>
|
||||
@@ -1274,7 +1357,31 @@ export default function Dashboard() {
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
<LogView logs={logs} />
|
||||
<div className="log-tools">
|
||||
<Select
|
||||
size="small"
|
||||
value={logFilter}
|
||||
style={{ width: 116 }}
|
||||
onChange={setLogFilter}
|
||||
options={logFilterOptions}
|
||||
/>
|
||||
<Space size={8}>
|
||||
<Tooltip title="开启后记录逐条网络请求(debug 级)">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={debugLog}
|
||||
checkedChildren="调试"
|
||||
unCheckedChildren="常规"
|
||||
onChange={toggleDebugLog}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="log-path">共 {logs.length} 条</span>
|
||||
</Space>
|
||||
</div>
|
||||
{logStats && logStats.dropped > 0 ? (
|
||||
<div className="log-path">日志队列已丢弃 {logStats.dropped} 条(磁盘写入压力过大)</div>
|
||||
) : null}
|
||||
<LogView logs={visibleLogs} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -1301,6 +1408,7 @@ export default function Dashboard() {
|
||||
<Statistic title="异常事件" value={errorCount} />
|
||||
<Statistic title="待处理账号" value={pendingCount} />
|
||||
<Statistic title="日志条数" value={logs.length} />
|
||||
<Statistic title="丢弃日志" value={logStats?.dropped || 0} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -1401,6 +1509,11 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<CountdownBadge countdown={scheduleCountdown} now={now} running={running} />
|
||||
<Space size={12}>
|
||||
<Tooltip
|
||||
title={instanceInfo?.dataDir ? `实例数据目录: ${instanceInfo.dataDir}` : '实例信息未就绪'}
|
||||
>
|
||||
<Tag color="blue">{instanceInfo?.name || '默认实例'}</Tag>
|
||||
</Tooltip>
|
||||
<Tag>{String(watchedTransport || 'http').toUpperCase()}</Tag>
|
||||
<Button icon={<Settings size={16} />} onClick={() => setSettingsOpen(true)}>
|
||||
设置
|
||||
@@ -1923,11 +2036,14 @@ function padNumber(value: number, size = 2): string {
|
||||
function LogView({ logs }: { logs: LogEntry[] }) {
|
||||
return (
|
||||
<div className="log-view">
|
||||
{logs.map((item, index) => (
|
||||
<div className={`log-line level-${item.level}`} key={`${item.time}-${index}`}>
|
||||
{logs.map((item) => (
|
||||
<div className={`log-line level-${item.level}`} key={item.id}>
|
||||
<span>{item.time}</span>
|
||||
<Tag>{item.level}</Tag>
|
||||
<code>{item.message}</code>
|
||||
<code>
|
||||
{item.category ? `[${item.category}]` : ''}
|
||||
{item.account ? `[账号:${item.account}]` : ''} {item.message}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user