import { nanoid } from 'nanoid'; import { checkNetdiskLink } from '@/lib/pancheck'; import type { NetdiskCheckPlatform, NetdiskCheckResult, NetdiskCheckStartInput, NetdiskCheckTask, NetdiskCheckTaskResultItem, } from '@/lib/pancheck/types'; const NETDISK_CHECK_RULE = { batchSize: 3, batchIntervalMs: 2000, requestTimeoutMs: 12000, maxTaskLinks: 60, cooldownMs: 60000, taskRetentionMs: 60 * 60 * 1000, maxActiveTasks: 5, } as const; const tasks = new Map(); const activeTaskIds = new Set(); let cooldownUntil = 0; function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function withTimeout(promise: Promise, timeoutMs: number) { return Promise.race([ promise, new Promise((_, reject) => { setTimeout(() => reject(new Error(`执行超时(${timeoutMs}ms)`)), timeoutMs); }), ]); } function cleanupOldTasks() { const now = Date.now(); for (const [id, task] of Array.from(tasks.entries())) { if (now - task.updatedAt > NETDISK_CHECK_RULE.taskRetentionMs) { tasks.delete(id); } } } function cloneTask(task: NetdiskCheckTask): NetdiskCheckTask { return JSON.parse(JSON.stringify(task)) as NetdiskCheckTask; } function createTaskResultItem(status: NetdiskCheckTaskResultItem['status']): NetdiskCheckTaskResultItem { return { status }; } function countBatchSize(total: number) { return Math.max(1, Math.ceil(total / NETDISK_CHECK_RULE.batchSize)); } function updateTaskAfterResult(task: NetdiskCheckTask, url: string, result: NetdiskCheckResult) { task.results[url] = { status: result.status, reason: result.reason, fromCache: result.fromCache, checkedAt: result.checkedAt, durationMs: result.durationMs, }; task.progress.done += 1; if (result.status === 'valid') task.progress.valid += 1; else if (result.status === 'invalid') task.progress.invalid += 1; else if (result.status === 'rate_limited') task.progress.rateLimited += 1; else task.progress.unknown += 1; task.updatedAt = Date.now(); } async function runTask(taskId: string, links: string[]) { const task = tasks.get(taskId); if (!task) return; try { const pendingLinks = links.slice(0, NETDISK_CHECK_RULE.maxTaskLinks); let batchIndex = 0; while (pendingLinks.length > 0) { if (task.shouldStop) { task.status = 'cancelled'; task.updatedAt = Date.now(); return; } if (Date.now() < cooldownUntil) { task.status = 'failed'; task.error = '检测功能冷却中,请稍后再试'; task.updatedAt = Date.now(); return; } batchIndex += 1; task.progress.currentBatch = batchIndex; task.updatedAt = Date.now(); const batch = pendingLinks.splice(0, NETDISK_CHECK_RULE.batchSize); batch.forEach((url) => { task.results[url] = createTaskResultItem('checking'); }); const batchResults = await Promise.all( batch.map(async (url) => { try { return await withTimeout(checkNetdiskLink(task.platform, url), NETDISK_CHECK_RULE.requestTimeoutMs); } catch (error) { const reason = error instanceof Error ? error.message : '检测失败'; return { platform: task.platform, url, normalizedUrl: url, status: 'unknown', valid: null, reason, checkedAt: Date.now(), durationMs: NETDISK_CHECK_RULE.requestTimeoutMs, } satisfies NetdiskCheckResult; } }) ); let hasRateLimited = false; batchResults.forEach((result) => { updateTaskAfterResult(task, result.url, result); if (result.status === 'rate_limited') { hasRateLimited = true; } }); if (hasRateLimited) { cooldownUntil = Date.now() + NETDISK_CHECK_RULE.cooldownMs; task.status = 'failed'; task.error = '触发网盘平台限流,请稍后重试'; task.updatedAt = Date.now(); return; } if (pendingLinks.length > 0) { await sleep(NETDISK_CHECK_RULE.batchIntervalMs); } } task.status = 'completed'; task.updatedAt = Date.now(); } catch (error) { task.status = 'failed'; task.error = error instanceof Error ? error.message : '检测任务失败'; task.updatedAt = Date.now(); } finally { activeTaskIds.delete(taskId); } } export function startNetdiskCheckTask(input: NetdiskCheckStartInput) { cleanupOldTasks(); if (Date.now() < cooldownUntil) { throw new Error('检测功能冷却中,请稍后再试'); } for (const taskId of Array.from(activeTaskIds)) { const activeTask = tasks.get(taskId); if (!activeTask || activeTask.status !== 'running') { activeTaskIds.delete(taskId); } } if (activeTaskIds.size >= NETDISK_CHECK_RULE.maxActiveTasks) { throw new Error(`当前最多只允许 ${NETDISK_CHECK_RULE.maxActiveTasks} 个检测任务同时执行,请稍后再试`); } const uniqueLinks = Array.from( new Set( input.links .map((item) => item.trim()) .filter(Boolean) ) ).slice(0, NETDISK_CHECK_RULE.maxTaskLinks); if (uniqueLinks.length === 0) { throw new Error('没有可检测的链接'); } const taskId = `netdisk_check_${nanoid(10)}`; const task: NetdiskCheckTask = { id: taskId, platform: input.platform, status: 'running', progress: { total: uniqueLinks.length, done: 0, valid: 0, invalid: 0, unknown: 0, rateLimited: 0, currentBatch: 0, totalBatches: countBatchSize(uniqueLinks.length), }, results: Object.fromEntries(uniqueLinks.map((url) => [url, createTaskResultItem('pending')])), createdAt: Date.now(), updatedAt: Date.now(), }; tasks.set(taskId, task); activeTaskIds.add(taskId); void runTask(taskId, uniqueLinks); return cloneTask(task); } export function getNetdiskCheckTask(taskId: string) { cleanupOldTasks(); const task = tasks.get(taskId); return task ? cloneTask(task) : null; } export function cancelNetdiskCheckTask(taskId: string) { const task = tasks.get(taskId); if (!task) return null; task.shouldStop = true; task.updatedAt = Date.now(); if (task.status !== 'running') { return cloneTask(task); } return cloneTask(task); } export function getNetdiskCheckCooldownRemainingMs() { return Math.max(0, cooldownUntil - Date.now()); } export function assertNetdiskCheckPlatform(value: string): NetdiskCheckPlatform { const platform = value as NetdiskCheckPlatform; const allowed: NetdiskCheckPlatform[] = ['115', 'aliyun', 'baidu', 'cmcc', 'pan123', 'quark', 'tianyi', 'uc', 'xunlei']; if (!allowed.includes(platform)) { throw new Error('不支持的网盘平台'); } return platform; }