const FIVE_MINUTES_MS = 5 * 60 * 1000 const STORAGE_KEY_PREFIX = 'asset-action-rate-limit' export type AssetRateLimitedAction = 'refresh' | 'start' | 'stop' export class FrontendRateLimitError extends Error { readonly remainingMs: number readonly retryAt: number constructor(remainingMs: number, retryAt: number) { super(`操作过于频繁,请${formatRemainingTime(remainingMs)}后再试`) this.name = 'FrontendRateLimitError' this.remainingMs = remainingMs this.retryAt = retryAt } } const getStorageKey = (action: AssetRateLimitedAction, identifier: string) => `${STORAGE_KEY_PREFIX}:${action}:${identifier}` const getTimestamp = (key: string): number => { const value = Number(localStorage.getItem(key)) return Number.isFinite(value) ? value : 0 } export const formatRemainingTime = (remainingMs: number): string => { const totalSeconds = Math.max(1, Math.ceil(remainingMs / 1000)) const minutes = Math.floor(totalSeconds / 60) const seconds = totalSeconds % 60 if (minutes <= 0) return `${seconds}秒` if (seconds === 0) return `${minutes}分钟` return `${minutes}分${seconds}秒` } export const getAssetActionRateLimitState = ( action: AssetRateLimitedAction, identifier: string, now: number = Date.now() ) => { const key = getStorageKey(action, identifier) const lastCalledAt = getTimestamp(key) const retryAt = lastCalledAt + FIVE_MINUTES_MS const remainingMs = Math.max(0, retryAt - now) if (remainingMs <= 0 && lastCalledAt > 0) { localStorage.removeItem(key) } return { limited: remainingMs > 0, remainingMs, retryAt } } export const markAssetActionCalled = ( action: AssetRateLimitedAction, identifier: string, now: number = Date.now() ) => { localStorage.setItem(getStorageKey(action, identifier), String(now)) } export const assertAssetActionAllowed = ( action: AssetRateLimitedAction, identifier: string, now: number = Date.now() ) => { const state = getAssetActionRateLimitState(action, identifier, now) if (state.limited) { throw new FrontendRateLimitError(state.remainingMs, state.retryAt) } }