chore: initial commit - CloudSearch v0.0.2
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user