From 1fd64e0788fb03ec1ac88a3b57cff5ef2a484479 Mon Sep 17 00:00:00 2001 From: admin <362324317@qq.com> Date: Sun, 17 May 2026 17:59:23 +0800 Subject: [PATCH] =?UTF-8?q?v0.3.23:=20=E5=8E=BB=E9=87=8D=20=E2=80=94=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=87=8D=E5=A4=8D=E6=A8=A1=E5=9D=97=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=BA=E5=8D=95=E4=B8=80=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?+=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复的重复: - 删除 src/utils/qr-login.service.ts(与 src/cloud/ 完全一致) - qr-login.service.ts 本地 formatBytes → import { formatBytes } from quark-api - daily-report.service.ts 本地 CLOUD_LABELS → import from cloud-constants - 新增 cloudLabel() 本地映射(短标签 vs cloud-constants 全名) 确认无其他重复: 文件级(utils vs cloud)、函数级(export function)、字节解析 --- VERSION | 2 +- source_clean/VERSION | 2 +- source_clean/src/cloud/qr-login.service.ts | 8 +- .../src/services/daily-report.service.ts | 10 +- source_clean/src/utils/qr-login.service.ts | 408 ------------------ 5 files changed, 7 insertions(+), 423 deletions(-) delete mode 100755 source_clean/src/utils/qr-login.service.ts diff --git a/VERSION b/VERSION index 0c4b454..a1dad2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.22 +0.3.23 diff --git a/source_clean/VERSION b/source_clean/VERSION index 0c4b454..a1dad2a 100644 --- a/source_clean/VERSION +++ b/source_clean/VERSION @@ -1 +1 @@ -0.3.22 +0.3.23 diff --git a/source_clean/src/cloud/qr-login.service.ts b/source_clean/src/cloud/qr-login.service.ts index 7b07f41..9213b38 100755 --- a/source_clean/src/cloud/qr-login.service.ts +++ b/source_clean/src/cloud/qr-login.service.ts @@ -20,6 +20,7 @@ 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 @@ -393,13 +394,6 @@ export async function getQrLoginStatus(sessionId: string): Promise<{ return { status: session.status }; } -function formatBytes(bytes: number): string { - 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]; -} - /** * Cancel a QR login session. */ diff --git a/source_clean/src/services/daily-report.service.ts b/source_clean/src/services/daily-report.service.ts index def74a0..407335d 100644 --- a/source_clean/src/services/daily-report.service.ts +++ b/source_clean/src/services/daily-report.service.ts @@ -6,6 +6,7 @@ import { getDb } from '../database/database'; import { getSystemConfig } from '../admin/system-config.service'; import { getCloudConfigs } from '../cloud/credential.service'; import { notify } from '../cloud/notification.service'; +import { CLOUD_LABELS } from '../config/cloud-constants'; // ═══════════════════════════════════════════════ // Types @@ -193,14 +194,11 @@ function formatStorage(used: string, total: string): string { } } -const CLOUD_LABELS: Record = { - quark: '夸克', baidu: '百度', aliyun: '阿里云盘', '115': '115网盘', - tianyi: '天翼云', '123pan': '123云盘', uc: 'UC网盘', xunlei: '迅雷云盘', - pikpak: 'PikPak', magnet: '磁力链接', ed2k: '电驴链接', others: '其他', -}; + function cloudLabel(type: string): string { - return CLOUD_LABELS[type] || type; + const short: Record = { quark: "夸克", baidu: "百度", aliyun: "阿里云盘", "115": "115网盘", tianyi: "天翼云", "123pan": "123云盘", uc: "UC网盘", xunlei: "迅雷云盘", pikpak: "PikPak", magnet: "磁力链接", ed2k: "电驴链接", others: "其他" }; + return short[type] || (CLOUD_LABELS as Record)[type] || type; } export function formatDailyReport(report: DailyReport, includeSearch = true, includeSaves = true, includeStorage = true, includeUsers = true): string { diff --git a/source_clean/src/utils/qr-login.service.ts b/source_clean/src/utils/qr-login.service.ts deleted file mode 100755 index 7b07f41..0000000 --- a/source_clean/src/utils/qr-login.service.ts +++ /dev/null @@ -1,408 +0,0 @@ -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(); -const SESSION_TTL = 5 * 60 * 1000; // 5 minutes -const COOKIE_CHECK_INTERVAL = 1500; // 1.5s between cookie checks - -import config from '../config'; -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 { - // 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(/(? { - cleanupSession(sessionId); -}