diff --git a/VERSION b/VERSION index 0b9c019..e473765 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.12 +0.3.13 diff --git a/docker-compose.env b/docker-compose.env new file mode 100644 index 0000000..76a1976 --- /dev/null +++ b/docker-compose.env @@ -0,0 +1,37 @@ +# CloudSearch 环境变量 — 统一管理 +# 复制此文件为 .env 使用:cp docker-compose.env .env + +# ── 核心服务 ── +PORT=9527 +NODE_ENV=production +TZ=Asia/Shanghai + +# ── 安全 ── +JWT_SECRET=cloudsearch-jwt-prod-v1 +ADMIN_USERNAME=admin +ADMIN_PASSWORD=0nL5kLhMIJ1121PYmQb25A +COOKIE_ENCRYPTION_KEY= +CORS_ORIGIN=http://jp-cs.timaa.cn + +# ── 数据库 & 缓存 ── +DB_PATH=/data/database.sqlite +REDIS_URL=redis://:redis_GbR7XZ@1Panel-redis-aDp3:6379 + +# ── 外部服务 ── +PANSOU_URL=http://pansou:80 +PANSOU_AUTH_TOKEN= +VIDEO_PARSER_URL=http://localhost:3001 + +# ── 网盘校验 ── +VALIDATION_CONCURRENCY=10 +VALIDATION_TIMEOUT=5000 +CACHE_TTL_VALID=14400 +CACHE_TTL_INVALID=3600 + +# ── 路径 ── +CHROMIUM_PATH=/usr/bin/chromium-browser +APP_VERSION_FILE=/data/VERSION +UPLOAD_DIR=/app/uploads + +# ── 日志 ── +LOG_LEVEL=info diff --git a/source_clean/VERSION b/source_clean/VERSION index 0b9c019..e473765 100644 --- a/source_clean/VERSION +++ b/source_clean/VERSION @@ -1 +1 @@ -0.3.12 +0.3.13 diff --git a/source_clean/src/cloud/credential.service.ts b/source_clean/src/cloud/credential.service.ts index 6d0cfa5..d2662e2 100644 --- a/source_clean/src/cloud/credential.service.ts +++ b/source_clean/src/cloud/credential.service.ts @@ -23,13 +23,13 @@ export interface CloudConfig { // ── Cookie UID Extraction ──────────────────────────────────────── -function extractCookieUid(cookie: string): string { - 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]; @@ -42,22 +42,26 @@ function decryptCookie(encrypted: string): string { export function getCloudConfigs(): CloudConfig[] { const db = getDb(); - return db.prepare( + const rows = db.prepare( `SELECT id, cloud_type, cookie, nickname, is_active, 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(); - return db.prepare( + const rows = db.prepare( `SELECT id, cloud_type, nickname, is_active, 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. */ @@ -70,6 +74,7 @@ export function getCloudConfigByType(cloudType: string): CloudConfig | undefined 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; } @@ -81,19 +86,22 @@ export function getCloudConfigById(id: number): CloudConfig | undefined { 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(); - return db.prepare( + const rows = db.prepare( `SELECT id, cloud_type, cookie, nickname, is_active, 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: { @@ -210,6 +218,8 @@ export async function testCloudConnection(id: number): Promise<{ return { success: false, message: 'Cookie not configured' }; } + const cookie = decryptCookie(config.cookie); + try { let valid = false; let nickname = ''; @@ -218,7 +228,7 @@ export async function testCloudConnection(id: number): Promise<{ if (config.cloud_type === 'baidu') { const { BaiduDriver } = require('./drivers/baidu.driver'); - const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname }); + const driver = new BaiduDriver({ cookie: cookie, nickname: config.nickname }); valid = await driver.validate(); if (valid) { const info = await driver.getUserInfo(); @@ -231,10 +241,10 @@ export async function testCloudConnection(id: number): Promise<{ } } else { const { QuarkDriver } = require('./drivers/quark.driver'); - const driver = new QuarkDriver({ cookie: config.cookie, nickname: config.nickname }); + const driver = new QuarkDriver({ cookie: cookie, nickname: config.nickname }); valid = await driver.validate(); if (valid) { - nickname = config.nickname || (await fetchQuarkNickname(config.cookie)) || '夸克网盘'; + nickname = config.nickname || (await fetchQuarkNickname(cookie)) || '夸克网盘'; const storage = await driver.getStorageInfoQuick(); storageTotal = (storage.total !== '-' && storage.total !== '0 B') ? storage.total : (config.storage_total || ''); } @@ -345,15 +355,17 @@ export async function getAndValidateCredential(cloudType: string): Promise 0; +} + +// ── Cookie Validation ──────────────────────────────────────────── + +async function fetchQuarkNickname(cookie: string): Promise { + 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' }; + } + + 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: config.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: config.cookie, nickname: config.nickname }); + valid = await driver.validate(); + if (valid) { + nickname = config.nickname || (await fetchQuarkNickname(config.cookie)) || '夸克网盘'; + const storage = await driver.getStorageInfoQuick(); + 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 无效或已过期,或网络暂时异常' }; + } + + db.prepare( + `UPDATE cloud_configs SET nickname = ?, storage_total = ?, storage_used = ?, is_active = 1, verification_status = 'valid', updated_at = ? WHERE id = ?` + ).run(nickname, storageTotal, storageUsed, 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 { + 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}`, + }; + } + + try { + let cookieValid = false; + if (cloudType === 'baidu') { + const { BaiduDriver } = require('./drivers/baidu.driver'); + const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname }); + cookieValid = await driver.validate(); + } else { + const { QuarkDriver } = require('./drivers/quark.driver'); + const driver = new QuarkDriver({ cookie: config.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}`, + }; + } + + return { + valid: true, + config, + message: 'ok', + }; + } catch (err: any) { + return { + valid: false, + errorCode: 'VALIDATION_ERROR', + message: `Credential validation failed: ${err.message}`, + }; + } +} diff --git a/source_clean/src/config/index.ts b/source_clean/src/config/index.ts index b044b94..d976ad8 100755 --- a/source_clean/src/config/index.ts +++ b/source_clean/src/config/index.ts @@ -14,6 +14,11 @@ export interface Config { cacheTtlValid: number; cacheTtlInvalid: number; }; + corsOrigin: string; + cookieEncryptionKey: string; + logLevel: string; + appVersionFile: string; + uploadDir: string; chromiumPath: string; dbPath: string; } @@ -34,6 +39,11 @@ const config: Config = { cacheTtlValid: parseInt(process.env.CACHE_TTL_VALID || '14400', 10), // 4小时 cacheTtlInvalid: parseInt(process.env.CACHE_TTL_INVALID || '3600', 10), // 1小时 }, + corsOrigin: process.env.CORS_ORIGIN || '', + cookieEncryptionKey: process.env.COOKIE_ENCRYPTION_KEY || '', + logLevel: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'), + appVersionFile: process.env.APP_VERSION_FILE || '/app/VERSION', + uploadDir: process.env.UPLOAD_DIR || '/app/uploads', chromiumPath: process.env.CHROMIUM_PATH || "/usr/bin/chromium-browser", dbPath: process.env.DB_PATH || './data/cloudsearch.db', }; diff --git a/source_clean/src/main.ts b/source_clean/src/main.ts index fb7bc1e..0742c36 100755 --- a/source_clean/src/main.ts +++ b/source_clean/src/main.ts @@ -20,7 +20,7 @@ const app = express(); app.set('trust proxy', true); // CORS — 生产环境必须配置真实域名(空值或占位符用 * 并打警告日志) -const corsOrigin = process.env.CORS_ORIGIN || ''; +const corsOrigin = config.corsOrigin; const isPlaceholder = !corsOrigin || corsOrigin === 'https://your-domain.com'; if (config.nodeEnv === 'production' && isPlaceholder) { console.error('[FATAL] CORS_ORIGIN 未配置或使用了占位符 https://your-domain.com,生产环境必须设置真实域名。应用拒绝启动。'); @@ -56,7 +56,7 @@ app.use(express.static(frontendDist)); app.use(rateLimiter); // ============ Routes ============ -app.use('/api/uploads', express.static('/app/uploads')); +app.use('/api/uploads', express.static(config.uploadDir)); app.use('/api', routes); // ============ Health Check(增强版:覆盖 Redis / PanSou / VideoParser 状态) ============