koishi-plugin-aka-ai-generator 0.2.10 → 0.2.13

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 +231 -200
  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
 
@@ -886,7 +883,7 @@ function apply(ctx, config) {
886
883
  rateLimitMap.set(userId, userTimestamps);
887
884
  }
888
885
  __name(updateRateLimit, "updateRateLimit");
889
- async function checkDailyLimit(userId) {
886
+ async function checkDailyLimit(userId, numImages = 1) {
890
887
  if (isAdmin(userId)) {
891
888
  return { allowed: true, isAdmin: true };
892
889
  }
@@ -897,25 +894,33 @@ function apply(ctx, config) {
897
894
  const usersData = await loadUsersData();
898
895
  const userData = usersData[userId];
899
896
  if (!userData) {
897
+ if (numImages > config.dailyFreeLimit) {
898
+ return {
899
+ allowed: false,
900
+ message: `生成 ${numImages} 张图片需要 ${numImages} 次可用次数,但您的可用次数不足(今日免费:${config.dailyFreeLimit}次,充值:0次)`,
901
+ isAdmin: false
902
+ };
903
+ }
900
904
  return { allowed: true, isAdmin: false };
901
905
  }
902
906
  const today = (/* @__PURE__ */ new Date()).toDateString();
903
907
  const lastReset = new Date(userData.lastDailyReset || userData.createdAt).toDateString();
908
+ let dailyCount = userData.dailyUsageCount;
904
909
  if (today !== lastReset) {
910
+ dailyCount = 0;
905
911
  userData.dailyUsageCount = 0;
906
912
  userData.lastDailyReset = (/* @__PURE__ */ new Date()).toISOString();
907
913
  }
908
- if (userData.dailyUsageCount < config.dailyFreeLimit) {
909
- return { allowed: true, isAdmin: false };
910
- }
911
- if (userData.remainingPurchasedCount > 0) {
912
- return { allowed: true, isAdmin: false };
914
+ const remainingToday = Math.max(0, config.dailyFreeLimit - dailyCount);
915
+ const totalAvailable = remainingToday + userData.remainingPurchasedCount;
916
+ if (totalAvailable < numImages) {
917
+ return {
918
+ allowed: false,
919
+ message: `生成 ${numImages} 张图片需要 ${numImages} 次可用次数,但您的可用次数不足(今日免费剩余:${remainingToday}次,充值剩余:${userData.remainingPurchasedCount}次,共${totalAvailable}次)`,
920
+ isAdmin: false
921
+ };
913
922
  }
914
- return {
915
- allowed: false,
916
- message: `今日免费次数已用完(${config.dailyFreeLimit}次),充值次数也已用完。请联系管理员充值或明天再试`,
917
- isAdmin: false
918
- };
923
+ return { allowed: true, isAdmin: false };
919
924
  }
920
925
  __name(checkDailyLimit, "checkDailyLimit");
921
926
  async function getPromptInput(session, message) {
@@ -1005,7 +1010,7 @@ function apply(ctx, config) {
1005
1010
  return usersData[userId];
1006
1011
  }
1007
1012
  __name(getUserData, "getUserData");
1008
- async function updateUserData(userId, userName, commandName) {
1013
+ async function updateUserData(userId, userName, commandName, numImages = 1) {
1009
1014
  const usersData = await loadUsersData();
1010
1015
  const now = (/* @__PURE__ */ new Date()).toISOString();
1011
1016
  const today = (/* @__PURE__ */ new Date()).toDateString();
@@ -1013,8 +1018,8 @@ function apply(ctx, config) {
1013
1018
  usersData[userId] = {
1014
1019
  userId,
1015
1020
  userName: userId,
1016
- totalUsageCount: 1,
1017
- dailyUsageCount: 1,
1021
+ totalUsageCount: numImages,
1022
+ dailyUsageCount: numImages,
1018
1023
  lastDailyReset: now,
1019
1024
  purchasedCount: 0,
1020
1025
  remainingPurchasedCount: 0,
@@ -1024,35 +1029,49 @@ function apply(ctx, config) {
1024
1029
  createdAt: now
1025
1030
  };
1026
1031
  await saveUsersData(usersData);
1027
- return { userData: usersData[userId], consumptionType: "free" };
1032
+ return { userData: usersData[userId], consumptionType: "free", freeUsed: numImages, purchasedUsed: 0 };
1028
1033
  }
1029
- usersData[userId].totalUsageCount += 1;
1034
+ usersData[userId].totalUsageCount += numImages;
1030
1035
  usersData[userId].lastUsed = now;
1031
1036
  const lastReset = new Date(usersData[userId].lastDailyReset || usersData[userId].createdAt).toDateString();
1032
1037
  if (today !== lastReset) {
1033
1038
  usersData[userId].dailyUsageCount = 0;
1034
1039
  usersData[userId].lastDailyReset = now;
1035
1040
  }
1036
- if (usersData[userId].dailyUsageCount < config.dailyFreeLimit) {
1037
- usersData[userId].dailyUsageCount += 1;
1038
- await saveUsersData(usersData);
1039
- return { userData: usersData[userId], consumptionType: "free" };
1040
- }
1041
- if (usersData[userId].remainingPurchasedCount > 0) {
1042
- usersData[userId].remainingPurchasedCount -= 1;
1043
- await saveUsersData(usersData);
1044
- return { userData: usersData[userId], consumptionType: "purchased" };
1041
+ let remainingToConsume = numImages;
1042
+ let freeUsed = 0;
1043
+ let purchasedUsed = 0;
1044
+ const availableFree = Math.max(0, config.dailyFreeLimit - usersData[userId].dailyUsageCount);
1045
+ if (availableFree > 0) {
1046
+ const freeToUse = Math.min(availableFree, remainingToConsume);
1047
+ usersData[userId].dailyUsageCount += freeToUse;
1048
+ freeUsed = freeToUse;
1049
+ remainingToConsume -= freeToUse;
1050
+ }
1051
+ if (remainingToConsume > 0) {
1052
+ const purchasedToUse = Math.min(usersData[userId].remainingPurchasedCount, remainingToConsume);
1053
+ usersData[userId].remainingPurchasedCount -= purchasedToUse;
1054
+ purchasedUsed = purchasedToUse;
1055
+ remainingToConsume -= purchasedToUse;
1045
1056
  }
1046
1057
  await saveUsersData(usersData);
1047
- return { userData: usersData[userId], consumptionType: "free" };
1058
+ let consumptionType;
1059
+ if (freeUsed > 0 && purchasedUsed > 0) {
1060
+ consumptionType = "mixed";
1061
+ } else if (freeUsed > 0) {
1062
+ consumptionType = "free";
1063
+ } else {
1064
+ consumptionType = "purchased";
1065
+ }
1066
+ return { userData: usersData[userId], consumptionType, freeUsed, purchasedUsed };
1048
1067
  }
1049
1068
  __name(updateUserData, "updateUserData");
1050
- async function recordUserUsage(session, commandName) {
1069
+ async function recordUserUsage(session, commandName, numImages = 1) {
1051
1070
  const userId = session.userId;
1052
1071
  const userName = session.username || session.userId || "未知用户";
1053
1072
  if (!userId) return;
1054
1073
  updateRateLimit(userId);
1055
- const { userData, consumptionType } = await updateUserData(userId, userName, commandName);
1074
+ const { userData, consumptionType, freeUsed, purchasedUsed } = await updateUserData(userId, userName, commandName, numImages);
1056
1075
  if (isAdmin(userId)) {
1057
1076
  await session.send(`📊 使用统计 [管理员]
1058
1077
  用户:${userData.userName}
@@ -1060,10 +1079,18 @@ function apply(ctx, config) {
1060
1079
  状态:无限制使用`);
1061
1080
  } else {
1062
1081
  const remainingToday = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
1063
- const consumptionText = consumptionType === "free" ? "每日免费次数" : "充值次数";
1082
+ let consumptionText = "";
1083
+ if (consumptionType === "mixed") {
1084
+ consumptionText = `每日免费次数 -${freeUsed},充值次数 -${purchasedUsed}`;
1085
+ } else if (consumptionType === "free") {
1086
+ consumptionText = `每日免费次数 -${freeUsed}`;
1087
+ } else {
1088
+ consumptionText = `充值次数 -${purchasedUsed}`;
1089
+ }
1064
1090
  await session.send(`📊 使用统计
1065
1091
  用户:${userData.userName}
1066
- 本次消费:${consumptionText} -1
1092
+ 本次生成:${numImages}张图片
1093
+ 本次消费:${consumptionText}
1067
1094
  总调用次数:${userData.totalUsageCount}次
1068
1095
  今日剩余免费:${remainingToday}次
1069
1096
  充值剩余:${userData.remainingPurchasedCount}次`);
@@ -1072,10 +1099,13 @@ function apply(ctx, config) {
1072
1099
  userId,
1073
1100
  userName: userData.userName,
1074
1101
  commandName,
1102
+ numImages,
1103
+ consumptionType,
1104
+ freeUsed,
1105
+ purchasedUsed,
1075
1106
  totalUsageCount: userData.totalUsageCount,
1076
1107
  dailyUsageCount: userData.dailyUsageCount,
1077
1108
  remainingPurchasedCount: userData.remainingPurchasedCount,
1078
- consumptionType,
1079
1109
  isAdmin: isAdmin(userId)
1080
1110
  });
1081
1111
  }
@@ -1210,7 +1240,7 @@ ${infoParts.join("\n")}`;
1210
1240
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1211
1241
  }
1212
1242
  }
1213
- await recordUserUsage(session, styleName);
1243
+ await recordUserUsage(session, styleName, images.length);
1214
1244
  activeTasks.delete(userId);
1215
1245
  } catch (error) {
1216
1246
  activeTasks.delete(userId);
@@ -1228,10 +1258,6 @@ ${infoParts.join("\n")}`;
1228
1258
  ctx.command(`${style.commandName} [img:text]`, "图像风格转换").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async (argv, img) => {
1229
1259
  const { session, options } = argv;
1230
1260
  if (!session?.userId) return "会话无效";
1231
- const limitCheck = await checkDailyLimit(session.userId);
1232
- if (!limitCheck.allowed) {
1233
- return limitCheck.message;
1234
- }
1235
1261
  const modifiers = parseStyleCommandModifiers(argv, img);
1236
1262
  let userPromptParts = [];
1237
1263
  if (modifiers.customAdditions?.length) {
@@ -1253,13 +1279,18 @@ ${infoParts.join("\n")}`;
1253
1279
  }
1254
1280
  }
1255
1281
  }
1282
+ const numImages = options?.num || promptNumImages || config.defaultNumImages;
1283
+ const limitCheck = await checkDailyLimit(session.userId, numImages);
1284
+ if (!limitCheck.allowed) {
1285
+ return limitCheck.message;
1286
+ }
1256
1287
  const promptSegments = [style.prompt];
1257
1288
  if (cleanedUserPrompt) {
1258
1289
  promptSegments.push(cleanedUserPrompt);
1259
1290
  }
1260
1291
  const mergedPrompt = promptSegments.filter(Boolean).join(" - ");
1261
1292
  const requestContext = {
1262
- numImages: options?.num || promptNumImages
1293
+ numImages
1263
1294
  };
1264
1295
  if (modifiers.modelMapping?.provider) {
1265
1296
  requestContext.provider = modifiers.modelMapping.provider;
@@ -1283,10 +1314,6 @@ ${infoParts.join("\n")}`;
1283
1314
  }
1284
1315
  ctx.command(COMMANDS.GENERATE_IMAGE, "使用自定义prompt进行图像处理").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async ({ session, options }) => {
1285
1316
  if (!session?.userId) return "会话无效";
1286
- const limitCheck = await checkDailyLimit(session.userId);
1287
- if (!limitCheck.allowed) {
1288
- return limitCheck.message;
1289
- }
1290
1317
  return Promise.race([
1291
1318
  (async () => {
1292
1319
  const userId = session.userId;
@@ -1353,6 +1380,10 @@ ${infoParts.join("\n")}`;
1353
1380
  if (imageCount < 1 || imageCount > 4) {
1354
1381
  return "生成数量必须在 1-4 之间";
1355
1382
  }
1383
+ const limitCheck = await checkDailyLimit(userId, imageCount);
1384
+ if (!limitCheck.allowed) {
1385
+ return limitCheck.message;
1386
+ }
1356
1387
  logger.info("开始自定义图像处理", {
1357
1388
  userId,
1358
1389
  imageUrl,
@@ -1375,7 +1406,7 @@ Prompt: ${prompt}`);
1375
1406
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1376
1407
  }
1377
1408
  }
1378
- await recordUserUsage(session, COMMANDS.GENERATE_IMAGE);
1409
+ await recordUserUsage(session, COMMANDS.GENERATE_IMAGE, resultImages.length);
1379
1410
  activeTasks.delete(userId);
1380
1411
  } catch (error) {
1381
1412
  activeTasks.delete(userId);
@@ -1398,10 +1429,6 @@ Prompt: ${prompt}`);
1398
1429
  });
1399
1430
  ctx.command(COMMANDS.COMPOSE_IMAGE, "合成多张图片,使用自定义prompt控制合成效果").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async ({ session, options }) => {
1400
1431
  if (!session?.userId) return "会话无效";
1401
- const limitCheck = await checkDailyLimit(session.userId);
1402
- if (!limitCheck.allowed) {
1403
- return limitCheck.message;
1404
- }
1405
1432
  return Promise.race([
1406
1433
  (async () => {
1407
1434
  const userId = session.userId;
@@ -1458,6 +1485,10 @@ Prompt: ${prompt}`);
1458
1485
  if (imageCount < 1 || imageCount > 4) {
1459
1486
  return "生成数量必须在 1-4 之间";
1460
1487
  }
1488
+ const limitCheck = await checkDailyLimit(userId, imageCount);
1489
+ if (!limitCheck.allowed) {
1490
+ return limitCheck.message;
1491
+ }
1461
1492
  logger.info("开始图片合成处理", {
1462
1493
  userId,
1463
1494
  imageUrls: collectedImages,
@@ -1481,7 +1512,7 @@ Prompt: ${prompt}`);
1481
1512
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1482
1513
  }
1483
1514
  }
1484
- await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE);
1515
+ await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, resultImages.length);
1485
1516
  activeTasks.delete(userId);
1486
1517
  } catch (error) {
1487
1518
  activeTasks.delete(userId);
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.13",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [