koishi-plugin-aka-ai-generator 0.2.10 → 0.2.12
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/lib/index.js +150 -153
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -39,18 +39,13 @@ async function downloadImageAsBase64(ctx, url, timeout, logger) {
|
|
|
39
39
|
const buffer = Buffer.from(response);
|
|
40
40
|
const base64 = buffer.toString("base64");
|
|
41
41
|
let mimeType = "image/jpeg";
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
mimeType =
|
|
45
|
-
} else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} else if (urlLower.endsWith(".webp")) {
|
|
50
|
-
mimeType = "image/webp";
|
|
51
|
-
} else if (urlLower.endsWith(".gif")) {
|
|
52
|
-
mimeType = "image/gif";
|
|
53
|
-
}
|
|
42
|
+
const urlLower = url.toLowerCase();
|
|
43
|
+
if (urlLower.endsWith(".png")) {
|
|
44
|
+
mimeType = "image/png";
|
|
45
|
+
} else if (urlLower.endsWith(".webp")) {
|
|
46
|
+
mimeType = "image/webp";
|
|
47
|
+
} else if (urlLower.endsWith(".gif")) {
|
|
48
|
+
mimeType = "image/gif";
|
|
54
49
|
}
|
|
55
50
|
logger.debug("图片下载并转换为Base64", { url, mimeType, size: base64.length });
|
|
56
51
|
return { data: base64, mimeType };
|
|
@@ -184,18 +179,13 @@ async function downloadImageAsBase642(ctx, url, timeout, logger) {
|
|
|
184
179
|
const buffer = Buffer.from(response);
|
|
185
180
|
const base64 = buffer.toString("base64");
|
|
186
181
|
let mimeType = "image/jpeg";
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
mimeType =
|
|
190
|
-
} else {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
} else if (urlLower.endsWith(".webp")) {
|
|
195
|
-
mimeType = "image/webp";
|
|
196
|
-
} else if (urlLower.endsWith(".gif")) {
|
|
197
|
-
mimeType = "image/gif";
|
|
198
|
-
}
|
|
182
|
+
const urlLower = url.toLowerCase();
|
|
183
|
+
if (urlLower.endsWith(".png")) {
|
|
184
|
+
mimeType = "image/png";
|
|
185
|
+
} else if (urlLower.endsWith(".webp")) {
|
|
186
|
+
mimeType = "image/webp";
|
|
187
|
+
} else if (urlLower.endsWith(".gif")) {
|
|
188
|
+
mimeType = "image/gif";
|
|
199
189
|
}
|
|
200
190
|
if (logger) {
|
|
201
191
|
logger.debug("图片下载并转换为Base64", { url, mimeType, size: base64.length });
|
|
@@ -404,13 +394,7 @@ var GptGodProvider = class {
|
|
|
404
394
|
if (this.config.logLevel === "debug") {
|
|
405
395
|
logger.debug("调用 GPTGod 图像编辑 API", { prompt, imageCount: urls.length, numImages });
|
|
406
396
|
}
|
|
407
|
-
const
|
|
408
|
-
{
|
|
409
|
-
type: "text",
|
|
410
|
-
text: `${prompt}
|
|
411
|
-
请生成 ${numImages} 张图片。`
|
|
412
|
-
}
|
|
413
|
-
];
|
|
397
|
+
const imageParts = [];
|
|
414
398
|
for (const url of urls) {
|
|
415
399
|
const imagePart = await buildImageContentPart(
|
|
416
400
|
ctx,
|
|
@@ -418,91 +402,91 @@ var GptGodProvider = class {
|
|
|
418
402
|
this.config.apiTimeout,
|
|
419
403
|
logger
|
|
420
404
|
);
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
n: numImages,
|
|
427
|
-
// 使用 n 参数指定生成数量
|
|
428
|
-
messages: [
|
|
405
|
+
imageParts.push(imagePart);
|
|
406
|
+
}
|
|
407
|
+
const allImages = [];
|
|
408
|
+
for (let i = 0; i < numImages; i++) {
|
|
409
|
+
const contentParts = [
|
|
429
410
|
{
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const requestBodySize = JSON.stringify(requestData).length;
|
|
440
|
-
if (this.config.logLevel === "debug") {
|
|
441
|
-
logger.debug(`GPTGod API 请求 (尝试 ${attempt}/${maxRetries})`, {
|
|
442
|
-
requestBodySize: `${(requestBodySize / 1024).toFixed(2)} KB`,
|
|
443
|
-
imageCount: urls.length
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
const response = await ctx.http.post(
|
|
447
|
-
GPTGOD_DEFAULT_API_URL,
|
|
448
|
-
requestData,
|
|
411
|
+
type: "text",
|
|
412
|
+
text: prompt
|
|
413
|
+
},
|
|
414
|
+
...imageParts
|
|
415
|
+
];
|
|
416
|
+
const requestData = {
|
|
417
|
+
model: this.config.modelId,
|
|
418
|
+
stream: false,
|
|
419
|
+
messages: [
|
|
449
420
|
{
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
"Authorization": `Bearer ${this.config.apiKey}`
|
|
453
|
-
},
|
|
454
|
-
timeout: this.config.apiTimeout * 1e3
|
|
455
|
-
}
|
|
456
|
-
);
|
|
457
|
-
logger.success("GPTGod 图像编辑 API 调用成功");
|
|
458
|
-
if (response?.choices?.length > 0) {
|
|
459
|
-
const firstChoice = response.choices[0];
|
|
460
|
-
const messageContent = firstChoice.message?.content;
|
|
461
|
-
let errorMessage = "";
|
|
462
|
-
if (typeof messageContent === "string") {
|
|
463
|
-
errorMessage = messageContent;
|
|
464
|
-
} else if (Array.isArray(messageContent)) {
|
|
465
|
-
const textParts = messageContent.filter((part) => part?.type === "text" && part?.text).map((part) => part.text).join(" ");
|
|
466
|
-
errorMessage = textParts;
|
|
467
|
-
} else if (messageContent?.text) {
|
|
468
|
-
errorMessage = messageContent.text;
|
|
421
|
+
role: "user",
|
|
422
|
+
content: contentParts
|
|
469
423
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
424
|
+
]
|
|
425
|
+
};
|
|
426
|
+
logger.debug("调用 GPTGod 图像编辑 API", { prompt, imageCount: urls.length, numImages, current: i + 1 });
|
|
427
|
+
const maxRetries = 3;
|
|
428
|
+
let lastError = null;
|
|
429
|
+
let success = false;
|
|
430
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
431
|
+
try {
|
|
432
|
+
const requestBodySize = JSON.stringify(requestData).length;
|
|
433
|
+
if (this.config.logLevel === "debug") {
|
|
434
|
+
logger.debug(`GPTGod API 请求 (尝试 ${attempt}/${maxRetries})`, {
|
|
435
|
+
requestBodySize: `${(requestBodySize / 1024).toFixed(2)} KB`,
|
|
436
|
+
imageCount: urls.length,
|
|
437
|
+
current: i + 1
|
|
474
438
|
});
|
|
475
|
-
throw new Error("内容被安全策略拦截");
|
|
476
439
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
440
|
+
const response = await ctx.http.post(
|
|
441
|
+
GPTGOD_DEFAULT_API_URL,
|
|
442
|
+
requestData,
|
|
443
|
+
{
|
|
444
|
+
headers: {
|
|
445
|
+
"Content-Type": "application/json",
|
|
446
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
447
|
+
},
|
|
448
|
+
timeout: this.config.apiTimeout * 1e3
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
if (response?.choices?.length > 0) {
|
|
452
|
+
const firstChoice = response.choices[0];
|
|
453
|
+
const messageContent = firstChoice.message?.content;
|
|
454
|
+
let errorMessage = "";
|
|
455
|
+
if (typeof messageContent === "string") {
|
|
456
|
+
errorMessage = messageContent;
|
|
457
|
+
} else if (Array.isArray(messageContent)) {
|
|
458
|
+
const textParts = messageContent.filter((part) => part?.type === "text" && part?.text).map((part) => part.text).join(" ");
|
|
459
|
+
errorMessage = textParts;
|
|
460
|
+
} else if (messageContent?.text) {
|
|
461
|
+
errorMessage = messageContent.text;
|
|
462
|
+
}
|
|
463
|
+
if (errorMessage && (errorMessage.includes("PROHIBITED_CONTENT") || errorMessage.includes("blocked by Google Gemini") || errorMessage.includes("prohibited under official usage policies") || errorMessage.toLowerCase().includes("content is prohibited"))) {
|
|
464
|
+
logger.error("内容被 Google Gemini 政策拦截", {
|
|
465
|
+
errorMessage: errorMessage.substring(0, 200),
|
|
466
|
+
finishReason: firstChoice.finish_reason
|
|
467
|
+
});
|
|
468
|
+
throw new Error("内容被安全策略拦截");
|
|
469
|
+
}
|
|
470
|
+
if (errorMessage && (errorMessage.toLowerCase().includes("error") || errorMessage.toLowerCase().includes("failed") || errorMessage.toLowerCase().includes("blocked")) && !errorMessage.match(/https?:\/\//)) {
|
|
471
|
+
logger.error("API 返回错误消息", {
|
|
472
|
+
errorMessage: errorMessage.substring(0, 200),
|
|
473
|
+
finishReason: firstChoice.finish_reason
|
|
474
|
+
});
|
|
475
|
+
const shortError = errorMessage.length > 50 ? errorMessage.substring(0, 50) + "..." : errorMessage;
|
|
476
|
+
throw new Error(`处理失败:${shortError}`);
|
|
477
|
+
}
|
|
484
478
|
}
|
|
485
|
-
}
|
|
486
|
-
if (this.config.logLevel === "debug") {
|
|
487
|
-
logger.debug("GPTGod API 响应结构", {
|
|
488
|
-
hasChoices: !!response?.choices,
|
|
489
|
-
choicesLength: response?.choices?.length,
|
|
490
|
-
hasImages: !!response?.images,
|
|
491
|
-
hasImage: !!response?.image,
|
|
492
|
-
responseKeys: Object.keys(response || {}),
|
|
493
|
-
firstChoiceContent: response?.choices?.[0]?.message?.content ? typeof response.choices[0].message.content === "string" ? response.choices[0].message.content.substring(0, 200) : JSON.stringify(response.choices[0].message.content).substring(0, 200) : "none"
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
const images = parseGptGodResponse(response, this.config.logLevel === "debug" ? logger : null);
|
|
497
|
-
if (images.length < numImages) {
|
|
498
|
-
const warnData = {
|
|
499
|
-
requested: numImages,
|
|
500
|
-
received: images.length
|
|
501
|
-
};
|
|
502
479
|
if (this.config.logLevel === "debug") {
|
|
503
|
-
|
|
480
|
+
logger.debug("GPTGod API 响应结构", {
|
|
481
|
+
hasChoices: !!response?.choices,
|
|
482
|
+
choicesLength: response?.choices?.length,
|
|
483
|
+
hasImages: !!response?.images,
|
|
484
|
+
hasImage: !!response?.image,
|
|
485
|
+
responseKeys: Object.keys(response || {}),
|
|
486
|
+
firstChoiceContent: response?.choices?.[0]?.message?.content ? typeof response.choices[0].message.content === "string" ? response.choices[0].message.content.substring(0, 200) : JSON.stringify(response.choices[0].message.content).substring(0, 200) : "none"
|
|
487
|
+
});
|
|
504
488
|
}
|
|
505
|
-
|
|
489
|
+
const images = parseGptGodResponse(response, this.config.logLevel === "debug" ? logger : null);
|
|
506
490
|
if (images.length === 0 && response?.choices?.[0]?.message?.content) {
|
|
507
491
|
const content = response.choices[0].message.content;
|
|
508
492
|
const contentText = typeof content === "string" ? content : Array.isArray(content) ? content.map((p) => p?.text || "").join(" ") : "";
|
|
@@ -511,56 +495,69 @@ var GptGodProvider = class {
|
|
|
511
495
|
throw new Error(`生成失败:${shortError}`);
|
|
512
496
|
}
|
|
513
497
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
498
|
+
allImages.push(...images);
|
|
499
|
+
logger.success("GPTGod 图像编辑 API 调用成功", { current: i + 1, total: numImages });
|
|
500
|
+
success = true;
|
|
501
|
+
break;
|
|
502
|
+
} catch (error) {
|
|
503
|
+
lastError = error;
|
|
504
|
+
if (error?.message && (error.message.includes("内容被安全策略拦截") || error.message.includes("生成失败") || error.message.includes("处理失败"))) {
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
const isRetryableError = error?.cause?.code === "UND_ERR_SOCKET" || // Socket 错误
|
|
508
|
+
error?.code === "UND_ERR_SOCKET" || error?.message?.includes("other side closed") || error?.message?.includes("fetch failed") || error?.message?.includes("ECONNRESET") || error?.message?.includes("ETIMEDOUT") || error?.response?.status >= 500 && error?.response?.status < 600;
|
|
509
|
+
if (isRetryableError && attempt < maxRetries) {
|
|
510
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 5e3);
|
|
511
|
+
logger.warn(`GPTGod API 调用失败,将在 ${delay}ms 后重试 (${attempt}/${maxRetries})`, {
|
|
512
|
+
error: error?.message || error?.cause?.message || "连接错误",
|
|
513
|
+
code: error?.code || error?.cause?.code,
|
|
514
|
+
current: i + 1
|
|
515
|
+
});
|
|
516
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
logger.error("GPTGod 图像编辑 API 调用失败", {
|
|
520
|
+
message: error?.message || "未知错误",
|
|
521
|
+
name: error?.name,
|
|
522
|
+
code: error?.code,
|
|
523
|
+
status: error?.response?.status,
|
|
524
|
+
statusText: error?.response?.statusText,
|
|
525
|
+
data: error?.response?.data,
|
|
526
|
+
stack: error?.stack,
|
|
527
|
+
cause: error?.cause,
|
|
528
|
+
attempt,
|
|
529
|
+
maxRetries,
|
|
530
|
+
current: i + 1,
|
|
531
|
+
total: numImages,
|
|
532
|
+
// 如果是 axios 错误,通常会有 config 和 request 信息
|
|
533
|
+
url: error?.config?.url,
|
|
534
|
+
method: error?.config?.method,
|
|
535
|
+
headers: error?.config?.headers
|
|
528
536
|
});
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
maxRetries,
|
|
543
|
-
// 如果是 axios 错误,通常会有 config 和 request 信息
|
|
544
|
-
url: error?.config?.url,
|
|
545
|
-
method: error?.config?.method,
|
|
546
|
-
headers: error?.config?.headers
|
|
547
|
-
});
|
|
548
|
-
if (error?.cause?.code === "UND_ERR_SOCKET" || error?.message?.includes("other side closed")) {
|
|
549
|
-
throw new Error("图像处理失败:服务器连接中断,可能是服务器负载过高或网络不稳定,请稍后重试");
|
|
550
|
-
}
|
|
551
|
-
if (error?.message?.includes("fetch") && error?.message?.includes(GPTGOD_DEFAULT_API_URL)) {
|
|
552
|
-
throw new Error("图像处理失败:无法连接 GPTGod API 服务器,请检查网络连接或稍后重试");
|
|
553
|
-
}
|
|
554
|
-
if (error?.response?.status === 413) {
|
|
555
|
-
throw new Error("图像处理失败:请求体过大,请尝试使用较小的图片");
|
|
537
|
+
if (error?.cause?.code === "UND_ERR_SOCKET" || error?.message?.includes("other side closed")) {
|
|
538
|
+
throw new Error("图像处理失败:服务器连接中断,可能是服务器负载过高或网络不稳定,请稍后重试");
|
|
539
|
+
}
|
|
540
|
+
if (error?.message?.includes("fetch") && error?.message?.includes(GPTGOD_DEFAULT_API_URL)) {
|
|
541
|
+
throw new Error("图像处理失败:无法连接 GPTGod API 服务器,请检查网络连接或稍后重试");
|
|
542
|
+
}
|
|
543
|
+
if (error?.response?.status === 413) {
|
|
544
|
+
throw new Error("图像处理失败:请求体过大,请尝试使用较小的图片");
|
|
545
|
+
}
|
|
546
|
+
if (error?.response?.status === 429) {
|
|
547
|
+
throw new Error("图像处理失败:请求过于频繁,请稍后重试");
|
|
548
|
+
}
|
|
549
|
+
throw new Error("图像处理API调用失败");
|
|
556
550
|
}
|
|
557
|
-
|
|
558
|
-
|
|
551
|
+
}
|
|
552
|
+
if (!success) {
|
|
553
|
+
if (allImages.length > 0) {
|
|
554
|
+
logger.warn("部分图片生成失败,返回已生成的图片", { generated: allImages.length, requested: numImages });
|
|
555
|
+
break;
|
|
559
556
|
}
|
|
560
|
-
throw
|
|
557
|
+
throw lastError;
|
|
561
558
|
}
|
|
562
559
|
}
|
|
563
|
-
|
|
560
|
+
return allImages;
|
|
564
561
|
}
|
|
565
562
|
};
|
|
566
563
|
|