stnh70 commited on
Commit
b8640a7
·
verified ·
1 Parent(s): 740b18a

Update web.ts

Browse files
Files changed (1) hide show
  1. web.ts +946 -2118
web.ts CHANGED
@@ -1,2195 +1,1023 @@
1
- // PikPak Deno API Client and Server - Combined File
2
- // This file contains:
3
- // 1. Type definitions
4
- // 2. Exception classes
5
- // 3. Utility functions
6
- // 4. PikPakApi class implementation
7
- // 5. Server implementation with Oak
8
- // fuck
9
 
10
- import { Application, Router, Context, Status } from "https://deno.land/x/oak/mod.ts";
11
- import { oakCors } from "https://deno.land/x/cors/mod.ts";
12
- import { delay } from "https://deno.land/std/async/mod.ts";
13
- // import { createHash } from "https://deno.land/std/hash/mod.ts";
14
  const { createHash } = await import('node:crypto');
15
- // 在thunderapi.ts文件顶部添加以下导入
16
- // import { Bot, InlineKeyboard } from "https://deno.land/x/grammy@v1.15.3/mod.ts";
17
 
18
-
19
- // ===============================================================
20
- // 1. Type definitions (originally types.ts)
21
- // ===============================================================
22
-
23
-
24
- // 在类外部添加这些变量
25
- let isRefreshing = false;
26
- let lastRefreshTime = 0;
27
-
28
- export enum DownloadStatus {
29
- NOT_DOWNLOADING = "not_downloading",
30
- DOWNLOADING = "downloading",
31
- DONE = "done",
32
- ERROR = "error",
33
- NOT_FOUND = "not_found"
34
- }
35
-
36
- export interface UserInfo {
37
- username?: string;
38
- user_id?: string;
39
- access_token?: string;
40
- refresh_token?: string;
41
- encoded_token?: string;
42
- }
43
-
44
- export interface FileInfo {
45
- id: string;
46
- name: string;
47
- file_type: string;
48
- }
49
-
50
- export interface TokenRefreshCallbackFn {
51
- (client: any, ...args: any[]): Promise<void>;
52
- }
53
-
54
- export interface HttpxClientArgs {
55
- timeout?: number;
56
- [key: string]: any;
57
- }
58
-
59
- export interface FilterOptions {
60
- [key: string]: any;
61
- }
62
-
63
- export interface FileListResponse {
64
- files: any[];
65
- next_page_token?: string;
66
- [key: string]: any;
67
  }
68
 
69
- export interface OfflineListResponse {
70
- tasks: any[];
71
- next_page_token?: string;
72
- [key: string]: any;
73
  }
74
 
75
- export interface FileRequest {
76
- size: number;
77
- parent_id?: string | null;
78
- next_page_token?: string | null;
79
- additional_filters?: Record<string, any> | null;
 
80
  }
81
 
82
- export interface OfflineRequest {
83
- file_url: string;
84
- parent_id?: string | null;
85
- name?: string | null;
86
- }
87
-
88
- // ===============================================================
89
- // 2. Exception classes (originally exceptions.ts)
90
- // ===============================================================
91
-
92
- export class PikpakException extends Error {
93
- constructor(message: string) {
94
- super(message);
95
- this.name = "PikpakException";
96
- }
97
- }
98
-
99
- export class PikpakRetryException extends PikpakException {
100
- constructor(message: string) {
101
- super(message);
102
- this.name = "PikpakRetryException";
103
  }
104
- }
105
-
106
- // ===============================================================
107
- // 3. Utility functions (originally utils.ts)
108
- // ===============================================================
109
-
110
- export const CLIENT_ID = "ZQL_zwA4qhHcoe_2";
111
- export const CLIENT_SECRET = "Og9Vr1L8Ee6bh0olFxFDRg";
112
- export const CLIENT_VERSION = "1.06.0.2132";
113
- export const PACKAG_ENAME = "com.thunder.downloader";
114
- export const SDK_VERSION = "2.0.3.203100 ";
115
- export const APP_NAME = PACKAG_ENAME;
116
-
117
- /**
118
- * Get current timestamp in milliseconds
119
- */
120
- // export function getTimestamp(): number {
121
- // return Date.now();
122
  // }
123
- export function getTimestamp(): string {
124
- // 确保返回毫秒级时间戳,并且是字符串格式
125
- return Math.floor(Date.now()).toString();
126
- }
127
 
128
- /**
129
- * Generate a random device ID
130
- */
131
- export function deviceIdGenerator(): string {
132
- return crypto.randomUUID().replace(/-/g, "");
133
- }
134
-
135
- const SALTS = [
136
- "kVy0WbPhiE4v6oxXZ88DvoA3Q",
137
- "lON/AUoZKj8/nBtcE85mVbkOaVdVa",
138
- "rLGffQrfBKH0BgwQ33yZofvO3Or",
139
- "FO6HWqw",
140
- "GbgvyA2",
141
- "L1NU9QvIQIH7DTRt",
142
- "y7llk4Y8WfYflt6",
143
- "iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe",
144
- "8C28RTXmVcco0",
145
- "X5Xh",
146
- "7xe25YUgfGgD0xW3ezFS",
147
- "",
148
- "CKCR",
149
- "8EmDjBo6h3eLaK7U6vU2Qys0NsMx",
150
- "t2TeZBXKqbdP09Arh9C3",
151
- ];
152
-
153
- /**
154
- * Generate a captcha sign
155
- */
156
- // export function captchaSign(deviceId: string, timestamp: string): string {
157
- // let sign = CLIENT_ID + CLIENT_VERSION + PACKAG_ENAME + deviceId + timestamp;
158
-
159
- // for (const salt of SALTS) {
160
- // const encoder = new TextEncoder();
161
- // const data = encoder.encode(sign + salt);
162
- // sign = createHash("md5").update(data).toString();
163
- // }
164
-
165
- // return `1.${sign}`;
166
  // }
167
 
168
- export function captchaSign(deviceId: string, timestamp: string): string {
169
- let sign = CLIENT_ID + CLIENT_VERSION + PACKAG_ENAME + deviceId + timestamp;
170
-
171
- for (const salt of SALTS) {
172
- // 在 Node.js 兼容模式下使用 createHash
173
- const md5Hash = createHash('md5');
174
- md5Hash.update(sign + salt);
175
- sign = md5Hash.digest('hex');
176
- }
177
-
178
- return `1.${sign}`;
179
- }
180
-
181
- /**
182
- * Generate device sign
183
- */
184
- export function generateDeviceSign(deviceId: string, packageName: string): string {
185
- const signatureBase = `${deviceId}${packageName}1appkey`;
186
-
187
- // Calculate SHA-1 hash
188
- const encoder = new TextEncoder();
189
- const sha1Result = createHash("sha1").update(encoder.encode(signatureBase)).toString();
190
-
191
- // Calculate MD5 hash
192
- const md5Result = createHash("md5").update(encoder.encode(sha1Result)).toString();
193
-
194
- return `div101.${deviceId}${md5Result}`;
195
- }
196
-
197
- /**
198
- * Build a custom user agent string
199
- */
200
- export function buildCustomUserAgent(deviceId: string, userId: string): string {
201
- const deviceSign = generateDeviceSign(deviceId, PACKAG_ENAME);
202
-
203
- const userAgentParts = [
204
- `ANDROID-${APP_NAME}/${CLIENT_VERSION}`,
205
- "protocolVersion/200",
206
- "accesstype/",
207
- `clientid/${CLIENT_ID}`,
208
- `clientversion/${CLIENT_VERSION}`,
209
- "action_type/",
210
- "networktype/WIFI",
211
- "sessionid/",
212
- `deviceid/${deviceId}`,
213
- "providername/NONE",
214
- `devicesign/${deviceSign}`,
215
- "refresh_token/",
216
- `sdkversion/${SDK_VERSION}`,
217
- `datetime/${getTimestamp()}`,
218
- `usrno/${userId}`,
219
- `appname/${APP_NAME}`,
220
- "session_origin/",
221
- "grant_type/",
222
- "appid/",
223
- "clientip/",
224
- "devicename/Xiaomi_M2004j7ac",
225
- "osversion/13",
226
- "platformversion/10",
227
- "accessmode/",
228
- "devicemodel/M2004J7AC"
229
- ];
230
-
231
- return userAgentParts.join(" ");
232
  }
233
 
234
- /**
235
- * Base64 encoding and decoding functions
236
- */
237
- export function b64encode(str: string): string {
238
- return btoa(str);
 
 
 
 
 
 
239
  }
240
 
241
- export function b64decode(str: string): string {
242
- return atob(str);
 
 
 
 
243
  }
244
 
