// Native fetch available in Node 20+ import * as crypto from 'crypto'; /** * HTTP 封装层 — 统一处理夸克 API 的请求签名、headers、query params。 * 所有模块共用此单例/函数集,不持有状态。 */ export interface QuarkConfig { cookie: string; nickname?: string; } // ==================== Headers & Params ==================== const BASE_URL = 'https://drive-pc.quark.cn'; export function getHeaders(cookie: string): Record { return { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Cookie': cookie, 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': 'https://pan.quark.cn/', 'Origin': 'https://pan.quark.cn', }; } export function getCommonParams(): Record { return { pr: 'ucpro', fr: 'pc' }; } /** Generate query string with common params + random timing to mimic browser */ export function makeQuery(extra: Record = {}): string { const __dt = Math.floor(Math.random() * 240000 + 60000); const __t = Date.now() / 1000; return new URLSearchParams({ ...getCommonParams(), uc_param_str: '', app: 'clouddrive', __dt: String(__dt), __t: String(__t), ...extra, }).toString(); } /** Random delay to mimic human behavior (500-2000ms) */ export async function humanDelay(): Promise { const ms = Math.floor(Math.random() * 1500) + 500; await new Promise(r => setTimeout(r, ms)); } /** Generate a random password for share links */ export function randomSharePwd(): string { return Math.floor(1000 + Math.random() * 9000).toString(); } /** * Extract kps/sign/vcode from cookie for API signing (bare keys, no __ prefix). */ export function getMparam(cookie: string): { kps?: string; sign?: string; vcode?: string } { // Match both __kps and kps (with or without __ prefix) const kpsMatch = cookie.match(/__?kps=([a-zA-Z0-9%+/=]+)/); const signMatch = cookie.match(/__?sign=([a-zA-Z0-9%+/=]+)/); const vcodeMatch = cookie.match(/__?vcode=([a-zA-Z0-9%+/=]+)/); if (kpsMatch && signMatch && vcodeMatch) { return { kps: kpsMatch[1], sign: signMatch[1].replace(/%25/g, '%'), vcode: vcodeMatch[1], }; } return {}; } // ==================== Shared fetch helpers ==================== /** * Raw fetch wrapper with JSON parse + status check. * Returns parsed JSON body on 2xx, null on network error. */ export async function apiFetch( path: string, options: { method?: string; query?: Record; body?: any; cookie: string; timeout?: number; }, ): Promise { const { method = 'GET', query, body, cookie, timeout = 10000 } = options; let url = `${BASE_URL}${path}`; if (query) url += `?${new URLSearchParams(query).toString()}`; try { const resp = await fetch(url, { method, headers: { ...getHeaders(cookie), ...(body ? { 'Content-Type': 'application/json' } : {}), }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(timeout), }); if (!resp.ok) return null; return (await resp.json()) as T; } catch { return null; } } // ==================== File listing (shared across modules) ==================== export interface QuarkFile { fid: string; file_name: string; share_fid_token?: string; dir: boolean; size?: number; } /** * List files in a directory by FID. */ export async function listDir(cookie: string, pdirFid: string, page = 1, pageSize = 50): Promise { try { const params = new URLSearchParams({ ...getCommonParams(), uc_param_str: '', pdir_fid: pdirFid, _page: String(page), _size: String(pageSize), _fetch_total: '1', _fetch_sub_dirs: '0', _sort: 'file_type:asc,updated_at:desc', fetch_all_file: '1', fetch_risk_file_name: '1', }); const resp = await fetch( `${BASE_URL}/1/clouddrive/file/sort?${params.toString()}`, { headers: getHeaders(cookie), signal: AbortSignal.timeout(15000) }, ); if (!resp.ok) return []; const data = await resp.json() as any; if (data.status !== 200) return []; return (data.data?.list || []).filter((f: any) => f.fid).map((f: any) => ({ fid: f.fid, file_name: f.file_name, share_fid_token: '', dir: f.dir || false, size: f.size || 0, })); } catch { return []; } } /** * List root directory (pdir_fid=0) — returns all top-level dirs/files. */ export async function listRootDir(cookie: string): Promise { try { const params = new URLSearchParams({ pr: 'ucpro', fr: 'pc', pdir_fid: '0', _page: '1', _size: '200', _fetch_total: '1', _fetch_sub_dirs: '0', _sort: 'file_type:asc,updated_at:desc', fetch_all_file: '1', fetch_risk_file_name: '1', }); const resp = await fetch( `${BASE_URL}/1/clouddrive/file/sort?${params.toString()}`, { headers: getHeaders(cookie), signal: AbortSignal.timeout(15000) }, ); if (!resp.ok) return []; const data = await resp.json() as any; if (data.status !== 200 || !data.data?.list) return []; return (data.data.list || []).map((f: any) => ({ fid: f.fid, file_name: f.file_name, dir: f.dir || false, size: f.size || 0, })); } catch { return []; } } /** * List all files in a directory, handling pagination. * Fetches all pages until no more results. */ export async function listDirAllPages(cookie: string, pdirFid: string): Promise { const allFiles: QuarkFile[] = []; let page = 1; const pageSize = 100; let total = -1; while (total === -1 || (page - 1) * pageSize < total) { const files = await listDir(cookie, pdirFid, page, pageSize); if (!files.length) break; allFiles.push(...files); if (total === -1) { total = files.length; } page++; } return allFiles; } // ==================== Format utilities ==================== export function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i]; } /** Generate a daily folder name (e.g. "2026-05-03") for organizing saves */ export function dailyFolderName(): string { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } /** Generate a random folder name for saving (fallback) */ export function randomFolderName(): string { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let name = ''; for (let i = 0; i < 12; i++) { name += chars[Math.floor(Math.random() * chars.length)]; } return name; }