// PikPak Deno API Client and Server - Combined File // This file contains: // 1. Type definitions // 2. Exception classes // 3. Utility functions // 4. PikPakApi class implementation // 5. Server implementation with Oak // fuck // 导入原始机器人代码 import "./tgbot.ts"; import { Application, Router, Context, Status } from "https://deno.land/x/oak/mod.ts"; import { oakCors } from "https://deno.land/x/cors/mod.ts"; import { delay } from "https://deno.land/std/async/mod.ts"; // import { createHash } from "https://deno.land/std/hash/mod.ts"; const { createHash } = await import('node:crypto'); // =============================================================== // 1. Type definitions (originally types.ts) // =============================================================== // 在类外部添加这些变量 let isRefreshing = false; let lastRefreshTime = 0; export enum DownloadStatus { NOT_DOWNLOADING = "not_downloading", DOWNLOADING = "downloading", DONE = "done", ERROR = "error", NOT_FOUND = "not_found" } export interface UserInfo { username?: string; user_id?: string; access_token?: string; refresh_token?: string; encoded_token?: string; } export interface FileInfo { id: string; name: string; file_type: string; } export interface TokenRefreshCallbackFn { (client: any, ...args: any[]): Promise; } export interface HttpxClientArgs { timeout?: number; [key: string]: any; } export interface FilterOptions { [key: string]: any; } export interface FileListResponse { files: any[]; next_page_token?: string; [key: string]: any; } export interface OfflineListResponse { tasks: any[]; next_page_token?: string; [key: string]: any; } export interface FileRequest { size: number; parent_id?: string | null; next_page_token?: string | null; additional_filters?: Record | null; } export interface OfflineRequest { file_url: string; parent_id?: string | null; name?: string | null; } // =============================================================== // 2. Exception classes (originally exceptions.ts) // =============================================================== export class PikpakException extends Error { constructor(message: string) { super(message); this.name = "PikpakException"; } } export class PikpakRetryException extends PikpakException { constructor(message: string) { super(message); this.name = "PikpakRetryException"; } } // =============================================================== // 3. Utility functions (originally utils.ts) // =============================================================== export const CLIENT_ID = "ZQL_zwA4qhHcoe_2"; export const CLIENT_SECRET = "Og9Vr1L8Ee6bh0olFxFDRg"; export const CLIENT_VERSION = "1.06.0.2132"; export const PACKAG_ENAME = "com.thunder.downloader"; export const SDK_VERSION = "2.0.3.203100 "; export const APP_NAME = PACKAG_ENAME; /** * Get current timestamp in milliseconds */ // export function getTimestamp(): number { // return Date.now(); // } export function getTimestamp(): string { // 确保返回毫秒级时间戳,并且是字符串格式 return Math.floor(Date.now()).toString(); } /** * Generate a random device ID */ export function deviceIdGenerator(): string { return crypto.randomUUID().replace(/-/g, ""); } const SALTS = [ "kVy0WbPhiE4v6oxXZ88DvoA3Q", "lON/AUoZKj8/nBtcE85mVbkOaVdVa", "rLGffQrfBKH0BgwQ33yZofvO3Or", "FO6HWqw", "GbgvyA2", "L1NU9QvIQIH7DTRt", "y7llk4Y8WfYflt6", "iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe", "8C28RTXmVcco0", "X5Xh", "7xe25YUgfGgD0xW3ezFS", "", "CKCR", "8EmDjBo6h3eLaK7U6vU2Qys0NsMx", "t2TeZBXKqbdP09Arh9C3", ]; /** * Generate a captcha sign */ // export function captchaSign(deviceId: string, timestamp: string): string { // let sign = CLIENT_ID + CLIENT_VERSION + PACKAG_ENAME + deviceId + timestamp; // for (const salt of SALTS) { // const encoder = new TextEncoder(); // const data = encoder.encode(sign + salt); // sign = createHash("md5").update(data).toString(); // } // return `1.${sign}`; // } export function captchaSign(deviceId: string, timestamp: string): string { let sign = CLIENT_ID + CLIENT_VERSION + PACKAG_ENAME + deviceId + timestamp; for (const salt of SALTS) { // 在 Node.js 兼容模式下使用 createHash const md5Hash = createHash('md5'); md5Hash.update(sign + salt); sign = md5Hash.digest('hex'); } return `1.${sign}`; } /** * Generate device sign */ export function generateDeviceSign(deviceId: string, packageName: string): string { const signatureBase = `${deviceId}${packageName}1appkey`; // Calculate SHA-1 hash const encoder = new TextEncoder(); const sha1Result = createHash("sha1").update(encoder.encode(signatureBase)).toString(); // Calculate MD5 hash const md5Result = createHash("md5").update(encoder.encode(sha1Result)).toString(); return `div101.${deviceId}${md5Result}`; } /** * Build a custom user agent string */ export function buildCustomUserAgent(deviceId: string, userId: string): string { const deviceSign = generateDeviceSign(deviceId, PACKAG_ENAME); const userAgentParts = [ `ANDROID-${APP_NAME}/${CLIENT_VERSION}`, "protocolVersion/200", "accesstype/", `clientid/${CLIENT_ID}`, `clientversion/${CLIENT_VERSION}`, "action_type/", "networktype/WIFI", "sessionid/", `deviceid/${deviceId}`, "providername/NONE", `devicesign/${deviceSign}`, "refresh_token/", `sdkversion/${SDK_VERSION}`, `datetime/${getTimestamp()}`, `usrno/${userId}`, `appname/${APP_NAME}`, "session_origin/", "grant_type/", "appid/", "clientip/", "devicename/Xiaomi_M2004j7ac", "osversion/13", "platformversion/10", "accessmode/", "devicemodel/M2004J7AC" ]; return userAgentParts.join(" "); } /** * Base64 encoding and decoding functions */ export function b64encode(str: string): string { return btoa(str); } export function b64decode(str: string): string { return atob(str); } // =============================================================== // 4. PikPakApi class implementation (originally pikpak_api.ts) // =============================================================== export class PikPakApi { /** * PikPakApi class * * Attributes similar to the Python version */ static readonly PIKPAK_API_HOST = "api-pan.xunleix.com"; static readonly PIKPAK_USER_HOST = "xluser-ssl.xunleix.com"; // User credentials and tokens username?: string; password?: string; encoded_token?: string; access_token?: string; refresh_token?: string; user_id?: string; // Request configuration max_retries: number; initial_backoff: number; device_id: string; captcha_token?: string; data_response?: any; user_agent?: string; // Callback for token refresh token_refresh_callback?: TokenRefreshCallbackFn; token_refresh_callback_kwargs: Record; // Path-to-ID cache private _path_id_cache: Record = {}; constructor( options: { username?: string; password?: string; encoded_token?: string; httpx_client_args?: HttpxClientArgs; device_id?: string; request_max_retries?: number; request_initial_backoff?: number; token_refresh_callback?: TokenRefreshCallbackFn; token_refresh_callback_kwargs?: Record; } = {} ) { this.username = options.username; this.password = options.password; this.encoded_token = options.encoded_token; this.max_retries = options.request_max_retries || 3; this.initial_backoff = options.request_initial_backoff || 3.0; this.token_refresh_callback = options.token_refresh_callback; this.token_refresh_callback_kwargs = options.token_refresh_callback_kwargs || {}; // Generate device_id if not provided this.device_id = options.device_id || this.generateDeviceId(); if (this.encoded_token) { this.decodeToken(); } else if (this.username && this.password) { // Login will be done later } else { throw new PikpakException("username and password or encoded_token is required"); } } /** * Generate device ID based on username and password */ private generateDeviceId(): string { const idBase = `${this.username || ""}${this.password || ""}`; return createHash("md5").update(new TextEncoder().encode(idBase)).toString(); } /** * Create PikPakApi object from a dictionary/object */ // static fromDict(data: Record): PikPakApi { // const client = new PikPakApi(); // // Copy all properties from data to client // for (const [key, value] of Object.entries(data)) { // if (typeof value !== 'function') { // (client as any)[key] = value; // } // } // return client; // } static fromDict(data: Record): PikPakApi { // 创建一个临时客户端,绕过构造函数验证 const client = new PikPakApi({ encoded_token: "temporary" // 临时值,稍后会被覆盖 }); // 从数据中复制所有属性 for (const [key, value] of Object.entries(data)) { if (typeof value !== 'function') { (client as any)[key] = value; } } // 手动解码令牌 if (client.encoded_token && client.encoded_token !== "temporary") { try { client.decodeToken(); } catch (error) { console.log("Warning: Failed to decode token"); } } return client; } /** * Returns the PikPakApi object as a dictionary/object */ toDict(): Record { const data: Record = {}; // Copy all properties that can be serialized for (const [key, value] of Object.entries(this)) { if ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value) || (typeof value === 'object' && value !== null) || value === null || value === undefined ) { if (typeof value !== 'function') { data[key] = value; } } } return data; } /** * Build custom user agent */ buildCustomUserAgent(): string { this.user_agent = buildCustomUserAgent( this.device_id, this.user_id || "" ); return this.user_agent; } /** * Get headers for API requests */ getHeaders(access_token?: string): Record { const headers: Record = { "User-Agent": this.captcha_token ? this.buildCustomUserAgent() : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", "Content-Type": "application/json; charset=utf-8" }; if (this.access_token) { headers["Authorization"] = `Bearer ${this.access_token}`; } if (access_token) { headers["Authorization"] = `Bearer ${access_token}`; } if (this.captcha_token) { headers["X-Captcha-Token"] = this.captcha_token; } if (this.device_id) { headers["X-Device-Id"] = this.device_id; } return headers; } /** * Make an HTTP request with retry logic */ private async _makeRequest( method: string, url: string, data?: Record, params?: Record, headers?: Record ): Promise { let lastError: Error | null = null; for (let attempt = 0; attempt < this.max_retries; attempt++) { try { const response = await this._sendRequest(method, url, data, params, headers); return await this._handleResponse(response); } catch (error) { if (error instanceof PikpakRetryException) { console.info(`Retry attempt ${attempt + 1}/${this.max_retries}`); lastError = error; } else if (error instanceof PikpakException) { throw error; } else { console.error(`Unexpected error on attempt ${attempt + 1}/${this.max_retries}: ${String(error)}`); lastError = error instanceof Error ? error : new Error(String(error)); } } await delay(this.initial_backoff * Math.pow(2, attempt) * 1000); } // If we've exhausted all retries, raise an exception with the last error throw new PikpakException(`Max retries reached. Last error: ${lastError?.message || "Unknown error"}`); } /** * Send an HTTP request */ private async _sendRequest( method: string, url: string, data?: Record, params?: Record, headers?: Record ): Promise { const reqHeaders = headers || this.getHeaders(); const options: RequestInit = { method, headers: reqHeaders, }; // Add request body if data is provided if (data && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { options.body = JSON.stringify(data); } // Add URL parameters if provided if (params) { const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { if (typeof value === 'object') { queryParams.set(key, JSON.stringify(value)); } else { queryParams.set(key, String(value)); } } } const queryString = queryParams.toString(); if (queryString) { url = `${url}${url.includes('?') ? '&' : '?'}${queryString}`; } } return await fetch(url, options); } /** * Handle HTTP response */ private async _handleResponse(response: Response): Promise { // Check if response is empty or not JSON let jsonData: any; try { jsonData = await response.json(); } catch (error) { if (response.status === 200) { return {}; } throw new PikpakRetryException("Empty JSON data"); } this.data_response = jsonData; if (!jsonData) { if (response.status === 200) { return {}; } throw new PikpakRetryException("Empty JSON data"); } if (!("error" in jsonData)) { return jsonData; } if ("captcha_token" in jsonData) { this.captcha_token = jsonData.captcha_token; } if (jsonData.error === "invalid_account_or_password") { throw new PikpakException("Invalid username or password"); } // if (jsonData.error_code === 16) { // await this.refreshAccessToken(); // throw new PikpakRetryException("Token refreshed, please retry"); // } if (jsonData.error_code === 16) { const now = Date.now(); // 防止30秒内多次刷新 if (isRefreshing && now - lastRefreshTime < 30000) { console.log("Token refresh already in progress, waiting..."); // 等待一小段时间后重试 await delay(2000); throw new PikpakRetryException("Token refresh in progress, please retry"); } isRefreshing = true; try { console.log("Refreshing access token on demand..."); await this.refreshAccessToken(); lastRefreshTime = Date.now(); console.log("Token refreshed successfully"); } catch (refreshError) { console.error("Failed to refresh token:", refreshError); try { console.log("Attempting to re-login..."); await this.login(); console.log("Re-login successful"); } catch (loginError) { console.error("Re-login failed:", loginError); throw new PikpakException("Authentication failed, unable to refresh token or login"); } } finally { isRefreshing = false; } throw new PikpakRetryException("Token refreshed, please retry"); } throw new PikpakException(jsonData.error_description || "Unknown Error"); } /** * HTTP GET request */ private async _requestGet(url: string, params?: Record): Promise { return await this._makeRequest("GET", url, undefined, params); } /** * HTTP POST request */ private async _requestPost(url: string, data?: Record, headers?: Record): Promise { return await this._makeRequest("POST", url, data, undefined, headers); } /** * HTTP PATCH request */ private async _requestPatch(url: string, data?: Record): Promise { return await this._makeRequest("PATCH", url, data); } /** * HTTP DELETE request */ private async _requestDelete(url: string, params?: Record, data?: Record): Promise { return await this._makeRequest("DELETE", url, data, params); } /** * Decode encoded token */ decodeToken(): void { try { const decodedData = JSON.parse(b64decode(this.encoded_token || "")); if (!decodedData.access_token || !decodedData.refresh_token) { throw new PikpakException("Invalid encoded token"); } this.access_token = decodedData.access_token; this.refresh_token = decodedData.refresh_token; } catch (error) { throw new PikpakException("Invalid encoded token"); } } /** * Encode access and refresh tokens */ encodeToken(): void { const tokenData = { access_token: this.access_token, refresh_token: this.refresh_token }; this.encoded_token = b64encode(JSON.stringify(tokenData)); } /** * Initialize captcha */ async captchaInit(action: string, meta?: Record): Promise { const url = `https://${PikPakApi.PIKPAK_USER_HOST}/v1/shield/captcha/init`; if (!meta) { const t = `${getTimestamp()}`; meta = { captcha_sign: captchaSign(this.device_id, t), client_version: CLIENT_VERSION, package_name: PACKAG_ENAME, user_id: this.user_id || "", timestamp: t }; } const params = { client_id: CLIENT_ID, action: action, device_id: this.device_id, meta: meta }; return await this._requestPost(url, params); } /** * Login to PikPak */ async login(): Promise { const loginUrl = `https://${PikPakApi.PIKPAK_USER_HOST}/v1/auth/signin`; const metas: Record = {}; if (!this.username || !this.password) { throw new PikpakException("username and password are required"); } // Determine login method based on username format if (/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(this.username)) { metas.email = this.username; } else if (/\d{11,18}/.test(this.username)) { metas.phone_number = this.username; } else { metas.username = this.username; } const result = await this.captchaInit(`POST:${loginUrl}`, metas); const captchaToken = result.captcha_token || ""; if (!captchaToken) { throw new PikpakException("captcha_token get failed"); } // Prepare login data const loginData = new URLSearchParams(); loginData.append("client_id", CLIENT_ID); loginData.append("client_secret", CLIENT_SECRET); loginData.append("password", this.password); loginData.append("username", this.username); loginData.append("captcha_token", captchaToken); // Custom headers for form data const headers = { "Content-Type": "application/x-www-form-urlencoded", }; // Send login request with form data const response = await fetch(loginUrl, { method: "POST", headers: headers, body: loginData }); const userInfo = await response.json(); if (userInfo.error) { throw new PikpakException(userInfo.error_description || "Login failed"); } this.access_token = userInfo.access_token; this.refresh_token = userInfo.refresh_token; this.user_id = userInfo.sub; this.encodeToken(); } /** * Refresh access token */ async refreshAccessToken(): Promise { const refreshUrl = `https://${PikPakApi.PIKPAK_USER_HOST}/v1/auth/token`; const refreshData = { client_id: CLIENT_ID, refresh_token: this.refresh_token, grant_type: "refresh_token" }; const userInfo = await this._requestPost(refreshUrl, refreshData); this.access_token = userInfo.access_token; this.refresh_token = userInfo.refresh_token; this.user_id = userInfo.sub; this.encodeToken(); if (this.token_refresh_callback) { await this.token_refresh_callback(this, ...Object.values(this.token_refresh_callback_kwargs)); } } /** * Get user info */ getUserInfo(): UserInfo { return { username: this.username, user_id: this.user_id, access_token: this.access_token, refresh_token: this.refresh_token, encoded_token: this.encoded_token }; } /** * Create a folder */ async createFolder(name: string = "新建文件夹", parent_id?: string): Promise { const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files`; const data = { kind: "drive#folder", name: name, parent_id: parent_id }; const captchaResult = await this.captchaInit("GET:/drive/v1/files"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost(url, data); return result; } /** * Move files/folders to trash */ async deleteToTrash(ids: string[]): Promise { const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchTrash`; const data = { ids: ids }; const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchTrash"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost(url, data); return result; } /** * Restore files/folders from trash */ async untrash(ids: string[]): Promise { const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchUntrash`; const data = { ids: ids }; const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchUntrash"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost(url, data); return result; } /** * Empty trash */ async emptytrash(): Promise { const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/trash:empty`; const data = {}; const captchaResult = await this.captchaInit("PATCH:/drive/v1/files/trash:empty"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPatch(url, data); return result; } /** * Permanently delete files/folders */ async deleteForever(ids: string[]): Promise { const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchDelete`; const data = { ids: ids }; const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchDelete"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost(url, data); return result; } /** * Offline download */ async offlineDownload(file_url: string, parent_id?: string, name?: string): Promise { const downloadUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files`; const downloadData = { kind: "drive#file", name: name, upload_type: "UPLOAD_TYPE_URL", url: { url: file_url, parent_id: parent_id }, parent_id: parent_id }; const captchaResult = await this.captchaInit("GET:/drive/v1/files"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost(downloadUrl, downloadData); return result; } /** * Get offline download list */ async offlineList( size: number = 10000, next_page_token?: string, phase?: string[] ): Promise { if (!phase) { phase = ["PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR"]; } const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/tasks`; const listData = { type: "offline", thumbnail_size: "SIZE_SMALL", limit: size, page_token: next_page_token, filters: JSON.stringify({ phase: { in: phase.join(",") } }), with: "reference_resource" }; const captchaResult = await this.captchaInit("GET:/drive/v1/tasks"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestGet(listUrl, listData); return result; } /** * Get offline file info */ async offlineFileInfo(file_id: string): Promise { const captchaResult = await this.captchaInit(`GET:/drive/v1/files/${file_id}`); this.captcha_token = captchaResult.captcha_token; const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/${file_id}`; const result = await this._requestGet(url, { thumbnail_size: "SIZE_LARGE" }); return result; } /** * Get file list */ async fileList( size: number = 100, parent_id?: string, next_page_token?: string, additional_filters?: Record ): Promise { const defaultFilters: Record = { trashed: { eq: false }, phase: { eq: "PHASE_TYPE_COMPLETE" } }; if (additional_filters) { Object.assign(defaultFilters, additional_filters); } const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files`; const listData = { parent_id: parent_id, thumbnail_size: "SIZE_MEDIUM", limit: size, with_audit: "true", page_token: next_page_token, filters: JSON.stringify(defaultFilters) }; const captchaResult = await this.captchaInit("GET:/drive/v1/files"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestGet(listUrl, listData); return result; } /** * Get events list */ async events(size: number = 100, next_page_token?: string): Promise { const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/events`; const listData = { thumbnail_size: "SIZE_MEDIUM", limit: size, next_page_token: next_page_token }; const captchaResult = await this.captchaInit("GET:/drive/v1/files"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestGet(listUrl, listData); return result; } /** * Retry offline download task */ async offlineTaskRetry(task_id: string): Promise { const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/task`; const listData = { type: "offline", create_type: "RETRY", id: task_id }; try { const captchaResult = await this.captchaInit("GET:/drive/v1/task"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost(listUrl, listData); return result; } catch (error) { throw new PikpakException(`Failed to retry offline task: ${task_id}. ${error}`); } } /** * Delete tasks */ async deleteTasks(task_ids: string[], delete_files: boolean = false): Promise { const deleteUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/tasks`; const params = { task_ids: task_ids, delete_files: delete_files }; try { const captchaResult = await this.captchaInit("GET:/drive/v1/tasks"); this.captcha_token = captchaResult.captcha_token; await this._requestDelete(deleteUrl, params); } catch (error) { throw new PikpakException(`Failing to delete tasks: ${task_ids}. ${error}`); } } /** * Get task status */ async getTaskStatus(task_id: string, file_id: string): Promise { try { const infos = await this.offlineList(); if (infos && infos.tasks) { for (const task of infos.tasks) { if (task_id === task.id) { return DownloadStatus.DOWNLOADING; } } } const fileInfo = await this.offlineFileInfo(file_id); if (fileInfo) { return DownloadStatus.DONE; } else { return DownloadStatus.NOT_FOUND; } } catch (error) { return DownloadStatus.ERROR; } } /** * Convert path to ID */ async pathToId(path: string, create: boolean = false): Promise { if (!path || path.length <= 0) { return []; } const paths = path.split('/').filter(p => p.trim().length > 0); // Construct multi-level paths for cache lookup const multiLevelPaths = []; for (let i = 0; i < paths.length; i++) { multiLevelPaths.push('/' + paths.slice(0, i + 1).join('/')); } // Check cache hits const pathIds: FileInfo[] = []; for (const p of multiLevelPaths) { if (this._path_id_cache[p]) { pathIds.push(this._path_id_cache[p]); } else { break; } } // Determine how much of the path we've already found let count = pathIds.length; let parentId = count > 0 ? pathIds[count - 1].id : null; // Find or create the remaining path components let nextPageToken: string | null = null; while (count < paths.length) { const data = await this.fileList(100, parentId, nextPageToken); let recordOfTargetPath = null; for (const f of data.files || []) { const currentPath = '/' + [...paths.slice(0, count), f.name].join('/'); const fileType = f.kind.includes('folder') ? 'folder' : 'file'; const record: FileInfo = { id: f.id, name: f.name, file_type: fileType }; this._path_id_cache[currentPath] = record; if (f.name === paths[count]) { recordOfTargetPath = record; } } if (recordOfTargetPath) { pathIds.push(recordOfTargetPath); count++; parentId = recordOfTargetPath.id; } else if (data.next_page_token && (!nextPageToken || nextPageToken !== data.next_page_token)) { nextPageToken = data.next_page_token; } else if (create) { const createdData = await this.createFolder(paths[count], parentId); const fileId = createdData.file.id; const record: FileInfo = { id: fileId, name: paths[count], file_type: 'folder' }; pathIds.push(record); const currentPath = '/' + paths.slice(0, count + 1).join('/'); this._path_id_cache[currentPath] = record; count++; parentId = fileId; } else { break; } } return pathIds; } /** * Batch move files */ async fileBatchMove(ids: string[], to_parent_id?: string): Promise { const to = to_parent_id ? { parent_id: to_parent_id } : {}; const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchMove"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchMove`, { ids: ids, to: to } ); return result; } /** * Batch copy files */ async fileBatchCopy(ids: string[], to_parent_id?: string): Promise { const to = to_parent_id ? { parent_id: to_parent_id } : {}; const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchCopy"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchCopy`, { ids: ids, to: to } ); return result; } /** * Move or copy files by path */ async fileMoveOrCopyByPath( from_path: string[], to_path: string, move: boolean = false, create: boolean = false ): Promise { const fromIds: string[] = []; for (const path of from_path) { const pathIds = await this.pathToId(path); if (pathIds.length > 0) { const fileId = pathIds[pathIds.length - 1].id; if (fileId) { fromIds.push(fileId); } } } if (fromIds.length === 0) { throw new PikpakException("Files to move do not exist"); } const toPathIds = await this.pathToId(to_path, create); const toParentId = toPathIds.length > 0 ? toPathIds[toPathIds.length - 1].id : undefined; let result; if (move) { result = await this.fileBatchMove(fromIds, toParentId); } else { result = await this.fileBatchCopy(fromIds, toParentId); } return result; } /** * Get download URL */ async getDownloadUrl(file_id: string): Promise { const result = await this.captchaInit(`GET:/drive/v1/files/${file_id}`); this.captcha_token = result.captcha_token; const fileResult = await this._requestGet( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/${file_id}?` ); this.captcha_token = null; return fileResult; } /** * Rename file */ async fileRename(id: string, new_file_name: string): Promise { const data = { name: new_file_name }; const captchaResult = await this.captchaInit(`GET:/drive/v1/files/${id}`); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPatch( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/${id}`, data ); return result; } /** * Batch star files */ async fileBatchStar(ids: string[]): Promise { const data = { ids: ids }; const captchaResult = await this.captchaInit("GET:/drive/v1/files/star"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:star`, data ); return result; } /** * Batch unstar files */ async fileBatchUnstar(ids: string[]): Promise { const data = { ids: ids }; const captchaResult = await this.captchaInit("GET:/drive/v1/files/unstar"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:unstar`, data ); return result; } /** * Get starred file list */ async fileStarList(size: number = 100, next_page_token?: string): Promise { const additionalFilters = { system_tag: { in: "STAR" } }; const result = await this.fileList( size, "*", next_page_token, additionalFilters ); return result; } /** * Batch share files */ // async fileBatchShare( // ids: string[], // need_password: boolean = false, // expiration_days: number = -1 // ): Promise { // const data = { // file_ids: ids, // share_to: need_password ? "encryptedlink" : "publiclink", // expiration_days: expiration_days, // pass_code_option: need_password ? "REQUIRED" : "NOT_REQUIRED" // }; // const captchaResult = await this.captchaInit("GET:/drive/v1/share"); // this.captcha_token = captchaResult.captcha_token; // const result = await this._requestPost( // `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share`, // data // ); // return result; // } async fileBatchShare( ids: string[], need_password: boolean = false, expiration_days: number = -1 ): Promise { const data = { file_ids: ids, share_to: "copy", // 修改为"copy" restore_limit: "-1", // 添加这个参数 expiration_days: expiration_days, pass_code_option: need_password ? "REQUIRED" : "NOT_REQUIRED" }; const captchaResult = await this.captchaInit("POST:/drive/v1/share"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share`, data ); return result; } /** * Get quota info */ async getQuotaInfo(): Promise { const result = await this._requestGet( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/about` ); return result; } /** * Get invite code */ async getInviteCode(): Promise { const captchaResult = await this.captchaInit("GET:/vip/v1/activity/inviteCode"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestGet( `https://${PikPakApi.PIKPAK_API_HOST}/vip/v1/activity/inviteCode` ); return result.code; } /** * Get VIP info */ async vipInfo(): Promise { const captchaResult = await this.captchaInit("GET:/drive/v1/privilege/vip"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestGet( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/privilege/vip` ); return result; } /** * Get transfer quota */ async getTransferQuota(): Promise { const url = `https://${PikPakApi.PIKPAK_API_HOST}/vip/v1/quantity/list?type=transfer`; const captchaResult = await this.captchaInit("GET:/vip/v1/quantity/list?type=transfer"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestGet(url); return result; } /** * Get share folder */ async getShareFolder(share_id: string, pass_code_token: string, parent_id?: string): Promise { const data = { limit: "100", thumbnail_size: "SIZE_LARGE", order: "6", share_id: share_id, parent_id: parent_id, pass_code_token: pass_code_token }; const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share/detail`; const captchaResult = await this.captchaInit("GET:/drive/v1/share/detail"); this.captcha_token = captchaResult.captcha_token; return await this._requestGet(url, data); } /** * Get share info */ async getShareInfo(share_link: string, pass_code?: string): Promise { const match = share_link.match(/\/s\/([^/]+)(?:.*\/([^/]+))?$/); if (!match) { throw new Error("Share Link Is Not Right"); } const share_id = match[1]; const parent_id = match[2] || null; const data = { limit: "100", thumbnail_size: "SIZE_LARGE", order: "3", share_id: share_id, parent_id: parent_id, pass_code: pass_code }; const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share`; const captchaResult = await this.captchaInit("GET:/drive/v1/share"); this.captcha_token = captchaResult.captcha_token; return await this._requestGet(url, data); } /** * Restore shared files */ // async restore(share_id: string, pass_code_token: string, file_ids: string[]): Promise { // const data = { // share_id: share_id, // pass_code_token: pass_code_token, // file_ids: file_ids // }; // const captchaResult = await this.captchaInit("GET:/drive/v1/share/restore"); // this.captcha_token = captchaResult.captcha_token; // const result = await this._requestPost( // `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share/restore`, // data // ); // return result; // } async restore(share_id: string, pass_code_token: string, file_ids: string[]): Promise { const data = { share_id: share_id, pass_code_token: pass_code_token, file_ids: file_ids, folder_type: "NORMAL", // 添加此字段 specify_parent_id: true, // 添加此字段 parent_id: "" // 添加此字段 }; const captchaResult = await this.captchaInit("GET:/drive/v1/share/restore"); this.captcha_token = captchaResult.captcha_token; const result = await this._requestPost( `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share/restore`, data ); return result; } } // =============================================================== // 5. Server implementation with Oak (originally main.ts) // =============================================================== // Environment variables handling // 将服务器代码包装在一个函数中 const SECRET_TOKEN = Deno.env.get("SECRET_TOKEN"); if (!SECRET_TOKEN) { throw new Error("Please set SECRET_TOKEN environment variable for security!"); } const THUNDERX_USERNAME = Deno.env.get("THUNDERX_USERNAME"); if (!THUNDERX_USERNAME) { throw new Error("Please set THUNDERX_USERNAME environment variable for login!"); } const THUNDERX_PASSWORD = Deno.env.get("THUNDERX_PASSWORD"); if (!THUNDERX_PASSWORD) { throw new Error("Please set THUNDERX_PASSWORD environment variable for login!"); } const PROXY_URL = Deno.env.get("PROXY_URL"); // Global client instance let THUNDERX_CLIENT: PikPakApi | null = null; // Token logger function // async function logToken(client: PikPakApi, extraData: any): Promise { // console.log(`Token: ${client.encoded_token}, Extra Data: ${JSON.stringify(extraData)}`); // } async function logToken(client: PikPakApi, extraData: any): Promise { console.log(`Token: ${client.encoded_token}, Extra Data: ${JSON.stringify(extraData)}`); } // Create application const app = new Application(); // Add CORS middleware app.use(oakCors({ origin: "*", optionsSuccessStatus: 200 })); // Middleware for token verification async function verifyToken(ctx: Context, next: () => Promise): Promise { const authHeader = ctx.request.headers.get("Authorization"); if (!authHeader) { ctx.response.status = Status.Unauthorized; ctx.response.body = { detail: "Authorization header missing" }; return; } const [scheme, token] = authHeader.split(" "); if (scheme !== "Bearer" || !token) { ctx.response.status = Status.Unauthorized; ctx.response.body = { detail: "Invalid authentication scheme" }; return; } if (token !== SECRET_TOKEN) { ctx.response.status = Status.Unauthorized; ctx.response.body = { detail: "Invalid or expired token" }; return; } await next(); } // Initialize client // app.addEventListener("listen", async () => { // try { // if (!await Deno.stat("thunderx.json").catch(() => null)) { // THUNDERX_CLIENT = new PikPakApi({ // username: THUNDERX_USERNAME, // password: THUNDERX_PASSWORD, // token_refresh_callback: logToken, // token_refresh_callback_kwargs: { extra_data: "test" } // }); // await THUNDERX_CLIENT.login(); // await THUNDERX_CLIENT.refreshAccessToken(); // const clientData = THUNDERX_CLIENT.toDict(); // await Deno.writeTextFile("thunderx.json", JSON.stringify(clientData, null, 4)); // } else { // const fileData = await Deno.readTextFile("thunderx.json"); // const data = JSON.parse(fileData); // THUNDERX_CLIENT = PikPakApi.fromDict(data); // console.log(JSON.stringify(THUNDERX_CLIENT.getUserInfo(), null, 4)); // console.log(JSON.stringify(await THUNDERX_CLIENT.events(), null, 4)); // } // // 初始化后主动检查并刷新令牌 // if (THUNDERX_CLIENT) { // try { // await THUNDERX_CLIENT.refreshAccessToken(); // console.log("Token refreshed during initialization"); // } catch (error) { // // 如果刷新失败,可能需要重新登录 // console.log("Token refresh failed, attempting to login again"); // await THUNDERX_CLIENT.login(); // } // } // } catch (error) { // console.error("Failed to initialize client:", error); // } // }); // app.addEventListener("listen", async () => { // try { // console.log("Creating client with username/password"); // THUNDERX_CLIENT = new PikPakApi({ // username: THUNDERX_USERNAME, // password: THUNDERX_PASSWORD, // token_refresh_callback: logToken, // token_refresh_callback_kwargs: { extra_data: "test" } // }); // await THUNDERX_CLIENT.login(); // await THUNDERX_CLIENT.refreshAccessToken(); // console.log("Client initialized successfully"); // } catch (error) { // console.error("Failed to initialize client:", error); // } // }); // 只修改应用初始化部分,不改动其他代码 // app.addEventListener("listen", async () => { // try { // console.log("Creating client with username/password"); // THUNDERX_CLIENT = new PikPakApi({ // username: THUNDERX_USERNAME, // password: THUNDERX_PASSWORD, // token_refresh_callback: logToken, // token_refresh_callback_kwargs: { extra_data: "test" } // }); // await THUNDERX_CLIENT.login(); // await THUNDERX_CLIENT.refreshAccessToken(); // console.log("Client initialized successfully"); // // 设置定时刷新令牌(每90分钟一次) // setInterval(async () => { // if (THUNDERX_CLIENT) { // try { // console.log("Automatically refreshing token..."); // await THUNDERX_CLIENT.refreshAccessToken(); // console.log("Token refreshed successfully"); // } catch (error) { // console.error("Failed to refresh token:", error); // try { // await THUNDERX_CLIENT.login(); // console.log("Re-login successful"); // } catch (e) { // console.error("Re-login failed:", e); // } // } // } // }, 5400000); // 90分钟 = 5400000毫秒 // } catch (error) { // console.error("Failed to initialize client:", error); // } // }); // app.addEventListener("listen", async () => { // try { // console.log("Creating client with username/password"); // THUNDERX_CLIENT = new PikPakApi({ // username: THUNDERX_USERNAME, // password: THUNDERX_PASSWORD, // token_refresh_callback: logToken, // token_refresh_callback_kwargs: { extra_data: "test" } // }); // await THUNDERX_CLIENT.login(); // const refreshResult = await THUNDERX_CLIENT.refreshAccessToken(); // console.log("Client initialized successfully"); // // 从响应中提取过期时间(如果可用)或使用默认值 // let expiresIn = 7200; // 默认2小时 // if (refreshResult && refreshResult.expires_in) { // expiresIn = refreshResult.expires_in; // console.log(`Token expires in ${expiresIn} seconds`); // } // // 设置在过期前30分钟刷新 // const refreshInterval = Math.max((expiresIn - 1800) * 1000, 3600000); // console.log(`Setting token refresh interval to ${refreshInterval/1000/60} minutes`); // // 设置定时刷新令牌 // setInterval(async () => { // if (THUNDERX_CLIENT) { // try { // console.log("Automatically refreshing token..."); // await THUNDERX_CLIENT.refreshAccessToken(); // console.log("Token refreshed successfully"); // } catch (error) { // console.error("Failed to refresh token:", error); // try { // await THUNDERX_CLIENT.login(); // console.log("Re-login successful"); // } catch (e) { // console.error("Re-login failed:", e); // } // } // } // }, refreshInterval); // } catch (error) { // console.error("Failed to initialize client:", error); // } // }); // 简化的初始化代码 // app.addEventListener("listen", async () => { // try { // console.log("Creating client with username/password"); // THUNDERX_CLIENT = new PikPakApi({ // username: THUNDERX_USERNAME, // password: THUNDERX_PASSWORD, // token_refresh_callback: logToken, // token_refresh_callback_kwargs: { extra_data: "test" } // }); // await THUNDERX_CLIENT.login(); // await THUNDERX_CLIENT.refreshAccessToken(); // console.log("Client initialized successfully"); // // // 使用固定的保守刷新间隔:每60分钟刷新一次(比默认2小时过期时间提前很多) // // const refreshInterval = 60 * 60 * 1000; // 60分钟 // // console.log(`Setting token refresh interval to ${refreshInterval/1000/60} minutes`); // // // 设置定时刷新令牌 // // setInterval(async () => { // // if (THUNDERX_CLIENT) { // // try { // // console.log("Automatically refreshing token..."); // // await THUNDERX_CLIENT.refreshAccessToken(); // // console.log("Token refreshed successfully"); // // } catch (error) { // // console.error("Failed to refresh token:", error); // // try { // // await THUNDERX_CLIENT.login(); // // console.log("Re-login successful"); // // } catch (e) { // // console.error("Re-login failed:", e); // // } // // } // // } // // }, refreshInterval); // } catch (error) { // console.error("Failed to initialize client:", error); // } // }); app.addEventListener("listen", async () => { try { console.log("Creating client with username/password"); THUNDERX_CLIENT = new PikPakApi({ username: THUNDERX_USERNAME, password: THUNDERX_PASSWORD, token_refresh_callback: logToken, token_refresh_callback_kwargs: { extra_data: "test" } }); await THUNDERX_CLIENT.login(); await THUNDERX_CLIENT.refreshAccessToken(); console.log("Client initialized successfully"); console.log("Using on-demand token refresh strategy"); } catch (error) { console.error("Failed to initialize client:", error); } }); // Create routers const apiRouter = new Router(); const frontRouter = new Router(); // Front-end routes frontRouter.get("/", async (ctx) => { try { // A very simple HTML template for demonstration const html = ` PikPak API Client

