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行壳子
This commit is contained in:
356
source_clean/src/cloud/drivers/quark-share.ts
Normal file
356
source_clean/src/cloud/drivers/quark-share.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
// @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' };
|
||||
}
|
||||
Reference in New Issue
Block a user