utils.js
1 import { ArgumentError, AuthRequiredError, CommandExecutionError, getErrorMessage } from '@jackwener/opencli/errors'; 2 export const SHARE_API = 'https://drive-h.quark.cn/1/clouddrive/share/sharepage'; 3 export const DRIVE_API = 'https://drive-pc.quark.cn/1/clouddrive/file'; 4 export const TASK_API = 'https://drive-pc.quark.cn/1/clouddrive/task'; 5 const QUARK_DOMAIN = 'pan.quark.cn'; 6 const AUTH_HINT = 'Quark Drive requires a logged-in browser session'; 7 function isAuthFailure(message, status) { 8 if (status === 401 || status === 403) 9 return true; 10 return /not logged in|login required|please log in|authentication required|unauthorized|forbidden|未登录|请先登录|需要登录|登录/.test(message.toLowerCase()); 11 } 12 function getErrorStatus(error) { 13 if (!error || typeof error !== 'object' || !('status' in error)) 14 return undefined; 15 const status = error.status; 16 return typeof status === 'number' ? status : undefined; 17 } 18 function unwrapApiData(resp, action) { 19 if (resp.status === 200) 20 return resp.data; 21 if (isAuthFailure(resp.message, resp.status)) { 22 throw new AuthRequiredError(QUARK_DOMAIN, AUTH_HINT); 23 } 24 throw new CommandExecutionError(`quark: ${action}: ${resp.message}`); 25 } 26 export function extractPwdId(url) { 27 const m = url.match(/\/s\/([a-zA-Z0-9]+)/); 28 if (m) 29 return m[1]; 30 if (/^[a-zA-Z0-9]+$/.test(url)) 31 return url; 32 throw new ArgumentError(`Invalid Quark share URL: ${url}`); 33 } 34 export async function fetchJson(page, url, options) { 35 const method = options?.method || 'GET'; 36 const body = options?.body ? JSON.stringify(options.body) : undefined; 37 const js = `fetch(${JSON.stringify(url)}, { 38 method: ${JSON.stringify(method)}, 39 headers: { 'Content-Type': 'application/json' }, 40 credentials: 'include', 41 ${body ? `body: ${JSON.stringify(body)},` : ''} 42 }).then(async r => { 43 const ct = r.headers.get('content-type') || ''; 44 if (!ct.includes('json')) { 45 const text = await r.text().catch(() => ''); 46 throw Object.assign(new Error('Non-JSON response: ' + text.slice(0, 200)), { status: r.status }); 47 } 48 return r.json(); 49 })`; 50 try { 51 return await page.evaluate(js); 52 } 53 catch (error) { 54 if (isAuthFailure(getErrorMessage(error), getErrorStatus(error))) { 55 throw new AuthRequiredError(QUARK_DOMAIN, AUTH_HINT); 56 } 57 throw error; 58 } 59 } 60 export async function apiGet(page, url) { 61 const resp = await fetchJson(page, url); 62 return unwrapApiData(resp, 'API error'); 63 } 64 export async function apiPost(page, url, body) { 65 const resp = await fetchJson(page, url, { method: 'POST', body }); 66 return unwrapApiData(resp, 'API error'); 67 } 68 export async function getToken(page, pwdId, passcode = '') { 69 const data = await fetchJson(page, `${SHARE_API}/token?pr=ucpro&fr=pc`, { 70 method: 'POST', 71 body: { pwd_id: pwdId, passcode, support_visit_limit_private_share: true }, 72 }); 73 return unwrapApiData(data, 'Failed to get token').stoken; 74 } 75 export async function getShareList(page, pwdId, stoken, pdirFid = '0', options) { 76 const allFiles = []; 77 let pageNum = 1; 78 let total = 0; 79 do { 80 const sortParam = options?.sort ? `&_sort=${options.sort}` : ''; 81 const url = `${SHARE_API}/detail?pr=ucpro&fr=pc&ver=2&pwd_id=${pwdId}&stoken=${encodeURIComponent(stoken)}&pdir_fid=${pdirFid}&force=0&_page=${pageNum}&_size=200&_fetch_total=1${sortParam}`; 82 const data = await fetchJson(page, url); 83 const files = unwrapApiData(data, 'Failed to get share list')?.list || []; 84 allFiles.push(...files); 85 total = data.metadata?._total || 0; 86 pageNum++; 87 } while (allFiles.length < total); 88 return allFiles; 89 } 90 export async function listMyDrive(page, pdirFid) { 91 const allFiles = []; 92 let pageNum = 1; 93 let total = 0; 94 do { 95 const url = `${DRIVE_API}/sort?pr=ucpro&fr=pc&pdir_fid=${pdirFid}&_page=${pageNum}&_size=200&_fetch_total=1&_sort=file_type:asc,file_name:asc`; 96 const data = await fetchJson(page, url); 97 const files = unwrapApiData(data, 'Failed to list drive')?.list || []; 98 allFiles.push(...files); 99 total = data.metadata?._total || 0; 100 pageNum++; 101 } while (allFiles.length < total); 102 return allFiles; 103 } 104 export async function findFolder(page, path) { 105 const parts = path.split('/').filter(Boolean); 106 let currentFid = '0'; 107 for (const part of parts) { 108 const files = await listMyDrive(page, currentFid); 109 const existing = files.find(f => f.dir && f.file_name === part); 110 if (existing) { 111 currentFid = existing.fid; 112 } 113 else { 114 throw new CommandExecutionError(`quark: Folder "${part}" not found in "${path}"`); 115 } 116 } 117 return currentFid; 118 } 119 export function formatDate(ts) { 120 if (!ts) 121 return ''; 122 const d = new Date(ts); 123 return d.toISOString().replace('T', ' ').slice(0, 19); 124 } 125 export function formatSize(bytes) { 126 if (bytes <= 0) 127 return '0 B'; 128 const units = ['B', 'KB', 'MB', 'GB', 'TB']; 129 const i = Math.floor(Math.log(bytes) / Math.log(1024)); 130 return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`; 131 } 132 export async function getTaskStatus(page, taskId) { 133 const url = `${TASK_API}?pr=ucpro&fr=pc&task_id=${taskId}&retry_index=0`; 134 return apiGet(page, url); 135 } 136 export async function pollTask(page, taskId, onDone, maxAttempts = 30, intervalMs = 500) { 137 for (let i = 0; i < maxAttempts; i++) { 138 await new Promise(r => setTimeout(r, intervalMs)); 139 const task = await getTaskStatus(page, taskId); 140 if (task?.status === 2) { 141 onDone?.(task); 142 return true; 143 } 144 } 145 return false; 146 }