迅雷X API Client

Authentication

User Info


    

File List


    

Offline Download


    
`; ctx.response.headers.set("Content-Type", "text/html"); ctx.response.body = html; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: "Template error" }; } }); // Helper function to ensure client is initialized function ensureClient(ctx: Context): boolean { if (!THUNDERX_CLIENT) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: "API client not initialized" }; return false; } return true; } // API routes // File list endpoint // apiRouter.post("/files", async (ctx) => { // if (!ensureClient(ctx)) return; // const body = await ctx.request.body.json() as FileRequest; // const size = body.size || 100; // const parent_id = body.parent_id || undefined; // const next_page_token = body.next_page_token || undefined; // const additional_filters = body.additional_filters || {}; // try { // const result = await THUNDERX_CLIENT!.fileList( // size, // parent_id, // next_page_token, // additional_filters // ); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); // // Get file info endpoint // apiRouter.get("/files/:file_id", async (ctx) => { // if (!ensureClient(ctx)) return; // const { file_id } = ctx.params; // try { // const result = await THUNDERX_CLIENT!.getDownloadUrl(file_id); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); // // Empty trash endpoint // apiRouter.post("/emptytrash", async (ctx) => { // if (!ensureClient(ctx)) return; // try { // const result = await THUNDERX_CLIENT!.emptytrash(); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); // // Offline task list endpoint // apiRouter.get("/offline", async (ctx) => { // if (!ensureClient(ctx)) return; // const url = new URL(ctx.request.url); // const size = parseInt(url.searchParams.get("size") || "10000"); // const next_page_token = url.searchParams.get("next_page_token") || undefined; // try { // const result = await THUNDERX_CLIENT!.offlineList( // size, // next_page_token // ); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); // // Add offline task endpoint // apiRouter.post("/offline", async (ctx) => { // if (!ensureClient(ctx)) return; // // const body = await ctx.request.body({ type: "json" }).value as OfflineRequest; // const body = await ctx.request.body.json() as FileRequest; // try { // const result = await THUNDERX_CLIENT!.offlineDownload( // body.file_url, // body.parent_id, // body.name // ); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); // // User info endpoint // apiRouter.get("/userinfo", (ctx) => { // if (!ensureClient(ctx)) return; // ctx.response.body = THUNDERX_CLIENT!.getUserInfo(); // }); // // Quota info endpoint // apiRouter.get("/quota", async (ctx) => { // if (!ensureClient(ctx)) return; // try { // const result = await THUNDERX_CLIENT!.getQuotaInfo(); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); // 添加一个包装函数来处理验证码错误并自动重试 async function withCaptchaRetry(captchaPath, apiCall) { // 首先尝试刷新验证码 try { await THUNDERX_CLIENT.captchaInit(captchaPath); console.log(`Captcha refreshed for ${captchaPath}`); } catch (captchaError) { console.error(`Failed to refresh captcha for ${captchaPath}:`, captchaError); } try { // 尝试执行API调用 return await apiCall(); } catch (error) { // 检查是否是验证码错误 if (error.message && ( error.message.includes("Verification code is invalid") || error.message.includes("invalid captcha_sign"))) { console.log(`Detected invalid verification code, retrying ${captchaPath}...`); // 再次刷新验证码 await THUNDERX_CLIENT.captchaInit(captchaPath); // 重试API调用 return await apiCall(); } // 其他类型的错误则继续抛出 throw error; } } apiRouter.use(verifyToken); // 文件列表路由 apiRouter.post("/files", async (ctx) => { if (!ensureClient(ctx)) return; try { const body = await ctx.request.body.json(); const size = body.size || 100; const parent_id = body.parent_id || undefined; const next_page_token = body.next_page_token || undefined; const additional_filters = body.additional_filters || {}; const result = await withCaptchaRetry( "GET:/drive/v1/files", () => THUNDERX_CLIENT.fileList( size, parent_id, next_page_token, additional_filters ) ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 文件详情路由 apiRouter.get("/files/:file_id", async (ctx) => { if (!ensureClient(ctx)) return; const { file_id } = ctx.params; try { const result = await withCaptchaRetry( `GET:/drive/v1/files/${file_id}`, () => THUNDERX_CLIENT.getDownloadUrl(file_id) ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 清空回收站路由 apiRouter.post("/emptytrash", async (ctx) => { if (!ensureClient(ctx)) return; try { const result = await withCaptchaRetry( "PATCH:/drive/v1/files/trash:empty", () => THUNDERX_CLIENT.emptytrash() ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 离线任务列表路由 apiRouter.get("/offline", async (ctx) => { if (!ensureClient(ctx)) return; try { const url = new URL(ctx.request.url); const size = parseInt(url.searchParams.get("size") || "10000"); const next_page_token = url.searchParams.get("next_page_token") || undefined; const result = await withCaptchaRetry( "GET:/drive/v1/tasks", () => THUNDERX_CLIENT.offlineList( size, next_page_token ) ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 添加离线任务路由 apiRouter.post("/offline", async (ctx) => { if (!ensureClient(ctx)) return; try { const body = await ctx.request.body.json(); const result = await withCaptchaRetry( "GET:/drive/v1/files", () => THUNDERX_CLIENT.offlineDownload( body.file_url, body.parent_id, body.name ) ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 用户信息路由 (不需要验证码刷新) apiRouter.get("/userinfo", (ctx) => { if (!ensureClient(ctx)) return; // 用户信息不需要验证码刷新,因为它只是返回客户端中已有的信息 ctx.response.body = THUNDERX_CLIENT.getUserInfo(); }); // 配额信息路由 apiRouter.get("/quota", async (ctx) => { if (!ensureClient(ctx)) return; try { const result = await withCaptchaRetry( "GET:/drive/v1/about", () => THUNDERX_CLIENT.getQuotaInfo() ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 添加移动到回收站接口 // delete_to_trash // apiRouter.post("/delete_to_trash", async (ctx) => { // if (!ensureClient(ctx)) return; // try { // const body = await ctx.request.body.json(); // const ids = body.ids || []; // const result = await withCaptchaRetry( // "GET:/drive/v1/files:batchTrash", // () => THUNDERX_CLIENT.deleteToTrash(ids) // ); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); apiRouter.post("/delete_to_trash", async (ctx) => { if (!ensureClient(ctx)) return; try { // 直接将请求体解析为JSON数组 const ids = await ctx.request.body.json(); const result = await withCaptchaRetry( "GET:/drive/v1/files:batchTrash", () => THUNDERX_CLIENT.deleteToTrash(ids) ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 添加彻底删除接口 // apiRouter.post("/delete_forever", async (ctx) => { // if (!ensureClient(ctx)) return; // try { // const body = await ctx.request.body.json(); // const ids = body.ids || []; // const result = await withCaptchaRetry( // "GET:/drive/v1/files:batchDelete", // () => THUNDERX_CLIENT.deleteToTrash(ids) // ); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); apiRouter.post("/delete_forever", async (ctx) => { if (!ensureClient(ctx)) return; try { // 直接将请求体解析为JSON数组 const ids = await ctx.request.body.json(); const result = await withCaptchaRetry( "GET:/drive/v1/files:batchTrash", () => THUNDERX_CLIENT.deleteToTrash(ids) ); ctx.response.body = result; } catch (error) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // // 批量分享文件 // apiRouter.post("/file_batch_share", async (ctx) => { // if (!ensureClient(ctx)) return; // try { // // 解析请求体 // const requestBody = await ctx.request.body.json(); // const fileId = requestBody.file_id; // 单个文件ID // const duration = requestBody.duration || -1; // 过期时间(秒),默认永久 // const no_password = requestBody.no_password !== undefined ? requestBody.no_password : true; // 是否不需要密码 // // 构造文件ID数组(即使只有一个ID也用数组) // const ids = Array.isArray(fileId) ? fileId : [fileId]; // // 计算天数(如果提供的是秒数) // let expiration_days = -1; // if (duration > 0) { // expiration_days = Math.ceil(duration / (24 * 60 * 60)); // } // // 调用迅雷分享API // const result = await withCaptchaRetry( // "POST:/drive/v1/files:batchShare", // () => THUNDERX_CLIENT.fileBatchShare(ids, !no_password, expiration_days) // ); // ctx.response.body = result; // } catch (error) { // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); apiRouter.post("/file_batch_share", async (ctx) => { if (!ensureClient(ctx)) return; try { // 从请求体获取文件ID数组 const ids = await ctx.request.body.json(); // 从查询参数获取其他选项 const url = new URL(ctx.request.url); const needPassword = url.searchParams.get("need_password") === "true"; const expirationDays = parseInt(url.searchParams.get("expiration_days") || "-1"); // console.log("分享请求参数:", { // ids, // needPassword, // expirationDays // }); // 调用迅雷分享API const result = await withCaptchaRetry( "POST:/drive/v1/share", () => THUNDERX_CLIENT.fileBatchShare(ids, needPassword, expirationDays) ); ctx.response.body = result; } catch (error) { console.error("分享失败:", error); ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 转存分享文件 // 转存分享文件 - 修复后的实现 // apiRouter.post("/restore", async (ctx) => { // if (!ensureClient(ctx)) return; // try { // // 解析请求体 // const requestBody = await ctx.request.body.json(); // // 提取必要参数 // const shareId = requestBody.share_id; // const passCodeToken = requestBody.pass_code_token || null; // const fileIds = requestBody.file_ids || null; // console.log("转存请求参数:", { // shareId, // passCodeToken, // fileIds: fileIds ? `${Array.isArray(fileIds) ? fileIds.length : 1}个文件` : "无" // }); // // 验证必要参数 // if (!shareId) { // ctx.response.status = Status.BadRequest; // ctx.response.body = { error: "share_id is required" }; // return; // } // if (!fileIds || (Array.isArray(fileIds) && fileIds.length === 0)) { // ctx.response.status = Status.BadRequest; // ctx.response.body = { error: "file_ids is required and cannot be empty" }; // return; // } // // 确保fileIds是数组格式 // const normalizedFileIds = Array.isArray(fileIds) ? fileIds : [fileIds]; // // 调用PikPak API - 注意参数顺序和类型 // const result = await withCaptchaRetry( // "GET:/drive/v1/share/restore", // () => THUNDERX_CLIENT.restore(shareId, passCodeToken, normalizedFileIds) // ); // console.log("转存结果:", result); // ctx.response.body = result; // } catch (error) { // console.error("转存分享文件失败:", error); // ctx.response.status = Status.InternalServerError; // ctx.response.body = { error: error.message }; // } // }); // 转存分享文件 - 深度调试版 apiRouter.post("/restore", async (ctx) => { if (!ensureClient(ctx)) return; try { // 记录原始请求 const originalBody = await ctx.request.body.text(); console.log("原始请求体:", originalBody); // 解析请求体 const requestBody = JSON.parse(originalBody); console.log("解析后请求体:", requestBody); // 提取必要参数 const shareId = requestBody.share_id; const passCodeToken = requestBody.pass_code_token || null; let fileIds = requestBody.file_ids || null; // 确保fileIds总是一个数组 if (fileIds && !Array.isArray(fileIds)) { fileIds = [fileIds]; } console.log("转存参数提取:", { shareId, passCodeToken, fileIds: fileIds ? JSON.stringify(fileIds) : "无" }); // 验证必要参数 if (!shareId) { ctx.response.status = Status.BadRequest; ctx.response.body = { error: "share_id is required" }; return; } if (!fileIds || fileIds.length === 0) { ctx.response.status = Status.BadRequest; ctx.response.body = { error: "file_ids is required and cannot be empty" }; return; } // 构造与Python完全相同的请求体 const pythonStyleData = { share_id: shareId, pass_code_token: passCodeToken, file_ids: fileIds }; console.log("Python风格请求体:", JSON.stringify(pythonStyleData)); // 直接使用重构的请求数据调用API方法 const result = await withCaptchaRetry( "GET:/drive/v1/share/restore", () => THUNDERX_CLIENT.restore(shareId, passCodeToken, fileIds) ); console.log("转存API返回:", JSON.stringify(result)); ctx.response.body = result; } catch (error) { console.error(`转存分享文件失败: ${error.message}`); // 如果错误消息中包含更多细节,也记录下来 if (error.response) { console.error("错误响应:", error.response); } ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 获取分享信息 - 对应Python版本的get_share_folder apiRouter.post("/get_share_folder", async (ctx) => { if (!ensureClient(ctx)) return; try { // 关键修改:从请求体获取参数,而不是查询参数 const requestBody = await ctx.request.body.json(); const shareId = requestBody.share_id; const passCodeToken = requestBody.pass_code_token || null; const parentId = requestBody.parent_id || null; // 验证必要参数 if (!shareId) { ctx.response.status = Status.BadRequest; ctx.response.body = { error: "share_id is required" }; return; } // 调用PikPak API const result = await withCaptchaRetry( "GET:/drive/v1/share/detail", () => THUNDERX_CLIENT.getShareFolder(shareId, passCodeToken, parentId) ); ctx.response.body = result; } catch (error) { console.error("获取分享信息失败:", error); ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 验证分享密码 - 获取pass_code_token // 验证分享密码 - 获取pass_code_token apiRouter.post("/verify_share_password", async (ctx) => { if (!ensureClient(ctx)) return; try { const requestBody = await ctx.request.body.json(); const shareId = requestBody.share_id; const passCode = requestBody.pass_code; if (!shareId || !passCode) { ctx.response.status = Status.BadRequest; ctx.response.body = { error: "share_id and pass_code are required" }; return; } // 构造分享链接 - 使用迅雷网盘的格式 const shareLink = `https://pan.xunleix.com/s/${shareId}`; // 调用验证密码的API const result = await withCaptchaRetry( "GET:/drive/v1/share", () => THUNDERX_CLIENT.getShareInfo(shareLink, passCode) ); // 如果成功,返回pass_code_token if (result && result.pass_code_token) { ctx.response.body = { pass_code_token: result.pass_code_token, success: true }; } else { throw new Error("Failed to get pass_code_token"); } } catch (error) { console.error("验证分享密码失败:", error); ctx.response.status = Status.InternalServerError; ctx.response.body = { error: error.message }; } }); // 添加健康检查路由 apiRouter.get("/health", (ctx) => { ctx.response.body = { status: "ok", client_initialized: !!THUNDERX_CLIENT, last_token_refresh: lastRefreshTime ? new Date(lastRefreshTime).toISOString() : "never" }; }); // Add routes to application app.use(frontRouter.routes()); app.use(frontRouter.allowedMethods()); // Apply verification middleware to API routes app.use(apiRouter.routes()); app.use(apiRouter.allowedMethods()); // Error handler app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.response.status = Status.InternalServerError; ctx.response.body = { error: err.message }; console.error(err); } }); // Start the server if this is the main module if (import.meta.main) { // const port = parseInt(Deno.env.get("PORT") || "8000"); const port = parseInt(Deno.env.get("PORT") || "7860"); console.log(`Starting server on port ${port}...`); app.addEventListener("listen", ({ hostname, port, secure }) => { console.log( `Server listening on: ${secure ? "https://" : "http://"}${ hostname ?? "localhost" }:${port}` ); }); await app.listen({ port }); }