Files
CloudSearch/packages/backend/src/cloud/drivers/quark-api.ts
T

238 lines
6.9 KiB
TypeScript

// 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<string, string> {
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<string, string> {
return { pr: 'ucpro', fr: 'pc' };
}
/** Generate query string with common params + random timing to mimic browser */
export function makeQuery(extra: Record<string, string> = {}): 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<void> {
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<T = any>(
path: string,
options: {
method?: string;
query?: Record<string, string>;
body?: any;
cookie: string;
timeout?: number;
},
): Promise<T | null> {
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<QuarkFile[]> {
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<QuarkFile[]> {
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<QuarkFile[]> {
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;
}