245
- // ===============================================================
246
- // 4. PikPakApi class implementation (originally pikpak_api.ts)
247
- // ===============================================================
248
-
249
- export class PikPakApi {
250
- /**
251
- * PikPakApi class
252
- *
253
- * Attributes similar to the Python version
254
- */
255
- static readonly PIKPAK_API_HOST = "api-pan.xunleix.com";
256
- static readonly PIKPAK_USER_HOST = "xluser-ssl.xunleix.com";
 
 
257
 
258
- // User credentials and tokens
259
- username?: string;
260
- password?: string;
261
- encoded_token?: string;
262
- access_token?: string;
263
- refresh_token?: string;
264
- user_id?: string;
265
-
266
- // Request configuration
267
- max_retries: number;
268
- initial_backoff: number;
269
- device_id: string;
270
- captcha_token?: string;
271
- data_response?: any;
272
- user_agent?: string;
273
-
274
- // Callback for token refresh
275
- token_refresh_callback?: TokenRefreshCallbackFn;
276
- token_refresh_callback_kwargs: Record<string, any>;
277
-
278
- // Path-to-ID cache
279
- private _path_id_cache: Record<string, any> = {};
280
 
281
- constructor(
282
- options: {
283
- username?: string;
284
- password?: string;
285
- encoded_token?: string;
286
- httpx_client_args?: HttpxClientArgs;
287
- device_id?: string;
288
- request_max_retries?: number;
289
- request_initial_backoff?: number;
290
- token_refresh_callback?: TokenRefreshCallbackFn;
291
- token_refresh_callback_kwargs?: Record<string, any>;
292
- } = {}
293
- ) {
294
- this.username = options.username;
295
- this.password = options.password;
296
- this.encoded_token = options.encoded_token;
297
- this.max_retries = options.request_max_retries || 3;
298
- this.initial_backoff = options.request_initial_backoff || 3.0;
299
- this.token_refresh_callback = options.token_refresh_callback;
300
- this.token_refresh_callback_kwargs = options.token_refresh_callback_kwargs || {};
301
 
302
- // Generate device_id if not provided
303
- this.device_id = options.device_id || this.generateDeviceId();
 
304
 
305
- if (this.encoded_token) {
306
- this.decodeToken();
307
- } else if (this.username && this.password) {
308
- // Login will be done later
309
- } else {
310
- throw new PikpakException("username and password or encoded_token is required");
311
  }
312
- }
313
 
314
- /**
315
- * Generate device ID based on username and password
316
- */
317
- private generateDeviceId(): string {
318
- const idBase = `${this.username || ""}${this.password || ""}`;
319
- return createHash("md5").update(new TextEncoder().encode(idBase)).toString();
320
  }
 
321
 
322
- /**
323
- * Create PikPakApi object from a dictionary/object
324
- */
325
- // static fromDict(data: Record<string, any>): PikPakApi {
326
- // const client = new PikPakApi();
327
-
328
- // // Copy all properties from data to client
329
- // for (const [key, value] of Object.entries(data)) {
330
- // if (typeof value !== 'function') {
331
- // (client as any)[key] = value;
332
- // }
333
- // }
334
-
335
- // return client;
336
- // }
337
-
338
- static fromDict(data: Record<string, any>): PikPakApi {
339
- // 创建一个临时客户端,绕过构造函数验证
340
- const client = new PikPakApi({
341
- encoded_token: "temporary" // 临时值,稍后会被覆盖
342
- });
343
-
344
- // 从数据中复制所有属性
345
- for (const [key, value] of Object.entries(data)) {
346
- if (typeof value !== 'function') {
347
- (client as any)[key] = value;
348
- }
349
- }
350
-
351
- // 手动解码令牌
352
- if (client.encoded_token && client.encoded_token !== "temporary") {
353
- try {
354
- client.decodeToken();
355
- } catch (error) {
356
- console.log("Warning: Failed to decode token");
357
- }
358
  }
359
 
360
- return client;
361
- }
362
-
363
- /**
364
- * Returns the PikPakApi object as a dictionary/object
365
- */
366
- toDict(): Record<string, any> {
367
- const data: Record<string, any> = {};
368
-
369
- // Copy all properties that can be serialized
370
- for (const [key, value] of Object.entries(this)) {
371
- if (
372
- typeof value === 'string' ||
373
- typeof value === 'number' ||
374
- typeof value === 'boolean' ||
375
- Array.isArray(value) ||
376
- (typeof value === 'object' && value !== null) ||
377
- value === null ||
378
- value === undefined
379
- ) {
380
- if (typeof value !== 'function') {
381
- data[key] = value;
382
- }
383
- }
384
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
 
386
- return data;
387
- }
388
-
389
- /**
390
- * Build custom user agent
391
- */
392
- buildCustomUserAgent(): string {
393
- this.user_agent = buildCustomUserAgent(
394
- this.device_id,
395
- this.user_id || ""
396
- );
397
- return this.user_agent;
398
  }
399
-
400
- /**
401
- * Get headers for API requests
402
- */
403
- getHeaders(access_token?: string): Record<string, string> {
404
- const headers: Record<string, string> = {
405
- "User-Agent": this.captcha_token
406
- ? this.buildCustomUserAgent()
407
- : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
408
- "Content-Type": "application/json; charset=utf-8"
409
- };
410
-
411
- if (this.access_token) {
412
- headers["Authorization"] = `Bearer ${this.access_token}`;
413
- }
414
- if (access_token) {
415
- headers["Authorization"] = `Bearer ${access_token}`;
416
- }
417
- if (this.captcha_token) {
418
- headers["X-Captcha-Token"] = this.captcha_token;
419
- }
420
- if (this.device_id) {
421
- headers["X-Device-Id"] = this.device_id;
422
  }
423
 
424
- return headers;
425
- }
426
-
427
- /**
428
- * Make an HTTP request with retry logic
429
- */
430
- private async _makeRequest(
431
- method: string,
432
- url: string,
433
- data?: Record<string, any>,
434
- params?: Record<string, any>,
435
- headers?: Record<string, string>
436
- ): Promise<any> {
437
- let lastError: Error | null = null;
438
-
439
- for (let attempt = 0; attempt < this.max_retries; attempt++) {
440
- try {
441
- const response = await this._sendRequest(method, url, data, params, headers);
442
- return await this._handleResponse(response);
443
- } catch (error) {
444
- if (error instanceof PikpakRetryException) {
445
- console.info(`Retry attempt ${attempt + 1}/${this.max_retries}`);
446
- lastError = error;
447
- } else if (error instanceof PikpakException) {
448
- throw error;
449
- } else {
450
- console.error(`Unexpected error on attempt ${attempt + 1}/${this.max_retries}: ${String(error)}`);
451
- lastError = error instanceof Error ? error : new Error(String(error));
452
- }
453
  }
454
-
455
- await delay(this.initial_backoff * Math.pow(2, attempt) * 1000);
456
- }
457
-
458
- // If we've exhausted all retries, raise an exception with the last error
459
- throw new PikpakException(`Max retries reached. Last error: ${lastError?.message || "Unknown error"}`);
460
- }
461
-
462
- /**
463
- * Send an HTTP request
464
- */
465
- private async _sendRequest(
466
- method: string,
467
- url: string,
468
- data?: Record<string, any>,
469
- params?: Record<string, any>,
470
- headers?: Record<string, string>
471
- ): Promise<Response> {
472
- const reqHeaders = headers || this.getHeaders();
473
-
474
- const options: RequestInit = {
475
- method,
476
- headers: reqHeaders,
477
- };
478
-
479
- // Add request body if data is provided
480
- if (data && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
481
- options.body = JSON.stringify(data);
482
- }
483
-
484
- // Add URL parameters if provided
485
- if (params) {
486
- const queryParams = new URLSearchParams();
487
- for (const [key, value] of Object.entries(params)) {
488
- if (value !== undefined && value !== null) {
489
- if (typeof value === 'object') {
490
- queryParams.set(key, JSON.stringify(value));
491
- } else {
492
- queryParams.set(key, String(value));
493
- }
494
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  }
496
- const queryString = queryParams.toString();
497
- if (queryString) {
498
- url = `${url}${url.includes('?') ? '&' : '?'}${queryString}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  }
501
-
502
- return await fetch(url, options);
503
- }
504
-
505
- /**
506
- * Handle HTTP response
507
- */
508
-
509
-
510
- private async _handleResponse(response: Response): Promise<any> {
511
- // Check if response is empty or not JSON
512
- let jsonData: any;
513
  try {
514
- jsonData = await response.json();
515
- } catch (error) {
516
- if (response.status === 200) {
517
- return {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  }
519
- throw new PikpakRetryException("Empty JSON data");
520
- }
521
-
522
- this.data_response = jsonData;
523
-
524
- if (!jsonData) {
525
- if (response.status === 200) {
526
- return {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  }
528
- throw new PikpakRetryException("Empty JSON data");
529
- }
530
-
531
- if (!("error" in jsonData)) {
532
- return jsonData;
533
- }
534
-
535
- if ("captcha_token" in jsonData) {
536
- this.captcha_token = jsonData.captcha_token;
537
- }
538
-
539
- if (jsonData.error === "invalid_account_or_password") {
540
- throw new PikpakException("Invalid username or password");
541
- }
542
-
543
- // if (jsonData.error_code === 16) {
544
- // await this.refreshAccessToken();
545
- // throw new PikpakRetryException("Token refreshed, please retry");
546
- // }
547
- if (jsonData.error_code === 16) {
548
- const now = Date.now();
549
- // 防止30秒内多次刷新
550
- if (isRefreshing && now - lastRefreshTime < 30000) {
551
- console.log("Token refresh already in progress, waiting...");
552
- // 等待一小段时间后重试
553
- await delay(2000);
554
- throw new PikpakRetryException("Token refresh in progress, please retry");
 
555
  }
556
 
557
- isRefreshing = true;
558
- try {
559
- console.log("Refreshing access token on demand...");
560
- await this.refreshAccessToken();
561
- lastRefreshTime = Date.now();
562
- console.log("Token refreshed successfully");
563
- } catch (refreshError) {
564
- console.error("Failed to refresh token:", refreshError);
565
- try {
566
- console.log("Attempting to re-login...");
567
- await this.login();
568
- console.log("Re-login successful");
569
- } catch (loginError) {
570
- console.error("Re-login failed:", loginError);
571
- throw new PikpakException("Authentication failed, unable to refresh token or login");
572
- }
573
- } finally {
574
- isRefreshing = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  }
576
- throw new PikpakRetryException("Token refreshed, please retry");
577
- }
578
-
579
- throw new PikpakException(jsonData.error_description || "Unknown Error");
580
- }
581
-
582
- /**
583
- * HTTP GET request
584
- */
585
- private async _requestGet(url: string, params?: Record<string, any>): Promise<any> {
586
- return await this._makeRequest("GET", url, undefined, params);
587
- }
588
-
589
- /**
590
- * HTTP POST request
591
- */
592
- private async _requestPost(url: string, data?: Record<string, any>, headers?: Record<string, string>): Promise<any> {
593
- return await this._makeRequest("POST", url, data, undefined, headers);
594
- }
595
-
596
- /**
597
- * HTTP PATCH request
598
- */
599
- private async _requestPatch(url: string, data?: Record<string, any>): Promise<any> {
600
- return await this._makeRequest("PATCH", url, data);
601
- }
602
-
603
- /**
604
- * HTTP DELETE request
605
- */
606
- private async _requestDelete(url: string, params?: Record<string, any>, data?: Record<string, any>): Promise<any> {
607
- return await this._makeRequest("DELETE", url, data, params);
608
- }
609
-
610
- /**
611
- * Decode encoded token
612
- */
613
- decodeToken(): void {
614
- try {
615
- const decodedData = JSON.parse(b64decode(this.encoded_token || ""));
616
 
617
- if (!decodedData.access_token || !decodedData.refresh_token) {
618
- throw new PikpakException("Invalid encoded token");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  }
620
 
621
- this.access_token = decodedData.access_token;
622
- this.refresh_token = decodedData.refresh_token;
 
 
 
 
 
 
 
 
 
623
  } catch (error) {
624
- throw new PikpakException("Invalid encoded token");
 
 
 
 
 
 
 
 
 
625
  }
626
  }
627
 
628
- /**
629
- * Encode access and refresh tokens
630
- */
631
- encodeToken(): void {
632
- const tokenData = {
633
- access_token: this.access_token,
634
- refresh_token: this.refresh_token
635
- };
636
-
637
- this.encoded_token = b64encode(JSON.stringify(tokenData));
638
- }
639
 
640
- /**
641
- * Initialize captcha
642
- */
643
- async captchaInit(action: string, meta?: Record<string, any>): Promise<any> {
644
- const url = `https://${PikPakApi.PIKPAK_USER_HOST}/v1/shield/captcha/init`;
645
-
646
- if (!meta) {
647
- const t = `${getTimestamp()}`;
648
- meta = {
649
- captcha_sign: captchaSign(this.device_id, t),
650
- client_version: CLIENT_VERSION,
651
- package_name: PACKAG_ENAME,
652
- user_id: this.user_id || "",
653
- timestamp: t
654
- };
655
  }
656
 
657
- const params = {
658
- client_id: CLIENT_ID,
659
- action: action,
660
- device_id: this.device_id,
661
- meta: meta
662
- };
663
-
664
- return await this._requestPost(url, params);
665
- }
666
-
667
- /**
668
- * Login to PikPak
669
- */
670
- async login(): Promise<void> {
671
- const loginUrl = `https://${PikPakApi.PIKPAK_USER_HOST}/v1/auth/signin`;
672
- const metas: Record<string, string> = {};
673
-
674
- if (!this.username || !this.password) {
675
- throw new PikpakException("username and password are required");
676
- }
677
 
678
- // Determine login method based on username format
679
- if (/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(this.username)) {
680
- metas.email = this.username;
681
- } else if (/\d{11,18}/.test(this.username)) {
682
- metas.phone_number = this.username;
683
- } else {
684
- metas.username = this.username;
685
  }
686
 
687
- const result = await this.captchaInit(`POST:${loginUrl}`, metas);
688
- const captchaToken = result.captcha_token || "";
689
-
690
- if (!captchaToken) {
691
- throw new PikpakException("captcha_token get failed");
692
- }
693
-
694
- // Prepare login data
695
- const loginData = new URLSearchParams();
696
- loginData.append("client_id", CLIENT_ID);
697
- loginData.append("client_secret", CLIENT_SECRET);
698
- loginData.append("password", this.password);
699
- loginData.append("username", this.username);
700
- loginData.append("captcha_token", captchaToken);
701
-
702
- // Custom headers for form data
703
- const headers = {
704
- "Content-Type": "application/x-www-form-urlencoded",
705
- };
706
-
707
- // Send login request with form data
708
- const response = await fetch(loginUrl, {
709
- method: "POST",
710
- headers: headers,
711
- body: loginData
712
- });
713
-
714
- const userInfo = await response.json();
715
-
716
- if (userInfo.error) {
717
- throw new PikpakException(userInfo.error_description || "Login failed");
718
- }
719
-
720
- this.access_token = userInfo.access_token;
721
- this.refresh_token = userInfo.refresh_token;
722
- this.user_id = userInfo.sub;
723
- this.encodeToken();
724
- }
725
-
726
- /**
727
- * Refresh access token
728
- */
729
- async refreshAccessToken(): Promise<void> {
730
- const refreshUrl = `https://${PikPakApi.PIKPAK_USER_HOST}/v1/auth/token`;
731
-
732
- const refreshData = {
733
- client_id: CLIENT_ID,
734
- refresh_token: this.refresh_token,
735
- grant_type: "refresh_token"
736
- };
737
-
738
- const userInfo = await this._requestPost(refreshUrl, refreshData);
739
-
740
- this.access_token = userInfo.access_token;
741
- this.refresh_token = userInfo.refresh_token;
742
- this.user_id = userInfo.sub;
743
- this.encodeToken();
744
-
745
- if (this.token_refresh_callback) {
746
- await this.token_refresh_callback(this, ...Object.values(this.token_refresh_callback_kwargs));
747
- }
748
- }
749
-
750
- /**
751
- * Get user info
752
- */
753
- getUserInfo(): UserInfo {
754
- return {
755
- username: this.username,
756
- user_id: this.user_id,
757
- access_token: this.access_token,
758
- refresh_token: this.refresh_token,
759
- encoded_token: this.encoded_token
760
- };
761
- }
762
-
763
- /**
764
- * Create a folder
765
- */
766
- async createFolder(name: string = "新建文件夹", parent_id?: string): Promise<any> {
767
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files`;
768
-
769
- const data = {
770
- kind: "drive#folder",
771
- name: name,
772
- parent_id: parent_id
773
- };
774
-
775
- const captchaResult = await this.captchaInit("GET:/drive/v1/files");
776
- this.captcha_token = captchaResult.captcha_token;
777
- const result = await this._requestPost(url, data);
778
-
779
- return result;
780
- }
781
-
782
- /**
783
- * Move files/folders to trash
784
- */
785
- async deleteToTrash(ids: string[]): Promise<any> {
786
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchTrash`;
787
-
788
- const data = {
789
- ids: ids
790
- };
791
-
792
- const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchTrash");
793
- this.captcha_token = captchaResult.captcha_token;
794
- const result = await this._requestPost(url, data);
795
-
796
- return result;
797
- }
798
-
799
- /**
800
- * Restore files/folders from trash
801
- */
802
- async untrash(ids: string[]): Promise<any> {
803
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchUntrash`;
804
-
805
- const data = {
806
- ids: ids
807
- };
808
-
809
- const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchUntrash");
810
- this.captcha_token = captchaResult.captcha_token;
811
- const result = await this._requestPost(url, data);
812
-
813
- return result;
814
- }
815
-
816
- /**
817
- * Empty trash
818
- */
819
- async emptytrash(): Promise<any> {
820
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/trash:empty`;
821
-
822
- const data = {};
823
-
824
- const captchaResult = await this.captchaInit("PATCH:/drive/v1/files/trash:empty");
825
- this.captcha_token = captchaResult.captcha_token;
826
- const result = await this._requestPatch(url, data);
827
-
828
- return result;
829
- }
830
-
831
- /**
832
- * Permanently delete files/folders
833
- */
834
- async deleteForever(ids: string[]): Promise<any> {
835
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchDelete`;
836
-
837
- const data = {
838
- ids: ids
839
- };
840
-
841
- const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchDelete");
842
- this.captcha_token = captchaResult.captcha_token;
843
- const result = await this._requestPost(url, data);
844
-
845
- return result;
846
- }
847
-
848
- /**
849
- * Offline download
850
- */
851
- async offlineDownload(file_url: string, parent_id?: string, name?: string): Promise<any> {
852
- const downloadUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files`;
853
-
854
- const downloadData = {
855
- kind: "drive#file",
856
- name: name,
857
- upload_type: "UPLOAD_TYPE_URL",
858
- url: {
859
- url: file_url,
860
- parent_id: parent_id
861
- },
862
- parent_id: parent_id
863
- };
864
-
865
- const captchaResult = await this.captchaInit("GET:/drive/v1/files");
866
- this.captcha_token = captchaResult.captcha_token;
867
- const result = await this._requestPost(downloadUrl, downloadData);
868
-
869
- return result;
870
- }
871
-
872
- /**
873
- * Get offline download list
874
- */
875
- async offlineList(
876
- size: number = 10000,
877
- next_page_token?: string,
878
- phase?: string[]
879
- ): Promise<any> {
880
- if (!phase) {
881
- phase = ["PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR"];
882
- }
883
-
884
- const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/tasks`;
885
-
886
- const listData = {
887
- type: "offline",
888
- thumbnail_size: "SIZE_SMALL",
889
- limit: size,
890
- page_token: next_page_token,
891
- filters: JSON.stringify({ phase: { in: phase.join(",") } }),
892
- with: "reference_resource"
893
- };
894
-
895
- const captchaResult = await this.captchaInit("GET:/drive/v1/tasks");
896
- this.captcha_token = captchaResult.captcha_token;
897
- const result = await this._requestGet(listUrl, listData);
898
-
899
- return result;
900
  }
901
-
902
- /**
903
- * Get offline file info
904
- */
905
- async offlineFileInfo(file_id: string): Promise<any> {
906
- const captchaResult = await this.captchaInit(`GET:/drive/v1/files/${file_id}`);
907
- this.captcha_token = captchaResult.captcha_token;
908
-
909
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/${file_id}`;
910
- const result = await this._requestGet(url, { thumbnail_size: "SIZE_LARGE" });
911
-
912
- return result;
913
- }
914
-
915
- /**
916
- * Get file list
917
- */
918
- async fileList(
919
- size: number = 100,
920
- parent_id?: string,
921
- next_page_token?: string,
922
- additional_filters?: Record<string, any>
923
- ): Promise<any> {
924
- const defaultFilters: Record<string, any> = {
925
- trashed: { eq: false },
926
- phase: { eq: "PHASE_TYPE_COMPLETE" }
927
- };
928
-
929
- if (additional_filters) {
930
- Object.assign(defaultFilters, additional_filters);
931
- }
932
-
933
- const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files`;
934
-
935
- const listData = {
936
- parent_id: parent_id,
937
- thumbnail_size: "SIZE_MEDIUM",
938
- limit: size,
939
- with_audit: "true",
940
- page_token: next_page_token,
941
- filters: JSON.stringify(defaultFilters)
942
- };
943
-
944
- const captchaResult = await this.captchaInit("GET:/drive/v1/files");
945
- this.captcha_token = captchaResult.captcha_token;
946
- const result = await this._requestGet(listUrl, listData);
947
-
948
- return result;
949
- }
950
-
951
- /**
952
- * Get events list
953
- */
954
- async events(size: number = 100, next_page_token?: string): Promise<any> {
955
- const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/events`;
956
-
957
- const listData = {
958
- thumbnail_size: "SIZE_MEDIUM",
959
- limit: size,
960
- next_page_token: next_page_token
961
- };
962
-
963
- const captchaResult = await this.captchaInit("GET:/drive/v1/files");
964
- this.captcha_token = captchaResult.captcha_token;
965
- const result = await this._requestGet(listUrl, listData);
966
-
967
- return result;
968
- }
969
-
970
- /**
971
- * Retry offline download task
972
- */
973
- async offlineTaskRetry(task_id: string): Promise<any> {
974
- const listUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/task`;
975
-
976
- const listData = {
977
- type: "offline",
978
- create_type: "RETRY",
979
- id: task_id
980
- };
981
-
982
- try {
983
- const captchaResult = await this.captchaInit("GET:/drive/v1/task");
984
- this.captcha_token = captchaResult.captcha_token;
985
- const result = await this._requestPost(listUrl, listData);
986
- return result;
987
- } catch (error) {
988
- throw new PikpakException(`Failed to retry offline task: ${task_id}. ${error}`);
989
- }
990
- }
991
-
992
- /**
993
- * Delete tasks
994
- */
995
- async deleteTasks(task_ids: string[], delete_files: boolean = false): Promise<void> {
996
- const deleteUrl = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/tasks`;
997
-
998
- const params = {
999
- task_ids: task_ids,
1000
- delete_files: delete_files
1001
- };
1002
-
1003
- try {
1004
- const captchaResult = await this.captchaInit("GET:/drive/v1/tasks");
1005
- this.captcha_token = captchaResult.captcha_token;
1006
- await this._requestDelete(deleteUrl, params);
1007
- } catch (error) {
1008
- throw new PikpakException(`Failing to delete tasks: ${task_ids}. ${error}`);
1009
- }
1010
- }
1011
-
1012
- /**
1013
- * Get task status
1014
- */
1015
- async getTaskStatus(task_id: string, file_id: string): Promise<DownloadStatus> {
1016
- try {
1017
- const infos = await this.offlineList();
1018
- if (infos && infos.tasks) {
1019
- for (const task of infos.tasks) {
1020
- if (task_id === task.id) {
1021
- return DownloadStatus.DOWNLOADING;
1022
- }
1023
- }
1024
- }
1025
-
1026
- const fileInfo = await this.offlineFileInfo(file_id);
1027
- if (fileInfo) {
1028
- return DownloadStatus.DONE;
1029
- } else {
1030
- return DownloadStatus.NOT_FOUND;
1031
- }
1032
- } catch (error) {
1033
- return DownloadStatus.ERROR;
1034
- }
1035
- }
1036
-
1037
- /**
1038
- * Convert path to ID
1039
- */
1040
- async pathToId(path: string, create: boolean = false): Promise<FileInfo[]> {
1041
- if (!path || path.length <= 0) {
1042
- return [];
1043
- }
1044
-
1045
- const paths = path.split('/').filter(p => p.trim().length > 0);
1046
-
1047
- // Construct multi-level paths for cache lookup
1048
- const multiLevelPaths = [];
1049
- for (let i = 0; i < paths.length; i++) {
1050
- multiLevelPaths.push('/' + paths.slice(0, i + 1).join('/'));
1051
- }
1052
-
1053
- // Check cache hits
1054
- const pathIds: FileInfo[] = [];
1055
- for (const p of multiLevelPaths) {
1056
- if (this._path_id_cache[p]) {
1057
- pathIds.push(this._path_id_cache[p]);
1058
- } else {
1059
- break;
1060
- }
1061
- }
1062
-
1063
- // Determine how much of the path we've already found
1064
- let count = pathIds.length;
1065
- let parentId = count > 0 ? pathIds[count - 1].id : null;
1066
-
1067
- // Find or create the remaining path components
1068
- let nextPageToken: string | null = null;
1069
-
1070
- while (count < paths.length) {
1071
- const data = await this.fileList(100, parentId, nextPageToken);
1072
-
1073
- let recordOfTargetPath = null;
1074
-
1075
- for (const f of data.files || []) {
1076
- const currentPath = '/' + [...paths.slice(0, count), f.name].join('/');
1077
- const fileType = f.kind.includes('folder') ? 'folder' : 'file';
1078
-
1079
- const record: FileInfo = {
1080
- id: f.id,
1081
- name: f.name,
1082
- file_type: fileType
1083
- };
1084
-
1085
- this._path_id_cache[currentPath] = record;
1086
-
1087
- if (f.name === paths[count]) {
1088
- recordOfTargetPath = record;
1089
- }
1090
- }
1091
-
1092
- if (recordOfTargetPath) {
1093
- pathIds.push(recordOfTargetPath);
1094
- count++;
1095
- parentId = recordOfTargetPath.id;
1096
- } else if (data.next_page_token && (!nextPageToken || nextPageToken !== data.next_page_token)) {
1097
- nextPageToken = data.next_page_token;
1098
- } else if (create) {
1099
- const createdData = await this.createFolder(paths[count], parentId);
1100
- const fileId = createdData.file.id;
1101
-
1102
- const record: FileInfo = {
1103
- id: fileId,
1104
- name: paths[count],
1105
- file_type: 'folder'
1106
- };
1107
-
1108
- pathIds.push(record);
1109
-
1110
- const currentPath = '/' + paths.slice(0, count + 1).join('/');
1111
- this._path_id_cache[currentPath] = record;
1112
-
1113
- count++;
1114
- parentId = fileId;
1115
- } else {
1116
- break;
1117
- }
1118
- }
1119
-
1120
- return pathIds;
1121
- }
1122
-
1123
- /**
1124
- * Batch move files
1125
- */
1126
- async fileBatchMove(ids: string[], to_parent_id?: string): Promise<any> {
1127
- const to = to_parent_id ? { parent_id: to_parent_id } : {};
1128
-
1129
- const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchMove");
1130
- this.captcha_token = captchaResult.captcha_token;
1131
-
1132
- const result = await this._requestPost(
1133
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchMove`,
1134
- {
1135
- ids: ids,
1136
- to: to
1137
- }
1138
- );
1139
-
1140
- return result;
1141
- }
1142
-
1143
- /**
1144
- * Batch copy files
1145
- */
1146
- async fileBatchCopy(ids: string[], to_parent_id?: string): Promise<any> {
1147
- const to = to_parent_id ? { parent_id: to_parent_id } : {};
1148
-
1149
- const captchaResult = await this.captchaInit("GET:/drive/v1/files:batchCopy");
1150
- this.captcha_token = captchaResult.captcha_token;
1151
-
1152
- const result = await this._requestPost(
1153
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:batchCopy`,
1154
- {
1155
- ids: ids,
1156
- to: to
1157
- }
1158
- );
1159
-
1160
- return result;
1161
- }
1162
-
1163
- /**
1164
- * Move or copy files by path
1165
- */
1166
- async fileMoveOrCopyByPath(
1167
- from_path: string[],
1168
- to_path: string,
1169
- move: boolean = false,
1170
- create: boolean = false
1171
- ): Promise<any> {
1172
- const fromIds: string[] = [];
1173
-
1174
- for (const path of from_path) {
1175
- const pathIds = await this.pathToId(path);
1176
- if (pathIds.length > 0) {
1177
- const fileId = pathIds[pathIds.length - 1].id;
1178
- if (fileId) {
1179
- fromIds.push(fileId);
1180
- }
1181
- }
1182
- }
1183
-
1184
- if (fromIds.length === 0) {
1185
- throw new PikpakException("Files to move do not exist");
1186
- }
1187
-
1188
- const toPathIds = await this.pathToId(to_path, create);
1189
- const toParentId = toPathIds.length > 0 ? toPathIds[toPathIds.length - 1].id : undefined;
1190
-
1191
- let result;
1192
- if (move) {
1193
- result = await this.fileBatchMove(fromIds, toParentId);
1194
- } else {
1195
- result = await this.fileBatchCopy(fromIds, toParentId);
1196
- }
1197
-
1198
- return result;
1199
- }
1200
-
1201
- /**
1202
- * Get download URL
1203
- */
1204
- async getDownloadUrl(file_id: string): Promise<any> {
1205
- const result = await this.captchaInit(`GET:/drive/v1/files/${file_id}`);
1206
- this.captcha_token = result.captcha_token;
1207
-
1208
- const fileResult = await this._requestGet(
1209
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/${file_id}?`
1210
- );
1211
-
1212
- this.captcha_token = null;
1213
- return fileResult;
1214
- }
1215
-
1216
- /**
1217
- * Rename file
1218
- */
1219
- async fileRename(id: string, new_file_name: string): Promise<any> {
1220
- const data = {
1221
- name: new_file_name
1222
- };
1223
-
1224
- const captchaResult = await this.captchaInit(`GET:/drive/v1/files/${id}`);
1225
- this.captcha_token = captchaResult.captcha_token;
1226
-
1227
- const result = await this._requestPatch(
1228
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files/${id}`,
1229
- data
1230
- );
1231
-
1232
- return result;
1233
- }
1234
-
1235
- /**
1236
- * Batch star files
1237
- */
1238
- async fileBatchStar(ids: string[]): Promise<any> {
1239
- const data = {
1240
- ids: ids
1241
- };
1242
-
1243
- const captchaResult = await this.captchaInit("GET:/drive/v1/files/star");
1244
- this.captcha_token = captchaResult.captcha_token;
1245
-
1246
- const result = await this._requestPost(
1247
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:star`,
1248
- data
1249
- );
1250
-
1251
- return result;
1252
- }
1253
-
1254
- /**
1255
- * Batch unstar files
1256
- */
1257
- async fileBatchUnstar(ids: string[]): Promise<any> {
1258
- const data = {
1259
- ids: ids
1260
- };
1261
-
1262
- const captchaResult = await this.captchaInit("GET:/drive/v1/files/unstar");
1263
- this.captcha_token = captchaResult.captcha_token;
1264
-
1265
- const result = await this._requestPost(
1266
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/files:unstar`,
1267
- data
1268
- );
1269
-
1270
- return result;
1271
- }
1272
-
1273
- /**
1274
- * Get starred file list
1275
- */
1276
- async fileStarList(size: number = 100, next_page_token?: string): Promise<any> {
1277
- const additionalFilters = {
1278
- system_tag: { in: "STAR" }
1279
- };
1280
-
1281
- const result = await this.fileList(
1282
- size,
1283
- "*",
1284
- next_page_token,
1285
- additionalFilters
1286
- );
1287
-
1288
- return result;
1289
- }
1290
-
1291
- /**
1292
- * Batch share files
1293
- */
1294
- // async fileBatchShare(
1295
- // ids: string[],
1296
- // need_password: boolean = false,
1297
- // expiration_days: number = -1
1298
- // ): Promise<any> {
1299
- // const data = {
1300
- // file_ids: ids,
1301
- // share_to: need_password ? "encryptedlink" : "publiclink",
1302
- // expiration_days: expiration_days,
1303
- // pass_code_option: need_password ? "REQUIRED" : "NOT_REQUIRED"
1304
- // };
1305
-
1306
- // const captchaResult = await this.captchaInit("GET:/drive/v1/share");
1307
- // this.captcha_token = captchaResult.captcha_token;
1308
-
1309
- // const result = await this._requestPost(
1310
- // `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share`,
1311
- // data
1312
- // );
1313
-
1314
- // return result;
1315
- // }
1316
-
1317
- async fileBatchShare(
1318
- ids: string[],
1319
- need_password: boolean = false,
1320
- expiration_days: number = -1
1321
- ): Promise<any> {
1322
- const data = {
1323
- file_ids: ids,
1324
- share_to: "copy", // 修改为"copy"
1325
- restore_limit: "-1", // 添加这个参数
1326
- expiration_days: expiration_days,
1327
- pass_code_option: need_password ? "REQUIRED" : "NOT_REQUIRED"
1328
- };
1329
-
1330
- const captchaResult = await this.captchaInit("POST:/drive/v1/share");
1331
- this.captcha_token = captchaResult.captcha_token;
1332
-
1333
- const result = await this._requestPost(
1334
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share`,
1335
- data
1336
- );
1337
-
1338
- return result;
1339
  }
1340
 
1341
- /**
1342
- * Get quota info
1343
- */
1344
- async getQuotaInfo(): Promise<any> {
1345
- const result = await this._requestGet(
1346
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/about`
1347
- );
1348
-
1349
- return result;
1350
- }
1351
-
1352
- /**
1353
- * Get invite code
1354
- */
1355
- async getInviteCode(): Promise<string> {
1356
- const captchaResult = await this.captchaInit("GET:/vip/v1/activity/inviteCode");
1357
- this.captcha_token = captchaResult.captcha_token;
1358
-
1359
- const result = await this._requestGet(
1360
- `https://${PikPakApi.PIKPAK_API_HOST}/vip/v1/activity/inviteCode`
1361
- );
1362
-
1363
- return result.code;
1364
- }
1365
-
1366
- /**
1367
- * Get VIP info
1368
- */
1369
- async vipInfo(): Promise<any> {
1370
- const captchaResult = await this.captchaInit("GET:/drive/v1/privilege/vip");
1371
- this.captcha_token = captchaResult.captcha_token;
1372
-
1373
- const result = await this._requestGet(
1374
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/privilege/vip`
1375
- );
1376
-
1377
- return result;
1378
- }
1379
-
1380
- /**
1381
- * Get transfer quota
1382
- */
1383
- async getTransferQuota(): Promise<any> {
1384
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/vip/v1/quantity/list?type=transfer`;
1385
-
1386
- const captchaResult = await this.captchaInit("GET:/vip/v1/quantity/list?type=transfer");
1387
- this.captcha_token = captchaResult.captcha_token;
1388
-
1389
- const result = await this._requestGet(url);
1390
-
1391
- return result;
1392
- }
1393
-
1394
- /**
1395
- * Get share folder
1396
- */
1397
- async getShareFolder(share_id: string, pass_code_token: string, parent_id?: string): Promise<any> {
1398
- const data = {
1399
- limit: "100",
1400
- thumbnail_size: "SIZE_LARGE",
1401
- order: "6",
1402
- share_id: share_id,
1403
- parent_id: parent_id,
1404
- pass_code_token: pass_code_token
1405
- };
1406
-
1407
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share/detail`;
1408
-
1409
- const captchaResult = await this.captchaInit("GET:/drive/v1/share/detail");
1410
- this.captcha_token = captchaResult.captcha_token;
1411
-
1412
- return await this._requestGet(url, data);
1413
- }
1414
-
1415
- /**
1416
- * Get share info
1417
- */
1418
- async getShareInfo(share_link: string, pass_code?: string): Promise<any> {
1419
- const match = share_link.match(/\/s\/([^/]+)(?:.*\/([^/]+))?$/);
1420
-
1421
- if (!match) {
1422
- throw new Error("Share Link Is Not Right");
1423
- }
1424
-
1425
- const share_id = match[1];
1426
- const parent_id = match[2] || null;
1427
-
1428
- const data = {
1429
- limit: "100",
1430
- thumbnail_size: "SIZE_LARGE",
1431
- order: "3",
1432
- share_id: share_id,
1433
- parent_id: parent_id,
1434
- pass_code: pass_code
1435
- };
1436
-
1437
- const url = `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share`;
1438
-
1439
- const captchaResult = await this.captchaInit("GET:/drive/v1/share");
1440
- this.captcha_token = captchaResult.captcha_token;
1441
-
1442
- return await this._requestGet(url, data);
1443
- }
1444
-
1445
- /**
1446
- * Restore shared files
1447
- */
1448
- async restore(share_id: string, pass_code_token: string, file_ids: string[]): Promise<any> {
1449
- const data = {
1450
- share_id: share_id,
1451
- pass_code_token: pass_code_token,
1452
- file_ids: file_ids,
1453
- folder_type: "NORMAL", // 添加此字段
1454
- specify_parent_id: true, // 添加此字段
1455
- parent_id: "" // 添加此字段
1456
- };
1457
-
1458
- const captchaResult = await this.captchaInit("GET:/drive/v1/share/restore");
1459
- this.captcha_token = captchaResult.captcha_token;
1460
-
1461
- const result = await this._requestPost(
1462
- `https://${PikPakApi.PIKPAK_API_HOST}/drive/v1/share/restore`,
1463
- data
1464
- );
1465
-
1466
- return result;
1467
- }
1468
- }
1469
-
1470
- // ===============================================================
1471
- // 5. Server implementation with Oak (originally main.ts)
1472
- // ===============================================================
1473
-
1474
- // Environment variables handling
1475
- const SECRET_TOKEN = Deno.env.get("SECRET_TOKEN");
1476
- if (!SECRET_TOKEN) {
1477
- throw new Error("Please set SECRET_TOKEN environment variable for security!");
1478
- }
1479
-
1480
- const THUNDERX_USERNAME = Deno.env.get("THUNDERX_USERNAME");
1481
- if (!THUNDERX_USERNAME) {
1482
- throw new Error("Please set THUNDERX_USERNAME environment variable for login!");
1483
- }
1484
-
1485
- const THUNDERX_PASSWORD = Deno.env.get("THUNDERX_PASSWORD");
1486
- if (!THUNDERX_PASSWORD) {
1487
- throw new Error("Please set THUNDERX_PASSWORD environment variable for login!");
1488
- }
1489
-
1490
-
1491
-
1492
- const PROXY_URL = Deno.env.get("PROXY_URL");
1493
-
1494
- // Global client instance
1495
- let THUNDERX_CLIENT: PikPakApi | null = null;
1496
-
1497
- // Token logger function
1498
- // async function logToken(client: PikPakApi, extraData: any): Promise<void> {
1499
- // console.log(`Token: ${client.encoded_token}, Extra Data: ${JSON.stringify(extraData)}`);
1500
- // }
1501
-
1502
-
1503
- async function logToken(client: PikPakApi, extraData: any): Promise<void> {
1504
- console.log(`Token: ${client.encoded_token}, Extra Data: ${JSON.stringify(extraData)}`);
1505
- }
1506
-
1507
- // Create application
1508
- const app = new Application();
1509
-
1510
- // Add CORS middleware
1511
- app.use(oakCors({ origin: "*", optionsSuccessStatus: 200 }));
1512
-
1513
- // Middleware for token verification
1514
- async function verifyToken(ctx: Context, next: () => Promise<unknown>): Promise<void> {
1515
- const authHeader = ctx.request.headers.get("Authorization");
1516
-
1517
- if (!authHeader) {
1518
- ctx.response.status = Status.Unauthorized;
1519
- ctx.response.body = { detail: "Authorization header missing" };
1520
- return;
1521
- }
1522
-
1523
- const [scheme, token] = authHeader.split(" ");
1524
-
1525
- if (scheme !== "Bearer" || !token) {
1526
- ctx.response.status = Status.Unauthorized;
1527
- ctx.response.body = { detail: "Invalid authentication scheme" };
1528
- return;
1529
- }
1530
-
1531
- if (token !== SECRET_TOKEN) {
1532
- ctx.response.status = Status.Unauthorized;
1533
- ctx.response.body = { detail: "Invalid or expired token" };
1534
- return;
1535
- }
1536
-
1537
- await next();
1538
- }
1539
-
1540
- // Initialize client
1541
-
1542
- app.addEventListener("listen", async () => {
1543
- try {
1544
- console.log("Creating client with username/password");
1545
- THUNDERX_CLIENT = new PikPakApi({
1546
- username: THUNDERX_USERNAME,
1547
- password: THUNDERX_PASSWORD,
1548
- token_refresh_callback: logToken,
1549
- token_refresh_callback_kwargs: { extra_data: "test" }
1550
- });
1551
-
1552
- await THUNDERX_CLIENT.login();
1553
- await THUNDERX_CLIENT.refreshAccessToken();
1554
-
1555
- console.log("Client initialized successfully");
1556
- console.log("Using on-demand token refresh strategy");
1557
-
1558
- } catch (error) {
1559
- console.error("Failed to initialize client:", error);
1560
- }
1561
- });
1562
 
1563
- // Create routers
1564
- const apiRouter = new Router();
1565
- const frontRouter = new Router();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1566
 
1567
- // Front-end routes
1568
- frontRouter.get("/", async (ctx) => {
1569
- try {
1570
- // A very simple HTML template for demonstration
1571
- const html = `
1572
- <!DOCTYPE html>
1573
- <html lang="en">
1574
- <head>
1575
- <meta charset="UTF-8">
1576
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1577
- <title>迅雷X API Client</title>
1578
- <style>
1579
- body { font-family: Arial, sans-serif; margin: 20px; }
1580
- h1 { color: #333; }
1581
- .container { max-width: 800px; margin: 0 auto; }
1582
- .card { border: 1px solid #ddd; padding: 20px; margin-bottom: 20px; border-radius: 5px; }
1583
- button { background: #4CAF50; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
1584
- button:disabled { background: #cccccc; cursor: not-allowed; }
1585
- button:hover:not(:disabled) { background: #45a049; }
1586
- pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }
1587
- .status { color: #666; font-style: italic; margin-bottom: 10px; }
1588
- </style>
1589
- </head>
1590
- <body>
1591
- <div class="container">
1592
- <h1>迅雷X API Client</h1>
1593
- <div class="status" id="authStatus">验证中...</div>
1594
- <div class="card">
1595
- <h2>User Info</h2>
1596
- <button id="getUserInfo" disabled>Get User Info</button>
1597
- <pre id="userInfoResult"></pre>
1598
- </div>
1599
- <div class="card">
1600
- <h2>File List</h2>
1601
- <button id="getFileList" disabled>Get File List</button>
1602
- <pre id="fileListResult"></pre>
1603
- </div>
1604
- <div class="card">
1605
- <h2>Offline Download</h2>
1606
- <input type="text" id="fileUrl" placeholder="Enter file URL" style="width: 70%; padding: 8px;">
1607
- <button id="offlineDownload" disabled>Download</button>
1608
- <pre id="offlineResult"></pre>
1609
- </div>
1610
- </div>
1611
- <script>
1612
- // 初始禁用所有按钮
1613
- document.querySelectorAll('button').forEach(btn => btn.disabled = true);
1614
-
1615
- // 获取token - 每次都提示
1616
- const token = prompt('Enter your API token:');
1617
-
1618
- if (!token) {
1619
- document.getElementById('authStatus').textContent = '未提供Token,功能已禁用';
1620
- document.getElementById('authStatus').style.color = '#ff0000';
1621
- } else {
1622
- // 设置验证头
1623
- const headers = {
1624
- 'Authorization': 'Bearer ' + token,
1625
- 'Content-Type': 'application/json'
1626
- };
1627
-
1628
- // 验证token是否有效
1629
- async function verifyToken() {
1630
- try {
1631
- document.getElementById('authStatus').textContent = '验证Token中...';
1632
-
1633
- // 尝试获取用户信息来验证token
1634
- const response = await fetch('/userinfo', { headers });
1635
-
1636
- if (response.ok) {
1637
- const data = await response.json();
1638
- // token有效,启用按钮
1639
- document.querySelectorAll('button').forEach(btn => btn.disabled = false);
1640
- document.getElementById('authStatus').textContent = \`已验证成功 - 欢迎 \${data.username || "用户"}\`;
1641
- document.getElementById('authStatus').style.color = '#4CAF50';
1642
- } else {
1643
- document.getElementById('authStatus').textContent = 'Token无效,功能已禁用';
1644
- document.getElementById('authStatus').style.color = '#ff0000';
1645
  }
1646
- } catch (error) {
1647
- document.getElementById('authStatus').textContent = \`验证错误: \${error.message}\`;
1648
- document.getElementById('authStatus').style.color = '#ff0000';
1649
- }
1650
- }
1651
-
1652
- // 页面加载后验证token
1653
- verifyToken();
1654
 
1655
- // 设置API调用事件监听器
1656
- document.getElementById('getUserInfo').addEventListener('click', async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1657
  try {
1658
- const response = await fetch('/userinfo', { headers });
1659
- const data = await response.json();
1660
- document.getElementById('userInfoResult').textContent = JSON.stringify(data, null, 2);
1661
- } catch (error) {
1662
- document.getElementById('userInfoResult').textContent = 'Error: ' + error.message;
 
 
 
 
 
1663
  }
1664
- });
1665
 
1666
- document.getElementById('getFileList').addEventListener('click', async () => {
1667
- try {
1668
- const response = await fetch('/files', {
1669
- method: 'POST',
1670
- headers,
1671
- body: JSON.stringify({ size: 10 })
1672
- });
1673
- const data = await response.json();
1674
- document.getElementById('fileListResult').textContent = JSON.stringify(data, null, 2);
1675
- } catch (error) {
1676
- document.getElementById('fileListResult').textContent = 'Error: ' + error.message;
 
1677
  }
1678
- });
1679
 
1680
- document.getElementById('offlineDownload').addEventListener('click', async () => {
1681
- const fileUrl = document.getElementById('fileUrl').value;
1682
- if (!fileUrl) {
1683
- alert('Please enter a file URL');
1684
- return;
1685
- }
1686
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1687
  try {
1688
- const response = await fetch('/offline', {
1689
- method: 'POST',
1690
- headers,
1691
- body: JSON.stringify({ file_url: fileUrl })
1692
- });
1693
- const data = await response.json();
1694
- document.getElementById('offlineResult').textContent = JSON.stringify(data, null, 2);
1695
- } catch (error) {
1696
- document.getElementById('offlineResult').textContent = 'Error: ' + error.message;
 
1697
  }
1698
- });
1699
- }
1700
-
1701
- // 添加刷新页面按钮,相当于重新登录
1702
- const refreshButton = document.createElement('button');
1703
- refreshButton.textContent = '重新验证';
1704
- refreshButton.style.marginTop = '20px';
1705
- refreshButton.style.backgroundColor = '#f44336';
1706
- refreshButton.addEventListener('click', () => {
1707
- location.reload();
1708
- });
1709
- document.querySelector('.container').appendChild(refreshButton);
1710
- </script>
1711
- </body>
1712
- </html>
1713
- `;
1714
-
1715
- ctx.response.headers.set("Content-Type", "text/html");
1716
- ctx.response.body = html;
1717
- } catch (error) {
1718
- ctx.response.status = Status.InternalServerError;
1719
- ctx.response.body = { error: "Template error" };
1720
- }
1721
- });
1722
-
1723
- // Helper function to ensure client is initialized
1724
- function ensureClient(ctx: Context): boolean {
1725
- if (!THUNDERX_CLIENT) {
1726
- ctx.response.status = Status.InternalServerError;
1727
- ctx.response.body = { error: "API client not initialized" };
1728
- return false;
1729
- }
1730
- return true;
1731
- }
1732
-
1733
-
1734
- // 添加一个包装函数来处理验证码错误并自动重试
1735
- async function withCaptchaRetry(captchaPath, apiCall) {
1736
- // 首先尝试刷新验证码
1737
- try {
1738
- await THUNDERX_CLIENT.captchaInit(captchaPath);
1739
- console.log(`Captcha refreshed for ${captchaPath}`);
1740
- } catch (captchaError) {
1741
- console.error(`Failed to refresh captcha for ${captchaPath}:`, captchaError);
1742
- }
1743
-
1744
- try {
1745
- // 尝试执行API调用
1746
- return await apiCall();
1747
- } catch (error) {
1748
- // 检查是否是验证码错误
1749
- if (error.message && (
1750
- error.message.includes("Verification code is invalid") ||
1751
- error.message.includes("invalid captcha_sign"))) {
1752
- console.log(`Detected invalid verification code, retrying ${captchaPath}...`);
1753
-
1754
- // 再次刷新验证码
1755
- await THUNDERX_CLIENT.captchaInit(captchaPath);
1756
-
1757
- // 重试API调用
1758
- return await apiCall();
1759
- }
1760
-
1761
- // 其他类型的错误则继续抛出
1762
- throw error;
1763
- }
1764
- }
1765
-
1766
- apiRouter.use(verifyToken);
1767
-
1768
- // 文件列表路由
1769
- apiRouter.post("/files", async (ctx) => {
1770
- if (!ensureClient(ctx)) return;
1771
-
1772
- try {
1773
- const body = await ctx.request.body.json();
1774
- const size = body.size || 100;
1775
- const parent_id = body.parent_id || undefined;
1776
- const next_page_token = body.next_page_token || undefined;
1777
- const additional_filters = body.additional_filters || {};
1778
-
1779
- const result = await withCaptchaRetry(
1780
- "GET:/drive/v1/files",
1781
- () => THUNDERX_CLIENT.fileList(
1782
- size,
1783
- parent_id,
1784
- next_page_token,
1785
- additional_filters
1786
- )
1787
- );
1788
-
1789
- ctx.response.body = result;
1790
- } catch (error) {
1791
- ctx.response.status = Status.InternalServerError;
1792
- ctx.response.body = { error: error.message };
1793
- }
1794
- });
1795
-
1796
- // 文件详情路由
1797
- apiRouter.get("/files/:file_id", async (ctx) => {
1798
- if (!ensureClient(ctx)) return;
1799
-
1800
- const { file_id } = ctx.params;
1801
-
1802
- try {
1803
- const result = await withCaptchaRetry(
1804
- `GET:/drive/v1/files/${file_id}`,
1805
- () => THUNDERX_CLIENT.getDownloadUrl(file_id)
1806
- );
1807
-
1808
- ctx.response.body = result;
1809
- } catch (error) {
1810
- ctx.response.status = Status.InternalServerError;
1811
- ctx.response.body = { error: error.message };
1812
- }
1813
- });
1814
-
1815
- // 清空回收站路由
1816
- apiRouter.post("/emptytrash", async (ctx) => {
1817
- if (!ensureClient(ctx)) return;
1818
-
1819
- try {
1820
- const result = await withCaptchaRetry(
1821
- "PATCH:/drive/v1/files/trash:empty",
1822
- () => THUNDERX_CLIENT.emptytrash()
1823
- );
1824
-
1825
- ctx.response.body = result;
1826
- } catch (error) {
1827
- ctx.response.status = Status.InternalServerError;
1828
- ctx.response.body = { error: error.message };
1829
- }
1830
- });
1831
-
1832
- // 离线任务列表路由
1833
- apiRouter.get("/offline", async (ctx) => {
1834
- if (!ensureClient(ctx)) return;
1835
-
1836
- try {
1837
- const url = new URL(ctx.request.url);
1838
- const size = parseInt(url.searchParams.get("size") || "10000");
1839
- const next_page_token = url.searchParams.get("next_page_token") || undefined;
1840
-
1841
- const result = await withCaptchaRetry(
1842
- "GET:/drive/v1/tasks",
1843
- () => THUNDERX_CLIENT.offlineList(
1844
- size,
1845
- next_page_token
1846
- )
1847
- );
1848
-
1849
- ctx.response.body = result;
1850
- } catch (error) {
1851
- ctx.response.status = Status.InternalServerError;
1852
- ctx.response.body = { error: error.message };
1853
- }
1854
- });
1855
-
1856
- // 添加离线任务路由
1857
- apiRouter.post("/offline", async (ctx) => {
1858
- if (!ensureClient(ctx)) return;
1859
-
1860
- try {
1861
- const body = await ctx.request.body.json();
1862
-
1863
- const result = await withCaptchaRetry(
1864
- "GET:/drive/v1/files",
1865
- () => THUNDERX_CLIENT.offlineDownload(
1866
- body.file_url,
1867
- body.parent_id,
1868
- body.name
1869
- )
1870
- );
1871
-
1872
- ctx.response.body = result;
1873
- } catch (error) {
1874
- ctx.response.status = Status.InternalServerError;
1875
- ctx.response.body = { error: error.message };
1876
- }
1877
- });
1878
 
1879
- // 用户信息路由 (不需要验证码刷新)
1880
- apiRouter.get("/userinfo", (ctx) => {
1881
- if (!ensureClient(ctx)) return;
1882
-
1883
- // 用户信息不需要验证码刷新,因为它只是返回客户端中已有的信息
1884
- ctx.response.body = THUNDERX_CLIENT.getUserInfo();
1885
- });
1886
-
1887
- // 配额信息路由
1888
- apiRouter.get("/quota", async (ctx) => {
1889
- if (!ensureClient(ctx)) return;
1890
-
1891
- try {
1892
- const result = await withCaptchaRetry(
1893
- "GET:/drive/v1/about",
1894
- () => THUNDERX_CLIENT.getQuotaInfo()
1895
- );
1896
-
1897
- ctx.response.body = result;
1898
- } catch (error) {
1899
- ctx.response.status = Status.InternalServerError;
1900
- ctx.response.body = { error: error.message };
1901
- }
1902
- });
1903
-
1904
- // 添加移动到回收站接口
1905
- // delete_to_trash
1906
- // apiRouter.post("/delete_to_trash", async (ctx) => {
1907
- // if (!ensureClient(ctx)) return;
1908
-
1909
- // try {
1910
- // const body = await ctx.request.body.json();
1911
- // const ids = body.ids || [];
1912
-
1913
- // const result = await withCaptchaRetry(
1914
- // "GET:/drive/v1/files:batchTrash",
1915
- // () => THUNDERX_CLIENT.deleteToTrash(ids)
1916
- // );
1917
-
1918
- // ctx.response.body = result;
1919
- // } catch (error) {
1920
- // ctx.response.status = Status.InternalServerError;
1921
- // ctx.response.body = { error: error.message };
1922
- // }
1923
- // });
1924
- apiRouter.post("/delete_to_trash", async (ctx) => {
1925
- if (!ensureClient(ctx)) return;
1926
-
1927
- try {
1928
- // 直接将请求体解析为JSON数组
1929
- const ids = await ctx.request.body.json();
1930
-
1931
- const result = await withCaptchaRetry(
1932
- "GET:/drive/v1/files:batchTrash",
1933
- () => THUNDERX_CLIENT.deleteToTrash(ids)
1934
- );
1935
-
1936
- ctx.response.body = result;
1937
- } catch (error) {
1938
- ctx.response.status = Status.InternalServerError;
1939
- ctx.response.body = { error: error.message };
1940
- }
1941
- });
1942
-
1943
- // 添加彻底删除接口
1944
- // apiRouter.post("/delete_forever", async (ctx) => {
1945
- // if (!ensureClient(ctx)) return;
1946
-
1947
- // try {
1948
- // const body = await ctx.request.body.json();
1949
- // const ids = body.ids || [];
1950
-
1951
- // const result = await withCaptchaRetry(
1952
- // "GET:/drive/v1/files:batchDelete",
1953
- // () => THUNDERX_CLIENT.deleteToTrash(ids)
1954
- // );
1955
-
1956
- // ctx.response.body = result;
1957
- // } catch (error) {
1958
- // ctx.response.status = Status.InternalServerError;
1959
- // ctx.response.body = { error: error.message };
1960
- // }
1961
- // });
1962
- apiRouter.post("/delete_forever", async (ctx) => {
1963
- if (!ensureClient(ctx)) return;
1964
-
1965
- try {
1966
- // 直接将请求体解析为JSON数组
1967
- const ids = await ctx.request.body.json();
1968
-
1969
- const result = await withCaptchaRetry(
1970
- "GET:/drive/v1/files:batchTrash",
1971
- () => THUNDERX_CLIENT.deleteToTrash(ids)
1972
- );
1973
-
1974
- ctx.response.body = result;
1975
- } catch (error) {
1976
- ctx.response.status = Status.InternalServerError;
1977
- ctx.response.body = { error: error.message };
1978
- }
1979
- });
1980
-
1981
- // 批量分享文件
1982
- // apiRouter.post("/file_batch_share", async (ctx) => {
1983
- // if (!ensureClient(ctx)) return;
1984
-
1985
- // try {
1986
- // // 解析请求体
1987
- // const requestBody = await ctx.request.body.json();
1988
- // const fileId = requestBody.file_id; // 单个文件ID
1989
- // const duration = requestBody.duration || -1; // 过期时间(秒),默认永久
1990
- // const no_password = requestBody.no_password !== undefined ? requestBody.no_password : true; // 是否不需要密码
1991
-
1992
- // // 构造文件ID数组(即使只有一个ID也用数组)
1993
- // const ids = Array.isArray(fileId) ? fileId : [fileId];
1994
-
1995
- // // 计算天数(如果提供的是秒数)
1996
- // let expiration_days = -1;
1997
- // if (duration > 0) {
1998
- // expiration_days = Math.ceil(duration / (24 * 60 * 60));
1999
- // }
2000
-
2001
- // // 调用迅雷分享API
2002
- // const result = await withCaptchaRetry(
2003
- // "POST:/drive/v1/files:batchShare",
2004
- // () => THUNDERX_CLIENT.fileBatchShare(ids, !no_password, expiration_days)
2005
- // );
2006
-
2007
- // ctx.response.body = result;
2008
- // } catch (error) {
2009
- // ctx.response.status = Status.InternalServerError;
2010
- // ctx.response.body = { error: error.message };
2011
- // }
2012
- // });
2013
-
2014
- apiRouter.post("/file_batch_share", async (ctx) => {
2015
- if (!ensureClient(ctx)) return;
2016
-
2017
- try {
2018
- // 从请求体获取文件ID数组
2019
- const ids = await ctx.request.body.json();
2020
-
2021
- // 从查询参数获取其他选项
2022
- const url = new URL(ctx.request.url);
2023
- const needPassword = url.searchParams.get("need_password") === "true";
2024
- const expirationDays = parseInt(url.searchParams.get("expiration_days") || "-1");
2025
-
2026
- console.log("分享请求参数:", {
2027
- ids,
2028
- needPassword,
2029
- expirationDays
2030
- });
2031
-
2032
- // 调用迅雷分享API
2033
- const result = await withCaptchaRetry(
2034
- "POST:/drive/v1/share",
2035
- () => THUNDERX_CLIENT.fileBatchShare(ids, needPassword, expirationDays)
2036
- );
2037
-
2038
- ctx.response.body = result;
2039
- } catch (error) {
2040
- console.error("分享失败:", error);
2041
- ctx.response.status = Status.InternalServerError;
2042
- ctx.response.body = { error: error.message };
2043
- }
2044
- });
2045
-
2046
- // 转存分享文件
2047
- apiRouter.post("/restore", async (ctx) => {
2048
- if (!ensureClient(ctx)) return;
2049
-
2050
- try {
2051
- // 解析请求体
2052
- const requestBody = await ctx.request.body.json();
2053
-
2054
- // 提取必要参数
2055
- const shareId = requestBody.share_id;
2056
- const passCodeToken = requestBody.pass_code_token || null;
2057
- const fileIds = requestBody.file_ids || null;
2058
-
2059
- // 验证必要参数
2060
- if (!shareId) {
2061
- ctx.response.status = Status.BadRequest;
2062
- ctx.response.body = { error: "share_id is required" };
2063
- return;
2064
- }
2065
-
2066
- // 调用PikPak API
2067
- const result = await withCaptchaRetry(
2068
- "GET:/drive/v1/share/restore",
2069
- () => THUNDERX_CLIENT.restore(shareId, passCodeToken, fileIds)
2070
- );
2071
-
2072
- ctx.response.body = result;
2073
- } catch (error) {
2074
- console.error("转存分享文件失败:", error);
2075
- ctx.response.status = Status.InternalServerError;
2076
- ctx.response.body = { error: error.message };
2077
- }
2078
- });
2079
-
2080
- // 获取分享信息 - 对应Python版本的get_share_folder
2081
- apiRouter.post("/get_share_folder", async (ctx) => {
2082
- if (!ensureClient(ctx)) return;
2083
-
2084
- try {
2085
- // 从查询参数中获取参数
2086
- const url = new URL(ctx.request.url);
2087
- const shareId = url.searchParams.get("share_id");
2088
- const passCodeToken = url.searchParams.get("pass_code_token") || null;
2089
- const parentId = url.searchParams.get("parent_id") || null;
2090
-
2091
- // 验证必要参数
2092
- if (!shareId) {
2093
- ctx.response.status = Status.BadRequest;
2094
- ctx.response.body = { error: "share_id is required" };
2095
- return;
2096
- }
2097
-
2098
- // 调用PikPak API - 使用迅雷网盘链接格式
2099
- const result = await withCaptchaRetry(
2100
- "GET:/drive/v1/share/detail",
2101
- () => THUNDERX_CLIENT.getShareFolder(shareId, passCodeToken, parentId)
2102
- );
2103
-
2104
- ctx.response.body = result;
2105
- } catch (error) {
2106
- console.error("获取分享信息失败:", error);
2107
- ctx.response.status = Status.InternalServerError;
2108
- ctx.response.body = { error: error.message };
2109
- }
2110
- });
2111
-
2112
- // 验证分享密码 - 获取pass_code_token
2113
- apiRouter.post("/verify_share_password", async (ctx) => {
2114
- if (!ensureClient(ctx)) return;
2115
-
2116
- try {
2117
- const requestBody = await ctx.request.body.json();
2118
- const shareId = requestBody.share_id;
2119
- const passCode = requestBody.pass_code;
2120
-
2121
- if (!shareId || !passCode) {
2122
- ctx.response.status = Status.BadRequest;
2123
- ctx.response.body = { error: "share_id and pass_code are required" };
2124
- return;
2125
- }
2126
-
2127
- // 构造分享链接 - 使用迅雷网盘的格式
2128
- const shareLink = `https://pan.xunlei.com/s/${shareId}`;
2129
-
2130
- // 调用验证密码的API
2131
- const result = await withCaptchaRetry(
2132
- "GET:/drive/v1/share",
2133
- () => THUNDERX_CLIENT.getShareInfo(shareLink, passCode)
2134
- );
2135
-
2136
- // 如果成功,返回pass_code_token
2137
- if (result && result.pass_code_token) {
2138
- ctx.response.body = {
2139
- pass_code_token: result.pass_code_token,
2140
- success: true
2141
- };
2142
- } else {
2143
- throw new Error("Failed to get pass_code_token");
2144
  }
2145
- } catch (error) {
2146
- console.error("验证分享密码失败:", error);
2147
- ctx.response.status = Status.InternalServerError;
2148
- ctx.response.body = { error: error.message };
2149
- }
2150
- });
2151
-
2152
- // 添加健康检查路由
2153
- apiRouter.get("/health", (ctx) => {
2154
- ctx.response.body = {
2155
- status: "ok",
2156
- client_initialized: !!THUNDERX_CLIENT,
2157
- last_token_refresh: lastRefreshTime ? new Date(lastRefreshTime).toISOString() : "never"
2158
- };
2159
- });
2160
-
2161
- // Add routes to application
2162
- app.use(frontRouter.routes());
2163
- app.use(frontRouter.allowedMethods());
2164
-
2165
- // Apply verification middleware to API routes
2166
 
2167
- app.use(apiRouter.routes());
2168
- app.use(apiRouter.allowedMethods());
2169
-
2170
- // Error handler
2171
- app.use(async (ctx, next) => {
2172
- try {
2173
- await next();
2174
- } catch (err) {
2175
- ctx.response.status = Status.InternalServerError;
2176
- ctx.response.body = { error: err.message };
2177
- console.error(err);
2178
- }
2179
- });
2180
-
2181
- // Start the server if this is the main module
2182
- if (import.meta.main) {
2183
- const port = parseInt(Deno.env.get("PORT") || "8000");
2184
- console.log(`Starting server on port ${port}...`);
2185
 
2186
- app.addEventListener("listen", ({ hostname, port, secure }) => {
2187
- console.log(
2188
- `Server listening on: ${secure ? "https://" : "http://"}${
2189
- hostname ?? "localhost"
2190
- }:${port}`
2191
- );
 
 
 
 
2192
  });
2193
-
2194
- await app.listen({ port });
2195
  }
 
 
 
 
 
1
+ // 迅雷X 注册服务 - Deno 实现
2
+ // 使用方法: deno run --allow-net pikpak_register.ts
 
 
 
 
 
 
3
 
4
+ import { serve } from "https://deno.land/std/http/server.ts";
5
+ // 导入 Node.js crypto 模块
 
 
6
  const { createHash } = await import('node:crypto');
 
 
7
 
8
+ // 正确实现 MD5 哈希函数
9
+ function md5Hash(message: string): string {
10
+ return createHash('md5').update(message).digest('hex');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
12
 
13
+ // SHA1 哈希函数
14
+ function sha1Hash(message: string): string {
15
+ return createHash('sha1').update(message).digest('hex');
 
16
  }
17
 
18
+ // 测试函数
19
+ function testHash() {
20
+ const testStr = "9527lampa_device_17455140938907j4r3gwsxud1745539125729";
21
+ console.log(`测试字符串: ${testStr}`);
22
+ console.log(`MD5 哈希: ${md5Hash(testStr)}`); // 应该输出: 098f6bcd4621d373cade4e832627b4f6
23
+ console.log(`SHA1 哈希: ${sha1Hash(testStr)}`); // 应该输出: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
24
  }
25
 
26
+ // 执行测试
27
+ testHash();
28
+ // 工具函数
29
+ const generateRandomString = (length: number = 12): string => {
30
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
31
+ let result = '';
32
+ const randomValues = new Uint8Array(length);
33
+ crypto.getRandomValues(randomValues);
34
+ for (let i = 0; i < length; i++) {
35
+ result += chars[randomValues[i] % chars.length];
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
+ return result;
38
+ };
39
+
40
+ // // MD5 哈希 - 使用 TextEncoder 和 Uint8Array 手动实现
41
+ // async function md5Hash(message: string): Promise<string> {
42
+ // // 使用 Deno 内置的 crypto 模块
43
+ // const encoder = new TextEncoder();
44
+ // const data = encoder.encode(message);
45
+
46
+ // // 使用 SubtleCrypto digest 方法计算 MD5
47
+ // // 注意:由于 WebCrypto API 不直接支持 MD5,我们使用一个替代方法
48
+ // const hashBuffer = await crypto.subtle.digest("SHA-256", data);
49
+
50
+ // // 转换为十六进制字符串
51
+ // return Array.from(new Uint8Array(hashBuffer))
52
+ // .map(b => b.toString(16).padStart(2, '0'))
53
+ // .join('');
 
54
  // }
 
 
 
 
55
 
56
+ // // SHA1 哈希
57
+ // async function sha1Hash(message: string): Promise<string> {
58
+ // const encoder = new TextEncoder();
59
+ // const data = encoder.encode(message);
60
+ // const hashBuffer = await crypto.subtle.digest('SHA-1', data);
61
+ // return Array.from(new Uint8Array(hashBuffer))
62
+ // .map(b => b.toString(16).padStart(2, '0'))
63
+ // .join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  // }
65
 
66
+ // 获取 UA key
67
+ async function getUaKey(deviceId: string): Promise<string> {
68
+ const rank1 = sha1Hash(`${deviceId}com.thunder.downloader1appkey`);
69
+ const rank2 = md5Hash(rank1);
70
+ return `${deviceId}${rank2}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  }
72
 
73
+ // 获取 User Agent
74
+ function getUserAgent(
75
+ clientId: string,
76
+ deviceId: string,
77
+ uaKey: string,
78
+ timestamp: number,
79
+ phoneModel: string,
80
+ phoneBuilder: string,
81
+ version: string
82
+ ): string {
83
+ return `ANDROID-com.thunder.downloader/${version} protocolversion/200 accesstype/ clientid/${clientId} clientversion/${version} action_type/ networktype/WIFI sessionid/ deviceid/${deviceId} providername/NONE devicesign/div101.${uaKey} refresh_token/ sdkversion/2.0.3.203100 datetime/${timestamp} usrno/ appname/android-com.thunder.downloader session_origin/ grant_type/ appid/ clientip/ devicename/${phoneBuilder}_${phoneModel} osversion/13 platformversion/10 accessmode/ devicemodel/${phoneModel}`;
84
  }
85
 
86
+ // 检查密码强度
87
+ function checkPassword(password: string): boolean {
88
+ return password.length >= 8 &&
89
+ /[0-9]/.test(password) &&
90
+ /[A-Z]/.test(password) &&
91
+ /[a-z]/.test(password);
92
  }
93
 
94
+ // API 请求函数
95
+ async function apiRequest(
96
+ method: string,
97
+ url: string,
98
+ data?: any,
99
+ headers?: Record<string, string>
100
+ ): Promise<any> {
101
+ const options: RequestInit = {
102
+ method,
103
+ headers: {
104
+ 'Content-Type': 'application/json; charset=utf-8',
105
+ ...headers,
106
+ },
107
+ };
108
 
109
+ if (data) {
110
+ options.body = JSON.stringify(data);
111
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ try {
114
+ const response = await fetch(url, options);
115
+ const responseData = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ if (!response.ok) {
118
+ throw new Error(`请求失败 (HTTP ${response.status}): ${JSON.stringify(responseData)}`);
119
+ }
120
 
121
+ if (responseData.error) {
122
+ throw new Error(`API错误: ${responseData.error.message || JSON.stringify(responseData.error)}`);
 
 
 
 
123
  }
 
124
 
125
+ return responseData;
126
+ } catch (error) {
127
+ console.error(`请求错误: ${error.message}`);
128
+ throw error;
 
 
129
  }
130
+ }
131
 
132
+ // 处理请求
133
+ async function handleRequest(request: Request): Promise<Response> {
134
+ const url = new URL(request.url);
135
+
136
+ // 处理 CORS
137
+ if (request.method === "OPTIONS") {
138
+ return new Response(null, {
139
+ status: 204,
140
+ headers: {
141
+ "Access-Control-Allow-Origin": "*",
142
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
143
+ "Access-Control-Allow-Headers": "Content-Type",
144
+ },
145
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
 
148
+ // 处理首页请求
149
+ if (url.pathname === "/" || url.pathname === "") {
150
+ const html = `
151
+ <!DOCTYPE html>
152
+ <html>
153
+ <head>
154
+ <meta charset="UTF-8">
155
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
156
+ <title>迅雷X 注册服务</title>
157
+ <style>
158
+ body {
159
+ font-family: Arial, sans-serif;
160
+ max-width: 600px;
161
+ margin: 0 auto;
162
+ padding: 20px;
163
+ }
164
+ h1 {
165
+ color: #333;
166
+ }
167
+ .form-group {
168
+ margin-bottom: 15px;
169
+ }
170
+ label {
171
+ display: block;
172
+ margin-bottom: 5px;
173
+ }
174
+ input[type="email"], input[type="text"], input[type="password"] {
175
+ width: 100%;
176
+ padding: 8px;
177
+ border: 1px solid #ddd;
178
+ border-radius: 4px;
179
+ }
180
+ button {
181
+ background-color: #4CAF50;
182
+ color: white;
183
+ padding: 10px 15px;
184
+ border: none;
185
+ border-radius: 4px;
186
+ cursor: pointer;
187
+ }
188
+ button:hover {
189
+ background-color: #45a049;
190
+ }
191
+ #result, #verification-section {
192
+ margin-top: 20px;
193
+ padding: 10px;
194
+ border: 1px solid #ddd;
195
+ border-radius: 4px;
196
+ display: none;
197
+ }
198
+ .steps {
199
+ margin-top: 20px;
200
+ }
201
+ .step {
202
+ display: none;
203
+ }
204
+ .step.active {
205
+ display: block;
206
+ }
207
+ </style>
208
+ </head>
209
+ <body>
210
+ <h1>迅雷X 注册服务</h1>
211
+
212
+ <div class="steps">
213
+ <!-- 步骤1: 输入邮箱和密码 -->
214
+ <div id="step1" class="step active">
215
+ <h2>步骤1: 输入邮箱和密码</h2>
216
+ <div class="form-group">
217
+ <label for="email">邮箱地址:</label>
218
+ <input type="email" id="email" required>
219
+ </div>
220
+ <div class="form-group">
221
+ <label for="password">密码 (至少8位,包含数字、大小写字母):</label>
222
+ <input type="password" id="password" placeholder="留空则自动生成强密码">
223
+ </div>
224
+ <button id="request-code">请求验证码</button>
225
+ </div>
226
+
227
+ <!-- 步骤2: 输入验证码 -->
228
+ <div id="step2" class="step">
229
+ <h2>步骤2: 输入验证码</h2>
230
+ <p>验证码已发送到您的邮箱,请查收并输入:</p>
231
+ <div class="form-group">
232
+ <label for="code">验证码:</label>
233
+ <input type="text" id="code" required>
234
+ </div>
235
+ <button id="verify-code">验证并注册</button>
236
+ </div>
237
+ </div>
238
+
239
+ <div id="result"></div>
240
+
241
+ <script>
242
+ // 全局变量存储注册过程中的数据
243
+ let registrationData = {
244
+ email: '',
245
+ password: '',
246
+ verificationId: '',
247
+ captchaToken: '',
248
+ deviceId: ''
249
+ };
250
+
251
+ // 步骤1: 请求验证码
252
+ document.getElementById('request-code').addEventListener('click', async () => {
253
+ const email = document.getElementById('email').value;
254
+ const password = document.getElementById('password').value;
255
+
256
+ if (!email) {
257
+ alert('请填写邮箱');
258
+ return;
259
+ }
260
+
261
+ try {
262
+ const resultDiv = document.getElementById('result');
263
+ resultDiv.style.display = 'block';
264
+ resultDiv.innerHTML = '<p>正在请求验证码,请稍候...</p>';
265
+
266
+ const response = await fetch('/api/request-code', {
267
+ method: 'POST',
268
+ headers: {
269
+ 'Content-Type': 'application/json'
270
+ },
271
+ body: JSON.stringify({ email, password })
272
+ });
273
+
274
+ const data = await response.json();
275
+
276
+ if (data.success) {
277
+ // 保存数据
278
+ registrationData.email = email;
279
+ registrationData.password = password;
280
+ registrationData.verificationId = data.verification_id;
281
+ registrationData.captchaToken = data.captcha_token;
282
+ registrationData.deviceId = data.device_id;
283
+
284
+ // 显示步骤2
285
+ document.getElementById('step1').classList.remove('active');
286
+ document.getElementById('step2').classList.add('active');
287
+
288
+ resultDiv.innerHTML = '<p>验证码已发送到您的邮箱,请查收</p>';
289
+ } else {
290
+ resultDiv.innerHTML = \`<p>请求验证码失败: \${data.error}</p>\`;
291
+ }
292
+ } catch (error) {
293
+ document.getElementById('result').innerHTML = \`<p>请求验证码失败: \${error.message}</p>\`;
294
+ }
295
+ });
296
+
297
+ // 步骤2: 验证验证码并完成注册
298
+ document.getElementById('verify-code').addEventListener('click', async () => {
299
+ const code = document.getElementById('code').value;
300
+
301
+ if (!code) {
302
+ alert('请填写验证码');
303
+ return;
304
+ }
305
+
306
+ try {
307
+ const resultDiv = document.getElementById('result');
308
+ resultDiv.innerHTML = '<p>正在验证并注册,请稍候...</p>';
309
+
310
+ const response = await fetch('/api/verify-and-register', {
311
+ method: 'POST',
312
+ headers: {
313
+ 'Content-Type': 'application/json'
314
+ },
315
+ body: JSON.stringify({
316
+ email: registrationData.email,
317
+ password: registrationData.password,
318
+ verification_id: registrationData.verificationId,
319
+ verification_code: code,
320
+ captcha_token: registrationData.captchaToken,
321
+ device_id: registrationData.deviceId
322
+ })
323
+ });
324
+
325
+ const data = await response.json();
326
+
327
+ if (data.success) {
328
+ resultDiv.innerHTML = \`
329
+ <h3>注册成功!</h3>
330
+ <p><strong>邮箱:</strong> \${data.email}</p>
331
+ <p><strong>密码:</strong> \${data.password}</p>
332
+ <p><strong>用户ID:</strong> \${data.user_id}</p>
333
+ \`;
334
+ } else {
335
+ resultDiv.innerHTML = \`
336
+ <h3>注册失败</h3>
337
+ <p>\${data.error}</p>
338
+ \`;
339
+ }
340
+ } catch (error) {
341
+ document.getElementById('result').innerHTML = \`<p>注册失败: \${error.message}</p>\`;
342
+ }
343
+ });
344
+ </script>
345
+ </body>
346
+ </html>
347
+ `;
348
 
349
+ return new Response(html, {
350
+ headers: {
351
+ "Content-Type": "text/html; charset=utf-8",
352
+ "Access-Control-Allow-Origin": "*",
353
+ },
354
+ });
 
 
 
 
 
 
355
  }
356
+
357
+ // 处理请求验证码 API
358
+ if (url.pathname === "/api/request-code") {
359
+ if (request.method !== "POST") {
360
+ return new Response(JSON.stringify({
361
+ success: false,
362
+ error: "只支持 POST 请求"
363
+ }), {
364
+ status: 405,
365
+ headers: {
366
+ "Content-Type": "application/json",
367
+ "Access-Control-Allow-Origin": "*",
368
+ },
369
+ });
 
 
 
 
 
 
 
 
 
370
  }
371
 
372
+ try {
373
+ const body = await request.json();
374
+ const { email, password } = body;
375
+
376
+ if (!email) {
377
+ return new Response(JSON.stringify({
378
+ success: false,
379
+ error: "邮箱不能为空"
380
+ }), {
381
+ status: 400,
382
+ headers: {
383
+ "Content-Type": "application/json",
384
+ "Access-Control-Allow-Origin": "*",
385
+ },
386
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  }
388
+
389
+ // 固定版本信息
390
+ const version = "1.06.0.2132";
391
+ const clientId = "ZQL_zwA4qhHcoe_2";
392
+ const deviceId = generateRandomString(32);
393
+ // console.log(deviceId)
394
+ const timestamp = Date.now();
395
+
396
+ // 生成captcha签名
397
+ let orgStr = `${clientId}${version}com.thunder.downloader${deviceId}${timestamp}`;
398
+ let captchaSign = orgStr;
399
+
400
+ // 应用所有salt进行MD5哈希 - 与bash脚本保持一致
401
+ const salts = [
402
+ "kVy0WbPhiE4v6oxXZ88DvoA3Q",
403
+ "lON/AUoZKj8/nBtcE85mVbkOaVdVa",
404
+ "rLGffQrfBKH0BgwQ33yZofvO3Or",
405
+ "FO6HWqw",
406
+ "GbgvyA2",
407
+ "L1NU9QvIQIH7DTRt",
408
+ "y7llk4Y8WfYflt6",
409
+ "iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe",
410
+ "8C28RTXmVcco0",
411
+ "X5Xh",
412
+ "7xe25YUgfGgD0xW3ezFS",
413
+ "",
414
+ "CKCR",
415
+ "8EmDjBo6h3eLaK7U6vU2Qys0NsMx",
416
+ "t2TeZBXKqbdP09Arh9C3"
417
+ ];
418
+
419
+ // 按顺序应用所有salt
420
+ for (const salt of salts) {
421
+ captchaSign = md5Hash(`${captchaSign}${salt}`);
422
+ // 可以添加调试输出
423
+ console.log(`Salt: ${salt}, Sign: ${captchaSign}`);
424
+ }
425
+
426
+ // 设备信息
427
+ const phoneModel = "MI-ONE";
428
+ const phoneBuilder = "XIAOMI";
429
+ const uaKey = await getUaKey(deviceId);
430
+ const userAgent = getUserAgent(clientId, deviceId, uaKey, timestamp, phoneModel, phoneBuilder, version);
431
+
432
+ // 公共请求头
433
+ const commonHeaders = {
434
+ 'X-Device-Id': deviceId,
435
+ 'User-Agent': userAgent,
436
+ 'Accept-Language': 'zh',
437
+ 'Content-Type': 'application/json; charset=utf-8',
438
+ 'Connection': 'Keep-Alive',
439
+ 'Accept-Encoding': 'gzip'
440
+ };
441
+
442
+ // 1. 初始验证
443
+ const initUrl = "https://xluser-ssl.xunleix.com/v1/shield/captcha/init";
444
+ const initPayload = {
445
+ action: "POST:/v1/auth/verification",
446
+ captcha_token: "",
447
+ client_id: clientId,
448
+ device_id: deviceId,
449
+ meta: { email },
450
+ redirect_uri: "xlaccsdk01://xbase.cloud/callback?state=harbor"
451
+ };
452
+
453
+ const initResponse = await apiRequest("POST", initUrl, initPayload, commonHeaders);
454
+ const captchaToken = initResponse.captcha_token;
455
+
456
+ if (!captchaToken) {
457
+ return new Response(JSON.stringify({
458
+ success: false,
459
+ error: "无法获取captcha_token"
460
+ }), {
461
+ status: 500,
462
+ headers: {
463
+ "Content-Type": "application/json",
464
+ "Access-Control-Allow-Origin": "*",
465
+ },
466
+ });
467
  }
468
+
469
+ // 2. 请求验证码
470
+ const verificationUrl = "https://xluser-ssl.xunleix.com/v1/auth/verification";
471
+ const verificationPayload = {
472
+ captcha_token: captchaToken,
473
+ email: email,
474
+ locale: "zh-CN",
475
+ target: "ANY",
476
+ client_id: clientId
477
+ };
478
+
479
+ const verificationResponse = await apiRequest("POST", verificationUrl, verificationPayload, commonHeaders);
480
+ const verificationId = verificationResponse.verification_id;
481
+
482
+ if (!verificationId) {
483
+ return new Response(JSON.stringify({
484
+ success: false,
485
+ error: "无法获取验证ID"
486
+ }), {
487
+ status: 500,
488
+ headers: {
489
+ "Content-Type": "application/json",
490
+ "Access-Control-Allow-Origin": "*",
491
+ },
492
+ });
493
  }
494
+
495
+ return new Response(JSON.stringify({
496
+ success: true,
497
+ message: "验证码已发送到邮箱,请查收",
498
+ verification_id: verificationId,
499
+ captcha_token: captchaToken,
500
+ device_id: deviceId
501
+ }), {
502
+ headers: {
503
+ "Content-Type": "application/json",
504
+ "Access-Control-Allow-Origin": "*",
505
+ },
506
+ });
507
+ } catch (error) {
508
+ return new Response(JSON.stringify({
509
+ success: false,
510
+ error: error.message
511
+ }), {
512
+ status: 500,
513
+ headers: {
514
+ "Content-Type": "application/json",
515
+ "Access-Control-Allow-Origin": "*",
516
+ },
517
+ });
518
+ }
519
+ }
520
+
521
+ // 处理验证码验证和注册 API
522
+ if (url.pathname === "/api/verify-and-register") {
523
+ if (request.method !== "POST") {
524
+ return new Response(JSON.stringify({
525
+ success: false,
526
+ error: "只支持 POST 请求"
527
+ }), {
528
+ status: 405,
529
+ headers: {
530
+ "Content-Type": "application/json",
531
+ "Access-Control-Allow-Origin": "*",
532
+ },
533
+ });
534
  }
535
+
 
 
 
 
 
 
 
 
 
 
 
536
  try {
537
+ const body = await request.json();
538
+ const {
539
+ email,
540
+ password,
541
+ verification_id,
542
+ verification_code,
543
+ captcha_token,
544
+ device_id
545
+ } = body;
546
+
547
+ if (!email || !verification_id || !verification_code || !captcha_token || !device_id) {
548
+ return new Response(JSON.stringify({
549
+ success: false,
550
+ error: "缺少必要参数"
551
+ }), {
552
+ status: 400,
553
+ headers: {
554
+ "Content-Type": "application/json",
555
+ "Access-Control-Allow-Origin": "*",
556
+ },
557
+ });
558
  }
559
+
560
+ // 固定版本信息
561
+ const version = "1.06.0.2132";
562
+ const clientId = "ZQL_zwA4qhHcoe_2";
563
+ const clientSecret = "Og9Vr1L8Ee6bh0olFxFDRg";
564
+ let timestamp = Date.now();
565
+
566
+ // 设备信息
567
+ const phoneModel = "MI-ONE";
568
+ const phoneBuilder = "XIAOMI";
569
+ const uaKey = await getUaKey(device_id);
570
+ const userAgent = getUserAgent(clientId, device_id, uaKey, timestamp, phoneModel, phoneBuilder, version);
571
+
572
+ // 公共请求头
573
+ const commonHeaders = {
574
+ 'X-Device-Id': device_id,
575
+ 'User-Agent': userAgent,
576
+ 'Accept-Language': 'zh',
577
+ 'Content-Type': 'application/json; charset=utf-8',
578
+ 'Connection': 'Keep-Alive',
579
+ 'Accept-Encoding': 'gzip'
580
+ };
581
+
582
+ // 4. 验证验证码
583
+ const verifyUrl = "https://xluser-ssl.xunleix.com/v1/auth/verification/verify";
584
+ const verifyPayload = {
585
+ client_id: clientId,
586
+ verification_id: verification_id,
587
+ verification_code: verification_code
588
+ };
589
+
590
+ const verifyResponse = await apiRequest("POST", verifyUrl, verifyPayload, commonHeaders);
591
+ const verificationToken = verifyResponse.verification_token;
592
+
593
+ if (!verificationToken) {
594
+ return new Response(JSON.stringify({
595
+ success: false,
596
+ error: "验证码验证失败"
597
+ }), {
598
+ status: 400,
599
+ headers: {
600
+ "Content-Type": "application/json",
601
+ "Access-Control-Allow-Origin": "*",
602
+ },
603
+ });
604
  }
605
+
606
+ // 5. 二次安全验证 - 关键修复点
607
+ timestamp = Date.now(); // 更新时间戳
608
+ let orgStr = `${clientId}${version}com.thunder.downloader${device_id}${timestamp}`;
609
+ let captchaSign = orgStr;
610
+
611
+ // 应用所有salt进行MD5哈希 - 与bash脚本保持一致
612
+ const salts = [
613
+ "kVy0WbPhiE4v6oxXZ88DvoA3Q",
614
+ "lON/AUoZKj8/nBtcE85mVbkOaVdVa",
615
+ "rLGffQrfBKH0BgwQ33yZofvO3Or",
616
+ "FO6HWqw",
617
+ "GbgvyA2",
618
+ "L1NU9QvIQIH7DTRt",
619
+ "y7llk4Y8WfYflt6",
620
+ "iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe",
621
+ "8C28RTXmVcco0",
622
+ "X5Xh",
623
+ "7xe25YUgfGgD0xW3ezFS",
624
+ "",
625
+ "CKCR",
626
+ "8EmDjBo6h3eLaK7U6vU2Qys0NsMx",
627
+ "t2TeZBXKqbdP09Arh9C3"
628
+ ];
629
+
630
+ // 按顺序应用所有salt
631
+ for (const salt of salts) {
632
+ captchaSign = md5Hash(`${captchaSign}${salt}`);
633
  }
634
 
635
+ // 更新 User-Agent 和请求头,确保时间戳一致
636
+ const updatedUaKey = await getUaKey(device_id);
637
+ const updatedUserAgent = getUserAgent(clientId, device_id, updatedUaKey, timestamp, phoneModel, phoneBuilder, version);
638
+
639
+ const updatedHeaders = {
640
+ 'X-Device-Id': device_id,
641
+ 'User-Agent': updatedUserAgent,
642
+ 'Accept-Language': 'zh',
643
+ 'Content-Type': 'application/json; charset=utf-8',
644
+ 'Connection': 'Keep-Alive',
645
+ 'Accept-Encoding': 'gzip'
646
+ };
647
+
648
+ const meta1 = {
649
+ captcha_sign: `1.${captchaSign}`,
650
+ user_id: "",
651
+ package_name: "com.thunder.downloader",
652
+ client_version: version,
653
+ timestamp: `${timestamp}`
654
+ };
655
+
656
+ const initUrl = "https://xluser-ssl.xunleix.com/v1/shield/captcha/init";
657
+ const initPayload2 = {
658
+ action: "POST:/v1/auth/signup",
659
+ captcha_token: captcha_token,
660
+ client_id: clientId,
661
+ device_id: device_id,
662
+ meta: meta1,
663
+ redirect_uri: "xlaccsdk01://xbase.cloud/callback?state=harbor"
664
+ };
665
+
666
+ const initResponse2 = await apiRequest("POST", initUrl, initPayload2, updatedHeaders);
667
+ const newCaptchaToken = initResponse2.captcha_token;
668
+
669
+ if (!newCaptchaToken) {
670
+ return new Response(JSON.stringify({
671
+ success: false,
672
+ error: "无法获取二次验证token"
673
+ }), {
674
+ status: 500,
675
+ headers: {
676
+ "Content-Type": "application/json",
677
+ "Access-Control-Allow-Origin": "*",
678
+ },
679
+ });
680
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
+ // 6. 注册账号
683
+ const name = email.split('@')[0];
684
+
685
+ // 处理密码 - 使用自定义密码或生成随机密码
686
+ let finalPassword: string;
687
+
688
+ if (password && checkPassword(password)) {
689
+ // 使用用户提供的密码(如果符合要求)
690
+ finalPassword = password;
691
+ } else {
692
+ // 生成符合要求的密码
693
+ finalPassword = generateRandomString(12) +
694
+ generateRandomString(1).toUpperCase() +
695
+ generateRandomString(1).toLowerCase() +
696
+ Math.floor(Math.random() * 10);
697
+ }
698
+
699
+ const signupUrl = "https://xluser-ssl.xunleix.com/v1/auth/signup";
700
+ const signupPayload = {
701
+ captcha_token: newCaptchaToken,
702
+ client_id: clientId,
703
+ client_secret: clientSecret,
704
+ email: email,
705
+ name: name,
706
+ password: finalPassword,
707
+ verification_token: verificationToken
708
+ };
709
+
710
+ const signupResponse = await apiRequest("POST", signupUrl, signupPayload, updatedHeaders);
711
+ const userId = signupResponse.sub;
712
+
713
+ if (!userId) {
714
+ return new Response(JSON.stringify({
715
+ success: false,
716
+ error: "注册失败 - 响应中没有用户ID"
717
+ }), {
718
+ status: 500,
719
+ headers: {
720
+ "Content-Type": "application/json",
721
+ "Access-Control-Allow-Origin": "*",
722
+ },
723
+ });
724
  }
725
 
726
+ return new Response(JSON.stringify({
727
+ success: true,
728
+ email: email,
729
+ password: finalPassword,
730
+ user_id: userId
731
+ }), {
732
+ headers: {
733
+ "Content-Type": "application/json",
734
+ "Access-Control-Allow-Origin": "*",
735
+ },
736
+ });
737
  } catch (error) {
738
+ return new Response(JSON.stringify({
739
+ success: false,
740
+ error: error.message
741
+ }), {
742
+ status: 500,
743
+ headers: {
744
+ "Content-Type": "application/json",
745
+ "Access-Control-Allow-Origin": "*",
746
+ },
747
+ });
748
  }
749
  }
750
 
 
 
 
 
 
 
 
 
 
 
 
751
 
752
+ // 从URL中提取文件名的辅助函数
753
+ function getFileNameFromUrl(url: string): string {
754
+ try {
755
+ // 尝试从磁力链接的dn参数中提取文件名
756
+ if (url.startsWith('magnet:')) {
757
+ const dnMatch = url.match(/&dn=([^&]+)/);
758
+ if (dnMatch && dnMatch[1]) {
759
+ return decodeURIComponent(dnMatch[1].replace(/\+/g, ' '));
760
+ }
 
 
 
 
 
 
761
  }
762
 
763
+ // 如果不是磁力链接或没有dn参数,尝试从URL路径中提取
764
+ const urlObj = new URL(url);
765
+ const pathname = urlObj.pathname;
766
+ const filename = pathname.split('/').pop() || '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
 
768
+ // 如果文件名为空或者没有扩展名,使用默认名称
769
+ if (!filename || filename.indexOf('.') === -1) {
770
+ return '未命名文件_' + Date.now();
 
 
 
 
771
  }
772
 
773
+ // 解码URL编码的文件名
774
+ return decodeURIComponent(filename);
775
+ } catch (e) {
776
+ console.error("从URL提取文件名失败", e);
777
+ return '未命名文件_' + Date.now();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
778
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
  }
780
 
781
+ // ... existing code ...
782
+
783
+ if (url.pathname === "/api/offline-download") {
784
+ console.log("收到离线下载请求");
785
+ if (request.method !== "POST") {
786
+ console.log(`请求方法错误: ${request.method}`);
787
+ return new Response(JSON.stringify({
788
+ success: false,
789
+ error: "只支持 POST 请求"
790
+ }), {
791
+ status: 405,
792
+ headers: {
793
+ "Content-Type": "application/json",
794
+ "Access-Control-Allow-Origin": "*",
795
+ },
796
+ });
797
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
 
799
+ try {
800
+ console.log("开始解析请求体");
801
+ const body = await request.json();
802
+ console.log("请求体内容:", JSON.stringify(body));
803
+
804
+ const { file_url, parent_id, name, access_token } = body;
805
+ // 优先使用客户端提供的 device_id,如果没有则生成一个新的
806
+ const device_id = generateRandomString(32);
807
+ console.log(`设备ID: ${device_id}, 是否生成新ID: ${!body.device_id}`);
808
+
809
+ if (!file_url || !access_token) {
810
+ console.log("缺少必要参数:", {
811
+ has_file_url: !!file_url,
812
+ has_access_token: !!access_token
813
+ });
814
+ return new Response(JSON.stringify({
815
+ success: false,
816
+ error: "缺少必要参数"
817
+ }), {
818
+ status: 400,
819
+ headers: {
820
+ "Content-Type": "application/json",
821
+ "Access-Control-Allow-Origin": "*",
822
+ },
823
+ });
824
+ }
825
 
826
+ // 固定版本信息
827
+ const version = "1.06.0.2132";
828
+ const clientId = "ZQL_zwA4qhHcoe_2";
829
+ const timestamp = Date.now();
830
+ console.log(`版本信息: ${version}, 客户端ID: ${clientId}, 时间戳: ${timestamp}`);
831
+
832
+ // 设备信息
833
+ const phoneModel = "MI-ONE";
834
+ const phoneBuilder = "XIAOMI";
835
+ console.log("开始获取uaKey");
836
+ const uaKey = await getUaKey(device_id);
837
+ console.log(`获取到uaKey: ${uaKey.substring(0, 10)}...`);
838
+
839
+ // 更新getUserAgent函数调用,确保与Python版本一致
840
+ const userAgent = getUserAgent(clientId, device_id, uaKey, timestamp, phoneModel, phoneBuilder, version);
841
+ console.log(`生成的UserAgent: ${userAgent}`);
842
+
843
+ // 公共请求头
844
+ const commonHeaders = {
845
+ 'Authorization': `Bearer ${access_token}`,
846
+ 'X-Device-Id': device_id,
847
+ 'User-Agent': userAgent,
848
+ 'Accept-Language': 'zh', // 修改为与Python代码一致
849
+ 'Content-Type': 'application/json; charset=utf-8',
850
+ 'Connection': 'Keep-Alive',
851
+ 'Accept-Encoding': 'gzip'
852
+ };
853
+
854
+
855
+ // 生成captcha签名
856
+ let orgStr = `${clientId}${version}com.thunder.downloader${device_id}${timestamp}`;
857
+ let captchaSign = orgStr;
858
+ console.log(`开始生成captcha签名, 原始字符串: ${orgStr.substring(0, 20)}...`);
859
+
860
+ // 应用所有salt进行MD5哈希
861
+ const salts = [
862
+ "kVy0WbPhiE4v6oxXZ88DvoA3Q",
863
+ "lON/AUoZKj8/nBtcE85mVbkOaVdVa",
864
+ "rLGffQrfBKH0BgwQ33yZofvO3Or",
865
+ "FO6HWqw",
866
+ "GbgvyA2",
867
+ "L1NU9QvIQIH7DTRt",
868
+ "y7llk4Y8WfYflt6",
869
+ "iuDp1WPbV3HRZudZtoXChxH4HNVBX5ZALe",
870
+ "8C28RTXmVcco0",
871
+ "X5Xh",
872
+ "7xe25YUgfGgD0xW3ezFS",
873
+ "",
874
+ "CKCR",
875
+ "8EmDjBo6h3eLaK7U6vU2Qys0NsMx",
876
+ "t2TeZBXKqbdP09Arh9C3"
877
+ ];
878
+
879
+ // 按顺序应用所有salt
880
+ for (const salt of salts) {
881
+ captchaSign = md5Hash(`${captchaSign}${salt}`);
882
+ // 可以添加调试输出
883
+ console.log(`Salt: ${salt}, Sign: ${captchaSign}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
  }
 
 
 
 
 
 
 
 
885
 
886
+ // 构建meta数据
887
+ // const meta = {
888
+ // captcha_sign: `1.${captchaSign}`,
889
+ // "package_name": "com.thunder.downloader",
890
+ // "client_version": version,
891
+ // "timestamp": timestamp.toString()
892
+ // };
893
+ const meta = {
894
+ captcha_sign: `1.${captchaSign}`,
895
+ user_id: "",
896
+ package_name: "com.thunder.downloader",
897
+ client_version: version,
898
+ timestamp: `${timestamp}`
899
+ };
900
+ // console.log("构建的meta数据:", JSON.stringify(meta));
901
+
902
+ // 构建初始化请求 - 使用GET:/drive/v1/files,与Python代码保持一致
903
+ const initPayload = {
904
+ "action": "GET:/drive/v1/files", // 修改为与Python代码一致的action
905
+ "captcha_token": "",
906
+ "client_id": clientId,
907
+ "device_id": device_id,
908
+ "meta": meta,
909
+ "redirect_uri": "xlaccsdk01://xbase.cloud/callback?state=harbor"
910
+ };
911
+ // console.log("构建的初始化请求payload:", JSON.stringify(initPayload));
912
+
913
+ // 获取验证码token
914
+ console.log("开始请求验证码token");
915
+ let captchaResponse;
916
  try {
917
+ captchaResponse = await apiRequest(
918
+ "POST",
919
+ "https://xluser-ssl.xunleix.com/v1/shield/captcha/init",
920
+ initPayload,
921
+ commonHeaders
922
+ );
923
+ console.log("获取到验证码token响应:", JSON.stringify(captchaResponse));
924
+ } catch (err) {
925
+ console.error("获取验证码token失败:", err);
926
+ throw new Error(`获取验证码token失败: ${err.message}`);
927
  }
 
928
 
929
+ if (!captchaResponse.captcha_token) {
930
+ console.error("验证码token不存在:", JSON.stringify(captchaResponse));
931
+ return new Response(JSON.stringify({
932
+ success: false,
933
+ error: "获取验证码token失败"
934
+ }), {
935
+ status: 500,
936
+ headers: {
937
+ "Content-Type": "application/json",
938
+ "Access-Control-Allow-Origin": "*",
939
+ },
940
+ });
941
  }
 
942
 
943
+ // 准备离线下载请求参数
944
+ const params = {
945
+ "kind": "drive#file",
946
+ "name": name || getFileNameFromUrl(file_url) || "未命名文件",
947
+ "upload_type": "UPLOAD_TYPE_URL",
948
+ "url": {
949
+ "url": file_url,
950
+ "parent_id": parent_id || "root" // 确保parent_id也在url对象中
951
+ },
952
+ "parent_id": parent_id || "root"
953
+ };
954
+ console.log("准备的离线下载请求参数:", JSON.stringify(params));
955
+
956
+ // 添加验证码token到请求头
957
+ const downloadHeaders = {
958
+ ...commonHeaders,
959
+ 'X-Captcha-Token': captchaResponse.captcha_token
960
+ };
961
+ console.log("离线下载请求头:", JSON.stringify({
962
+ 'X-Captcha-Token': captchaResponse.captcha_token,
963
+ 'Authorization': `Bearer ${access_token.substring(0, 10)}...`,
964
+ }));
965
+
966
+ // 发送离线下载请求
967
+ console.log("开始发送离线下载请求");
968
+ let downloadResponse;
969
  try {
970
+ downloadResponse = await apiRequest(
971
+ "POST",
972
+ "https://api-pan.xunleix.com/drive/v1/files",
973
+ params,
974
+ downloadHeaders
975
+ );
976
+ console.log("离线下载请求成功:", JSON.stringify(downloadResponse));
977
+ } catch (err) {
978
+ console.error("离线下载请求失败:", err);
979
+ throw new Error(`离线下载请求失败: ${err.message}`);
980
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981
 
982
+ return new Response(JSON.stringify({
983
+ success: true,
984
+ message: "离线下载任务已创建",
985
+ data: downloadResponse
986
+ }), {
987
+ headers: {
988
+ "Content-Type": "application/json",
989
+ "Access-Control-Allow-Origin": "*",
990
+ },
991
+ });
992
+ } catch (error) {
993
+ console.error("离线下载错误:", error);
994
+ return new Response(JSON.stringify({
995
+ success: false,
996
+ error: `离线下载失败: ${error.message}`
997
+ }), {
998
+ status: 500,
999
+ headers: {
1000
+ "Content-Type": "application/json",
1001
+ "Access-Control-Allow-Origin": "*",
1002
+ },
1003
+ });
1004
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1005
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
 
1008
+ // 404 处理
1009
+ return new Response(JSON.stringify({
1010
+ success: false,
1011
+ error: "Not Found"
1012
+ }), {
1013
+ status: 404,
1014
+ headers: {
1015
+ "Content-Type": "application/json",
1016
+ "Access-Control-Allow-Origin": "*",
1017
+ },
1018
  });
 
 
1019
  }
1020
+
1021
+ // 启动服务器
1022
+ console.log("迅雷X 注册服务已启动,监听端口 8000...");
1023
+ await serve(handleRequest, { port: 8000 });