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.
Files changed (2) hide show
  1. package/lib/index.js +150 -153
  2. 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 contentType = response.headers?.["content-type"] || response.headers?.["Content-Type"];
43
- if (contentType && contentType.startsWith("image/")) {
44
- mimeType = contentType;
45
- } else {
46
- const urlLower = url.toLowerCase();
47
- if (urlLower.endsWith(".png")) {
48
- mimeType = "image/png";
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 contentType = response.headers?.["content-type"] || response.headers?.["Content-Type"];
188
- if (contentType && contentType.startsWith("image/")) {
189
- mimeType = contentType;
190
- } else {
191
- const urlLower = url.toLowerCase();
192
- if (urlLower.endsWith(".png")) {
193
- mimeType = "image/png";
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 contentParts = [
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
- contentParts.push(imagePart);
422
- }
423
- const requestData = {
424
- model: this.config.modelId,
425
- stream: false,
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
- role: "user",
431
- content: contentParts
432
- }
433
- ]
434
- };
435
- const maxRetries = 3;
436
- let lastError = null;
437
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
438
- try {
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
- headers: {
451
- "Content-Type": "application/json",
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
- 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"))) {
471
- logger.error("内容被 Google Gemini 政策拦截", {
472
- errorMessage: errorMessage.substring(0, 200),
473
- finishReason: firstChoice.finish_reason
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
- if (errorMessage && (errorMessage.toLowerCase().includes("error") || errorMessage.toLowerCase().includes("failed") || errorMessage.toLowerCase().includes("blocked")) && !errorMessage.match(/https?:\/\//)) {
478
- logger.error("API 返回错误消息", {
479
- errorMessage: errorMessage.substring(0, 200),
480
- finishReason: firstChoice.finish_reason
481
- });
482
- const shortError = errorMessage.length > 50 ? errorMessage.substring(0, 50) + "..." : errorMessage;
483
- throw new Error(`处理失败:${shortError}`);
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
- warnData.responsePreview = JSON.stringify(response).substring(0, 500);
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
- logger.warn("生成的图片数量不足", warnData);
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
- return images;
516
- } catch (error) {
517
- lastError = error;
518
- if (error?.message && (error.message.includes("内容被安全策略拦截") || error.message.includes("生成失败") || error.message.includes("处理失败"))) {
519
- throw error;
520
- }
521
- const isRetryableError = error?.cause?.code === "UND_ERR_SOCKET" || // Socket 错误
522
- 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;
523
- if (isRetryableError && attempt < maxRetries) {
524
- const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 5e3);
525
- logger.warn(`GPTGod API 调用失败,将在 ${delay}ms 后重试 (${attempt}/${maxRetries})`, {
526
- error: error?.message || error?.cause?.message || "连接错误",
527
- code: error?.code || error?.cause?.code
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
- await new Promise((resolve) => setTimeout(resolve, delay));
530
- continue;
531
- }
532
- logger.error("GPTGod 图像编辑 API 调用失败", {
533
- message: error?.message || "未知错误",
534
- name: error?.name,
535
- code: error?.code,
536
- status: error?.response?.status,
537
- statusText: error?.response?.statusText,
538
- data: error?.response?.data,
539
- stack: error?.stack,
540
- cause: error?.cause,
541
- attempt,
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
- if (error?.response?.status === 429) {
558
- throw new Error("图像处理失败:请求过于频繁,请稍后重试");
551
+ }
552
+ if (!success) {
553
+ if (allImages.length > 0) {
554
+ logger.warn("部分图片生成失败,返回已生成的图片", { generated: allImages.length, requested: numImages });
555
+ break;
559
556
  }
560
- throw new Error("图像处理API调用失败");
557
+ throw lastError;
561
558
  }
562
559
  }
563
- throw lastError;
560
+ return allImages;
564
561
  }
565
562
  };
566
563
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-aka-ai-generator",
3
3
  "description": "自用AI生成插件(GPTGod & Yunwu)",
4
- "version": "0.2.10",
4
+ "version": "0.2.12",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [