@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.
- package/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- 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
|
+
}
|