@pedrofariasx/qwenproxy 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +292 -0
  3. package/bin/qwenproxy.mjs +11 -0
  4. package/package.json +56 -0
  5. package/src/api/models.ts +183 -0
  6. package/src/api/server.ts +126 -0
  7. package/src/cache/memory-cache.ts +186 -0
  8. package/src/core/account-manager.ts +132 -0
  9. package/src/core/accounts.ts +78 -0
  10. package/src/core/config.ts +91 -0
  11. package/src/core/database.ts +92 -0
  12. package/src/core/logger.ts +96 -0
  13. package/src/core/metrics.ts +169 -0
  14. package/src/core/model-registry.ts +30 -0
  15. package/src/core/stream-registry.ts +40 -0
  16. package/src/core/watchdog.ts +130 -0
  17. package/src/index.ts +7 -0
  18. package/src/linter/extraction-engine.ts +165 -0
  19. package/src/linter/index.ts +258 -0
  20. package/src/linter/repair-normalize.ts +245 -0
  21. package/src/linter/safety-gate.ts +219 -0
  22. package/src/linter/streaming-state-machine.ts +252 -0
  23. package/src/linter/structural-parser.ts +352 -0
  24. package/src/linter/types.ts +74 -0
  25. package/src/login.ts +228 -0
  26. package/src/routes/chat.ts +801 -0
  27. package/src/routes/upload.ts +700 -0
  28. package/src/services/playwright.ts +778 -0
  29. package/src/services/qwen.ts +500 -0
  30. package/src/tests/advanced.test.ts +227 -0
  31. package/src/tests/agenticStress.test.ts +360 -0
  32. package/src/tests/concurrency.test.ts +103 -0
  33. package/src/tests/concurrentChat.test.ts +71 -0
  34. package/src/tests/delta.test.ts +63 -0
  35. package/src/tests/index.test.ts +356 -0
  36. package/src/tests/jsonFix.test.ts +98 -0
  37. package/src/tests/linter.test.ts +151 -0
  38. package/src/tests/parallel.test.ts +42 -0
  39. package/src/tests/parser.test.ts +89 -0
  40. package/src/tests/rotation.test.ts +45 -0
  41. package/src/tests/streamingOptimizations.test.ts +328 -0
  42. package/src/tests/structureVerification.test.ts +176 -0
  43. package/src/tools/ast.ts +15 -0
  44. package/src/tools/coercion.ts +67 -0
  45. package/src/tools/confidence.ts +48 -0
  46. package/src/tools/detector.ts +40 -0
  47. package/src/tools/executor.ts +236 -0
  48. package/src/tools/parser.ts +446 -0
  49. package/src/tools/pipeline.ts +122 -0
  50. package/src/tools/registry-runtime.ts +34 -0
  51. package/src/tools/registry.ts +142 -0
  52. package/src/tools/repair.ts +42 -0
  53. package/src/tools/schema.ts +285 -0
  54. package/src/tools/types.ts +104 -0
  55. package/src/tools/validator.ts +33 -0
  56. package/src/utils/context-truncation.ts +61 -0
  57. package/src/utils/json.ts +114 -0
  58. package/src/utils/qwen-stream-parser.ts +286 -0
  59. package/src/utils/types.ts +101 -0
@@ -0,0 +1,700 @@
1
+ /*
2
+ * File: upload.ts
3
+ * Project: qwenproxy
4
+ * File upload handler - forwards files to Qwen's OSS storage
5
+ */
6
+
7
+ import { Context } from "hono";
8
+ import { getQwenHeaders } from "../services/playwright.ts";
9
+ import { v4 as uuidv4 } from "uuid";
10
+
11
+ interface STSResponse {
12
+ success: boolean;
13
+ request_id: string;
14
+ data: {
15
+ access_key_id: string;
16
+ access_key_secret: string;
17
+ security_token: string;
18
+ file_url: string;
19
+ file_path: string;
20
+ file_id: string;
21
+ bucketname: string;
22
+ region: string;
23
+ endpoint: string;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Get STS token from Qwen for file upload
29
+ * Retries once with refreshed headers if 401/RateLimited
30
+ */
31
+ async function getSTSToken(
32
+ filename: string,
33
+ filesize: number,
34
+ filetype: string,
35
+ headers: Record<string, string>,
36
+ ): Promise<STSResponse["data"]> {
37
+ for (let attempt = 0; attempt < 2; attempt++) {
38
+ const response = await fetch(
39
+ "https://chat.qwen.ai/api/v2/files/getstsToken",
40
+ {
41
+ method: "POST",
42
+ headers: {
43
+ Accept: "application/json, text/plain, */*",
44
+ "Content-Type": "application/json",
45
+ Cookie: headers.cookie,
46
+ Origin: "https://chat.qwen.ai",
47
+ Referer: "https://chat.qwen.ai/",
48
+ "User-Agent": headers["user-agent"],
49
+ "X-Request-Id": uuidv4(),
50
+ "bx-ua": headers["bx-ua"],
51
+ "bx-umidtoken": headers["bx-umidtoken"],
52
+ "bx-v": headers["bx-v"],
53
+ },
54
+ body: JSON.stringify({ filename, filesize: String(filesize), filetype }),
55
+ },
56
+ );
57
+
58
+ if (!response.ok) {
59
+ const errorText = await response.text().catch(() => "");
60
+ // On 401, try refreshing headers once
61
+ if (response.status === 401 && attempt === 0) {
62
+ console.warn("[Upload] STS 401, refreshing headers and retrying...");
63
+ const refreshed = await refreshUploadHeaders();
64
+ if (refreshed) {
65
+ Object.assign(headers, refreshed);
66
+ continue;
67
+ }
68
+ }
69
+ throw new Error(
70
+ `STS token request failed: ${response.status} ${errorText.substring(0, 200)}`,
71
+ );
72
+ }
73
+
74
+ const data = await response.json();
75
+ if (!data.success || !data.data) {
76
+ // Check if it's a 401/RateLimited error inside the response body
77
+ const code = data.data?.code || data.code;
78
+ const details = data.data?.details || data.message || "";
79
+ if ((code === "RateLimited" && details.includes("401")) || details.includes("Unauthorized")) {
80
+ if (attempt === 0) {
81
+ console.warn("[Upload] STS returned 401 in body, refreshing headers and retrying...");
82
+ const refreshed = await refreshUploadHeaders();
83
+ if (refreshed) {
84
+ Object.assign(headers, refreshed);
85
+ continue;
86
+ }
87
+ }
88
+ }
89
+ throw new Error(
90
+ `STS token invalid: ${JSON.stringify(data).substring(0, 200)}`,
91
+ );
92
+ }
93
+
94
+ return data.data;
95
+ }
96
+
97
+ throw new Error("STS token request failed after retries");
98
+ }
99
+
100
+ /**
101
+ * Refresh upload headers by forcing a new Qwen headers intercept
102
+ */
103
+ async function refreshUploadHeaders(): Promise<Record<string, string> | null> {
104
+ try {
105
+ const { headers: qHeaders } = await getQwenHeaders(true);
106
+ if (qHeaders['cookie'] && qHeaders['bx-ua']) {
107
+ return {
108
+ cookie: qHeaders['cookie'] || '',
109
+ "user-agent": qHeaders['user-agent'] || '',
110
+ "bx-ua": qHeaders['bx-ua'] || '',
111
+ "bx-umidtoken": qHeaders['bx-umidtoken'] || '',
112
+ "bx-v": qHeaders['bx-v'] || '',
113
+ };
114
+ }
115
+ } catch (err: any) {
116
+ console.error("[Upload] Failed to refresh headers:", err.message);
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Upload file to Alibaba Cloud OSS using STS credentials
123
+ */
124
+ async function uploadToOSS(
125
+ fileBuffer: ArrayBuffer,
126
+ stsData: STSResponse["data"],
127
+ filename: string,
128
+ ): Promise<string> {
129
+ const {
130
+ access_key_id,
131
+ access_key_secret,
132
+ security_token,
133
+ file_url,
134
+ file_path,
135
+ bucketname,
136
+ region,
137
+ endpoint,
138
+ } = stsData;
139
+
140
+ const OSS = (await import("ali-oss")).default;
141
+ const client = new OSS({
142
+ region,
143
+ accessKeyId: access_key_id,
144
+ accessKeySecret: access_key_secret,
145
+ stsToken: security_token,
146
+ bucket: bucketname,
147
+ endpoint: `https://${endpoint}`,
148
+ secure: true,
149
+ refreshSTSToken: async () => ({
150
+ accessKeyId: access_key_id,
151
+ accessKeySecret: access_key_secret,
152
+ stsToken: security_token,
153
+ }),
154
+ refreshSTSTokenInterval: 300000,
155
+ });
156
+
157
+ const buffer = Buffer.from(fileBuffer);
158
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
159
+ const mimeMap: Record<string, string> = {
160
+ // Images
161
+ png: "image/png",
162
+ jpg: "image/jpeg",
163
+ jpeg: "image/jpeg",
164
+ gif: "image/gif",
165
+ webp: "image/webp",
166
+ // Video
167
+ mp4: "video/mp4",
168
+ mov: "video/quicktime",
169
+ avi: "video/x-msvideo",
170
+ webm: "video/webm",
171
+ mkv: "video/x-matroska",
172
+ // Audio
173
+ mp3: "audio/mpeg",
174
+ wav: "audio/wav",
175
+ ogg: "audio/ogg",
176
+ flac: "audio/flac",
177
+ m4a: "audio/mp4",
178
+ aac: "audio/aac",
179
+ // Documents
180
+ pdf: "application/pdf",
181
+ doc: "application/msword",
182
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
183
+ xls: "application/vnd.ms-excel",
184
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
185
+ ppt: "application/vnd.ms-powerpoint",
186
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
187
+ txt: "text/plain",
188
+ md: "text/markdown",
189
+ csv: "text/csv",
190
+ json: "application/json",
191
+ xml: "application/xml",
192
+ html: "text/html",
193
+ zip: "application/zip",
194
+ };
195
+ const contentType = mimeMap[ext] || "application/octet-stream";
196
+
197
+ await client.put(file_path, buffer, {
198
+ headers: { "Content-Type": contentType },
199
+ });
200
+
201
+ return file_url.split("?")[0];
202
+ }
203
+
204
+ /**
205
+ * Handle image upload endpoint
206
+ * POST /v1/upload
207
+ */
208
+ export async function uploadFile(c: Context) {
209
+ try {
210
+ const formData = await c.req.formData();
211
+ const file = formData.get("file") as File | null;
212
+
213
+ if (!file) {
214
+ return c.json({ error: "No file provided" }, 400);
215
+ }
216
+
217
+ // Detect MIME from filename if browser sends generic type
218
+ let fileType = file.type;
219
+ if (fileType === "application/octet-stream" || !fileType) {
220
+ const ext = file.name.split(".").pop()?.toLowerCase() || "";
221
+ const extMimeMap: Record<string, string> = {
222
+ jpg: "image/jpeg",
223
+ jpeg: "image/jpeg",
224
+ png: "image/png",
225
+ gif: "image/gif",
226
+ webp: "image/webp",
227
+ mp4: "video/mp4",
228
+ mov: "video/quicktime",
229
+ avi: "video/x-msvideo",
230
+ webm: "video/webm",
231
+ mkv: "video/x-matroska",
232
+ mp3: "audio/mpeg",
233
+ wav: "audio/wav",
234
+ ogg: "audio/ogg",
235
+ flac: "audio/flac",
236
+ m4a: "audio/mp4",
237
+ aac: "audio/aac",
238
+ pdf: "application/pdf",
239
+ doc: "application/msword",
240
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
241
+ xls: "application/vnd.ms-excel",
242
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
243
+ ppt: "application/vnd.ms-powerpoint",
244
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
245
+ txt: "text/plain",
246
+ md: "text/markdown",
247
+ csv: "text/csv",
248
+ json: "application/json",
249
+ xml: "application/xml",
250
+ html: "text/html",
251
+ zip: "application/zip",
252
+ };
253
+ fileType = extMimeMap[ext] || "application/octet-stream";
254
+ }
255
+
256
+ // Determine media category for size limits
257
+ const isVideo = fileType.startsWith("video/");
258
+ const isAudio = fileType.startsWith("audio/");
259
+ const isImage = fileType.startsWith("image/");
260
+ let maxSize = 20 * 1024 * 1024; // 20MB default for docs/images
261
+ if (isVideo)
262
+ maxSize = 100 * 1024 * 1024; // 100MB for video
263
+ else if (isAudio) maxSize = 50 * 1024 * 1024; // 50MB for audio
264
+ if (file.size > maxSize) {
265
+ const sizeLabel = isVideo
266
+ ? "100MB (video)"
267
+ : isAudio
268
+ ? "50MB (audio)"
269
+ : "20MB (image/doc)";
270
+ return c.json({ error: `File too large. Max size: ${sizeLabel}` }, 400);
271
+ }
272
+
273
+ // Get full Qwen headers with bx-ua/bx-umidtoken
274
+ let headers: Record<string, string> | null = null;
275
+ try {
276
+ const { headers: qHeaders } = await getQwenHeaders(false);
277
+ if (qHeaders['cookie'] && qHeaders['bx-ua']) {
278
+ headers = {
279
+ cookie: qHeaders['cookie'] || '',
280
+ "user-agent": qHeaders['user-agent'] || '',
281
+ "bx-ua": qHeaders['bx-ua'] || '',
282
+ "bx-umidtoken": qHeaders['bx-umidtoken'] || '',
283
+ "bx-v": qHeaders['bx-v'] || '',
284
+ };
285
+ }
286
+ } catch (err: any) {
287
+ console.error("[Upload] Failed to get Qwen headers:", err.message);
288
+ }
289
+
290
+ if (!headers) {
291
+ return c.json(
292
+ { error: "Authentication not ready. Send a chat message first." },
293
+ 503,
294
+ );
295
+ }
296
+
297
+ // Determine Qwen filetype for STS token
298
+ let qwenFileType = "file";
299
+ if (isVideo) qwenFileType = "video";
300
+ else if (isAudio) qwenFileType = "audio";
301
+ else if (isImage) qwenFileType = "image";
302
+
303
+ const stsData = await getSTSToken(
304
+ file.name,
305
+ file.size,
306
+ qwenFileType,
307
+ headers,
308
+ );
309
+ const fileBuffer = await file.arrayBuffer();
310
+ const fileUrl = await uploadToOSS(fileBuffer, stsData, file.name);
311
+
312
+ return c.json({
313
+ url: fileUrl,
314
+ file_id: stsData.file_id,
315
+ filename: file.name,
316
+ type: qwenFileType,
317
+ });
318
+ } catch (error: any) {
319
+ console.error("[Upload] Error:", error.message);
320
+ return c.json({ error: error.message }, 500);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Qwen file format for images
326
+ */
327
+ export interface QwenFileEntry {
328
+ type: string;
329
+ file: {
330
+ created_at: number;
331
+ data: Record<string, unknown>;
332
+ filename: string;
333
+ hash: string | null;
334
+ id: string;
335
+ user_id: string;
336
+ meta: { name: string; size: number; content_type: string };
337
+ update_at: number;
338
+ lastModified: number;
339
+ name: string;
340
+ webkitRelativePath: string;
341
+ size: number;
342
+ type: string;
343
+ };
344
+ id: string;
345
+ url: string;
346
+ name: string;
347
+ collection_name: string;
348
+ progress: number;
349
+ status: string;
350
+ greenNet: string;
351
+ size: number;
352
+ error: string;
353
+ itemId: string;
354
+ file_type: string;
355
+ showType: string;
356
+ file_class: string;
357
+ uploadTaskId: string;
358
+ }
359
+
360
+ /**
361
+ * Detect file type from URL or filename
362
+ */
363
+ function detectFileType(filename: string): {
364
+ mime: string;
365
+ showType: string;
366
+ fileClass: string;
367
+ qwenFileType: string;
368
+ } {
369
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
370
+
371
+ const typeMap: Record<
372
+ string,
373
+ { mime: string; showType: string; fileClass: string; qwenFileType: string }
374
+ > = {
375
+ // Images
376
+ png: {
377
+ mime: "image/png",
378
+ showType: "image",
379
+ fileClass: "vision",
380
+ qwenFileType: "image",
381
+ },
382
+ jpg: {
383
+ mime: "image/jpeg",
384
+ showType: "image",
385
+ fileClass: "vision",
386
+ qwenFileType: "image",
387
+ },
388
+ jpeg: {
389
+ mime: "image/jpeg",
390
+ showType: "image",
391
+ fileClass: "vision",
392
+ qwenFileType: "image",
393
+ },
394
+ gif: {
395
+ mime: "image/gif",
396
+ showType: "image",
397
+ fileClass: "vision",
398
+ qwenFileType: "image",
399
+ },
400
+ webp: {
401
+ mime: "image/webp",
402
+ showType: "image",
403
+ fileClass: "vision",
404
+ qwenFileType: "image",
405
+ },
406
+ // Video
407
+ mp4: {
408
+ mime: "video/mp4",
409
+ showType: "video",
410
+ fileClass: "video",
411
+ qwenFileType: "video",
412
+ },
413
+ mov: {
414
+ mime: "video/quicktime",
415
+ showType: "video",
416
+ fileClass: "video",
417
+ qwenFileType: "video",
418
+ },
419
+ avi: {
420
+ mime: "video/x-msvideo",
421
+ showType: "video",
422
+ fileClass: "video",
423
+ qwenFileType: "video",
424
+ },
425
+ webm: {
426
+ mime: "video/webm",
427
+ showType: "video",
428
+ fileClass: "video",
429
+ qwenFileType: "video",
430
+ },
431
+ mkv: {
432
+ mime: "video/x-matroska",
433
+ showType: "video",
434
+ fileClass: "video",
435
+ qwenFileType: "video",
436
+ },
437
+ // Audio
438
+ mp3: {
439
+ mime: "audio/mpeg",
440
+ showType: "audio",
441
+ fileClass: "audio",
442
+ qwenFileType: "audio",
443
+ },
444
+ wav: {
445
+ mime: "audio/wav",
446
+ showType: "audio",
447
+ fileClass: "audio",
448
+ qwenFileType: "audio",
449
+ },
450
+ ogg: {
451
+ mime: "audio/ogg",
452
+ showType: "audio",
453
+ fileClass: "audio",
454
+ qwenFileType: "audio",
455
+ },
456
+ flac: {
457
+ mime: "audio/flac",
458
+ showType: "audio",
459
+ fileClass: "audio",
460
+ qwenFileType: "audio",
461
+ },
462
+ m4a: {
463
+ mime: "audio/mp4",
464
+ showType: "audio",
465
+ fileClass: "audio",
466
+ qwenFileType: "audio",
467
+ },
468
+ aac: {
469
+ mime: "audio/aac",
470
+ showType: "audio",
471
+ fileClass: "audio",
472
+ qwenFileType: "audio",
473
+ },
474
+ // Documents
475
+ pdf: {
476
+ mime: "application/pdf",
477
+ showType: "file",
478
+ fileClass: "file",
479
+ qwenFileType: "file",
480
+ },
481
+ doc: {
482
+ mime: "application/msword",
483
+ showType: "file",
484
+ fileClass: "file",
485
+ qwenFileType: "file",
486
+ },
487
+ docx: {
488
+ mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
489
+ showType: "file",
490
+ fileClass: "file",
491
+ qwenFileType: "file",
492
+ },
493
+ xls: {
494
+ mime: "application/vnd.ms-excel",
495
+ showType: "file",
496
+ fileClass: "file",
497
+ qwenFileType: "file",
498
+ },
499
+ xlsx: {
500
+ mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
501
+ showType: "file",
502
+ fileClass: "file",
503
+ qwenFileType: "file",
504
+ },
505
+ ppt: {
506
+ mime: "application/vnd.ms-powerpoint",
507
+ showType: "file",
508
+ fileClass: "file",
509
+ qwenFileType: "file",
510
+ },
511
+ pptx: {
512
+ mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
513
+ showType: "file",
514
+ fileClass: "file",
515
+ qwenFileType: "file",
516
+ },
517
+ txt: {
518
+ mime: "text/plain",
519
+ showType: "file",
520
+ fileClass: "file",
521
+ qwenFileType: "file",
522
+ },
523
+ md: {
524
+ mime: "text/markdown",
525
+ showType: "file",
526
+ fileClass: "file",
527
+ qwenFileType: "file",
528
+ },
529
+ csv: {
530
+ mime: "text/csv",
531
+ showType: "file",
532
+ fileClass: "file",
533
+ qwenFileType: "file",
534
+ },
535
+ json: {
536
+ mime: "application/json",
537
+ showType: "file",
538
+ fileClass: "file",
539
+ qwenFileType: "file",
540
+ },
541
+ xml: {
542
+ mime: "application/xml",
543
+ showType: "file",
544
+ fileClass: "file",
545
+ qwenFileType: "file",
546
+ },
547
+ html: {
548
+ mime: "text/html",
549
+ showType: "file",
550
+ fileClass: "file",
551
+ qwenFileType: "file",
552
+ },
553
+ zip: {
554
+ mime: "application/zip",
555
+ showType: "file",
556
+ fileClass: "file",
557
+ qwenFileType: "file",
558
+ },
559
+ };
560
+
561
+ return (
562
+ typeMap[ext] || {
563
+ mime: "application/octet-stream",
564
+ showType: "file",
565
+ fileClass: "file",
566
+ qwenFileType: "file",
567
+ }
568
+ );
569
+ }
570
+
571
+ /**
572
+ * Process OpenAI-style image/video content into Qwen file format
573
+ */
574
+ export async function processImagesForQwen(
575
+ content: Array<{
576
+ type: string;
577
+ text?: string;
578
+ image_url?: { url: string };
579
+ video_url?: { url: string };
580
+ audio_url?: { url: string };
581
+ file_url?: { url: string };
582
+ }>,
583
+ headers: Record<string, string>,
584
+ ): Promise<{ text: string; files: QwenFileEntry[] }> {
585
+ const textParts: string[] = [];
586
+ const files: QwenFileEntry[] = [];
587
+
588
+ for (const part of content) {
589
+ if (part.type === "text" && part.text) {
590
+ textParts.push(part.text);
591
+ } else if (
592
+ (part.type === "image_url" && part.image_url?.url) ||
593
+ (part.type === "video_url" && part.video_url?.url) ||
594
+ (part.type === "audio_url" && part.audio_url?.url) ||
595
+ (part.type === "file_url" && part.file_url?.url)
596
+ ) {
597
+ const mediaUrl =
598
+ part.type === "video_url"
599
+ ? part.video_url!.url
600
+ : part.type === "audio_url"
601
+ ? part.audio_url!.url
602
+ : part.type === "file_url"
603
+ ? part.file_url!.url
604
+ : part.image_url!.url;
605
+ let fileUrl = "";
606
+ let filename = "";
607
+ let fileSize = 0;
608
+ let fileId = "";
609
+
610
+ if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
611
+ fileUrl = mediaUrl;
612
+ filename = mediaUrl.split("/").pop()?.split("?")[0] || "file.bin";
613
+ fileId = uuidv4();
614
+ } else if (mediaUrl.startsWith("data:")) {
615
+ try {
616
+ // Detect type from data URI
617
+ const dataMime = mediaUrl.match(/^data:([^;]+)/)?.[1] || "";
618
+ const isVideoData = dataMime.startsWith("video/");
619
+ const isAudioData = dataMime.startsWith("audio/");
620
+ const extFromMime: Record<string, string> = {
621
+ "video/mp4": "mp4",
622
+ "video/webm": "webm",
623
+ "video/quicktime": "mov",
624
+ "audio/mpeg": "mp3",
625
+ "audio/wav": "wav",
626
+ "audio/ogg": "ogg",
627
+ "audio/flac": "flac",
628
+ "audio/mp4": "m4a",
629
+ "audio/aac": "aac",
630
+ "image/png": "png",
631
+ "image/jpeg": "jpg",
632
+ "image/gif": "gif",
633
+ "image/webp": "webp",
634
+ };
635
+ const detectedExt =
636
+ extFromMime[dataMime] ||
637
+ (isVideoData ? "mp4" : isAudioData ? "mp3" : "png");
638
+ const base64Data = mediaUrl.split(",")[1];
639
+ const buffer = Buffer.from(base64Data, "base64");
640
+ filename = `${isVideoData ? "video" : isAudioData ? "audio" : "file"}_${Date.now()}.${detectedExt}`;
641
+ fileSize = buffer.length;
642
+ const typeInfo = detectFileType(filename);
643
+ const stsData = await getSTSToken(
644
+ filename,
645
+ fileSize,
646
+ typeInfo.qwenFileType,
647
+ headers,
648
+ );
649
+ fileUrl = await uploadToOSS(buffer.buffer, stsData, filename);
650
+ fileId = stsData.file_id;
651
+ } catch (err: any) {
652
+ console.error("[Upload] Failed to upload media:", err.message);
653
+ continue;
654
+ }
655
+ }
656
+
657
+ if (fileUrl) {
658
+ const typeInfo = detectFileType(filename);
659
+ files.push({
660
+ type: typeInfo.showType,
661
+ file: {
662
+ created_at: Date.now(),
663
+ data: {},
664
+ filename,
665
+ hash: null,
666
+ id: fileId,
667
+ user_id: "proxy-user",
668
+ meta: {
669
+ name: filename,
670
+ size: fileSize,
671
+ content_type: typeInfo.mime,
672
+ },
673
+ update_at: Date.now(),
674
+ lastModified: Date.now(),
675
+ name: filename,
676
+ webkitRelativePath: "",
677
+ size: fileSize,
678
+ type: typeInfo.mime,
679
+ },
680
+ id: fileId,
681
+ url: fileUrl,
682
+ name: filename,
683
+ collection_name: "",
684
+ progress: 100,
685
+ status: "uploaded",
686
+ greenNet: "success",
687
+ size: fileSize,
688
+ error: "",
689
+ itemId: uuidv4(),
690
+ file_type: typeInfo.mime,
691
+ showType: typeInfo.showType,
692
+ file_class: typeInfo.fileClass,
693
+ uploadTaskId: uuidv4(),
694
+ });
695
+ }
696
+ }
697
+ }
698
+
699
+ return { text: textParts.join("\n"), files };
700
+ }