release: v0.4.0
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { QUARK_PAN_HOST, EP as QE } from './drivers/quark-api';
|
||||
import { getDb } from '../database/database';
|
||||
import { encrypt, decrypt, isEncrypted } from '../utils/crypto';
|
||||
import { localTimestamp, formatLocalDate, formatLocalDateTime } from '../utils/time';
|
||||
@@ -184,11 +185,11 @@ async function fetchQuarkNickname(cookie: string): Promise<string | null> {
|
||||
const MAX_RETRIES = 2;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await fetch('https://pan.quark.cn/account/info', {
|
||||
const response = await fetch(QUARK_PAN_HOST + QE.ACCOUNT_INFO, {
|
||||
headers: {
|
||||
'Cookie': cookie,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'https://pan.quark.cn/',
|
||||
'Referer': QUARK_PAN_HOST + '/',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
import { getDb } from '../database/database';
|
||||
import { encrypt, decrypt, isEncrypted } from '../utils/crypto';
|
||||
import { localTimestamp, formatLocalDate, formatLocalDateTime } from '../utils/time';
|
||||
|
||||
export interface CloudConfig {
|
||||
id: number;
|
||||
cloud_type: string;
|
||||
cookie?: string;
|
||||
nickname?: string;
|
||||
is_active: number;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
checkin_status: string; // 'none'|'success'|'failed'|'pending'|'skipped'
|
||||
last_checkin_at?: string;
|
||||
checkin_message?: string;
|
||||
consecutive_failures: number;
|
||||
last_used_at?: string;
|
||||
total_saves: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
verification_status?: string;
|
||||
promotion_account?: string;
|
||||
cloud_type_uid?: string;
|
||||
cookie_uid?: string;
|
||||
}
|
||||
|
||||
// ── Cookie UID Extraction ────────────────────────────────────────
|
||||
|
||||
function decryptCookie(encrypted: string): string {
|
||||
if (!encrypted) return '';
|
||||
if (!isEncrypted(encrypted)) return encrypted;
|
||||
return decrypt(encrypted);
|
||||
}
|
||||
|
||||
function extractCookieUid(cookie: string): string {
|
||||
if (!cookie) return '';
|
||||
let m = cookie.match(/__uid=([a-zA-Z0-9+/=_-]+)/);
|
||||
if (m) return m[1];
|
||||
m = cookie.match(/b-user-id=([a-zA-Z0-9-]+)/);
|
||||
if (m) return m[1];
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── Config CRUD ──────────────────────────────────────────────────
|
||||
|
||||
export function getCloudConfigs(): CloudConfig[] {
|
||||
const db = getDb();
|
||||
const rows = db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, cloud_type_uid, cookie_uid, promotion_account, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||
FROM cloud_configs ORDER BY id ASC`
|
||||
).all() as CloudConfig[];
|
||||
rows.forEach(r => { if (r.cookie) r.cookie = decryptCookie(r.cookie); });
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function getAvailableClouds(): CloudConfig[] {
|
||||
const db = getDb();
|
||||
const rows = db.prepare(
|
||||
`SELECT id, cloud_type, nickname, is_active, cloud_type_uid, cookie_uid, promotion_account, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at
|
||||
FROM cloud_configs WHERE is_active = 1 ORDER BY id ASC`
|
||||
).all() as CloudConfig[];
|
||||
rows.forEach(r => { if (r.cookie) r.cookie = decryptCookie(r.cookie); });
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Returns the first active config matching the given cloud type. */
|
||||
export function getCloudConfigByType(cloudType: string): CloudConfig | undefined {
|
||||
const db = getDb();
|
||||
const cfg = db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, cloud_type_uid, cookie_uid, promotion_account, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||
FROM cloud_configs WHERE cloud_type = ? AND is_active = 1
|
||||
ORDER BY id ASC LIMIT 1`
|
||||
).get(cloudType) as CloudConfig | undefined;
|
||||
if (cfg && cfg.cookie) cfg.cookie = decryptCookie(cfg.cookie);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
export function getCloudConfigById(id: number): CloudConfig | undefined {
|
||||
const db = getDb();
|
||||
const cfg = db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, cloud_type_uid, cookie_uid, promotion_account, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||
FROM cloud_configs WHERE id = ?`
|
||||
).get(id) as CloudConfig | undefined;
|
||||
if (cfg && cfg.cookie) cfg.cookie = decryptCookie(cfg.cookie);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/** Returns all active cloud configs (used by save flow for cloud type switching). */
|
||||
export function getActiveCloudConfigs(): CloudConfig[] {
|
||||
const db = getDb();
|
||||
const rows = db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, cloud_type_uid, cookie_uid, promotion_account, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at
|
||||
FROM cloud_configs WHERE is_active = 1
|
||||
ORDER BY cloud_type ASC, id ASC`
|
||||
).all() as CloudConfig[];
|
||||
rows.forEach(r => { if (r.cookie) r.cookie = decryptCookie(r.cookie); });
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function saveCloudConfig(data: {
|
||||
id?: number;
|
||||
cloud_type: string;
|
||||
cookie?: string;
|
||||
nickname?: string;
|
||||
cookie_uid?: string;
|
||||
promotion_account?: string;
|
||||
is_active?: number;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
}): CloudConfig {
|
||||
const db = getDb();
|
||||
|
||||
const cookieUidForUpdate = data.cookie ? extractCookieUid(data.cookie) : null;
|
||||
const encryptedCookie = data.cookie ? encrypt(data.cookie) : null;
|
||||
|
||||
if (data.id) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET
|
||||
cloud_type = COALESCE(?, cloud_type),
|
||||
cookie = COALESCE(?, cookie),
|
||||
nickname = COALESCE(?, nickname),
|
||||
cookie_uid = COALESCE(?, cookie_uid),
|
||||
promotion_account = COALESCE(?, promotion_account),
|
||||
is_active = COALESCE(?, is_active),
|
||||
storage_used = COALESCE(?, storage_used),
|
||||
storage_total = COALESCE(?, storage_total),
|
||||
consecutive_failures = 0,
|
||||
updated_at = ?
|
||||
WHERE id = ?`
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), data.id);
|
||||
} else {
|
||||
const existing = db.prepare(
|
||||
'SELECT id, nickname FROM cloud_configs WHERE cloud_type = ? AND is_active = 1 LIMIT 1'
|
||||
).get(data.cloud_type) as any;
|
||||
if (existing) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET
|
||||
cookie = COALESCE(?, cookie),
|
||||
nickname = COALESCE(?, nickname),
|
||||
cookie_uid = COALESCE(?, cookie_uid),
|
||||
promotion_account = COALESCE(?, promotion_account),
|
||||
is_active = COALESCE(?, is_active),
|
||||
storage_used = COALESCE(?, storage_used),
|
||||
storage_total = COALESCE(?, storage_total),
|
||||
consecutive_failures = 0,
|
||||
updated_at = ?
|
||||
WHERE id = ?`
|
||||
).run(encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), existing.id);
|
||||
} else {
|
||||
db.prepare(
|
||||
'INSERT INTO cloud_configs (cloud_type, cookie, nickname, cookie_uid, promotion_account, is_active, storage_used, storage_total, consecutive_failures) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)'
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null);
|
||||
}
|
||||
}
|
||||
|
||||
const savedId = data.id || (db.prepare('SELECT last_insert_rowid() as id').get() as any).id;
|
||||
return db.prepare(
|
||||
`SELECT id, cloud_type, cookie, nickname, is_active, cloud_type_uid, cookie_uid, promotion_account, storage_used, storage_total,
|
||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||
last_used_at, total_saves, created_at, updated_at
|
||||
FROM cloud_configs WHERE id = ?`
|
||||
).get(savedId) as CloudConfig;
|
||||
}
|
||||
|
||||
export function deleteCloudConfig(id: number): boolean {
|
||||
const db = getDb();
|
||||
const result = db.prepare('DELETE FROM cloud_configs WHERE id = ?').run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
// ── Cookie Validation ────────────────────────────────────────────
|
||||
|
||||
async function fetchQuarkNickname(cookie: string): Promise<string | null> {
|
||||
const MAX_RETRIES = 2;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await fetch('https://pan.quark.cn/account/info', {
|
||||
headers: {
|
||||
'Cookie': cookie,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'https://pan.quark.cn/',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json() as any;
|
||||
if (data?.data?.nickname) return data.data.nickname;
|
||||
} catch {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function testCloudConnection(id: number): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
nickname?: string;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
}> {
|
||||
const config = getCloudConfigById(id);
|
||||
if (!config) {
|
||||
return { success: false, message: 'Cloud config not found' };
|
||||
}
|
||||
|
||||
if (!config.cookie) {
|
||||
return { success: false, message: 'Cookie not configured' };
|
||||
}
|
||||
|
||||
const cookie = decryptCookie(config.cookie);
|
||||
|
||||
try {
|
||||
let valid = false;
|
||||
let nickname = '';
|
||||
let storageUsed = config.storage_used || '';
|
||||
let storageTotal = config.storage_total || '';
|
||||
|
||||
if (config.cloud_type === 'baidu') {
|
||||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||
const driver = new BaiduDriver({ cookie: cookie, nickname: config.nickname });
|
||||
valid = await driver.validate();
|
||||
if (valid) {
|
||||
const info = await driver.getUserInfo();
|
||||
if (info) {
|
||||
nickname = config.nickname || info.nickname || '百度网盘';
|
||||
const fmt = (b: number) => b >= 1024**3 ? (b/1024**3).toFixed(2)+' GB' : (b/1024**2).toFixed(2)+' MB';
|
||||
storageUsed = fmt(info.usedBytes);
|
||||
storageTotal = fmt(info.totalBytes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie: cookie, nickname: config.nickname });
|
||||
valid = await driver.validate();
|
||||
if (valid) {
|
||||
nickname = config.nickname || (await fetchQuarkNickname(cookie)) || '夸克网盘';
|
||||
const storage = await driver.getStorageInfoQuick();
|
||||
storageUsed = (storage.used !== '-' && storage.used !== '0 B') ? storage.used : (config.storage_used || '');
|
||||
storageTotal = (storage.total !== '-' && storage.total !== '0 B') ? storage.total : (config.storage_total || '');
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
if (!valid) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||
).run(localTimestamp(), id);
|
||||
return { success: false, message: '连接失败:Cookie 无效或已过期,或网络暂时异常' };
|
||||
}
|
||||
|
||||
const cookieUid = extractCookieUid(cookie);
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET nickname = ?, storage_total = ?, storage_used = ?, cookie_uid = ?, is_active = 1, verification_status = 'valid', updated_at = ? WHERE id = ?`
|
||||
).run(nickname, storageTotal, storageUsed, cookieUid, localTimestamp(), id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
nickname,
|
||||
storage_used: storageUsed,
|
||||
storage_total: storageTotal,
|
||||
};
|
||||
} catch (err: any) {
|
||||
try {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||
).run(localTimestamp(), id);
|
||||
} catch {}
|
||||
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
export async function testCloudConnectionWithCookie(cloudType: string, cookie: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
nickname?: string;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
}> {
|
||||
try {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie, nickname: '' });
|
||||
const valid = await driver.validate();
|
||||
if (!valid) {
|
||||
return { success: false, message: '连接失败:Cookie 无效或已过期' };
|
||||
}
|
||||
const nickname = (await fetchQuarkNickname(cookie)) || cloudType;
|
||||
const storage = await driver.getStorageInfo();
|
||||
return {
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
nickname,
|
||||
storage_used: storage.used,
|
||||
storage_total: storage.total,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Unified Credential Validation ─────────────────────────────────
|
||||
|
||||
export interface CredentialValidationResult {
|
||||
valid: boolean;
|
||||
config?: CloudConfig;
|
||||
errorCode?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and validate a credential for the given cloud type.
|
||||
*
|
||||
* This is the unified entry point for all save/transfer operations.
|
||||
* It handles:
|
||||
* 1. Finding an active config with < 5 consecutive failures (round-robin)
|
||||
* 2. Validating cookie freshness via driver.validate()
|
||||
* 3. Returning structured result with error codes
|
||||
*
|
||||
* Reference: search-ucmao get_and_validate_credential() pattern.
|
||||
*/
|
||||
export async function getAndValidateCredential(cloudType: string): Promise<CredentialValidationResult> {
|
||||
const db = getDb();
|
||||
|
||||
const config = db.prepare(
|
||||
`SELECT * FROM cloud_configs
|
||||
WHERE cloud_type = ? AND is_active = 1
|
||||
AND consecutive_failures < 5
|
||||
ORDER BY last_used_at ASC NULLS FIRST
|
||||
LIMIT 1`
|
||||
).get(cloudType) as CloudConfig | undefined;
|
||||
|
||||
if (!config) {
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'NO_AVAILABLE_DRIVE',
|
||||
message: `Cloud type "${cloudType}" is not configured or no available drives`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.cookie) {
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'COOKIE_MISSING',
|
||||
message: `Cookie not configured for ${cloudType} drive #${config.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
const cookie = decryptCookie(config.cookie);
|
||||
|
||||
try {
|
||||
let cookieValid = false;
|
||||
if (cloudType === 'baidu') {
|
||||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||
const driver = new BaiduDriver({ cookie: cookie, nickname: config.nickname });
|
||||
cookieValid = await driver.validate();
|
||||
} else {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie: cookie, nickname: config.nickname });
|
||||
cookieValid = await driver.validate();
|
||||
}
|
||||
|
||||
if (!cookieValid) {
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||
).run(localTimestamp(), config.id);
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'COOKIE_EXPIRED',
|
||||
message: `Cookie expired or invalid for ${cloudType} drive #${config.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
config.cookie = cookie;
|
||||
return {
|
||||
valid: true,
|
||||
config,
|
||||
message: 'ok',
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
valid: false,
|
||||
errorCode: 'VALIDATION_ERROR',
|
||||
message: `Credential validation failed: ${err.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
// ===================================================================
|
||||
// 阿里云盘 API 统一模块
|
||||
// 所有阿里云盘 API 域名、端点集中管理
|
||||
// 换 API 只需改这个文件
|
||||
// ===================================================================
|
||||
|
||||
// ==================== 域名 ====================
|
||||
export const ALIYUN_API_HOST = 'https://api.aliyundrive.com';
|
||||
export const ALIYUN_WEB_HOST = 'https://www.aliyundrive.com';
|
||||
|
||||
// ==================== API 端点路径 ====================
|
||||
export const EP = {
|
||||
/** 匿名获取分享信息 (无需登录) */
|
||||
SHARE_GET_BY_ANONYMOUS: '/v2/share_link/get_share_by_anonymous',
|
||||
} as const;
|
||||
|
||||
// ==================== 请求头 ====================
|
||||
export function getShareHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'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',
|
||||
'Referer': ALIYUN_WEB_HOST + '/',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
// Native fetch available in Node 20+
|
||||
// @ts-nocheck
|
||||
// 阿里云盘驱动 — 分享链接验证
|
||||
// 依赖 aliyun-api.ts 统一端点模块 — URL 不再硬编码
|
||||
import { ALIYUN_API_HOST, ALIYUN_WEB_HOST, EP as AE, getShareHeaders } from './aliyun-api';
|
||||
|
||||
export interface AliyunConfig {
|
||||
cookie?: string;
|
||||
@@ -7,7 +10,6 @@ export interface AliyunConfig {
|
||||
|
||||
export class AliyunDriver {
|
||||
private config: AliyunConfig;
|
||||
private baseUrl = 'https://api.aliyundrive.com';
|
||||
|
||||
constructor(config: AliyunConfig = {}) {
|
||||
this.config = config;
|
||||
@@ -38,13 +40,6 @@ export class AliyunDriver {
|
||||
/**
|
||||
* Validate a share link using Aliyun's public anonymous API.
|
||||
* No cookie or token required — this endpoint is open.
|
||||
*
|
||||
* API:
|
||||
* POST https://api.aliyundrive.com/v2/share_link/get_share_by_anonymous
|
||||
* Body: { "share_id": "XXXYYY", "share_pwd": "" }
|
||||
*
|
||||
* Success: returns share_name, file_infos, creator info
|
||||
* Failure: returns error code (ShareLinkExpired, ShareLinkCancelled, etc.)
|
||||
*/
|
||||
async validateShareLink(shareUrl: string): Promise<{
|
||||
valid: boolean;
|
||||
@@ -59,15 +54,10 @@ export class AliyunDriver {
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/v2/share_link/get_share_by_anonymous`,
|
||||
ALIYUN_API_HOST + AE.SHARE_GET_BY_ANONYMOUS,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'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',
|
||||
'Referer': 'https://www.aliyundrive.com/',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
},
|
||||
headers: getShareHeaders(),
|
||||
body: JSON.stringify({
|
||||
share_id: shareId,
|
||||
share_pwd: '',
|
||||
@@ -77,37 +67,42 @@ export class AliyunDriver {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { valid: false, message: `HTTP ${response.status}: API 请求失败` };
|
||||
return { valid: false, message: 'HTTP ' + response.status + ': API 请求失败' };
|
||||
}
|
||||
|
||||
const data = await response.json() as any;
|
||||
const data = await response.json();
|
||||
|
||||
// Check for error codes
|
||||
if (data.code) {
|
||||
switch (data.code) {
|
||||
case 'ShareLinkExpired':
|
||||
return { valid: false, message: '分享已失效(已过期)' };
|
||||
case 'ShareLinkCancelled':
|
||||
return { valid: false, message: '分享已被取消' };
|
||||
case 'NotFound.ShareLink':
|
||||
return { valid: false, message: '分享链接不存在' };
|
||||
case 'ShareLinkPasswordIncorrect':
|
||||
return { valid: true, message: '需要提取码(链接有效)' };
|
||||
default:
|
||||
return { valid: false, message: data.message || `未知错误 (${data.code})` };
|
||||
}
|
||||
const errorMessages: Record<string, string> = {
|
||||
'ShareLinkExpired': '分享链接已过期',
|
||||
'ShareLinkCancelled': '分享链接已取消',
|
||||
'ShareLinkNotFound': '分享链接不存在',
|
||||
'ForbiddenFileInTheDrive': '文件已被禁止访问',
|
||||
'UserNotAuthenticated': '需要登录才能访问',
|
||||
'InvalidResource.NotFound': '资源不存在',
|
||||
};
|
||||
return {
|
||||
valid: false,
|
||||
message: errorMessages[data.code] || data.message || 'API 返回错误: ' + data.code,
|
||||
};
|
||||
}
|
||||
|
||||
// Success — valid share link
|
||||
const fileInfos = data.file_infos || [];
|
||||
// 成功:提取分享信息
|
||||
const fileCount = data.file_infos?.length || 0;
|
||||
const shareName = data.share_name || data.display_name || '';
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: `有效链接(${fileInfos.length} 个文件)`,
|
||||
fileCount: fileInfos.length,
|
||||
shareName: data.share_name || '',
|
||||
message: '链接有效',
|
||||
fileCount,
|
||||
shareName,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { valid: false, message: `网络错误: ${err.message || err}` };
|
||||
} catch (err) {
|
||||
return { valid: false, message: '网络错误: ' + (err instanceof Error ? err.message : String(err)) };
|
||||
}
|
||||
}
|
||||
|
||||
async getNickname(): Promise<string | null> {
|
||||
return this.config.nickname || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
// ===================================================================
|
||||
// 百度网盘 API 统一模块
|
||||
// 所有百度网盘 API 域名、端点、请求头集中管理
|
||||
// 换 API 只需改这个文件
|
||||
// ===================================================================
|
||||
|
||||
// ==================== 域名 ====================
|
||||
// 修改百度网盘域名只需改下面几行
|
||||
export const BAIDU_PAN_HOST = 'https://pan.baidu.com';
|
||||
export const BAIDU_PASSPORT_HOST = 'https://passport.baidu.com';
|
||||
export const BAIDU_WAPPASS_HOST = 'https://wappass.baidu.com';
|
||||
|
||||
// ==================== API 端点路径 ====================
|
||||
// 所有百度网盘 API 路径集中在此,换版本只需改这里
|
||||
export const EP = {
|
||||
// ── 认证/用户 ──
|
||||
GET_TEMPLATE_VARIABLE: '/api/gettemplatevariable', // GET 获取 bdstoken/token/uk/servertime
|
||||
USER_INFO: '/api/userinfo', // GET 用户信息
|
||||
QUOTA: '/api/quota', // GET 容量信息
|
||||
|
||||
// ── 文件操作 ──
|
||||
FILE_LIST: '/api/list', // GET 列出文件 (dir, order, page, num)
|
||||
FILE_CREATE: '/api/create', // POST 创建文件夹 (a=commit)
|
||||
FILE_DELETE: '/api/filemanager', // POST 删除文件 (opera=delete)
|
||||
|
||||
// ── 分享 ──
|
||||
SHARE_VERIFY: '/share/verify', // GET 验证分享链接/密码 (surl, bdstoken, t, channel, web, clienttype)
|
||||
SHARE_TRANSFER: '/share/transfer', // POST 转存分享文件 (shareid, from, bdstoken)
|
||||
SHARE_SET: '/share/set', // POST 创建分享链接 (bdstoken, channel, web, clienttype, app_id)
|
||||
|
||||
// ── NAS ──
|
||||
NAS_UINFO: '/rest/2.0/xpan/nas', // GET NAS 用户信息 (method=uinfo)
|
||||
|
||||
// ── 浏览器页面 (非 API,供 Playwright 导航) ──
|
||||
PAGE_LOGIN_QR: '/v2/', // QR 登录页 (?login&qrlogin&tpl=netdisk)
|
||||
PAGE_DISK_HOME: '/disk/home', // 网盘首页
|
||||
PAGE_QR_GEN: '/wp/', // 二维码生成 (?qrlogin&t=&error=0...)
|
||||
} as const;
|
||||
|
||||
// ==================== 请求头 ====================
|
||||
/** 构建标准百度网盘 API 请求头 */
|
||||
export function buildHeaders(cookie: string): Record<string, string> {
|
||||
const base: Record<string, string> = {
|
||||
'Host': 'pan.baidu.com',
|
||||
'Connection': 'keep-alive',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
||||
'Referer': BAIDU_PAN_HOST,
|
||||
};
|
||||
if (cookie) {
|
||||
base['Cookie'] = cookie;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BAIDU_PAN_HOST, BAIDU_PASSPORT_HOST, BAIDU_WAPPASS_HOST, EP as BE, buildHeaders as bdHeaders } from './baidu-api';
|
||||
// Baidu Netdisk Driver v4 — Cookie-based (Playwright QR login + HTTP API)
|
||||
const APP_ID_WEB = "38824127"; // Web app ID from BaiduPanFilesTransfers
|
||||
// Uses full browser Cookie string for all operations (no OAuth access_token needed).
|
||||
// Share operations use internal web API (/share/verify, /share/transfer, parse HTML).
|
||||
// Reference: https://github.com/hxz393/BaiduPanFilesTransfers
|
||||
@@ -36,28 +38,11 @@ interface ShareDetail {
|
||||
// ═══════════════════════════════════
|
||||
// Constants
|
||||
// ═══════════════════════════════════
|
||||
const API_HOST = "https://pan.baidu.com";
|
||||
import config from '../../config';
|
||||
const CHROMIUM_PATH = config.chromiumPath;
|
||||
const APP_ID_WEB = "38824127"; // Web app ID from BaiduPanFilesTransfers
|
||||
|
||||
// HTTP headers matching BaiduPanFilesTransfers
|
||||
const WEB_HEADERS: Record<string, string> = {
|
||||
'Host': 'pan.baidu.com',
|
||||
'Connection': 'keep-alive',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://pan.baidu.com',
|
||||
};
|
||||
|
||||
function buildHeaders(cookie: string): Record<string, string> {
|
||||
if (cookie) {
|
||||
return { ...WEB_HEADERS, 'Cookie': cookie };
|
||||
}
|
||||
return { ...WEB_HEADERS };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// Playwright singleton for QR login
|
||||
@@ -156,9 +141,9 @@ export class BaiduDriver {
|
||||
if (!cookie) return null;
|
||||
|
||||
try {
|
||||
const url = `${API_HOST}/api/gettemplatevariable?clienttype=0&app_id=${APP_ID_WEB}&web=1&fields=["bdstoken","token","uk","isdocuser","servertime"]`;
|
||||
const url = `${BAIDU_PAN_HOST}/api/gettemplatevariable?clienttype=0&app_id=${APP_ID_WEB}&web=1&fields=["bdstoken","token","uk","isdocuser","servertime"]`;
|
||||
const res = await fetch(url, {
|
||||
headers: buildHeaders(cookie),
|
||||
headers: bdHeaders(cookie),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -213,7 +198,7 @@ export class BaiduDriver {
|
||||
|
||||
// Navigate directly to passport QR login page (the actual login page with QR code)
|
||||
console.log('[BaiduQR] Navigating to passport QR login...');
|
||||
await page.goto('https://passport.baidu.com/v2/?login&qrlogin&tpl=netdisk', { waitUntil: 'commit', timeout: 30000 });
|
||||
await page.goto('BAIDU_PASSPORT_HOST + BE.PAGE_LOGIN_QR?login&qrlogin&tpl=netdisk', { waitUntil: 'commit', timeout: 30000 });
|
||||
await page.waitForTimeout(4000);
|
||||
|
||||
// Check if we landed on the right page
|
||||
@@ -248,7 +233,7 @@ export class BaiduDriver {
|
||||
const sign = imgUrlObj.searchParams.get('sign') || '';
|
||||
const logPage = imgUrlObj.searchParams.get('logPage') || '';
|
||||
const t = Math.floor(Date.now() / 1000);
|
||||
qrContent = `https://wappass.baidu.com/wp/?qrlogin&t=${t}&error=0&sign=${sign}&cmd=login&lp=pc&tpl=netdisk&adapter=3&logPage=${encodeURIComponent(logPage)}&qrloginfrom=pc`;
|
||||
qrContent = `BAIDU_WAPPASS_HOST + BE.PAGE_QR_GEN?qrlogin&t=${t}&error=0&sign=${sign}&cmd=login&lp=pc&tpl=netdisk&adapter=3&logPage=${encodeURIComponent(logPage)}&qrloginfrom=pc`;
|
||||
} catch {
|
||||
qrContent = qrImgSrc; // fallback: raw image URL
|
||||
}
|
||||
@@ -333,7 +318,7 @@ export class BaiduDriver {
|
||||
|
||||
// Navigate to disk home to ensure cookies are fully set
|
||||
try {
|
||||
await page.goto('https://pan.baidu.com/disk/home', { waitUntil: 'commit', timeout: 15000 });
|
||||
await page.goto('BAIDU_PAN_HOST + BE.PAGE_DISK_HOME', { waitUntil: 'commit', timeout: 15000 });
|
||||
await page.waitForTimeout(2000);
|
||||
} catch {}
|
||||
|
||||
@@ -350,8 +335,8 @@ export class BaiduDriver {
|
||||
let bdstoken = '';
|
||||
try {
|
||||
const bdres = await fetch(
|
||||
`${API_HOST}/api/gettemplatevariable?clienttype=0&app_id=${APP_ID_WEB}&web=1&fields=["bdstoken","uk"]`,
|
||||
{ headers: buildHeaders(cookieStr), signal: AbortSignal.timeout(10000) }
|
||||
`${BAIDU_PAN_HOST}/api/gettemplatevariable?clienttype=0&app_id=${APP_ID_WEB}&web=1&fields=["bdstoken","uk"]`,
|
||||
{ headers: bdHeaders(cookieStr), signal: AbortSignal.timeout(10000) }
|
||||
);
|
||||
if (bdres.ok) {
|
||||
const bddata = await bdres.json() as any;
|
||||
@@ -366,8 +351,8 @@ export class BaiduDriver {
|
||||
let storage_total = '';
|
||||
if (bdstoken) {
|
||||
try {
|
||||
const qRes = await fetch(`${API_HOST}/api/quota?checkfree=1&checkexpire=1&bdstoken=${bdstoken}`, {
|
||||
headers: buildHeaders(cookieStr),
|
||||
const qRes = await fetch(`${BAIDU_PAN_HOST}/api/quota?checkfree=1&checkexpire=1&bdstoken=${bdstoken}`, {
|
||||
headers: bdHeaders(cookieStr),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (qRes.ok) {
|
||||
@@ -389,8 +374,8 @@ export class BaiduDriver {
|
||||
// Get nickname from Baidu REST API (baidu_name field)
|
||||
if (bdstoken) {
|
||||
try {
|
||||
const uRes = await fetch(`${API_HOST}/rest/2.0/xpan/nas?method=uinfo`, {
|
||||
headers: buildHeaders(cookieStr),
|
||||
const uRes = await fetch(`${BAIDU_PAN_HOST}/rest/2.0/xpan/nas?method=uinfo`, {
|
||||
headers: bdHeaders(cookieStr),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (uRes.ok) {
|
||||
@@ -459,8 +444,8 @@ export class BaiduDriver {
|
||||
let totalBytes = 0;
|
||||
|
||||
// Try to get user info from /api/userinfo
|
||||
const uRes = await fetch(`${API_HOST}/api/userinfo?act=getuserinfo&bdstoken=${await this.getBdstoken()}`, {
|
||||
headers: buildHeaders(cookie),
|
||||
const uRes = await fetch(`${BAIDU_PAN_HOST}/api/userinfo?act=getuserinfo&bdstoken=${await this.getBdstoken()}`, {
|
||||
headers: bdHeaders(cookie),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (uRes.ok) {
|
||||
@@ -472,8 +457,8 @@ export class BaiduDriver {
|
||||
|
||||
// Get quota
|
||||
try {
|
||||
const qRes = await fetch(`${API_HOST}/api/quota?checkfree=1&checkexpire=1&bdstoken=${await this.getBdstoken()}`, {
|
||||
headers: buildHeaders(cookie),
|
||||
const qRes = await fetch(`${BAIDU_PAN_HOST}/api/quota?checkfree=1&checkexpire=1&bdstoken=${await this.getBdstoken()}`, {
|
||||
headers: bdHeaders(cookie),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (qRes.ok) {
|
||||
@@ -514,9 +499,9 @@ export class BaiduDriver {
|
||||
if (!bdstoken) return [];
|
||||
|
||||
try {
|
||||
const url = `${API_HOST}/api/list?order=time&desc=1&showempty=0&web=1&page=1&num=1000&dir=/&bdstoken=${bdstoken}`;
|
||||
const url = `${BAIDU_PAN_HOST}/api/list?order=time&desc=1&showempty=0&web=1&page=1&num=1000&dir=/&bdstoken=${bdstoken}`;
|
||||
const res = await fetch(url, {
|
||||
headers: buildHeaders(cookie),
|
||||
headers: bdHeaders(cookie),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
@@ -544,11 +529,11 @@ export class BaiduDriver {
|
||||
if (!bdstoken) return false;
|
||||
|
||||
try {
|
||||
const url = `${API_HOST}/api/create?a=commit&bdstoken=${bdstoken}`;
|
||||
const url = `${BAIDU_PAN_HOST}/api/create?a=commit&bdstoken=${bdstoken}`;
|
||||
const body = new URLSearchParams({ path, isdir: '1', block_list: '[]' });
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { ...buildHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: { ...bdHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
@@ -590,9 +575,9 @@ export class BaiduDriver {
|
||||
try {
|
||||
const filelist = JSON.stringify(fsIds);
|
||||
const body = new URLSearchParams({ async: '2', filelist });
|
||||
const res = await fetch(`${API_HOST}/api/filemanager?opera=delete&bdstoken=${bdstoken}`, {
|
||||
const res = await fetch(`${BAIDU_PAN_HOST}/api/filemanager?opera=delete&bdstoken=${bdstoken}`, {
|
||||
method: 'POST',
|
||||
headers: { ...buildHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: { ...bdHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
@@ -651,12 +636,12 @@ export class BaiduDriver {
|
||||
if (pwd) {
|
||||
console.log(`[Baidu:Share] Verifying password for surl=${surl}...`);
|
||||
const t = String(Date.now());
|
||||
const verifyUrl = `${API_HOST}/share/verify?surl=${surl}&bdstoken=${bdstoken}&t=${t}&channel=chunlei&web=1&clienttype=0`;
|
||||
const verifyUrl = `${BAIDU_PAN_HOST}/share/verify?surl=${surl}&bdstoken=${bdstoken}&t=${t}&channel=chunlei&web=1&clienttype=0`;
|
||||
const verifyBody = new URLSearchParams({ pwd, vcode: '', vcode_str: '' });
|
||||
|
||||
const vRes = await fetch(verifyUrl, {
|
||||
method: 'POST',
|
||||
headers: { ...buildHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: { ...bdHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: verifyBody.toString(),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
@@ -686,7 +671,7 @@ export class BaiduDriver {
|
||||
const shareUrl = `https://pan.baidu.com/s/1${surl}`;
|
||||
console.log(`[Baidu:Share] Fetching share page: ${shareUrl}`);
|
||||
const sRes = await fetch(shareUrl, {
|
||||
headers: buildHeaders(workingCookie),
|
||||
headers: bdHeaders(workingCookie),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
redirect: 'follow',
|
||||
});
|
||||
@@ -783,12 +768,12 @@ export class BaiduDriver {
|
||||
// Verify password first if needed
|
||||
if (pwd) {
|
||||
const t = String(Date.now());
|
||||
const vUrl = `${API_HOST}/share/verify?surl=${surl}&bdstoken=${bdstoken}&t=${t}&channel=chunlei&web=1&clienttype=0`;
|
||||
const vUrl = `${BAIDU_PAN_HOST}/share/verify?surl=${surl}&bdstoken=${bdstoken}&t=${t}&channel=chunlei&web=1&clienttype=0`;
|
||||
const vBody = new URLSearchParams({ pwd, vcode: '', vcode_str: '' });
|
||||
|
||||
const vRes = await fetch(vUrl, {
|
||||
method: 'POST',
|
||||
headers: { ...buildHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: { ...bdHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: vBody.toString(),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
@@ -805,7 +790,7 @@ export class BaiduDriver {
|
||||
|
||||
// Get share page to extract shareid + uk
|
||||
const sRes = await fetch(shareUrl, {
|
||||
headers: buildHeaders(workingCookie),
|
||||
headers: bdHeaders(workingCookie),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
redirect: 'follow',
|
||||
});
|
||||
@@ -827,7 +812,7 @@ export class BaiduDriver {
|
||||
const fsidlist = `[${fsIds.join(',')}]`;
|
||||
const path = destPath === '/' ? '/' : `/${destPath.replace(/^\//, '')}`;
|
||||
|
||||
const tUrl = `${API_HOST}/share/transfer?shareid=${shareid}&from=${uk}&bdstoken=${bdstoken}&channel=chunlei&web=1&clienttype=0`;
|
||||
const tUrl = `${BAIDU_PAN_HOST}/share/transfer?shareid=${shareid}&from=${uk}&bdstoken=${bdstoken}&channel=chunlei&web=1&clienttype=0`;
|
||||
const tBody = new URLSearchParams({ fsidlist, path });
|
||||
|
||||
// Retry up to 3 times for transient fetch failures
|
||||
@@ -837,7 +822,7 @@ export class BaiduDriver {
|
||||
try {
|
||||
tRes = await fetch(tUrl, {
|
||||
method: 'POST',
|
||||
headers: { ...buildHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: { ...bdHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: tBody.toString(),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
@@ -1020,8 +1005,8 @@ export class BaiduDriver {
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_HOST}/api/list?dir=${encodeURIComponent(parentPath)}&bdstoken=${bdstoken}&order=time&desc=1`,
|
||||
{ headers: buildHeaders(cookie), signal: AbortSignal.timeout(10000) }
|
||||
`${BAIDU_PAN_HOST}/api/list?dir=${encodeURIComponent(parentPath)}&bdstoken=${bdstoken}&order=time&desc=1`,
|
||||
{ headers: bdHeaders(cookie), signal: AbortSignal.timeout(10000) }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json() as any;
|
||||
@@ -1051,10 +1036,10 @@ export class BaiduDriver {
|
||||
pwd,
|
||||
});
|
||||
|
||||
const url = `${API_HOST}/share/set?bdstoken=${bdstoken}&channel=chunlei&web=1&clienttype=0&app_id=250528`;
|
||||
const url = `${BAIDU_PAN_HOST}/share/set?bdstoken=${bdstoken}&channel=chunlei&web=1&clienttype=0&app_id=250528`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { ...buildHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: { ...bdHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,204 +1,120 @@
|
||||
// @ts-nocheck
|
||||
import * as quark_api from "./quark-api";
|
||||
import * as system_config_service from "../../admin/system-config.service";
|
||||
|
||||
/**
|
||||
* 广告关键词清理模块。
|
||||
* 在转存完成后执行:
|
||||
* 1. 遍历转存的目录,删除文件名/文件夹名含广告关键词的内容
|
||||
* 2. 在转存根目录下创建警示文件夹(置顶提醒)
|
||||
* 夸克网盘 — 广告关键词清理模块
|
||||
* 转存完成后:删除广告文件名 + 创建警示文件夹
|
||||
* 依赖 quark-api.ts 统一端点模块
|
||||
*/
|
||||
import * as q from './quark-api';
|
||||
import * as system_config_service from '../../admin/system-config.service';
|
||||
|
||||
// ==================== 配置读取 ====================
|
||||
/** 从 DB 读取广告关键词列表 */
|
||||
|
||||
/** 从数据库读取广告关键词列表 */
|
||||
export function getAdKeywords() {
|
||||
const raw = system_config_service.getSystemConfig("quark_ad_keywords") || "";
|
||||
return raw
|
||||
.split("\n")
|
||||
.flatMap((line) => line.split(","))
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const raw = system_config_service.getSystemConfig('quark_ad_keywords') || '';
|
||||
return raw.split('\n').flatMap(line => line.split(',')).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
/** 从 DB 读取警示文件夹名称列表 */
|
||||
|
||||
/** 从数据库读取警示文件夹名称列表 */
|
||||
export function getWarningFolderNames() {
|
||||
const raw = system_config_service.getSystemConfig("quark_warning_folder_names") || "";
|
||||
return raw
|
||||
.split("\n")
|
||||
.flatMap((line) => line.split(","))
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const raw = system_config_service.getSystemConfig('quark_warning_folder_names') || '';
|
||||
return raw.split('\n').flatMap(line => line.split(',')).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
/** 从 DB 读取可疑文件后缀列表 */
|
||||
|
||||
/** 从数据库读取可疑文件后缀列表 */
|
||||
export function getSusExtensions() {
|
||||
const raw = system_config_service.getSystemConfig("quark_sus_extensions") || "";
|
||||
const raw = system_config_service.getSystemConfig('quark_sus_extensions') || '';
|
||||
if (raw.trim()) {
|
||||
return raw
|
||||
.split("\n")
|
||||
.map((s) => s.trim().toLowerCase().replace(/^\./, ""))
|
||||
.filter(Boolean);
|
||||
return raw.split('\n').map(s => s.trim().toLowerCase().replace(/^\./, '')).filter(Boolean);
|
||||
}
|
||||
// 默认可疑后缀
|
||||
return ["bat", "exe", "vbs", "scr", "cmd", "com", "pif", "js", "jar", "msi", "reg", "inf", "ps1"];
|
||||
return ['bat', 'exe', 'vbs', 'scr', 'cmd', 'com', 'pif', 'js', 'jar', 'msi', 'reg', 'inf', 'ps1'];
|
||||
}
|
||||
|
||||
// ==================== 关键词检测 ====================
|
||||
|
||||
/** 检查文件名是否包含任意广告关键词 */
|
||||
export function containsAdKeyword(fileName, keywords) {
|
||||
if (!keywords.length)
|
||||
return false;
|
||||
if (!keywords.length) return false;
|
||||
const lower = fileName.toLowerCase();
|
||||
return keywords.some((kw) => kw && lower.includes(kw.toLowerCase()));
|
||||
return keywords.some(kw => kw && lower.includes(kw.toLowerCase()));
|
||||
}
|
||||
|
||||
// ==================== 删除操作 ====================
|
||||
|
||||
/**
|
||||
* 遍历指定目录(含子目录),删除匹配广告关键词的文件和文件夹。
|
||||
* 返回删除的文件数。
|
||||
* 遍历指定目录(含子目录),删除匹配广告关键词的文件和文件夹
|
||||
* 返回删除的文件数
|
||||
*/
|
||||
export async function deleteAdFiles(cookie, dirFid, keywords) {
|
||||
const extensions = getSusExtensions();
|
||||
if (!keywords.length && !extensions.length)
|
||||
return 0;
|
||||
if (!keywords.length && !extensions.length) return 0;
|
||||
let deletedCount = 0;
|
||||
const stack = [dirFid];
|
||||
const visited = new Set();
|
||||
const visited = new Set<string>();
|
||||
while (stack.length > 0) {
|
||||
const fid = stack.pop();
|
||||
if (visited.has(fid))
|
||||
continue;
|
||||
const fid = stack.pop()!;
|
||||
if (visited.has(fid)) continue;
|
||||
visited.add(fid);
|
||||
await quark_api.humanDelay();
|
||||
const files = await quark_api.listDir(cookie, fid);
|
||||
if (!files || files.length === 0)
|
||||
continue;
|
||||
// 先收集所有需要删除的 fid
|
||||
const toDelete = [];
|
||||
const toKeep = [];
|
||||
const extensions = getSusExtensions();
|
||||
await q.humanDelay();
|
||||
const files = await q.listDir(cookie, fid);
|
||||
if (!files || files.length === 0) continue;
|
||||
const toDelete: string[] = [];
|
||||
for (const file of files) {
|
||||
const ext = file.file_name.split(".").pop()?.toLowerCase() || "";
|
||||
const ext = file.file_name.split('.').pop()?.toLowerCase() || '';
|
||||
const isSusExt = extensions.includes(ext);
|
||||
if (containsAdKeyword(file.file_name, keywords) || isSusExt) {
|
||||
toDelete.push(file.fid);
|
||||
console.log(`[Quark-AdCleanup] 标记删除: "${file.file_name}" (fid: ${file.fid})${isSusExt ? " [可疑后缀]" : " [广告关键词]"}`);
|
||||
}
|
||||
else {
|
||||
toKeep.push(file.fid);
|
||||
// 如果是目录且不删除,继续遍历子目录
|
||||
if (file.dir) {
|
||||
stack.push(file.fid);
|
||||
}
|
||||
const reason = isSusExt ? '[可疑后缀]' : '[广告关键词]';
|
||||
console.log('[Quark-AdCleanup] 标记删除: "' + file.file_name + '" (fid: ' + file.fid + ') ' + reason);
|
||||
} else if (file.dir) {
|
||||
stack.push(file.fid);
|
||||
}
|
||||
}
|
||||
// 批量删除
|
||||
if (toDelete.length > 0) {
|
||||
const deleteOk = await batchDeleteFiles(cookie, toDelete);
|
||||
const deleteOk = await q.permanentDelete(cookie, toDelete);
|
||||
if (deleteOk) {
|
||||
deletedCount += toDelete.length;
|
||||
console.log(`[Quark-AdCleanup] 已删除 ${toDelete.length} 个广告文件`);
|
||||
console.log('[Quark-AdCleanup] 已删除 ' + toDelete.length + ' 个广告文件');
|
||||
}
|
||||
}
|
||||
}
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
// ==================== 警示文件夹 ====================
|
||||
|
||||
/**
|
||||
* 批量删除文件/文件夹(移入回收站)。
|
||||
* 在转存根目录下创建警示文件夹
|
||||
* 文件夹名前加 emoji 和空格,让其按字母排序置顶
|
||||
*/
|
||||
async function batchDeleteFiles(cookie, fids) {
|
||||
if (!fids.length)
|
||||
return true;
|
||||
try {
|
||||
const resp = await fetch(`https://drive-pc.quark.cn/1/clouddrive/file/trash?${quark_api.makeQuery()}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...quark_api.getHeaders(cookie),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action_type: 1,
|
||||
filelist: fids,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.log(`[Quark-AdCleanup] batchDelete HTTP ${resp.status}`);
|
||||
return false;
|
||||
}
|
||||
const data = (await resp.json());
|
||||
if (data.status === 200) {
|
||||
return true;
|
||||
}
|
||||
console.log(`[Quark-AdCleanup] batchDelete 返回非200: status=${data.status} msg=${data.message}`);
|
||||
return false;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(`[Quark-AdCleanup] batchDelete 错误: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ==================== 警示文件夹创建 ====================
|
||||
/**
|
||||
* 在转存根目录下创建警示文件夹。
|
||||
* 文件夹名前加 ⚠️ 和空格,让其按字母排序置顶。
|
||||
* 已存在的则跳过。
|
||||
*/
|
||||
export async function createWarningDirectories(cookie, dirNames, parentDirFid = "0") {
|
||||
if (!dirNames.length)
|
||||
return;
|
||||
// 先获取根目录下所有文件夹,避免重复创建
|
||||
await quark_api.humanDelay();
|
||||
const rootFiles = await quark_api.listDirAllPages(cookie, parentDirFid);
|
||||
const existingDirs = new Set(rootFiles.filter((f) => f.dir).map((f) => f.file_name));
|
||||
export async function createWarningDirectories(cookie, dirNames, parentDirFid = '0') {
|
||||
if (!dirNames.length) return;
|
||||
await q.humanDelay();
|
||||
const rootFiles = await q.listDirAllPages(cookie, parentDirFid);
|
||||
const existingDirs = new Set(rootFiles.filter(f => f.dir).map(f => f.file_name));
|
||||
for (const name of dirNames) {
|
||||
// 格式化名称:确保以 ⚠️ 开头
|
||||
let formattedName = name;
|
||||
if (!formattedName.startsWith("⚠️") && !formattedName.startsWith("⚠")) {
|
||||
formattedName = `⚠️ ${formattedName}`;
|
||||
if (!formattedName.startsWith('\u26a0\ufe0f') && !formattedName.startsWith('\u26a0')) {
|
||||
formattedName = '\u26a0\ufe0f ' + formattedName;
|
||||
}
|
||||
// 去掉多余空格
|
||||
formattedName = formattedName.replace(/\s+/g, " ").trim();
|
||||
formattedName = formattedName.replace(/\s+/g, ' ').trim();
|
||||
if (existingDirs.has(formattedName)) {
|
||||
console.log(`[Quark-AdCleanup] 警示文件夹已存在,跳过: "${formattedName}"`);
|
||||
console.log('[Quark-AdCleanup] 警示文件夹已存在,跳过: "' + formattedName + '"');
|
||||
continue;
|
||||
}
|
||||
await createSingleDir(cookie, formattedName, parentDirFid);
|
||||
// 加入已存在集合,防止同名重试
|
||||
const result = await q.quarkCreateDir(cookie, formattedName, parentDirFid);
|
||||
if (result) {
|
||||
console.log('[Quark-AdCleanup] 已创建警示文件夹: "' + formattedName + '" (fid: ' + result.fid + ')');
|
||||
}
|
||||
existingDirs.add(formattedName);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建单个文件夹。
|
||||
*/
|
||||
async function createSingleDir(cookie, dirName, pdirFid = "0") {
|
||||
try {
|
||||
const resp = await fetch(`https://drive-pc.quark.cn/1/clouddrive/file?${quark_api.makeQuery()}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...quark_api.getHeaders(cookie),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pdir_fid: pdirFid,
|
||||
file_name: dirName,
|
||||
dir: true,
|
||||
dir_path: "",
|
||||
}),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
const data = (await resp.json());
|
||||
if (data.status === 200 && data.data?.fid) {
|
||||
console.log(`[Quark-AdCleanup] 已创建警示文件夹: "${dirName}" (fid: ${data.data.fid})`);
|
||||
return true;
|
||||
}
|
||||
console.log(`[Quark-AdCleanup] 创建文件夹失败: status=${data.status} msg=${data.message}`);
|
||||
return false;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(`[Quark-AdCleanup] 创建文件夹错误: "${dirName}" — ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 主入口 ====================
|
||||
|
||||
/**
|
||||
* 执行广告清理 + 创建警示文件夹。
|
||||
* 在转存重命名后调用。
|
||||
* 执行广告清理 + 创建警示文件夹
|
||||
* 在转存重命名后调用
|
||||
*/
|
||||
export async function runAdCleanup(cookie, savedDirFid) {
|
||||
const keywords = getAdKeywords();
|
||||
@@ -206,24 +122,23 @@ export async function runAdCleanup(cookie, savedDirFid) {
|
||||
const warningNames = getWarningFolderNames();
|
||||
let adDeleted = 0;
|
||||
let warningDirs = 0;
|
||||
// 1. 广告关键词 + 可疑后缀清理
|
||||
|
||||
if (keywords.length > 0 || susExtensions.length > 0) {
|
||||
console.log(`[Quark-AdCleanup] 开始文件清理: ${keywords.length} 个关键词, ${susExtensions.length} 个可疑后缀`);
|
||||
console.log('[Quark-AdCleanup] 开始文件清理: ' + keywords.length + ' 个关键词, ' + susExtensions.length + ' 个可疑后缀');
|
||||
adDeleted = await deleteAdFiles(cookie, savedDirFid, keywords);
|
||||
console.log(`[Quark-AdCleanup] 清理完成,共删除 ${adDeleted} 个文件/文件夹`);
|
||||
console.log('[Quark-AdCleanup] 清理完成,共删除 ' + adDeleted + ' 个文件/文件夹');
|
||||
} else {
|
||||
console.log('[Quark-AdCleanup] 无关键词/可疑后缀配置,跳过清理');
|
||||
}
|
||||
else {
|
||||
console.log("[Quark-AdCleanup] 无关键词/可疑后缀配置,跳过清理");
|
||||
}
|
||||
// 2. 创建警示文件夹
|
||||
|
||||
if (warningNames.length > 0) {
|
||||
console.log(`[Quark-AdCleanup] 开始创建警示文件夹: ${warningNames.length} 个`);
|
||||
console.log('[Quark-AdCleanup] 开始创建警示文件夹: ' + warningNames.length + ' 个');
|
||||
await createWarningDirectories(cookie, warningNames, savedDirFid);
|
||||
warningDirs = warningNames.length;
|
||||
console.log(`[Quark-AdCleanup] 警示文件夹创建完成(共 ${warningDirs} 个)`);
|
||||
}
|
||||
else {
|
||||
console.log("[Quark-AdCleanup] 无警示文件夹配置,跳过创建");
|
||||
console.log('[Quark-AdCleanup] 警示文件夹创建完成(共 ' + warningDirs + ' 个)');
|
||||
} else {
|
||||
console.log('[Quark-AdCleanup] 无警示文件夹配置,跳过创建');
|
||||
}
|
||||
|
||||
return { adDeleted, warningDirs };
|
||||
}
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
// @ts-nocheck
|
||||
// ==================== Headers & Params ====================
|
||||
const BASE_URL = 'https://drive-pc.quark.cn';
|
||||
// ===================================================================
|
||||
// 夸克网盘 API 统一模块
|
||||
// 所有夸克 API 端点、请求头、工具函数集中管理
|
||||
// 换 API 只需改这个文件
|
||||
// ===================================================================
|
||||
|
||||
// ==================== 域名 ====================
|
||||
// 修改夸克 API 域名只需改下面两行
|
||||
export const QUARK_DRIVE_HOST = 'https://drive-pc.quark.cn';
|
||||
export const QUARK_PAN_HOST = 'https://pan.quark.cn';
|
||||
|
||||
// ==================== API 端点路径 ====================
|
||||
// 所有夸克 API 路径集中在此,换版本只需改这里
|
||||
export const EP = {
|
||||
// ── 账号 ──
|
||||
ACCOUNT_INFO: '/account/info', // GET 获取账号信息
|
||||
|
||||
// ── 文件操作 ──
|
||||
FILE: '/1/clouddrive/file', // POST 创建文件夹
|
||||
FILE_SORT: '/1/clouddrive/file/sort', // GET 列出目录文件
|
||||
FILE_RENAME: '/1/clouddrive/file/rename', // POST 重命名
|
||||
FILE_DELETE: '/1/clouddrive/file/delete', // POST 永久删除 (action_type=2)
|
||||
FILE_TRASH_CLEAR: '/1/clouddrive/file/trash/clear', // POST 清空回收站
|
||||
|
||||
// ── 分享 ──
|
||||
SHARE: '/1/clouddrive/share', // POST 创建分享链接
|
||||
SHARE_PASSWORD: '/1/clouddrive/share/password', // POST 设置分享密码
|
||||
SHARE_PAGE_TOKEN: '/1/clouddrive/share/sharepage/token', // GET 获取分享页 token
|
||||
SHARE_PAGE_DETAIL: '/1/clouddrive/share/sharepage/detail', // GET 获取分享详情
|
||||
SHARE_PAGE_SAVE: '/1/clouddrive/share/sharepage/save', // POST 保存分享文件到网盘
|
||||
|
||||
// ── 任务 ──
|
||||
TASK: '/1/clouddrive/task', // GET 查询异步任务状态
|
||||
|
||||
// ── 容量 ──
|
||||
CAPACITY_DETAIL: '/1/clouddrive/capacity/detail', // GET 容量详情
|
||||
MEMBER: '/1/clouddrive/member', // GET 会员/容量快速查询
|
||||
} as const;
|
||||
|
||||
// ==================== 请求头 ====================
|
||||
export function getHeaders(cookie) {
|
||||
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',
|
||||
'Referer': QUARK_PAN_HOST + '/',
|
||||
'Origin': QUARK_PAN_HOST,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 通用参数 ====================
|
||||
export function getCommonParams() {
|
||||
return { pr: 'ucpro', fr: 'pc' };
|
||||
}
|
||||
/** Generate query string with common params + random timing to mimic browser */
|
||||
|
||||
/** 生成浏览器风格的 query string(随机延迟参数) */
|
||||
export function makeQuery(extra = {}) {
|
||||
const __dt = Math.floor(Math.random() * 240000 + 60000);
|
||||
const __t = Date.now() / 1000;
|
||||
@@ -27,20 +68,21 @@ export function makeQuery(extra = {}) {
|
||||
...extra,
|
||||
}).toString();
|
||||
}
|
||||
/** Random delay to mimic human behavior (500-2000ms) */
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
/** 随机延迟 500~2000ms,模拟人类操作 */
|
||||
export async function humanDelay() {
|
||||
const ms = Math.floor(Math.random() * 1500) + 500;
|
||||
await new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
/** Generate a random password for share links */
|
||||
|
||||
/** 生成 4 位数字分享密码 */
|
||||
export function randomSharePwd() {
|
||||
return Math.floor(1000 + Math.random() * 9000).toString();
|
||||
}
|
||||
/**
|
||||
* Extract kps/sign/vcode from cookie for API signing (bare keys, no __ prefix).
|
||||
*/
|
||||
|
||||
/** 从 Cookie 中提取 kps/sign/vcode */
|
||||
export function getMparam(cookie) {
|
||||
// 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%+/=]+)/);
|
||||
@@ -53,16 +95,40 @@ export function getMparam(cookie) {
|
||||
}
|
||||
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(path, options) {
|
||||
const { method = 'GET', query, body, cookie, timeout = 10000 } = options;
|
||||
let url = `${BASE_URL}${path}`;
|
||||
if (query)
|
||||
url += `?${new URLSearchParams(query).toString()}`;
|
||||
|
||||
/** 格式化字节为人类可读字符串 */
|
||||
export function formatBytes(bytes) {
|
||||
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];
|
||||
}
|
||||
|
||||
/** 当天日期文件夹名 (YYYY-MM-DD) */
|
||||
export function dailyFolderName() {
|
||||
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}`;
|
||||
}
|
||||
|
||||
/** 随机 12 位字母数字文件夹名 */
|
||||
export function randomFolderName() {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let name = '';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
name += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// ==================== 底层 fetch 封装 ====================
|
||||
/** 通用 JSON fetch,返回解析后的 body 或 null */
|
||||
export async function apiFetch(endpoint, options) {
|
||||
const { method = 'GET', query, body, cookie, timeout = 10000, host = QUARK_DRIVE_HOST } = options;
|
||||
let url = host + endpoint;
|
||||
if (query) url += '?' + new URLSearchParams(query).toString();
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method,
|
||||
@@ -73,17 +139,15 @@ export async function apiFetch(path, options) {
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(timeout),
|
||||
});
|
||||
if (!resp.ok)
|
||||
return null;
|
||||
return (await resp.json());
|
||||
}
|
||||
catch {
|
||||
if (!resp.ok) return null;
|
||||
return await resp.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List files in a directory by FID.
|
||||
*/
|
||||
|
||||
// ==================== 目录操作 ====================
|
||||
/** 列出指定目录下的文件(单页) */
|
||||
export async function listDir(cookie, pdirFid, page = 1, pageSize = 50) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
@@ -98,27 +162,26 @@ export async function listDir(cookie, pdirFid, page = 1, pageSize = 50) {
|
||||
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 resp = await fetch(QUARK_DRIVE_HOST + EP.FILE_SORT + '?' + params.toString(), {
|
||||
headers: 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) => ({
|
||||
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: '',
|
||||
dir: f.dir || false,
|
||||
size: f.size || 0,
|
||||
}));
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List root directory (pdir_fid=0) — returns all top-level dirs/files.
|
||||
*/
|
||||
|
||||
/** 列出根目录文件 */
|
||||
export async function listRootDir(cookie) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
@@ -130,27 +193,25 @@ export async function listRootDir(cookie) {
|
||||
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 resp = await fetch(QUARK_DRIVE_HOST + EP.FILE_SORT + '?' + params.toString(), {
|
||||
headers: getHeaders(cookie),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!resp.ok) return [];
|
||||
const data = await resp.json();
|
||||
if (data.status !== 200 || !data.data?.list)
|
||||
return [];
|
||||
return (data.data.list || []).map((f) => ({
|
||||
if (data.status !== 200 || !data.data?.list) return [];
|
||||
return (data.data.list || []).map(f => ({
|
||||
fid: f.fid,
|
||||
file_name: f.file_name,
|
||||
dir: f.dir || false,
|
||||
size: f.size || 0,
|
||||
}));
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List all files in a directory, handling pagination.
|
||||
* Fetches all pages until no more results.
|
||||
*/
|
||||
|
||||
/** 列出目录全部文件(自动翻页) */
|
||||
export async function listDirAllPages(cookie, pdirFid) {
|
||||
const allFiles = [];
|
||||
let page = 1;
|
||||
@@ -158,38 +219,119 @@ export async function listDirAllPages(cookie, pdirFid) {
|
||||
let total = -1;
|
||||
while (total === -1 || (page - 1) * pageSize < total) {
|
||||
const files = await listDir(cookie, pdirFid, page, pageSize);
|
||||
if (!files.length)
|
||||
break;
|
||||
if (!files.length) break;
|
||||
allFiles.push(...files);
|
||||
if (total === -1) {
|
||||
total = files.length;
|
||||
}
|
||||
if (total === -1) total = files.length;
|
||||
page++;
|
||||
}
|
||||
return allFiles;
|
||||
}
|
||||
// ==================== Format utilities ====================
|
||||
export function formatBytes(bytes) {
|
||||
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() {
|
||||
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() {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let name = '';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
name += chars[Math.floor(Math.random() * chars.length)];
|
||||
|
||||
// ==================== 高级操作封装 ====================
|
||||
/**
|
||||
* 永久删除文件/文件夹
|
||||
* @param cookie 认证 cookie
|
||||
* @param fids 要删除的文件/文件夹 fid 数组
|
||||
*/
|
||||
export async function permanentDelete(cookie, fids) {
|
||||
if (!fids.length) return true;
|
||||
try {
|
||||
const resp = await fetch(QUARK_DRIVE_HOST + EP.FILE_DELETE + '?' + makeQuery(), {
|
||||
method: 'POST',
|
||||
headers: { ...getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action_type: 2, filelist: fids }),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
if (!resp.ok) return false;
|
||||
const data = await resp.json();
|
||||
return data.status === 200;
|
||||
} catch (err) {
|
||||
console.error('[Quark] permanentDelete error:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空回收站
|
||||
*/
|
||||
export async function emptyTrashApi(cookie) {
|
||||
try {
|
||||
const resp = await fetch(QUARK_DRIVE_HOST + EP.FILE_TRASH_CLEAR + '?' + makeQuery(), {
|
||||
method: 'POST',
|
||||
headers: { ...getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
signal: AbortSignal.timeout(60000),
|
||||
});
|
||||
if (!resp.ok) return false;
|
||||
const data = await resp.json();
|
||||
return data.status === 200;
|
||||
} catch (err) {
|
||||
console.error('[Quark] emptyTrashApi error:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件夹
|
||||
* @returns 成功返回 { fid, file_name },失败返回 null
|
||||
*/
|
||||
export async function quarkCreateDir(cookie, dirName, pdirFid = '0') {
|
||||
try {
|
||||
const resp = await fetch(QUARK_DRIVE_HOST + EP.FILE + '?' + makeQuery(), {
|
||||
method: 'POST',
|
||||
headers: { ...getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pdir_fid: pdirFid, file_name: dirName, dir: true, dir_path: '' }),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.status === 200 && data.data?.fid) {
|
||||
return { fid: data.data.fid, file_name: dirName };
|
||||
}
|
||||
console.log('[Quark] quarkCreateDir failed: status=' + data.status + ' msg=' + data.message);
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('[Quark] quarkCreateDir error:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员/容量信息 (快速接口)
|
||||
*/
|
||||
export async function getMemberInfo(cookie) {
|
||||
try {
|
||||
const params = new URLSearchParams({ pr: 'ucpro', fr: 'pc', uc_param_str: '', __t: String(Date.now()), __dt: '1000' });
|
||||
const resp = await fetch(QUARK_PAN_HOST + EP.MEMBER + '?' + params.toString(), {
|
||||
headers: getHeaders(cookie),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json();
|
||||
if (data.status === 200 && data.data) {
|
||||
return {
|
||||
usedBytes: data.data.use_capacity ?? 0,
|
||||
totalBytes: data.data.total_capacity ?? 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号信息 (验证 cookie 有效性)
|
||||
*/
|
||||
export async function getAccountInfo(cookie) {
|
||||
const url = QUARK_PAN_HOST + EP.ACCOUNT_INFO + '?fr=pc&platform=pc';
|
||||
try {
|
||||
const resp = await fetch(url, { headers: getHeaders(cookie), signal: AbortSignal.timeout(10000) });
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data.status === 200 && data.data) return data.data;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import * as quark_api from "./quark-api";
|
||||
import * as q from './quark-api';
|
||||
|
||||
/**
|
||||
* 认证模块 — Cookie 验证、账号信息获取、QR 登录状态检查。
|
||||
* 所有方法以 cookie 字符串为参数,不持有驱动状态。
|
||||
* 依赖 quark-api.ts 统一端点模块 — URL 不再硬编码。
|
||||
*/
|
||||
// ==================== Validate ====================
|
||||
/**
|
||||
@@ -13,12 +14,10 @@ export async function validate(cookie) {
|
||||
const MAX_RETRIES = 2;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
// Use account/info API (same as quark-auto-save project)
|
||||
// Only needs __uid cookie, no mparam (kps/sign/vcode) required
|
||||
const url = 'https://pan.quark.cn/account/info?fr=pc&platform=pc';
|
||||
const url = q.QUARK_PAN_HOST + q.EP.ACCOUNT_INFO + '?fr=pc&platform=pc';
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...quark_api.getHeaders(cookie),
|
||||
...q.getHeaders(cookie),
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
@@ -31,11 +30,11 @@ export async function validate(cookie) {
|
||||
}
|
||||
catch (err) {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
console.log(`[Quark] validate attempt ${attempt + 1} failed: ${err.message}, retrying...`);
|
||||
console.log('[Quark] validate attempt ' + (attempt + 1) + ' failed: ' + err.message + ', retrying...');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
continue;
|
||||
}
|
||||
console.log(`[Quark] validate all ${MAX_RETRIES + 1} attempts failed: ${err.message}`);
|
||||
console.log('[Quark] validate all ' + (MAX_RETRIES + 1) + ' attempts failed: ' + err.message);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -43,10 +42,10 @@ export async function validate(cookie) {
|
||||
/** Fetch nickname from Quark account info (same API used by quark-auto-save) */
|
||||
export async function fetchNickname(cookie) {
|
||||
try {
|
||||
const url = 'https://pan.quark.cn/account/info?fr=pc&platform=pc';
|
||||
const url = q.QUARK_PAN_HOST + q.EP.ACCOUNT_INFO + '?fr=pc&platform=pc';
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...quark_api.getHeaders(cookie),
|
||||
...q.getHeaders(cookie),
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// @ts-nocheck
|
||||
import * as quark_api from "./quark-api";
|
||||
|
||||
/**
|
||||
* 认证模块 — Cookie 验证、账号信息获取、QR 登录状态检查。
|
||||
* 所有方法以 cookie 字符串为参数,不持有驱动状态。
|
||||
*/
|
||||
// ==================== Validate ====================
|
||||
/**
|
||||
* Validate the cookie by fetching user info.
|
||||
*/
|
||||
export async function validate(cookie) {
|
||||
const MAX_RETRIES = 2;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
// Use account/info API (same as quark-auto-save project)
|
||||
// Only needs __uid cookie, no mparam (kps/sign/vcode) required
|
||||
const url = 'https://pan.quark.cn/account/info?fr=pc&platform=pc';
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...quark_api.getHeaders(cookie),
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!response.ok)
|
||||
return false;
|
||||
const data = await response.json();
|
||||
if (data?.data?.nickname)
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
console.log(`[Quark] validate attempt ${attempt + 1} failed: ${err.message}, retrying...`);
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
continue;
|
||||
}
|
||||
console.log(`[Quark] validate all ${MAX_RETRIES + 1} attempts failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** Fetch nickname from Quark account info (same API used by quark-auto-save) */
|
||||
export async function fetchNickname(cookie) {
|
||||
try {
|
||||
const url = 'https://pan.quark.cn/account/info?fr=pc&platform=pc';
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...quark_api.getHeaders(cookie),
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!response.ok)
|
||||
return null;
|
||||
const data = await response.json();
|
||||
return data?.data?.nickname || null;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,38 @@
|
||||
// @ts-nocheck
|
||||
import * as quark_api from "./quark-api";
|
||||
/**
|
||||
* 夸克网盘 — 容量查询 & 空间清理
|
||||
* 依赖 quark-api.ts 统一端点模块
|
||||
*/
|
||||
import * as q from './quark-api';
|
||||
|
||||
// ==================== 容量查询 ====================
|
||||
|
||||
/** 容量缓存 (3小时窗口) */
|
||||
const storageCache = { bytes: 0, hourBlock: -1 };
|
||||
|
||||
/**
|
||||
* 容量信息 & 空间清理模块。
|
||||
*/
|
||||
const BASE_URL = 'https://drive-pc.quark.cn';
|
||||
// ==================== Storage Info ====================
|
||||
/** Cached used space, keyed by hour block (3h window) */
|
||||
const cachedUsedSpace = null;
|
||||
// We use a function-scoped cache instead of instance field
|
||||
const storageCache = { bytes: 0, hourBlock: -1 };
|
||||
/**
|
||||
* Get total capacity from /capacity/detail API.
|
||||
* Also does a quick used-space estimate by summing root-level file sizes + subdir sizes
|
||||
* (夸克目录的 size 字段 = 该目录内所有文件总大小,无需递归).
|
||||
* If the API fails (e.g. missing sign params), falls back to fallbackTotal if provided.
|
||||
*/
|
||||
/**
|
||||
* Get storage info from /member API — single fast call returns both used & total capacity.
|
||||
* Falls back to capacity/detail + root file sum if member API fails.
|
||||
* 快速获取容量信息 (优先 /member API,回退到 capacity/detail + 根目录统计)
|
||||
*/
|
||||
export async function getStorageInfoQuick(cookie, fallbackTotal?) {
|
||||
// 优先使用 /member 快速接口
|
||||
const member = await q.getMemberInfo(cookie);
|
||||
if (member && member.totalBytes > 0) {
|
||||
const currentHourBlock = Math.floor(new Date().getHours() / 3);
|
||||
storageCache.bytes = member.usedBytes;
|
||||
storageCache.hourBlock = currentHourBlock;
|
||||
return {
|
||||
total: q.formatBytes(member.totalBytes),
|
||||
totalBytes: member.totalBytes,
|
||||
used: q.formatBytes(member.usedBytes),
|
||||
usedBytes: member.usedBytes,
|
||||
};
|
||||
}
|
||||
|
||||
// 回退: capacity/detail + 根目录文件大小合计
|
||||
try {
|
||||
const memberParams = new URLSearchParams({ pr: 'ucpro', fr: 'pc', uc_param_str: '', __t: String(Date.now()), __dt: '1000' });
|
||||
const resp = await fetch(`https://pan.quark.cn/1/clouddrive/member?${memberParams.toString()}`, {
|
||||
headers: quark_api.getHeaders(cookie),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data.status === 200 && data.data) {
|
||||
const usedBytes = data.data.use_capacity ?? 0;
|
||||
const totalBytes = data.data.total_capacity ?? 0;
|
||||
if (totalBytes > 0) {
|
||||
// Cache for calculateUsedSpace compatibility
|
||||
const currentHourBlock = Math.floor(new Date().getHours() / 3);
|
||||
storageCache.bytes = usedBytes;
|
||||
storageCache.hourBlock = currentHourBlock;
|
||||
return {
|
||||
total: quark_api.formatBytes(totalBytes),
|
||||
totalBytes,
|
||||
used: quark_api.formatBytes(usedBytes),
|
||||
usedBytes,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Fallback: capacity/detail for total + root file sum for used
|
||||
try {
|
||||
const params = new URLSearchParams(quark_api.getCommonParams());
|
||||
const capResp = await fetch(`${BASE_URL}/1/clouddrive/capacity/detail?${params.toString()}`, {
|
||||
headers: quark_api.getHeaders(cookie),
|
||||
const params = new URLSearchParams(q.getCommonParams());
|
||||
const capResp = await fetch(q.QUARK_DRIVE_HOST + q.EP.CAPACITY_DETAIL + '?' + params.toString(), {
|
||||
headers: q.getHeaders(cookie),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
let totalBytes = 0;
|
||||
@@ -62,46 +42,32 @@ export async function getStorageInfoQuick(cookie, fallbackTotal?) {
|
||||
totalBytes = data.data.capacity_summary?.sum_capacity || 0;
|
||||
if (totalBytes === 0) {
|
||||
const memberships = [...(data.data.effect || []), ...(data.data.expired || [])];
|
||||
totalBytes = memberships.reduce((max: number, m: any) => Math.max(max, m.capacity || 0), 0);
|
||||
totalBytes = memberships.reduce((max, m) => Math.max(max, m.capacity || 0), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
let usedBytes = 0;
|
||||
try {
|
||||
const rootFiles = await quark_api.listRootDir(cookie);
|
||||
for (const f of rootFiles) usedBytes += f.size || 0;
|
||||
} catch {}
|
||||
try { const rootFiles = await q.listRootDir(cookie); for (const f of rootFiles) usedBytes += f.size || 0; } catch {}
|
||||
if (totalBytes > 0) {
|
||||
return { total: quark_api.formatBytes(totalBytes), totalBytes, used: quark_api.formatBytes(usedBytes), usedBytes };
|
||||
return { total: q.formatBytes(totalBytes), totalBytes, used: q.formatBytes(usedBytes), usedBytes };
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Last resort: parse fallbackTotal string
|
||||
// 兜底:从字符串解析
|
||||
if (fallbackTotal) {
|
||||
const match = String(fallbackTotal).match(/^([\d.]+)\s*([KMGT]B?)/i);
|
||||
if (match) {
|
||||
const num = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
const multipliers: Record<string,number> = { B:1, KB:1024, MB:1048576, GB:1073741824, TB:1099511627776 };
|
||||
const multipliers = { B:1, KB:1024, MB:1048576, GB:1073741824, TB:1099511627776 };
|
||||
const m = multipliers[unit] || multipliers[unit.replace('B','')+'B'] || 0;
|
||||
if (m > 0) return { total: String(fallbackTotal), totalBytes: Math.round(num*m), used: '-', usedBytes: 0 };
|
||||
}
|
||||
}
|
||||
return { total: '-', totalBytes: 0, used: '-', usedBytes: 0 };
|
||||
}
|
||||
/**
|
||||
* Get storage info with used space calculation.
|
||||
*/
|
||||
/**
|
||||
* Fast estimation (root-level files only) + background full traversal.
|
||||
* First call returns quickly; full traversal runs async and updates DB later.
|
||||
* `onBackgroundComplete` is called when traversal finishes.
|
||||
*/
|
||||
/**
|
||||
* Get accurate storage info via /member API (fast, no file traversal needed).
|
||||
* Returns { total, totalBytes, used, usedBytes } in ~1s.
|
||||
* onBackgroundComplete is kept for backward compat (called synchronously).
|
||||
*/
|
||||
|
||||
/** 获取容量信息 (兼容旧接口) */
|
||||
export async function getStorageInfo(cookie, onBackgroundComplete?) {
|
||||
const result = await getStorageInfoQuick(cookie);
|
||||
if (onBackgroundComplete && result.used !== '-' && result.usedBytes > 0) {
|
||||
@@ -109,10 +75,8 @@ export async function getStorageInfo(cookie, onBackgroundComplete?) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Calculate total used space by recursively traversing all files
|
||||
* and summing their sizes. Uses 3-hour time window cache.
|
||||
*/
|
||||
|
||||
/** 递归遍历所有文件计算已用空间 (带 3 小时缓存) */
|
||||
export async function calculateUsedSpace(cookie) {
|
||||
const currentHourBlock = Math.floor(new Date().getHours() / 3);
|
||||
if (storageCache.hourBlock === currentHourBlock && storageCache.bytes > 0) {
|
||||
@@ -120,22 +84,16 @@ export async function calculateUsedSpace(cookie) {
|
||||
}
|
||||
let totalUsed = 0;
|
||||
const stack = ['0'];
|
||||
const visited = new Set();
|
||||
const visited = new Set<string>();
|
||||
while (stack.length > 0) {
|
||||
const fid = stack.pop();
|
||||
if (visited.has(fid))
|
||||
continue;
|
||||
const fid = stack.pop()!;
|
||||
if (visited.has(fid)) continue;
|
||||
visited.add(fid);
|
||||
const files = await quark_api.listDirAllPages(cookie, fid);
|
||||
if (!files.length)
|
||||
continue;
|
||||
const files = await q.listDirAllPages(cookie, fid);
|
||||
if (!files.length) continue;
|
||||
for (const f of files) {
|
||||
if (f.dir) {
|
||||
stack.push(f.fid);
|
||||
}
|
||||
else {
|
||||
totalUsed += f.size || 0;
|
||||
}
|
||||
if (f.dir) stack.push(f.fid);
|
||||
else totalUsed += f.size || 0;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
}
|
||||
@@ -143,119 +101,69 @@ export async function calculateUsedSpace(cookie) {
|
||||
storageCache.hourBlock = currentHourBlock;
|
||||
return totalUsed;
|
||||
}
|
||||
// ==================== Cleanup ====================
|
||||
|
||||
// ==================== 清理操作 ====================
|
||||
|
||||
/**
|
||||
* Trash specified files/folders (move to recycle bin).
|
||||
* 永久删除文件/文件夹 (之前是移入回收站, 现改为直接永久删除)
|
||||
*/
|
||||
export async function trashFiles(cookie, fids) {
|
||||
if (!fids.length)
|
||||
return true;
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/1/clouddrive/file/trash?${quark_api.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action_type: 1, // 1 = move to trash
|
||||
filelist: fids,
|
||||
exclude_filelist: [],
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
if (!response.ok)
|
||||
return false;
|
||||
const data = await response.json();
|
||||
if (data.status === 200)
|
||||
return true;
|
||||
console.error(`[Quark] trashFiles failed: ${data.message || data.status}`);
|
||||
return false;
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`[Quark] trashFiles error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
return q.permanentDelete(cookie, fids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty the recycle bin — permanently delete all files in trash.
|
||||
* 清空回收站 (现在删除是永久删除, 此函数基本不需要了, 保留兼容)
|
||||
*/
|
||||
export async function emptyTrash(cookie) {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/1/clouddrive/file/trash/clear?${quark_api.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
signal: AbortSignal.timeout(60000),
|
||||
});
|
||||
if (!response.ok)
|
||||
return false;
|
||||
const data = await response.json();
|
||||
if (data.status === 200)
|
||||
return true;
|
||||
console.error(`[Quark] emptyTrash failed: ${data.message || data.status}`);
|
||||
return false;
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`[Quark] emptyTrash error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
return q.emptyTrashApi(cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup: trash date-named folders (YYYY-MM-DD) older than `days`.
|
||||
* 清理 N 天前的日期文件夹 (YYYY-MM-DD 格式)
|
||||
*/
|
||||
export async function cleanupOldDateFolders(cookie, days, whitelistDirs) {
|
||||
const errors = [];
|
||||
export async function cleanupOldDateFolders(cookie, days, whitelistDirs?) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
||||
try {
|
||||
const rootItems = await quark_api.listRootDir(cookie);
|
||||
const rootItems = await q.listRootDir(cookie);
|
||||
const oldFolders = rootItems.filter(item => {
|
||||
if (!item.dir)
|
||||
return false;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(item.file_name))
|
||||
return false;
|
||||
if (whitelistDirs && whitelistDirs.includes(item.file_name))
|
||||
return false;
|
||||
if (!item.dir) return false;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(item.file_name)) return false;
|
||||
if (whitelistDirs && whitelistDirs.includes(item.file_name)) return false;
|
||||
return item.file_name < cutoffStr;
|
||||
});
|
||||
if (oldFolders.length === 0) {
|
||||
return { trashed: 0, errors: [] };
|
||||
}
|
||||
if (oldFolders.length === 0) return { trashed: 0, errors: [] };
|
||||
const fids = oldFolders.map(f => f.fid);
|
||||
console.log(`[Quark] Trashing ${fids.length} old date folders (before ${cutoffStr}): ${oldFolders.map(f => f.file_name).join(', ')}`);
|
||||
console.log('[Quark] 永久删除 ' + fids.length + ' 个旧日期文件夹 (早于 ' + cutoffStr + '): ' + oldFolders.map(f => f.file_name).join(', '));
|
||||
const ok = await trashFiles(cookie, fids);
|
||||
if (ok) {
|
||||
return { trashed: fids.length, errors: [] };
|
||||
}
|
||||
return { trashed: 0, errors: [`Trash API returned failure for ${fids.length} folders`] };
|
||||
}
|
||||
catch (err) {
|
||||
if (ok) return { trashed: fids.length, errors: [] };
|
||||
return { trashed: 0, errors: ['删除 API 失败: ' + fids.length + ' 个文件夹'] };
|
||||
} catch (err) {
|
||||
return { trashed: 0, errors: [err.message] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup: if used space exceeds thresholdPercent% of total,
|
||||
* delete the oldest date folders until totalBytes * deletePercent/100
|
||||
* of total capacity is freed.
|
||||
* 根据空间阈值清理:已用空间超过 thresholdPercent% 时删除最老的日期文件夹
|
||||
* 直到释放 deletePercent% 的总容量
|
||||
*/
|
||||
export async function cleanupBySpaceThreshold(cookie, thresholdPercent, deletePercent, whitelistDirs) {
|
||||
const errors = [];
|
||||
export async function cleanupBySpaceThreshold(cookie, thresholdPercent, deletePercent, whitelistDirs?) {
|
||||
try {
|
||||
const storage = await getStorageInfo(cookie);
|
||||
if (storage.totalBytes <= 0)
|
||||
return { trashed: 0, errors: [] };
|
||||
if (storage.totalBytes <= 0) return { trashed: 0, errors: [] };
|
||||
const usagePercent = (storage.usedBytes / storage.totalBytes) * 100;
|
||||
if (usagePercent < thresholdPercent) {
|
||||
console.log(`[Quark] Usage ${usagePercent.toFixed(1)}% below threshold ${thresholdPercent}%, skipping`);
|
||||
console.log('[Quark] 使用率 ' + usagePercent.toFixed(1) + '% 低于阈值 ' + thresholdPercent + '%, 跳过');
|
||||
return { trashed: 0, errors: [] };
|
||||
}
|
||||
const targetBytesToFree = Math.floor(storage.totalBytes * Math.min(deletePercent, 100) / 100);
|
||||
const rootItems = await quark_api.listRootDir(cookie);
|
||||
const rootItems = await q.listRootDir(cookie);
|
||||
const dateFolders = rootItems
|
||||
.filter(item => item.dir && /^\d{4}-\d{2}-\d{2}$/.test(item.file_name))
|
||||
.filter(item => !whitelistDirs || !whitelistDirs.includes(item.file_name))
|
||||
.sort((a, b) => a.file.localeCompare(b.file_name));
|
||||
if (dateFolders.length === 0)
|
||||
return { trashed: 0, errors: [] };
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
||||
if (dateFolders.length === 0) return { trashed: 0, errors: [] };
|
||||
const hasSizes = dateFolders.some(f => f.size && f.size > 0);
|
||||
let cumulativeSize = 0;
|
||||
const foldersToTrash = [];
|
||||
@@ -263,11 +171,9 @@ export async function cleanupBySpaceThreshold(cookie, thresholdPercent, deletePe
|
||||
for (const folder of dateFolders) {
|
||||
foldersToTrash.push(folder);
|
||||
cumulativeSize += folder.size || 0;
|
||||
if (cumulativeSize >= targetBytesToFree)
|
||||
break;
|
||||
if (cumulativeSize >= targetBytesToFree) break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const avgSizePerFolder = storage.usedBytes / dateFolders.length;
|
||||
const estCount = Math.max(1, Math.ceil(targetBytesToFree / avgSizePerFolder));
|
||||
foldersToTrash.push(...dateFolders.slice(0, estCount));
|
||||
@@ -276,15 +182,15 @@ export async function cleanupBySpaceThreshold(cookie, thresholdPercent, deletePe
|
||||
const freedMB = (cumulativeSize / 1024 / 1024).toFixed(0);
|
||||
const targetMB = (targetBytesToFree / 1024 / 1024).toFixed(0);
|
||||
const fidsToTrash = foldersToTrash.map(f => f.fid);
|
||||
console.log(`[Quark] Space threshold: trashing ${foldersToTrash.length}/${dateFolders.length} oldest folders (~${freedMB} MB) to free ${targetMB} MB (${deletePercent}% of ${(storage.totalBytes / 1024 / 1024 / 1024).toFixed(0)} GB total)`);
|
||||
const gbTotal = (storage.totalBytes / 1024 / 1024 / 1024).toFixed(0);
|
||||
console.log('[Quark] 空间阈值清理:删除 ' + foldersToTrash.length + '/' + dateFolders.length + ' 个最老文件夹 (~' + freedMB + ' MB),目标释放 ' + targetMB + ' MB (' + deletePercent + '% of ' + gbTotal + ' GB)');
|
||||
const ok = await trashFiles(cookie, fidsToTrash);
|
||||
if (ok) {
|
||||
console.log(`[Quark] ✅ Space-threshold trashed ${foldersToTrash.length} folders (~${freedMB} MB)`);
|
||||
console.log('[Quark] OK 空间阈值清理完成:已删除 ' + foldersToTrash.length + ' 个文件夹 (~' + freedMB + ' MB)');
|
||||
return { trashed: foldersToTrash.length, errors: [] };
|
||||
}
|
||||
return { trashed: 0, errors: [`Space-threshold trash failed for ${foldersToTrash.length} folders`] };
|
||||
}
|
||||
catch (err) {
|
||||
return { trashed: 0, errors: ['空间阈值清理失败: ' + foldersToTrash.length + ' 个文件夹'] };
|
||||
} catch (err) {
|
||||
return { trashed: 0, errors: [err.message] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as quark_api from "./quark-api";
|
||||
import * as q from './quark-api';
|
||||
|
||||
/**
|
||||
* 分享模块 — 分享链接解析、转存任务、创建分享链接。
|
||||
*/
|
||||
const BASE_URL = 'https://drive-pc.quark.cn';
|
||||
// ==================== Acquire Stoken ====================
|
||||
/**
|
||||
* Acquire stoken for a share link (needed for detail/save).
|
||||
@@ -12,10 +11,10 @@ const BASE_URL = 'https://drive-pc.quark.cn';
|
||||
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()}`, {
|
||||
const params = new URLSearchParams(q.getCommonParams());
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PAGE_TOKEN?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pwd_id: pwdId, passcode: '' }),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
@@ -44,7 +43,7 @@ export async function acquireStoken(cookie, pwdId) {
|
||||
*/
|
||||
export async function getDetailAt(cookie, pwdId, stoken, pdirFid) {
|
||||
const params = new URLSearchParams({
|
||||
...quark_api.getCommonParams(),
|
||||
...q.getCommonParams(),
|
||||
pwd_id: pwdId,
|
||||
stoken,
|
||||
pdir_fid: pdirFid,
|
||||
@@ -58,7 +57,7 @@ export async function getDetailAt(cookie, pwdId, stoken, pdirFid) {
|
||||
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) });
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PAGE_DETAIL?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(15000) });
|
||||
if (!resp.ok)
|
||||
return [];
|
||||
const data = await resp.json();
|
||||
@@ -108,9 +107,9 @@ export async function getShareFiles(cookie, pwdId, stoken) {
|
||||
*/
|
||||
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()}`, {
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PAGE_SAVE?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fid_list: fids,
|
||||
fid_token_list: fidTokens,
|
||||
@@ -148,14 +147,14 @@ export async function waitForTask(cookie, taskId, timeoutMs) {
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
...quark_api.getCommonParams(),
|
||||
...q.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 resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.TASK?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
|
||||
const data = await resp.json();
|
||||
if (data.status === 200) {
|
||||
if (data.data?.status === 2) {
|
||||
@@ -180,9 +179,9 @@ export async function waitForTask(cookie, taskId, timeoutMs) {
|
||||
*/
|
||||
export async function renameFile(cookie, fid, newName) {
|
||||
try {
|
||||
const resp = await fetch(`${BASE_URL}/1/clouddrive/file/rename?${quark_api.makeQuery()}`, {
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.FILE_RENAME?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fid, file_name: newName }),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
@@ -200,16 +199,16 @@ export async function renameFile(cookie, fid, newName) {
|
||||
*/
|
||||
export async function createShareLink(cookie, fileId) {
|
||||
try {
|
||||
const sharePwd = quark_api.randomSharePwd();
|
||||
const sharePwd = q.randomSharePwd();
|
||||
// Try different share_type values (1=7天, 0=无限制)
|
||||
const shareTypes = ['1', '0'];
|
||||
let lastError = '';
|
||||
for (const st of shareTypes) {
|
||||
await quark_api.humanDelay();
|
||||
await q.humanDelay();
|
||||
// Step 1: Create share task - get task_id
|
||||
const response = await fetch(`${BASE_URL}/1/clouddrive/share?${quark_api.makeQuery()}`, {
|
||||
const response = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE + "?"${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fid_list: [fileId],
|
||||
share_type: st,
|
||||
@@ -256,9 +255,9 @@ export async function createShareLink(cookie, fileId) {
|
||||
*/
|
||||
async function submitShare(cookie, shareId, sharePwd) {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/1/clouddrive/share/password?${quark_api.makeQuery()}`, {
|
||||
const response = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PASSWORD?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ share_id: shareId, share_pwd: sharePwd || '' }),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
@@ -285,14 +284,14 @@ async function waitForShareTask(cookie, taskId, timeoutMs) {
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
...quark_api.getCommonParams(),
|
||||
...q.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 resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.TASK?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
|
||||
const data = await resp.json();
|
||||
if (data.data?.status === 2) {
|
||||
// Task completed — try multiple extraction approaches
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import * as quark_api from "./quark-api";
|
||||
import * as quark_share from "./quark-share";
|
||||
import * as q from './quark-api';
|
||||
import * as qs from './quark-share';
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
@@ -54,12 +54,12 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, ret
|
||||
return { success: false, message: 'Invalid share URL: could not extract share token' };
|
||||
}
|
||||
// Step 1: Acquire stoken
|
||||
const stoken = await quark_share.acquireStoken(cookie, pwdId);
|
||||
const stoken = await qs.acquireStoken(cookie, pwdId);
|
||||
if (!stoken) {
|
||||
return { success: false, message: '😅 Oops!资源好像偷偷溜走了,换个链接试试吧~' };
|
||||
}
|
||||
// Step 2: Get share detail
|
||||
const shareInfo = await quark_share.getShareFiles(cookie, pwdId, stoken);
|
||||
const shareInfo = await qs.getShareFiles(cookie, pwdId, stoken);
|
||||
if (!shareInfo || !shareInfo.files || shareInfo.files.length === 0) {
|
||||
return { success: false, message: '🌚 空的!这个分享里啥都没有…' };
|
||||
}
|
||||
@@ -68,8 +68,8 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, ret
|
||||
const fids = topFiles.map(f => f.fid);
|
||||
const fidTokens = topFiles.map(f => f.share_fid_token);
|
||||
// 按日期创建/查找文件夹,每天的转存存入当天文件夹
|
||||
await quark_api.humanDelay();
|
||||
const saveDirName = quark_api.dailyFolderName();
|
||||
await q.humanDelay();
|
||||
const saveDirName = q.dailyFolderName();
|
||||
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"`);
|
||||
const saveDirFid = await findOrCreateDir(cookie, saveDirName);
|
||||
let targetPdirFid = saveDirFid || '0';
|
||||
@@ -90,18 +90,18 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, ret
|
||||
console.log(`[Quark] WARNING: failed to create/find dir "${saveDirName}", saving to root`);
|
||||
}
|
||||
// Step 3: Save top-level item(s) to the target directory
|
||||
const saveResult = await quark_share.saveFiles(cookie, pwdId, stoken, fids, fidTokens.filter(Boolean), targetPdirFid);
|
||||
const saveResult = await qs.saveFiles(cookie, pwdId, stoken, fids, fidTokens.filter(Boolean), targetPdirFid);
|
||||
if (!saveResult.success) {
|
||||
return saveResult;
|
||||
}
|
||||
const taskId = saveResult.taskId;
|
||||
// Step 4: Wait for save task to complete (poll up to 30s)
|
||||
const savedFids = await quark_share.waitForTask(cookie, taskId, 30000);
|
||||
const savedFids = await qs.waitForTask(cookie, taskId, 30000);
|
||||
if (!savedFids || savedFids.length === 0) {
|
||||
return { success: true, message: '文件已保存,但获取保存结果超时' };
|
||||
}
|
||||
// Step 5: Magic rename files — with random delay to avoid detection
|
||||
await quark_api.humanDelay();
|
||||
await q.humanDelay();
|
||||
const renamed = [];
|
||||
let shareFid = '';
|
||||
let savedFolderName = '';
|
||||
@@ -122,7 +122,7 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, ret
|
||||
console.log(`[Quark] Retry: sharing subfolder (fid: ${retrySubFolderFid}) instead of saved content`);
|
||||
}
|
||||
// Step 6: Create share link FIRST (before rename), so all files are guaranteed to be shared
|
||||
await quark_api.humanDelay();
|
||||
await q.humanDelay();
|
||||
let shareUrlResult = '';
|
||||
let sharePwdResult = '';
|
||||
let shareMsg = '';
|
||||
@@ -146,7 +146,7 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, ret
|
||||
// ── Single folder share ──
|
||||
const savedDirFid = savedFids[0];
|
||||
// List files inside the saved directory
|
||||
const dirFiles = await quark_api.listDir(cookie, savedDirFid);
|
||||
const dirFiles = await q.listDir(cookie, savedDirFid);
|
||||
if (dirFiles && dirFiles.length > 0) {
|
||||
for (const file of dirFiles) {
|
||||
if (file.dir)
|
||||
@@ -255,9 +255,9 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, ret
|
||||
*/
|
||||
export async function createDir(cookie, dirName, parentFid = '0') {
|
||||
try {
|
||||
const resp = await fetch(`https://drive-pc.quark.cn/1/clouddrive/file?${quark_api.makeQuery()}`, {
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.FILE + '?'${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
pdir_fid: parentFid,
|
||||
file_name: dirName,
|
||||
@@ -284,7 +284,7 @@ export async function createDir(cookie, dirName, parentFid = '0') {
|
||||
*/
|
||||
export async function findOrCreateDir(cookie, dirName, parentFid = '0') {
|
||||
try {
|
||||
const rootFiles = await quark_api.listDirAllPages(cookie, parentFid);
|
||||
const rootFiles = await q.listDirAllPages(cookie, parentFid);
|
||||
const existing = rootFiles.find(f => f.dir && f.file_name === dirName);
|
||||
if (existing?.fid) {
|
||||
console.log(`[Quark] Found existing daily folder: ${dirName} (fid: ${existing.fid})`);
|
||||
@@ -313,7 +313,7 @@ export async function countRecursive(cookie, pdirFid) {
|
||||
if (visited.has(fid))
|
||||
continue;
|
||||
visited.add(fid);
|
||||
const files = await quark_api.listDir(cookie, fid);
|
||||
const files = await q.listDir(cookie, fid);
|
||||
if (!files)
|
||||
continue;
|
||||
for (const f of files) {
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
// @ts-nocheck
|
||||
import * as quark_api from "./quark-api";
|
||||
import * as quark_share from "./quark-share";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
/**
|
||||
* 转存 & 存储管理模块。
|
||||
* 处理分享链接解析 → 转存 → 查/创建目标文件夹 → 文件重命名 → 递归统计。
|
||||
*/
|
||||
// ==================== saveFromShare — 核心转存流水线 ====================
|
||||
/**
|
||||
* Save files from a share link → magic rename → create shared link.
|
||||
*
|
||||
* Flow: token → detail → save → wait_task → rename → share
|
||||
*/
|
||||
export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, retrySave = false) {
|
||||
try {
|
||||
// Parse share token from URL
|
||||
const urlObj = new URL(shareUrl);
|
||||
const pwdId = urlObj.pathname.split('/').filter(Boolean).pop();
|
||||
if (!pwdId) {
|
||||
return { success: false, message: 'Invalid share URL: could not extract share token' };
|
||||
}
|
||||
// Step 1: Acquire stoken
|
||||
const stoken = await quark_share.acquireStoken(cookie, pwdId);
|
||||
if (!stoken) {
|
||||
return { success: false, message: '😅 Oops!资源好像偷偷溜走了,换个链接试试吧~' };
|
||||
}
|
||||
// Step 2: Get share detail
|
||||
const shareInfo = await quark_share.getShareFiles(cookie, pwdId, stoken);
|
||||
if (!shareInfo || !shareInfo.files || shareInfo.files.length === 0) {
|
||||
return { success: false, message: '🌚 空的!这个分享里啥都没有…' };
|
||||
}
|
||||
const { files: topFiles, topDir, childFiles } = shareInfo;
|
||||
const originalFolderName = topFiles[0]?.file_name || '';
|
||||
const fids = topFiles.map(f => f.fid);
|
||||
const fidTokens = topFiles.map(f => f.share_fid_token);
|
||||
// 按日期创建/查找文件夹,每天的转存存入当天文件夹
|
||||
await quark_api.humanDelay();
|
||||
const saveDirName = quark_api.dailyFolderName();
|
||||
console.log(`[Quark] saveFromShare: looking for/create dir "${saveDirName}"`);
|
||||
const saveDirFid = await findOrCreateDir(cookie, saveDirName);
|
||||
let targetPdirFid = saveDirFid || '0';
|
||||
let retrySubFolderFid = '';
|
||||
if (saveDirFid) {
|
||||
console.log(`[Quark] Using save directory: ${saveDirName} (fid: ${saveDirFid})`);
|
||||
if (retrySave) {
|
||||
const subName = 'retry_' + Math.random().toString(36).slice(2, 6);
|
||||
console.log(`[Quark] Retry: creating subfolder "${subName}" inside "${saveDirName}"`);
|
||||
retrySubFolderFid = await findOrCreateDir(cookie, subName, saveDirFid);
|
||||
if (retrySubFolderFid) {
|
||||
targetPdirFid = retrySubFolderFid;
|
||||
console.log(`[Quark] Retry: saving to subfolder ${subName} (fid: ${retrySubFolderFid})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`[Quark] WARNING: failed to create/find dir "${saveDirName}", saving to root`);
|
||||
}
|
||||
// Step 3: Save top-level item(s) to the target directory
|
||||
const saveResult = await quark_share.saveFiles(cookie, pwdId, stoken, fids, fidTokens.filter(Boolean), targetPdirFid);
|
||||
if (!saveResult.success) {
|
||||
return saveResult;
|
||||
}
|
||||
const taskId = saveResult.taskId;
|
||||
// Step 4: Wait for save task to complete (poll up to 30s)
|
||||
const savedFids = await quark_share.waitForTask(cookie, taskId, 30000);
|
||||
if (!savedFids || savedFids.length === 0) {
|
||||
return { success: true, message: '文件已保存,但获取保存结果超时' };
|
||||
}
|
||||
// Step 5: Magic rename files — with random delay to avoid detection
|
||||
await quark_api.humanDelay();
|
||||
const renamed = [];
|
||||
let shareFid = '';
|
||||
let savedFolderName = '';
|
||||
let newInnerDirName = '';
|
||||
if (topDir && childFiles && childFiles.length > 0) {
|
||||
// ── Single folder share ──
|
||||
const savedDirFid = savedFids[0];
|
||||
shareFid = savedDirFid;
|
||||
savedFolderName = topFiles[0]?.file_name || '';
|
||||
}
|
||||
else {
|
||||
// ── Multiple files at top level ──
|
||||
shareFid = savedFids[0];
|
||||
savedFolderName = topFiles[0]?.file_name || '';
|
||||
}
|
||||
if (retrySave && retrySubFolderFid) {
|
||||
shareFid = retrySubFolderFid;
|
||||
console.log(`[Quark] Retry: sharing subfolder (fid: ${retrySubFolderFid}) instead of saved content`);
|
||||
}
|
||||
// Step 6: Create share link FIRST (before rename), so all files are guaranteed to be shared
|
||||
await quark_api.humanDelay();
|
||||
let shareUrlResult = '';
|
||||
let sharePwdResult = '';
|
||||
let shareMsg = '';
|
||||
let successCount = 0; // total items (files + folders) actually saved
|
||||
const { createShareLink } = await import('./quark-share');
|
||||
if (shareFid) {
|
||||
const shareResult = await createShareLink(cookie, shareFid);
|
||||
if (shareResult.success && shareResult.shareUrl) {
|
||||
shareUrlResult = shareResult.shareUrl;
|
||||
if (shareResult.sharePwd)
|
||||
sharePwdResult = shareResult.sharePwd;
|
||||
}
|
||||
else {
|
||||
shareMsg = `(分享失败:${shareResult.message})`;
|
||||
}
|
||||
}
|
||||
const { magicRenameDir, magicRename } = await import('./quark-rename');
|
||||
const { renameFile } = await import('./quark-share');
|
||||
// Step 7: Rename files AFTER creating the share link (anti-harmony, won't affect the share)
|
||||
if (topDir && childFiles && childFiles.length > 0) {
|
||||
// ── Single folder share ──
|
||||
const savedDirFid = savedFids[0];
|
||||
// List files inside the saved directory
|
||||
const dirFiles = await quark_api.listDir(cookie, savedDirFid);
|
||||
if (dirFiles && dirFiles.length > 0) {
|
||||
for (const file of dirFiles) {
|
||||
if (file.dir)
|
||||
continue;
|
||||
const newName = magicRename(file.file_name);
|
||||
const renameOk = await renameFile(cookie, file.fid, newName);
|
||||
if (renameOk) {
|
||||
renamed.push({ original: file.file_name, renamed: newName });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also rename the inner folder itself (the actual shared folder)
|
||||
const innerDirOriginalName = sourceTitle || topFiles[0]?.file_name || '';
|
||||
if (innerDirOriginalName) {
|
||||
newInnerDirName = magicRenameDir(innerDirOriginalName);
|
||||
const innerDirRenameOk = await renameFile(cookie, savedDirFid, newInnerDirName);
|
||||
if (innerDirRenameOk) {
|
||||
console.log(`[Quark] Renamed inner folder: ${innerDirOriginalName} → ${newInnerDirName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// ── Multiple files at top level ──
|
||||
for (let i = 0; i < savedFids.length && i < topFiles.length; i++) {
|
||||
const originalName = topFiles[i].file_name;
|
||||
if (topFiles[i].dir)
|
||||
continue;
|
||||
const newName = magicRename(originalName);
|
||||
const renameOk = await renameFile(cookie, savedFids[i], newName);
|
||||
if (renameOk) {
|
||||
renamed.push({ original: originalName, renamed: newName });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Step 7.5: 广告关键词清理 + 创建警示文件夹
|
||||
if (shareFid) {
|
||||
try {
|
||||
const { runAdCleanup } = await import('./quark-ad-cleanup');
|
||||
const adResult = await runAdCleanup(cookie, shareFid);
|
||||
if (adResult.adDeleted > 0) {
|
||||
console.log(`[Quark] 广告清理完成: 删除了 ${adResult.adDeleted} 个广告文件/文件夹`);
|
||||
}
|
||||
if (adResult.warningDirs > 0) {
|
||||
console.log(`[Quark] 已创建 ${adResult.warningDirs} 个警示文件夹`);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(`[Quark] 广告清理/警示文件夹创建失败(非致命): ${err.message}`);
|
||||
}
|
||||
}
|
||||
// Step 8: DAY FOLDER STAYS AS-IS (e.g. "2026-05-03")
|
||||
// DO NOT rename the date folder — it serves as the organizational container.
|
||||
savedFolderName = newInnerDirName ? `${saveDirName}/${newInnerDirName}` : saveDirName;
|
||||
// Recursively count files and folders from saved cloud directory
|
||||
let fileCount = 0;
|
||||
let folderCount = 0;
|
||||
if (shareFid) {
|
||||
try {
|
||||
const counts = await countRecursive(cookie, shareFid);
|
||||
fileCount = counts.fileCount;
|
||||
folderCount = counts.folderCount;
|
||||
}
|
||||
catch {
|
||||
console.log('[Quark] Recursive count failed, using fallback');
|
||||
}
|
||||
}
|
||||
// If recursive count returned nothing, try fallback
|
||||
if (fileCount === 0 && folderCount === 0) {
|
||||
if (topDir && childFiles) {
|
||||
folderCount = 1 + childFiles.filter(f => f.dir).length;
|
||||
fileCount = childFiles.filter(f => !f.dir).length;
|
||||
}
|
||||
else {
|
||||
folderCount = topFiles.filter(f => f.dir).length;
|
||||
fileCount = topFiles.filter(f => !f.dir).length;
|
||||
}
|
||||
}
|
||||
// Calculate total file size
|
||||
const allFiles = topDir && childFiles ? childFiles : topFiles;
|
||||
const fileSize = allFiles.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
||||
const renameMsg = renamed.length > 0
|
||||
? `,已重命名 ${renamed.length} 个文件`
|
||||
: '';
|
||||
const folderMsg = savedFolderName ? `到文件夹「${savedFolderName}」` : '';
|
||||
return {
|
||||
success: true,
|
||||
message: `已保存${folderMsg}${renameMsg}${shareMsg}`,
|
||||
shareUrl: shareUrlResult || undefined,
|
||||
sharePwd: sharePwdResult || undefined,
|
||||
folderName: savedFolderName,
|
||||
taskId,
|
||||
renamed: renamed.map(r => `${r.original} → ${r.renamed}`),
|
||||
fileCount,
|
||||
folderCount,
|
||||
fileSize,
|
||||
originalFolderName,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
return { success: false, message: err.message || 'Network error' };
|
||||
}
|
||||
}
|
||||
// ==================== Dir Management ====================
|
||||
/**
|
||||
* Create a new directory at root.
|
||||
*/
|
||||
export async function createDir(cookie, dirName, parentFid = '0') {
|
||||
try {
|
||||
const resp = await fetch(`https://drive-pc.quark.cn/1/clouddrive/file?${quark_api.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...quark_api.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
pdir_fid: parentFid,
|
||||
file_name: dirName,
|
||||
dir: true,
|
||||
dir_path: '',
|
||||
}),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.status === 200 && data.data?.fid) {
|
||||
console.log(`[Quark] Created dir "${dirName}" (fid: ${data.data.fid})`);
|
||||
return data.data.fid;
|
||||
}
|
||||
console.log(`[Quark] createDir API returned non-200: status=${data.status} msg=${data.message}`);
|
||||
return null;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(`[Quark] createDir error: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Find an existing directory by name, or create it if not found.
|
||||
*/
|
||||
export async function findOrCreateDir(cookie, dirName, parentFid = '0') {
|
||||
try {
|
||||
const rootFiles = await quark_api.listDirAllPages(cookie, parentFid);
|
||||
const existing = rootFiles.find(f => f.dir && f.file_name === dirName);
|
||||
if (existing?.fid) {
|
||||
console.log(`[Quark] Found existing daily folder: ${dirName} (fid: ${existing.fid})`);
|
||||
return existing.fid;
|
||||
}
|
||||
console.log(`[Quark] Daily folder "${dirName}" not found, creating...`);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(`[Quark] findOrCreateDir list error: ${err.message}`);
|
||||
}
|
||||
const fid = await createDir(cookie, dirName, parentFid);
|
||||
console.log(`[Quark] createDir result for "${dirName}": ${fid || 'null'}`);
|
||||
return fid;
|
||||
}
|
||||
// ==================== Recursive Count ====================
|
||||
/**
|
||||
* Recursively count files and folders for a saved cloud directory.
|
||||
*/
|
||||
export async function countRecursive(cookie, pdirFid) {
|
||||
let fileCount = 0;
|
||||
let folderCount = 0;
|
||||
const stack = [pdirFid];
|
||||
const visited = new Set();
|
||||
while (stack.length > 0) {
|
||||
const fid = stack.pop();
|
||||
if (visited.has(fid))
|
||||
continue;
|
||||
visited.add(fid);
|
||||
const files = await quark_api.listDir(cookie, fid);
|
||||
if (!files)
|
||||
continue;
|
||||
for (const f of files) {
|
||||
if (f.dir) {
|
||||
folderCount++;
|
||||
stack.push(f.fid);
|
||||
}
|
||||
else {
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { fileCount, folderCount };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { QUARK_PAN_HOST, QUARK_DRIVE_HOST, EP as QE } from './drivers/quark-api';
|
||||
import { chromium, BrowserContext, Page } from 'playwright';
|
||||
import jsQR from 'jsqr';
|
||||
import { getDb } from '../database/database';
|
||||
@@ -140,7 +141,7 @@ export async function startQrLogin(): Promise<{
|
||||
|
||||
try {
|
||||
// Navigate to Quark login page (now the homepage itself has QR login)
|
||||
await page.goto('https://pan.quark.cn/', {
|
||||
await page.goto(QUARK_PAN_HOST + '/', {
|
||||
waitUntil: 'commit',
|
||||
timeout: 30000,
|
||||
});
|
||||
@@ -243,7 +244,7 @@ async function pollLoginStatus(session: QrSession) {
|
||||
|
||||
// Check URL change as alternative indicator
|
||||
const url = session.page.url();
|
||||
if (!url.includes('login') && !url.includes('qrcode') && url !== 'about:blank' && url !== 'https://pan.quark.cn/' && url.length > 10) {
|
||||
if (!url.includes('login') && !url.includes('qrcode') && url !== 'about:blank' && url !== QUARK_PAN_HOST + '/' && url.length > 10) {
|
||||
await checkAndCaptureCookies(session);
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -275,7 +276,7 @@ async function checkAndCaptureCookies(session: QrSession) {
|
||||
session.cookieSnapshot = cookieStr;
|
||||
try {
|
||||
const resp = await session.page.evaluate(async () => {
|
||||
const r = await fetch('https://pan.quark.cn/account/info', {
|
||||
const r = await fetch(QUARK_PAN_HOST + QE.ACCOUNT_INFO, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return await r.text();
|
||||
@@ -319,7 +320,7 @@ export async function getQrLoginStatus(sessionId: string): Promise<{
|
||||
let nickname = '';
|
||||
try {
|
||||
const resp = await session.page.evaluate(async () => {
|
||||
const r = await fetch('https://pan.quark.cn/account/info', {
|
||||
const r = await fetch(QUARK_PAN_HOST + QE.ACCOUNT_INFO, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return await r.text();
|
||||
@@ -334,7 +335,7 @@ export async function getQrLoginStatus(sessionId: string): Promise<{
|
||||
try {
|
||||
const capResp = await session.page.evaluate(async () => {
|
||||
const r = await fetch(
|
||||
'https://pan.quark.cn/1/clouddrive/capacity/detail?pr=ucpro&fr=pc',
|
||||
QUARK_PAN_HOST + QE.CAPACITY_DETAIL + '?pr=ucpro&fr=pc',
|
||||
{ credentials: 'include' }
|
||||
);
|
||||
return await r.text();
|
||||
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
import { chromium, BrowserContext, Page } from 'playwright';
|
||||
import jsQR from 'jsqr';
|
||||
import { getDb } from '../database/database';
|
||||
import { escapeLike } from '../utils/time';
|
||||
|
||||
interface QrSession {
|
||||
id: string;
|
||||
browserContext: BrowserContext;
|
||||
page: Page;
|
||||
createdAt: number;
|
||||
cookieSnapshot: string;
|
||||
lastPollAt: number;
|
||||
qrUrl: string;
|
||||
status: 'pending' | 'scanned' | 'logged_in' | 'expired' | 'error';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const SESSIONS = new Map<string, QrSession>();
|
||||
const SESSION_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
const COOKIE_CHECK_INTERVAL = 1500; // 1.5s between cookie checks
|
||||
|
||||
import config from '../config';
|
||||
import { formatBytes } from './drivers/quark-api';
|
||||
const CHROMIUM_PATH = config.chromiumPath;
|
||||
|
||||
// Clean up old sessions periodically
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [id, session] of SESSIONS.entries()) {
|
||||
if (now - session.createdAt > SESSION_TTL) {
|
||||
cleanupSession(id);
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
function cleanupSession(id: string) {
|
||||
const session = SESSIONS.get(id);
|
||||
if (session) {
|
||||
try {
|
||||
session.browserContext.close().catch(() => {});
|
||||
} catch {}
|
||||
SESSIONS.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract QR code URL from the login page canvas using jsQR.
|
||||
* The actual login QR code is Canvas #0 (anonymous, 177x177), NOT #react-qrcode-logo.
|
||||
*/
|
||||
async function extractQrUrl(page: Page): Promise<string> {
|
||||
// Run inside Playwright's browser context (as a string to avoid Node TS type errors)
|
||||
const raw = await page.evaluate(`(() => {
|
||||
const canvases = document.querySelectorAll('canvas');
|
||||
var results = [];
|
||||
for (var i = 0; i < canvases.length; i++) {
|
||||
try {
|
||||
var c = canvases[i];
|
||||
var ctx = c.getContext('2d');
|
||||
if (!ctx) continue;
|
||||
var imageData = ctx.getImageData(0, 0, c.width, c.height);
|
||||
results.push({
|
||||
index: i,
|
||||
w: c.width,
|
||||
h: c.height,
|
||||
data: Array.from(imageData.data)
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
return results;
|
||||
})()`) as unknown as { index: number; w: number; h: number; data: number[] }[];
|
||||
|
||||
if (!raw || raw.length === 0) {
|
||||
throw new Error('页面没有可用的 canvas');
|
||||
}
|
||||
|
||||
// Try to decode each canvas, preferring the one with su.quark.cn URL
|
||||
let bestUrl = '';
|
||||
let bestResult: { index: number; w: number; h: number; data: number[] } | null = null;
|
||||
|
||||
for (const canvas of raw) {
|
||||
const code = jsQR(new Uint8ClampedArray(canvas.data), canvas.w, canvas.h);
|
||||
if (code && code.data) {
|
||||
// If this is the login QR code (has su.quark.cn), use it immediately
|
||||
if (code.data.includes('su.quark.cn')) {
|
||||
return code.data;
|
||||
}
|
||||
// Otherwise keep it as fallback
|
||||
if (!bestUrl) {
|
||||
bestUrl = code.data;
|
||||
bestResult = canvas;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestUrl) {
|
||||
return bestUrl;
|
||||
}
|
||||
|
||||
throw new Error('无法解析二维码内容');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a QR code login session.
|
||||
* Launches headless Chromium, navigates to Quark login page, extracts QR code URL.
|
||||
*/
|
||||
export async function startQrLogin(): Promise<{
|
||||
sessionId: string;
|
||||
qrUrl: string;
|
||||
expiresIn: number;
|
||||
}> {
|
||||
// Clean up any existing expired sessions
|
||||
for (const [id, session] of SESSIONS.entries()) {
|
||||
if (Date.now() - session.createdAt > SESSION_TTL) {
|
||||
cleanupSession(id);
|
||||
}
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: CHROMIUM_PATH,
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--no-first-run',
|
||||
'--no-zygote',
|
||||
],
|
||||
});
|
||||
|
||||
const browserContext = await browser.newContext({
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
viewport: { width: 1280, height: 800 },
|
||||
locale: 'zh-CN',
|
||||
});
|
||||
|
||||
const page = await browserContext.newPage();
|
||||
const sessionId = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
||||
|
||||
try {
|
||||
// Navigate to Quark login page (now the homepage itself has QR login)
|
||||
await page.goto('https://pan.quark.cn/', {
|
||||
waitUntil: 'commit',
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Wait for the QR code canvas to appear
|
||||
await page.waitForSelector('canvas', { timeout: 15000 });
|
||||
|
||||
// Extra wait for the QR code to fully render
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Extract the QR code URL from the canvas
|
||||
const qrUrl = await extractQrUrl(page);
|
||||
|
||||
// Take initial cookie snapshot
|
||||
const cookies = await browserContext.cookies();
|
||||
const cookieSnapshot = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
|
||||
const session: QrSession = {
|
||||
id: sessionId,
|
||||
browserContext,
|
||||
page,
|
||||
createdAt: Date.now(),
|
||||
cookieSnapshot,
|
||||
lastPollAt: Date.now(),
|
||||
qrUrl,
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
SESSIONS.set(sessionId, session);
|
||||
|
||||
// Start background polling for login detection
|
||||
pollLoginStatus(session);
|
||||
|
||||
// Handle page navigation (like redirect after login)
|
||||
page.on('framenavigated', async (frame) => {
|
||||
if (frame === page.mainFrame()) {
|
||||
const url = frame.url();
|
||||
if (url === 'about:blank') {
|
||||
await checkAndCaptureCookies(session);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle popups/dialogs
|
||||
page.on('popup', async (popup) => {
|
||||
try {
|
||||
await popup.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
await checkAndCaptureCookies(session);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
qrUrl,
|
||||
expiresIn: SESSION_TTL / 1000,
|
||||
};
|
||||
} catch (err: any) {
|
||||
// Clean up on failure
|
||||
try { await browserContext.close(); } catch {}
|
||||
try { browser.close().catch(() => {}); } catch {}
|
||||
SESSIONS.delete(sessionId);
|
||||
throw new Error(`启动扫码登录失败: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll login status in background.
|
||||
* Checks cookies every COOKIE_CHECK_INTERVAL ms for new session tokens.
|
||||
*/
|
||||
async function pollLoginStatus(session: QrSession) {
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
||||
// Check if expired
|
||||
if (now - session.createdAt > SESSION_TTL) {
|
||||
clearInterval(checkInterval);
|
||||
session.status = 'expired';
|
||||
cleanupSession(session.id);
|
||||
return;
|
||||
}
|
||||
|
||||
session.lastPollAt = now;
|
||||
|
||||
// Check cookies
|
||||
const cookies = await session.browserContext.cookies();
|
||||
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
|
||||
// Check for session cookies indicating login
|
||||
const hasSessionCookie = cookies.some(
|
||||
c => (c.name === '__st' || c.name === 'pus' || c.name === '__pus' || c.name === '__ktd')
|
||||
);
|
||||
|
||||
if (hasSessionCookie) {
|
||||
session.cookieSnapshot = cookieStr;
|
||||
session.status = 'logged_in';
|
||||
clearInterval(checkInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check URL change as alternative indicator
|
||||
const url = session.page.url();
|
||||
if (!url.includes('login') && !url.includes('qrcode') && url !== 'about:blank' && url !== 'https://pan.quark.cn/' && url.length > 10) {
|
||||
await checkAndCaptureCookies(session);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Page might have been closed
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, COOKIE_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check cookies after navigation/redirect and capture them if login succeeded.
|
||||
*/
|
||||
async function checkAndCaptureCookies(session: QrSession) {
|
||||
try {
|
||||
const cookies = await session.browserContext.cookies();
|
||||
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
const hasSessionCookie = cookies.some(
|
||||
c => (c.name === '__st' || c.name === 'pus' || c.name === '__pus' || c.name === '__ktd')
|
||||
);
|
||||
|
||||
if (hasSessionCookie) {
|
||||
session.cookieSnapshot = cookieStr;
|
||||
session.status = 'logged_in';
|
||||
} else if (cookies.length > 3) {
|
||||
const newCookies = cookies.filter(
|
||||
c => !['ctoken', 'b-user-id', '__wpkreporterwid_'].includes(c.name)
|
||||
);
|
||||
if (newCookies.length > 0) {
|
||||
session.cookieSnapshot = cookieStr;
|
||||
try {
|
||||
const resp = await session.page.evaluate(async () => {
|
||||
const r = await fetch('https://pan.quark.cn/account/info', {
|
||||
credentials: 'include',
|
||||
});
|
||||
return await r.text();
|
||||
});
|
||||
const data = JSON.parse(resp);
|
||||
if (data?.data?.nickname) {
|
||||
session.status = 'logged_in';
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the login status for a session.
|
||||
*/
|
||||
export async function getQrLoginStatus(sessionId: string): Promise<{
|
||||
status: string;
|
||||
cookie?: string;
|
||||
nickname?: string;
|
||||
storage_used?: string;
|
||||
storage_total?: string;
|
||||
autoUpdated?: boolean;
|
||||
updatedConfigId?: number;
|
||||
}> {
|
||||
const session = SESSIONS.get(sessionId);
|
||||
if (!session) {
|
||||
return { status: 'expired' };
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() - session.createdAt > SESSION_TTL) {
|
||||
session.status = 'expired';
|
||||
cleanupSession(sessionId);
|
||||
return { status: 'expired' };
|
||||
}
|
||||
|
||||
if (session.status === 'logged_in') {
|
||||
// Try to get nickname too
|
||||
let nickname = '';
|
||||
try {
|
||||
const resp = await session.page.evaluate(async () => {
|
||||
const r = await fetch('https://pan.quark.cn/account/info', {
|
||||
credentials: 'include',
|
||||
});
|
||||
return await r.text();
|
||||
});
|
||||
const data = JSON.parse(resp);
|
||||
nickname = data?.data?.nickname || '';
|
||||
} catch {}
|
||||
|
||||
// Fetch capacity info from within the browser context (has full JS signing)
|
||||
let storageTotal = '';
|
||||
let storageUsed = '';
|
||||
try {
|
||||
const capResp = await session.page.evaluate(async () => {
|
||||
const r = await fetch(
|
||||
'https://pan.quark.cn/1/clouddrive/capacity/detail?pr=ucpro&fr=pc',
|
||||
{ credentials: 'include' }
|
||||
);
|
||||
return await r.text();
|
||||
});
|
||||
const capData = JSON.parse(capResp);
|
||||
if (capData.status === 200 && capData.data?.capacity_summary) {
|
||||
const summary = capData.data.capacity_summary;
|
||||
const total = summary.sum_capacity || 0;
|
||||
storageTotal = formatBytes(total);
|
||||
storageUsed = '0 B'; // capacity/detail doesn't return used_size
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Build full cookie string including httpOnly cookies
|
||||
const cookies = await session.browserContext.cookies();
|
||||
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
||||
|
||||
// Extract __uid from cookie for duplicate detection
|
||||
const uidMatch = cookieStr.match(/(?<!\\w)__uid=([a-f0-9-]+)/);
|
||||
let autoUpdated = false;
|
||||
let updatedConfigId: number | undefined;
|
||||
|
||||
if (uidMatch) {
|
||||
const uid = uidMatch[1];
|
||||
try {
|
||||
const db = getDb();
|
||||
const existing = db.prepare(
|
||||
`SELECT id, nickname FROM cloud_configs WHERE cloud_type = 'quark' AND cookie LIKE ?`
|
||||
).get(`%${escapeLike(uid)}%`) as { id: number; nickname: string } | undefined;
|
||||
|
||||
if (existing) {
|
||||
// Same account → auto-update cookie with capacity info too
|
||||
const localTimestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET cookie = ?, storage_used = ?, storage_total = ?, updated_at = ? WHERE id = ?`
|
||||
).run(cookieStr, storageUsed || null, storageTotal || null, localTimestamp, existing.id);
|
||||
autoUpdated = true;
|
||||
updatedConfigId = existing.id;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Clean up session after successful login
|
||||
cleanupSession(sessionId);
|
||||
|
||||
return {
|
||||
status: 'logged_in',
|
||||
cookie: cookieStr,
|
||||
nickname,
|
||||
storage_used: storageUsed,
|
||||
storage_total: storageTotal,
|
||||
autoUpdated,
|
||||
updatedConfigId,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: session.status };
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a QR login session.
|
||||
*/
|
||||
export async function cancelQrLogin(sessionId: string): Promise<void> {
|
||||
cleanupSession(sessionId);
|
||||
}
|
||||
Reference in New Issue
Block a user