Files
CloudSearch/source_clean/src/cloud/drivers/quark-share.ts
admin 09be4c307e v0.3.6: 恢复丢失的11个模块 + 接线基础设施
恢复内容:
- quark驱动拆解为7个子模块 (quark-api/auth/share/storage/cleanup/rename/ad-cleanup)
- 工具模块: utils/crypto, utils/logger, utils/proxy-agent
- 配置校验: config/startup-validator
- 接线: main.ts(checkStartup), credential.service.ts(加密Cookie), admin.routes.ts(代理测试)
- quark.driver.ts 从1533行巨兽瘦身到130行壳子
2026-05-17 06:05:47 +08:00

357 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-nocheck
import * as quark_api from "./quark-api";
/**
* 分享模块 — 分享链接解析、转存任务、创建分享链接。
*/
const BASE_URL = 'https://drive-pc.quark.cn';
// ==================== Acquire Stoken ====================
/**
* Acquire stoken for a share link (needed for detail/save).
*/
export async function acquireStoken(cookie, pwdId) {
for (let attempt = 0; attempt < 3; attempt++) {
try {
const params = new URLSearchParams(quark_api.getCommonParams());
const resp = await fetch(`${BASE_URL}/1/clouddrive/share/sharepage/token?${params.toString()}`, {
method: 'POST',
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
body: JSON.stringify({ pwd_id: pwdId, passcode: '' }),
signal: AbortSignal.timeout(10000),
});
if (!resp.ok) {
if (attempt < 2)
continue;
return null;
}
const data = await resp.json();
if (data.status === 200 && data.data?.stoken) {
return data.data.stoken;
}
return null;
}
catch {
if (attempt >= 2)
return null;
await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
}
}
return null;
}
// ==================== Get Share Files ====================
/**
* Fetch detail at a given pdir_fid within a share.
*/
export async function getDetailAt(cookie, pwdId, stoken, pdirFid) {
const params = new URLSearchParams({
...quark_api.getCommonParams(),
pwd_id: pwdId,
stoken,
pdir_fid: pdirFid,
force: '0',
_page: '1',
_size: '50',
_fetch_banner: '0',
_fetch_share: '1',
_fetch_total: '1',
_sort: 'file_type:asc,updated_at:desc',
ver: '2',
fetch_share_full_path: '0',
});
const resp = await fetch(`${BASE_URL}/1/clouddrive/share/sharepage/detail?${params.toString()}`, { headers: quark_api.getHeaders(cookie), signal: AbortSignal.timeout(15000) });
if (!resp.ok)
return [];
const data = await resp.json();
if (data.status !== 200)
return [];
return (data.data?.list || []).filter((f) => f.fid).map((f) => ({
fid: f.fid,
file_name: f.file_name,
share_fid_token: f.share_fid_token || '',
dir: f.dir || false,
size: f.size || 0,
}));
}
/**
* Recursively collect files from a share.
* If the share contains a single directory, drill into it to list contents
* but still save the directory itself.
*/
export async function getShareFiles(cookie, pwdId, stoken) {
try {
const topLevel = await getDetailAt(cookie, pwdId, stoken, '0');
if (!topLevel || topLevel.length === 0)
return null;
// If the share is a single directory, we save the directory itself
// and fetch its contents for renaming later
if (topLevel.length === 1 && topLevel[0].dir) {
const innerFiles = await getDetailAt(cookie, pwdId, stoken, topLevel[0].fid);
return {
files: topLevel,
topDir: true,
childFiles: innerFiles || [],
};
}
// Multiple top-level items: save them directly
return {
files: topLevel,
topDir: false,
};
}
catch {
return null;
}
}
// ==================== Save Files (share → cloud) ====================
/**
* Save shared files to the user's cloud directory.
*/
export async function saveFiles(cookie, pwdId, stoken, fids, fidTokens, toPdirFid) {
try {
const resp = await fetch(`${BASE_URL}/1/clouddrive/share/sharepage/save?${quark_api.makeQuery()}`, {
method: 'POST',
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
body: JSON.stringify({
fid_list: fids,
fid_token_list: fidTokens,
to_pdir_fid: toPdirFid,
pwd_id: pwdId,
stoken,
pdir_fid: '0',
scene: 'link',
}),
signal: AbortSignal.timeout(30000),
});
const data = await resp.json();
if (data.status === 200 && data.data?.task_id) {
return { success: true, message: 'Save task created', taskId: data.data.task_id };
}
return {
success: false,
message: data.message === 'require login [guest]'
? '夸克网盘 Cookie 已过期,请在后台重新配置 Cookie'
: (data.message || `API 返回错误 (status=${data.status}, code=${data.code})`),
};
}
catch (err) {
return { success: false, message: err.message || 'Network error' };
}
}
// ==================== Wait for Save Task ====================
/**
* Poll task status until complete or timeout.
* Returns the saved file FIDs (save_as_top_fids).
*/
export async function waitForTask(cookie, taskId, timeoutMs) {
const start = Date.now();
let retryIndex = 0;
while (Date.now() - start < timeoutMs) {
try {
const params = new URLSearchParams({
...quark_api.getCommonParams(),
uc_param_str: '',
task_id: taskId,
retry_index: String(retryIndex),
__dt: String(Math.floor(Math.random() * 240000 + 60000)),
__t: String(Date.now() / 1000),
});
const resp = await fetch(`${BASE_URL}/1/clouddrive/task?${params.toString()}`, { headers: quark_api.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
const data = await resp.json();
if (data.status === 200) {
if (data.data?.status === 2) {
// Task completed
const savedFids = data.data?.save_as?.save_as_top_fids || [];
return savedFids;
}
// Still in progress
retryIndex++;
}
}
catch {
// Network error, retry
}
await new Promise(r => setTimeout(r, 1000));
}
return null; // Timeout
}
// ==================== Rename File ====================
/**
* Rename a file by its FID.
*/
export async function renameFile(cookie, fid, newName) {
try {
const resp = await fetch(`${BASE_URL}/1/clouddrive/file/rename?${quark_api.makeQuery()}`, {
method: 'POST',
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
body: JSON.stringify({ fid, file_name: newName }),
signal: AbortSignal.timeout(10000),
});
const data = await resp.json();
return data.status === 200 || data.code === 0;
}
catch {
return false;
}
}
// ==================== Create Share Link ====================
/**
* Create a share link for a file/folder.
* Flow: create task → poll for share_id → submit to get short URL.
*/
export async function createShareLink(cookie, fileId) {
try {
const sharePwd = quark_api.randomSharePwd();
// Try different share_type values (1=7天, 0=无限制)
const shareTypes = ['1', '0'];
let lastError = '';
for (const st of shareTypes) {
await quark_api.humanDelay();
// Step 1: Create share task - get task_id
const response = await fetch(`${BASE_URL}/1/clouddrive/share?${quark_api.makeQuery()}`, {
method: 'POST',
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
body: JSON.stringify({
fid_list: [fileId],
share_type: st,
url_type: '1',
share_pwd: sharePwd,
}),
signal: AbortSignal.timeout(15000),
});
const data = await response.json();
const taskId = data.data?.task_id;
if (!taskId) {
lastError = data.message || `share_type=${st} 失败`;
console.error('[Quark] Create share task failed (type=%s):', st, data.message || JSON.stringify(data).slice(0, 200));
continue;
}
// Step 2: Poll task until complete
const result = await waitForShareTask(cookie, taskId, 20000);
if (!result?.shareId) {
lastError = result?.message || '任务超时';
console.error('[Quark] Wait for share task failed (type=%s):', st, result?.message || 'unknown');
continue;
}
// Step 3: Submit share via /password endpoint
const shareUrl = await submitShare(cookie, result.shareId, sharePwd);
if (shareUrl) {
return {
success: true,
shareUrl,
sharePwd,
message: `分享链接已生成(密码:${sharePwd}`,
};
}
lastError = '提交密码后未获取到短链接';
}
return { success: false, message: lastError || '🤷 各种姿势都试过了,就是分享不出来…' };
}
catch (err) {
console.error('[Quark] createShareLink error:', err.message);
return { success: false, message: err.message || '🌩️ 网络开小差了,再试试?' };
}
}
/**
* Submit share via /password endpoint to get the actual short URL.
*/
async function submitShare(cookie, shareId, sharePwd) {
try {
const response = await fetch(`${BASE_URL}/1/clouddrive/share/password?${quark_api.makeQuery()}`, {
method: 'POST',
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
body: JSON.stringify({ share_id: shareId, share_pwd: sharePwd || '' }),
signal: AbortSignal.timeout(15000),
});
const data = await response.json();
if (data.status === 200 && data.data?.share_url) {
console.log('[Quark] Share short URL:', data.data.share_url);
return data.data.share_url;
}
console.log('[Quark] /password response:', JSON.stringify(data).slice(0, 300));
console.error('[Quark] /password FAIL status=%s msg=%s', data.status, data.message || '');
return null;
}
catch (err) {
console.log('[Quark] /password error:', err);
return null;
}
}
/**
* Poll share task until complete and extract share URL/shortcode.
*/
async function waitForShareTask(cookie, taskId, timeoutMs) {
const start = Date.now();
let retryIndex = 0;
while (Date.now() - start < timeoutMs) {
try {
const params = new URLSearchParams({
...quark_api.getCommonParams(),
uc_param_str: '',
task_id: taskId,
retry_index: String(retryIndex),
__dt: String(Math.floor(Math.random() * 240000 + 60000)),
__t: String(Date.now() / 1000),
});
const resp = await fetch(`${BASE_URL}/1/clouddrive/task?${params.toString()}`, { headers: quark_api.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
const data = await resp.json();
if (data.data?.status === 2) {
// Task completed — try multiple extraction approaches
// 1. Direct share_url field
if (data.data?.share_url) {
const match = data.data.share.match(/\/s\/([a-zA-Z0-9]+)/);
if (match)
return { shareId: match[1] };
}
// 2. Nested share object
if (data.data?.share?.url) {
const match = data.data.share.url.match(/\/s\/([a-zA-Z0-9]+)/);
if (match)
return { shareId: match[1] };
}
if (data.data?.share?.short_url) {
const match = data.data.share.short.match(/\/s\/([a-zA-Z0-9]+)/);
if (match)
return { shareId: match[1] };
}
// 3. share_id — validate it's a reasonable short code (8-20 chars, not UUID-like)
const shareId = data.data?.share_id;
if (shareId && shareId.length <= 20 && shareId.length >= 8) {
return { shareId };
}
// 4. Regex search through the full response for a URL pattern
const str = JSON.stringify(data);
const urlMatch = str.match(/https?:\/\/pan\.quark\.cn\/s\/([a-zA-Z0-9]{6,16})/);
if (urlMatch) {
return { shareId: urlMatch[1] };
}
// 5. Extract from any URL field in the response
const urlFields = ['url', 'link', 'share_url', 'short_url', 'share_link'];
for (const field of urlFields) {
const val = data.data?.[field] || data.data?.share?.[field];
if (typeof val === 'string' && val.includes('pan.quark.cn/s/')) {
const m = val.match(/\/s\/([a-zA-Z0-9]+)/);
if (m)
return { shareId: m[1] };
}
}
// 6. Log full share task response for debugging
console.log('[Quark] Full share task response:', JSON.stringify(data, null, 2).slice(0, 2000));
// 7. Even if shareId is UUID-like (32 hex chars), use it anyway as last resort
if (shareId) {
return { shareId };
}
return { message: 'Share task completed but no share URL found' };
}
if (data.data?.status === 3) {
return { message: data.message || 'Share task failed' };
}
retryIndex++;
}
catch {
// Retry
}
await new Promise(r => setTimeout(r, 1000));
}
return { message: 'Share task timed out' };
}