/ clis / quark / utils.js
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  }