qwen-api-proxy 1.0.10

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.
@@ -0,0 +1,459 @@
1
+ // imageGeneration.js - Модуль для генерации изображений через Qwen Image API
2
+ import axios from 'axios';
3
+ import { logInfo, logError, logDebug } from '../logger/index.js';
4
+ import { sendMessage } from './chat.js';
5
+ import { uploadFileToQwen } from './fileUpload.js';
6
+ import { IMAGE_GENERATION_MODE, DASHSCOPE_API_KEY } from '../config.js';
7
+
8
+ const DASHSCOPE_API_BASE = 'https://dashscope-intl.aliyuncs.com/api/v1';
9
+
10
+ // Модели для генерации изображений
11
+ const IMAGE_GENERATION_MODELS = [
12
+ 'qwen-image-max',
13
+ 'qwen-image-plus',
14
+ 'qwen-image',
15
+ 'wan2.6-t2i',
16
+ 'wan2.5-t2i-preview',
17
+ 'wan2.2-t2i-flash'
18
+ ];
19
+
20
+ /**
21
+ * Генерация изображения по текстовому описанию
22
+ * @param {string} prompt - Текстовое описание изображения
23
+ * @param {string} model - Модель для генерации
24
+ * @param {object} options - Дополнительные параметры
25
+ * @returns {Promise<object>} - Результат генерации
26
+ */
27
+ export async function generateImage(prompt, model = 'qwen-image-plus', options = {}) {
28
+ // Определяем режим генерации
29
+ const mode = IMAGE_GENERATION_MODE;
30
+
31
+ if (mode === 'browser') {
32
+ logInfo('🎨 Генерация изображения через browser mode...');
33
+ return generateImageViaBrowser(prompt, model, options);
34
+ } else {
35
+ logInfo('🎨 Генерация изображения через DashScope API...');
36
+ return generateImageViaDashScope(prompt, model, options);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Генерация изображения через DashScope API (прямой вызов)
42
+ */
43
+ async function generateImageViaDashScope(prompt, model = 'qwen-image-plus', options = {}) {
44
+ const apiKey = process.env.DASHSCOPE_API_KEY;
45
+
46
+ if (!apiKey) {
47
+ logError('API ключ DASHSCOPE_API_KEY не установлен');
48
+ return {
49
+ error: 'API ключ DASHSCOPE_API_KEY не установлен. Пожалуйста, настройте переменную окружения.'
50
+ };
51
+ }
52
+
53
+ try {
54
+ logInfo(`Генерация изображения через ${model}...`);
55
+ logDebug(`Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
56
+
57
+ const payload = {
58
+ model: model,
59
+ input: {
60
+ prompt: prompt,
61
+ negative_prompt: options.negativePrompt || ' '
62
+ },
63
+ parameters: {
64
+ size: options.size || '1024*1024',
65
+ n: options.n || 1,
66
+ prompt_extend: options.promptExtend !== false,
67
+ watermark: options.watermark || false
68
+ }
69
+ };
70
+
71
+ // Если есть изображение для image-to-image
72
+ if (options.imagePath) {
73
+ logInfo(`📸 Image-to-image mode: загружаем файл ${options.imagePath}`);
74
+
75
+ // Загружаем файл в Qwen и получаем URL
76
+ const uploadResult = await uploadFileToQwen(options.imagePath);
77
+
78
+ // Проверяем успешность загрузки
79
+ if (!uploadResult || uploadResult.success === false) {
80
+ const errorMsg = uploadResult?.error || 'Unknown error';
81
+ logError(`❌ Ошибка загрузки файла: ${errorMsg}`);
82
+ throw new Error(`Не удалось загрузить изображение: ${errorMsg}`);
83
+ }
84
+
85
+ if (uploadResult.file_url || uploadResult.url) {
86
+ const fileUrl = uploadResult.file_url || uploadResult.url;
87
+ logInfo(`✅ Файл загружен: ${fileUrl}`);
88
+ payload.input.image_url = fileUrl;
89
+ } else {
90
+ logError('❌ URL файла не найден в результате загрузки:', uploadResult);
91
+ throw new Error('Не удалось получить URL загруженного изображения');
92
+ }
93
+ }
94
+
95
+ // Асинхронный запрос для Wan моделей
96
+ const isWanModel = model.startsWith('wan');
97
+ const endpoint = isWanModel
98
+ ? `${DASHSCOPE_API_BASE}/services/aigc/text2image/image-synthesis`
99
+ : `${DASHSCOPE_API_BASE}/services/aigc/text2image/image-synthesis`;
100
+
101
+ const response = await axios.post(endpoint, payload, {
102
+ headers: {
103
+ 'Authorization': `Bearer ${apiKey}`,
104
+ 'Content-Type': 'application/json',
105
+ 'X-DashScope-Async': isWanModel ? 'enable' : undefined
106
+ },
107
+ timeout: 120000
108
+ });
109
+
110
+ const data = response.data;
111
+
112
+ // Асинхронный режим - получаем task_id и опрашиваем статус
113
+ if (data.output?.task_id) {
114
+ logInfo(`Задача создана: ${data.output.task_id}`);
115
+ return await pollTaskStatus(data.output.task_id, apiKey);
116
+ }
117
+
118
+ // Синхронный режим - сразу получаем результат
119
+ if (data.output?.results && data.output.results.length > 0) {
120
+ const imageUrl = data.output.results[0].url;
121
+ logInfo(`Изображение сгенерировано: ${imageUrl}`);
122
+ return {
123
+ success: true,
124
+ imageUrl: imageUrl,
125
+ taskId: data.output.task_id,
126
+ model: model,
127
+ prompt: prompt
128
+ };
129
+ }
130
+
131
+ return {
132
+ error: 'Неожиданный формат ответа от API',
133
+ rawData: data
134
+ };
135
+
136
+ } catch (error) {
137
+ logError('Ошибка при генерации изображения', error);
138
+ return {
139
+ error: error.response?.data?.message || error.message || 'Неизвестная ошибка'
140
+ };
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Генерация изображения через браузер (аналогично генерации текста)
146
+ * Использует Qwen Chat API с chat_type='t2i'
147
+ */
148
+ async function generateImageViaBrowser(prompt, model = 'qwen-image-plus', options = {}) {
149
+ try {
150
+ logInfo(`🖼️ Browser mode: генерация изображения через ${model}...`);
151
+ logDebug(`Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
152
+
153
+ // Подготавливаем файлы если есть imagePath
154
+ let files = null;
155
+ if (options.imagePath) {
156
+ logInfo(`📸 Image-to-image mode: загружаем файл ${options.imagePath}`);
157
+
158
+ // Загружаем файл в Qwen и получаем URL
159
+ const uploadResult = await uploadFileToQwen(options.imagePath);
160
+
161
+ // Проверяем успешность загрузки
162
+ if (!uploadResult || uploadResult.success === false) {
163
+ const errorMsg = uploadResult?.error || 'Unknown error';
164
+ logError(`❌ Ошибка загрузки файла: ${errorMsg}`);
165
+ throw new Error(`Не удалось загрузить изображение: ${errorMsg}`);
166
+ }
167
+
168
+ if (uploadResult.file_url || uploadResult.url) {
169
+ const fileUrl = uploadResult.file_url || uploadResult.url;
170
+ logInfo(`✅ Файл загружен: ${fileUrl}`);
171
+ // Формат файла для API: { url: '...' }
172
+ files = [{ url: fileUrl }];
173
+ } else {
174
+ logError('❌ URL файла не найден в результате загрузки:', uploadResult);
175
+ throw new Error('Не удалось получить URL загруженного изображения');
176
+ }
177
+ }
178
+
179
+ // Используем sendMessage с chatType='t2i' (text-to-image)
180
+ const result = await sendMessage(
181
+ prompt,
182
+ model,
183
+ null, // chatId - будет создан автоматически
184
+ null, // parentId
185
+ files, // files - изображение для image-to-image
186
+ null, // tools
187
+ null, // toolChoice
188
+ null, // systemMessage
189
+ 't2i', // chatType - text to image
190
+ options.size || '1024*1024', // size
191
+ true // waitForCompletion
192
+ );
193
+
194
+ // Проверяем результат
195
+ if (result.error) {
196
+ logError('❌ Ошибка генерации изображения (browser mode)', result.error);
197
+ return {
198
+ error: result.error,
199
+ details: result.details || 'Browser mode generation failed'
200
+ };
201
+ }
202
+
203
+ // Извлекаем URL изображения из ответа
204
+ let imageUrl = null;
205
+
206
+ // Проверяем разные форматы ответа
207
+ if (result.imageUrl) {
208
+ imageUrl = result.imageUrl;
209
+ } else if (result.choices?.[0]?.message?.content) {
210
+ const content = result.choices[0].message.content;
211
+ // Если контент - это URL изображения
212
+ if (content.startsWith('http') || content.startsWith('data:')) {
213
+ imageUrl = content;
214
+ } else {
215
+ // Пытаемся извлечь URL из markdown или JSON
216
+ const urlMatch = content.match(/\[(?:Generated Image)?\]\(([^)]+)\)/);
217
+ if (urlMatch) {
218
+ imageUrl = urlMatch[1];
219
+ } else {
220
+ // Пробуем распарсить как JSON
221
+ try {
222
+ const parsed = JSON.parse(content);
223
+ imageUrl = parsed.url || parsed.image_url || parsed.imageUrl;
224
+ } catch {
225
+ // Не JSON, используем как есть
226
+ logWarn('⚠️ Ответ не содержит явного URL изображения');
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ if (!imageUrl) {
233
+ logError('❌ URL изображения не найден в ответе');
234
+ logDebug('Response:', JSON.stringify(result, null, 2));
235
+
236
+ // Логируем структуру ошибки для отладки
237
+ if (result.error) logDebug('result.error exists:', JSON.stringify(result.error));
238
+ if (result.errorBody) logDebug('result.errorBody exists:', result.errorBody.substring(0, 200));
239
+ if (result.details) logDebug('result.details exists:', result.details.substring(0, 200));
240
+
241
+ // Проверяем, есть ли в ответе реальная ошибка
242
+ let errorMessage = 'Image URL not found in response';
243
+
244
+ // Проверяем формат ошибки API (прямое поле error)
245
+ if (result.error) {
246
+ // API вернул ошибку
247
+ if (result.error.code) {
248
+ errorMessage = `API Error: ${result.error.code}`;
249
+ if (result.error.details) {
250
+ errorMessage += ` - ${result.error.details}`;
251
+ }
252
+ } else if (typeof result.error === 'string') {
253
+ errorMessage = result.error;
254
+ }
255
+ }
256
+ // Проверяем errorBody (JSON строка с ошибкой)
257
+ else if (result.errorBody) {
258
+ try {
259
+ const errorData = JSON.parse(result.errorBody);
260
+ if (errorData.error) {
261
+ if (errorData.error.code) {
262
+ errorMessage = `API Error: ${errorData.error.code}`;
263
+ if (errorData.error.details) {
264
+ errorMessage += ` - ${errorData.error.details}`;
265
+ }
266
+ } else if (typeof errorData.error === 'string') {
267
+ errorMessage = errorData.error;
268
+ }
269
+ } else if (errorData.code) {
270
+ // Код ошибки на верхнем уровне
271
+ errorMessage = `API Error: ${errorData.code}`;
272
+ if (errorData.detail || errorData.details) {
273
+ errorMessage += ` - ${errorData.detail || errorData.details}`;
274
+ }
275
+ }
276
+ } catch {
277
+ // Не JSON, используем как есть
278
+ if (typeof result.errorBody === 'string') {
279
+ errorMessage = result.errorBody.substring(0, 200);
280
+ }
281
+ }
282
+ }
283
+ // Проверяем details (JSON строка с ошибкой из handleApiError)
284
+ else if (result.details && typeof result.details === 'string') {
285
+ try {
286
+ const errorData = JSON.parse(result.details);
287
+ if (errorData.error) {
288
+ if (errorData.error.code) {
289
+ errorMessage = `API Error: ${errorData.error.code}`;
290
+ if (errorData.error.details) {
291
+ errorMessage += ` - ${errorData.error.details}`;
292
+ }
293
+ } else if (typeof errorData.error === 'string') {
294
+ errorMessage = errorData.error;
295
+ }
296
+ } else if (errorData.code) {
297
+ // Код ошибки на верхнем уровне
298
+ errorMessage = `API Error: ${errorData.code}`;
299
+ if (errorData.detail || errorData.details) {
300
+ errorMessage += ` - ${errorData.detail || errorData.details}`;
301
+ }
302
+ }
303
+ } catch {
304
+ // Не JSON, используем как есть
305
+ errorMessage = result.details.substring(0, 200);
306
+ }
307
+ }
308
+ // Проверяем формат ошибки в choices
309
+ else if (result.choices?.[0]?.message?.content) {
310
+ const content = result.choices[0].message.content;
311
+ // Пытаемся распарсить как JSON для поиска ошибки
312
+ try {
313
+ const parsed = JSON.parse(content);
314
+ if (parsed.error) {
315
+ if (parsed.error.code) {
316
+ errorMessage = `API Error: ${parsed.error.code}`;
317
+ if (parsed.error.details) {
318
+ errorMessage += ` - ${parsed.error.details}`;
319
+ }
320
+ } else if (typeof parsed.error === 'string') {
321
+ errorMessage = parsed.error;
322
+ }
323
+ }
324
+ } catch {
325
+ // Не JSON, оставляем стандартное сообщение
326
+ }
327
+ }
328
+
329
+ return {
330
+ error: errorMessage,
331
+ rawResponse: result
332
+ };
333
+ }
334
+
335
+ logInfo(`✅ Изображение сгенерировано (browser mode): ${imageUrl}`);
336
+ return {
337
+ success: true,
338
+ imageUrl: imageUrl,
339
+ model: model,
340
+ prompt: prompt,
341
+ chatId: result.chatId,
342
+ parentId: result.parentId
343
+ };
344
+
345
+ } catch (error) {
346
+ logError('❌ Ошибка при генерации изображения (browser mode)', error);
347
+ return {
348
+ error: error.message || 'Unknown error in browser mode'
349
+ };
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Опрос статуса задачи генерации изображения
355
+ * @param {string} taskId - ID задачи
356
+ * @param {string} apiKey - API ключ
357
+ * @returns {Promise<object>} - Результат генерации
358
+ */
359
+ async function pollTaskStatus(taskId, apiKey) {
360
+ const maxAttempts = 60;
361
+ const pollInterval = 2000; // 2 секунды
362
+
363
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
364
+ try {
365
+ const response = await axios.get(
366
+ `${DASHSCOPE_API_BASE}/tasks/${taskId}`,
367
+ {
368
+ headers: {
369
+ 'Authorization': `Bearer ${apiKey}`
370
+ }
371
+ }
372
+ );
373
+
374
+ const task = response.data;
375
+ const taskStatus = task.output?.task_status;
376
+
377
+ logDebug(`Статус задачи ${taskId}: ${taskStatus} (попытка ${attempt + 1}/${maxAttempts})`);
378
+
379
+ if (taskStatus === 'SUCCEEDED') {
380
+ const imageUrl = task.output?.results?.[0]?.url;
381
+ if (imageUrl) {
382
+ logInfo(`Изображение сгенерировано: ${imageUrl}`);
383
+ return {
384
+ success: true,
385
+ imageUrl: imageUrl,
386
+ taskId: taskId,
387
+ model: task.input?.model || 'unknown'
388
+ };
389
+ }
390
+ return { error: 'Изображение не найдено в результате' };
391
+ }
392
+
393
+ if (taskStatus === 'FAILED' || taskStatus === 'CANCELLED') {
394
+ return {
395
+ error: `Задача завершена со статусом: ${taskStatus}`,
396
+ message: task.output?.message || 'Неизвестная ошибка'
397
+ };
398
+ }
399
+
400
+ // PENDING или RUNNING - продолжаем опрос
401
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
402
+
403
+ } catch (error) {
404
+ logError(`Ошибка при опросе задачи ${taskId}`, error);
405
+ if (attempt === maxAttempts - 1) {
406
+ return { error: `Ошибка опроса: ${error.message}` };
407
+ }
408
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
409
+ }
410
+ }
411
+
412
+ return { error: 'Превышено время ожидания генерации изображения' };
413
+ }
414
+
415
+ /**
416
+ * Получить список доступных моделей генерации изображений
417
+ * @returns {string[]} - Список моделей
418
+ */
419
+ export function getAvailableImageModels() {
420
+ return IMAGE_GENERATION_MODELS;
421
+ }
422
+
423
+ /**
424
+ * Проверка доступности API генерации изображений
425
+ * @returns {Promise<boolean>} - Статус доступности
426
+ */
427
+ export async function checkImageApiAvailability() {
428
+ const mode = IMAGE_GENERATION_MODE;
429
+
430
+ // Browser mode всегда доступен (если браузер работает)
431
+ if (mode === 'browser') {
432
+ logDebug('🖼️ Browser mode: проверка через статус браузера');
433
+ const { getBrowserContext, getAuthenticationStatus } = await import('../browser/browser.js');
434
+ const browserContext = getBrowserContext();
435
+ const isAuthenticated = getAuthenticationStatus();
436
+ return !!(browserContext && isAuthenticated);
437
+ }
438
+
439
+ // DashScope mode: проверяем API ключ
440
+ const apiKey = DASHSCOPE_API_KEY;
441
+
442
+ if (!apiKey) {
443
+ return false;
444
+ }
445
+
446
+ try {
447
+ // Простой тестовый запрос для проверки API
448
+ await axios.get(`${DASHSCOPE_API_BASE}/models`, {
449
+ headers: {
450
+ 'Authorization': `Bearer ${apiKey}`
451
+ },
452
+ timeout: 5000
453
+ });
454
+ return true;
455
+ } catch (error) {
456
+ logDebug(`API генерации изображений недоступен: ${error.message}`);
457
+ return false;
458
+ }
459
+ }