koishi-plugin-aka-ai-generator 0.6.8 → 0.6.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.
package/lib/index.js CHANGED
@@ -295,7 +295,7 @@ var GptGodProvider = class {
295
295
  constructor(config) {
296
296
  this.config = config;
297
297
  }
298
- async generateImages(prompt, imageUrls, numImages) {
298
+ async generateImages(prompt, imageUrls, numImages, onImageGenerated) {
299
299
  const urls = Array.isArray(imageUrls) ? imageUrls : [imageUrls];
300
300
  const logger = this.config.logger;
301
301
  const ctx = this.config.ctx;
@@ -410,7 +410,18 @@ var GptGodProvider = class {
410
410
  throw new Error(`生成失败:${shortError}`);
411
411
  }
412
412
  }
413
- allImages.push(...images);
413
+ for (let imgIdx = 0; imgIdx < images.length; imgIdx++) {
414
+ const imageUrl = images[imgIdx];
415
+ const currentIndex = allImages.length;
416
+ allImages.push(imageUrl);
417
+ if (onImageGenerated) {
418
+ try {
419
+ await onImageGenerated(imageUrl, currentIndex, numImages);
420
+ } catch (callbackError) {
421
+ logger.warn("图片生成回调函数执行失败", { error: sanitizeError(callbackError) });
422
+ }
423
+ }
424
+ }
414
425
  logger.success("GPTGod 图像编辑 API 调用成功", { current: i + 1, total: numImages });
415
426
  success = true;
416
427
  break;
@@ -609,7 +620,7 @@ var GeminiProvider = class {
609
620
  constructor(config) {
610
621
  this.config = config;
611
622
  }
612
- async generateImages(prompt, imageUrls, numImages) {
623
+ async generateImages(prompt, imageUrls, numImages, onImageGenerated) {
613
624
  let urls = [];
614
625
  if (Array.isArray(imageUrls)) {
615
626
  urls = imageUrls.filter((url) => url && typeof url === "string" && url.trim());
@@ -682,8 +693,19 @@ var GeminiProvider = class {
682
693
  });
683
694
  } else {
684
695
  logger.success("Gemini API 调用成功", { current: i + 1, total: numImages, imagesCount: images.length });
696
+ for (let imgIdx = 0; imgIdx < images.length; imgIdx++) {
697
+ const imageUrl = images[imgIdx];
698
+ const currentIndex = allImages.length;
699
+ allImages.push(imageUrl);
700
+ if (onImageGenerated) {
701
+ try {
702
+ await onImageGenerated(imageUrl, currentIndex, numImages);
703
+ } catch (callbackError) {
704
+ logger.warn("图片生成回调函数执行失败", { error: sanitizeError(callbackError) });
705
+ }
706
+ }
707
+ }
685
708
  }
686
- allImages.push(...images);
687
709
  } catch (error) {
688
710
  const sanitizedError = sanitizeError(error);
689
711
  const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
@@ -983,6 +1005,36 @@ var UserManager = class {
983
1005
  }
984
1006
  return { allowed: true, isAdmin: false };
985
1007
  }
1008
+ // 原子性地检查并预留额度(防止并发绕过)
1009
+ async checkAndReserveQuota(userId, userName, numImages, config) {
1010
+ if (this.isAdmin(userId, config)) {
1011
+ return { allowed: true, reservationId: "admin" };
1012
+ }
1013
+ const rateLimitCheck = this.checkRateLimit(userId, config);
1014
+ if (!rateLimitCheck.allowed) {
1015
+ return { ...rateLimitCheck };
1016
+ }
1017
+ this.updateRateLimit(userId);
1018
+ return await this.dataLock.acquire(async () => {
1019
+ const userData = await this.getUserData(userId, userName);
1020
+ const today = (/* @__PURE__ */ new Date()).toDateString();
1021
+ const lastReset = new Date(userData.lastDailyReset || userData.createdAt).toDateString();
1022
+ let dailyCount = userData.dailyUsageCount;
1023
+ if (today !== lastReset) {
1024
+ dailyCount = 0;
1025
+ }
1026
+ const remainingToday = Math.max(0, config.dailyFreeLimit - dailyCount);
1027
+ const totalAvailable = remainingToday + userData.remainingPurchasedCount;
1028
+ if (totalAvailable < numImages) {
1029
+ return {
1030
+ allowed: false,
1031
+ message: `生成 ${numImages} 张图片需要 ${numImages} 次可用次数,但您的可用次数不足(今日免费剩余:${remainingToday}次,充值剩余:${userData.remainingPurchasedCount}次,共${totalAvailable}次)`
1032
+ };
1033
+ }
1034
+ const reservationId = `${userId}_${Date.now()}_${Math.random()}`;
1035
+ return { allowed: true, reservationId };
1036
+ });
1037
+ }
986
1038
  // 扣减额度并记录使用
987
1039
  async consumeQuota(userId, userName, commandName, numImages, config) {
988
1040
  return await this.dataLock.acquire(async () => {
@@ -1059,7 +1111,7 @@ var UserManager = class {
1059
1111
  if (blockCount >= config.securityBlockWarningThreshold && !hasWarning) {
1060
1112
  this.securityWarningMap.set(userId, true);
1061
1113
  shouldWarn = true;
1062
- } else if (hasWarning) {
1114
+ } else if (blockCount > config.securityBlockWarningThreshold) {
1063
1115
  shouldDeduct = true;
1064
1116
  }
1065
1117
  return { shouldWarn, shouldDeduct, blockCount };
@@ -1318,34 +1370,36 @@ function apply(ctx, config) {
1318
1370
  return input || null;
1319
1371
  }
1320
1372
  __name(getPromptInput, "getPromptInput");
1321
- async function recordUserUsage(session, commandName, numImages = 1) {
1322
- const userId = session.userId;
1323
- const userName = session.username || session.userId || "未知用户";
1324
- if (!userId) return;
1325
- const { userData, consumptionType, freeUsed, purchasedUsed } = await userManager.consumeQuota(userId, userName, commandName, numImages, config);
1326
- if (userManager.isAdmin(userId, config)) {
1327
- await session.send(`📊 使用统计 [管理员]
1373
+ function buildStatsMessage(userData, numImages, consumptionType, freeUsed, purchasedUsed, config2) {
1374
+ if (userManager.isAdmin(userData.userId, config2)) {
1375
+ return `📊 使用统计 [管理员]
1328
1376
  用户:${userData.userName}
1329
1377
  总调用次数:${userData.totalUsageCount}次
1330
- 状态:无限制使用`);
1378
+ 状态:无限制使用`;
1379
+ }
1380
+ const remainingToday = Math.max(0, config2.dailyFreeLimit - userData.dailyUsageCount);
1381
+ let consumptionText = "";
1382
+ if (consumptionType === "mixed") {
1383
+ consumptionText = `每日免费次数 -${freeUsed},充值次数 -${purchasedUsed}`;
1384
+ } else if (consumptionType === "free") {
1385
+ consumptionText = `每日免费次数 -${freeUsed}`;
1331
1386
  } else {
1332
- const remainingToday = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
1333
- let consumptionText = "";
1334
- if (consumptionType === "mixed") {
1335
- consumptionText = `每日免费次数 -${freeUsed},充值次数 -${purchasedUsed}`;
1336
- } else if (consumptionType === "free") {
1337
- consumptionText = `每日免费次数 -${freeUsed}`;
1338
- } else {
1339
- consumptionText = `充值次数 -${purchasedUsed}`;
1340
- }
1341
- await session.send(`📊 使用统计
1387
+ consumptionText = `充值次数 -${purchasedUsed}`;
1388
+ }
1389
+ return `📊 使用统计
1342
1390
  用户:${userData.userName}
1343
1391
  本次生成:${numImages}张图片
1344
1392
  本次消费:${consumptionText}
1345
1393
  总调用次数:${userData.totalUsageCount}次
1346
1394
  今日剩余免费:${remainingToday}次
1347
- 充值剩余:${userData.remainingPurchasedCount}次`);
1348
- }
1395
+ 充值剩余:${userData.remainingPurchasedCount}次`;
1396
+ }
1397
+ __name(buildStatsMessage, "buildStatsMessage");
1398
+ async function recordUserUsage(session, commandName, numImages = 1) {
1399
+ const userId = session.userId;
1400
+ const userName = session.username || session.userId || "未知用户";
1401
+ if (!userId) return;
1402
+ const { userData, consumptionType, freeUsed, purchasedUsed } = await userManager.consumeQuota(userId, userName, commandName, numImages, config);
1349
1403
  logger.info("用户调用记录", {
1350
1404
  userId,
1351
1405
  userName: userData.userName,
@@ -1359,6 +1413,12 @@ function apply(ctx, config) {
1359
1413
  remainingPurchasedCount: userData.remainingPurchasedCount,
1360
1414
  isAdmin: userManager.isAdmin(userId, config)
1361
1415
  });
1416
+ try {
1417
+ const statsMessage = buildStatsMessage(userData, numImages, consumptionType, freeUsed, purchasedUsed, config);
1418
+ await session.send(statsMessage);
1419
+ } catch (error) {
1420
+ logger.warn("发送统计信息失败", { userId, error: sanitizeError(error) });
1421
+ }
1362
1422
  }
1363
1423
  __name(recordUserUsage, "recordUserUsage");
1364
1424
  async function recordSecurityBlock(session, numImages = 1) {
@@ -1467,7 +1527,7 @@ function apply(ctx, config) {
1467
1527
  return { images: collectedImages, text: collectedText };
1468
1528
  }
1469
1529
  __name(getInputData, "getInputData");
1470
- async function requestProviderImages(prompt, imageUrls, numImages, requestContext) {
1530
+ async function requestProviderImages(prompt, imageUrls, numImages, requestContext, onImageGenerated) {
1471
1531
  const providerType = requestContext?.provider || config.provider;
1472
1532
  const targetModelId = requestContext?.modelId;
1473
1533
  const providerInstance = getProviderInstance(providerType, targetModelId);
@@ -1478,7 +1538,7 @@ function apply(ctx, config) {
1478
1538
  numImages
1479
1539
  });
1480
1540
  }
1481
- return await providerInstance.generateImages(prompt, imageUrls, numImages);
1541
+ return await providerInstance.generateImages(prompt, imageUrls, numImages, onImageGenerated);
1482
1542
  }
1483
1543
  __name(requestProviderImages, "requestProviderImages");
1484
1544
  async function processImageWithTimeout(session, img, prompt, styleName, requestContext, displayInfo, mode = "single") {
@@ -1493,7 +1553,6 @@ function apply(ctx, config) {
1493
1553
  }, config.commandTimeout * 1e3)
1494
1554
  )
1495
1555
  ]).catch(async (error) => {
1496
- if (userId) userManager.endTask(userId);
1497
1556
  const sanitizedError = sanitizeError(error);
1498
1557
  logger.error("图像处理超时或失败", { userId, error: sanitizedError });
1499
1558
  if (error?.message !== "命令执行超时") {
@@ -1575,20 +1634,38 @@ ${infoParts.join("\n")}`;
1575
1634
  }
1576
1635
  statusMessage += "...";
1577
1636
  await session.send(statusMessage);
1578
- const images = await requestProviderImages(finalPrompt, imageUrls, imageCount, requestContext);
1637
+ const generatedImages = [];
1638
+ let creditDeducted = false;
1639
+ const onImageGenerated = /* @__PURE__ */ __name(async (imageUrl, index, total) => {
1640
+ if (checkTimeout && checkTimeout()) {
1641
+ throw new Error("命令执行超时");
1642
+ }
1643
+ generatedImages.push(imageUrl);
1644
+ if (!creditDeducted && generatedImages.length > 0) {
1645
+ creditDeducted = true;
1646
+ await recordUserUsage(session, styleName, total);
1647
+ logger.info("流式处理:第一张图片生成,积分已扣除", {
1648
+ userId,
1649
+ totalImages: total,
1650
+ currentIndex: index
1651
+ });
1652
+ }
1653
+ await session.send(import_koishi2.h.image(imageUrl));
1654
+ logger.debug("流式处理:图片已发送", { index: index + 1, total });
1655
+ if (total > 1 && index < total - 1) {
1656
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1657
+ }
1658
+ }, "onImageGenerated");
1659
+ const images = await requestProviderImages(finalPrompt, imageUrls, imageCount, requestContext, onImageGenerated);
1579
1660
  if (checkTimeout && checkTimeout()) throw new Error("命令执行超时");
1580
1661
  if (images.length === 0) {
1581
1662
  return "图像处理失败:未能生成图片";
1582
1663
  }
1583
- await session.send("图像处理完成!");
1584
- for (let i = 0; i < images.length; i++) {
1585
- if (checkTimeout && checkTimeout()) break;
1586
- await session.send(import_koishi2.h.image(images[i]));
1587
- if (images.length > 1 && i < images.length - 1) {
1588
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1589
- }
1664
+ if (!creditDeducted) {
1665
+ await recordUserUsage(session, styleName, images.length);
1666
+ logger.warn("流式处理:积分在最后扣除(异常情况)", { userId, imagesCount: images.length });
1590
1667
  }
1591
- await recordUserUsage(session, styleName, images.length);
1668
+ await session.send("图像处理完成!");
1592
1669
  } finally {
1593
1670
  userManager.endTask(userId);
1594
1671
  }
@@ -1610,7 +1687,8 @@ ${infoParts.join("\n")}`;
1610
1687
  }
1611
1688
  const userPromptText = userPromptParts.join(" - ");
1612
1689
  const numImages = options?.num || config.defaultNumImages;
1613
- const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
1690
+ const userName = session.username || session.userId || "未知用户";
1691
+ const limitCheck = await userManager.checkAndReserveQuota(session.userId, userName, numImages, config);
1614
1692
  if (!limitCheck.allowed) {
1615
1693
  return limitCheck.message;
1616
1694
  }
@@ -1646,7 +1724,8 @@ ${infoParts.join("\n")}`;
1646
1724
  ctx.command(`${COMMANDS.TXT_TO_IMG} [prompt:text]`, "根据文字描述生成图像").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async ({ session, options }, prompt) => {
1647
1725
  if (!session?.userId) return "会话无效";
1648
1726
  const numImages = options?.num || config.defaultNumImages;
1649
- const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
1727
+ const userName = session.username || session.userId || "未知用户";
1728
+ const limitCheck = await userManager.checkAndReserveQuota(session.userId, userName, numImages, config);
1650
1729
  if (!limitCheck.allowed) {
1651
1730
  return limitCheck.message;
1652
1731
  }
@@ -1659,7 +1738,8 @@ ${infoParts.join("\n")}`;
1659
1738
  if (!session?.userId) return "会话无效";
1660
1739
  const numImages = options?.num || config.defaultNumImages;
1661
1740
  const mode = options?.multiple ? "multiple" : "single";
1662
- const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
1741
+ const userName = session.username || session.userId || "未知用户";
1742
+ const limitCheck = await userManager.checkAndReserveQuota(session.userId, userName, numImages, config);
1663
1743
  if (!limitCheck.allowed) {
1664
1744
  return limitCheck.message;
1665
1745
  }
@@ -1674,11 +1754,9 @@ ${infoParts.join("\n")}`;
1674
1754
  if (!userManager.startTask(userId)) {
1675
1755
  return "您有一个图像处理任务正在进行中,请等待完成";
1676
1756
  }
1677
- userManager.endTask(userId);
1678
1757
  let isTimeout = false;
1679
1758
  return Promise.race([
1680
1759
  (async () => {
1681
- if (!userManager.startTask(userId)) return "您有一个图像处理任务正在进行中";
1682
1760
  try {
1683
1761
  await session.send("多张图片+描述");
1684
1762
  const collectedImages = [];
@@ -1723,7 +1801,8 @@ ${infoParts.join("\n")}`;
1723
1801
  if (imageCount < 1 || imageCount > 4) {
1724
1802
  return "生成数量必须在 1-4 之间";
1725
1803
  }
1726
- const limitCheck = await userManager.checkDailyLimit(userId, config, imageCount);
1804
+ const userName = session.username || userId || "未知用户";
1805
+ const limitCheck = await userManager.checkAndReserveQuota(userId, userName, imageCount, config);
1727
1806
  if (!limitCheck.allowed) {
1728
1807
  return limitCheck.message;
1729
1808
  }
@@ -1737,20 +1816,38 @@ ${infoParts.join("\n")}`;
1737
1816
  });
1738
1817
  await session.send(`开始合成图(${collectedImages.length}张)...
1739
1818
  Prompt: ${prompt}`);
1740
- const resultImages = await requestProviderImages(prompt, collectedImages, imageCount);
1819
+ const generatedImages = [];
1820
+ let creditDeducted = false;
1821
+ const onImageGenerated = /* @__PURE__ */ __name(async (imageUrl, index, total) => {
1822
+ if (isTimeout) {
1823
+ throw new Error("命令执行超时");
1824
+ }
1825
+ generatedImages.push(imageUrl);
1826
+ if (!creditDeducted && generatedImages.length > 0) {
1827
+ creditDeducted = true;
1828
+ await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, total);
1829
+ logger.info("流式处理:第一张图片生成,积分已扣除", {
1830
+ userId,
1831
+ totalImages: total,
1832
+ currentIndex: index
1833
+ });
1834
+ }
1835
+ await session.send(import_koishi2.h.image(imageUrl));
1836
+ logger.debug("流式处理:图片已发送", { index: index + 1, total });
1837
+ if (total > 1 && index < total - 1) {
1838
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1839
+ }
1840
+ }, "onImageGenerated");
1841
+ const resultImages = await requestProviderImages(prompt, collectedImages, imageCount, void 0, onImageGenerated);
1741
1842
  if (isTimeout) throw new Error("命令执行超时");
1742
1843
  if (resultImages.length === 0) {
1743
1844
  return "图片合成失败:未能生成图片";
1744
1845
  }
1745
- await session.send("图片合成完成!");
1746
- for (let i = 0; i < resultImages.length; i++) {
1747
- if (isTimeout) break;
1748
- await session.send(import_koishi2.h.image(resultImages[i]));
1749
- if (resultImages.length > 1 && i < resultImages.length - 1) {
1750
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1751
- }
1846
+ if (!creditDeducted) {
1847
+ await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, resultImages.length);
1848
+ logger.warn("流式处理:积分在最后扣除(异常情况)", { userId, imagesCount: resultImages.length });
1752
1849
  }
1753
- await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, resultImages.length);
1850
+ await session.send("图片合成完成!");
1754
1851
  } finally {
1755
1852
  userManager.endTask(userId);
1756
1853
  }
@@ -1762,7 +1859,6 @@ Prompt: ${prompt}`);
1762
1859
  }, config.commandTimeout * 1e3)
1763
1860
  )
1764
1861
  ]).catch(async (error) => {
1765
- if (userId) userManager.endTask(userId);
1766
1862
  const sanitizedError = sanitizeError(error);
1767
1863
  logger.error("图片合成超时或失败", { userId, error: sanitizedError });
1768
1864
  if (error?.message !== "命令执行超时") {
@@ -7,5 +7,5 @@ export interface GeminiConfig extends ProviderConfig {
7
7
  export declare class GeminiProvider implements ImageProvider {
8
8
  private config;
9
9
  constructor(config: GeminiConfig);
10
- generateImages(prompt: string, imageUrls: string | string[], numImages: number): Promise<string[]>;
10
+ generateImages(prompt: string, imageUrls: string | string[], numImages: number, onImageGenerated?: (imageUrl: string, index: number, total: number) => void | Promise<void>): Promise<string[]>;
11
11
  }
@@ -6,5 +6,5 @@ export interface GptGodConfig extends ProviderConfig {
6
6
  export declare class GptGodProvider implements ImageProvider {
7
7
  private config;
8
8
  constructor(config: GptGodConfig);
9
- generateImages(prompt: string, imageUrls: string | string[], numImages: number): Promise<string[]>;
9
+ generateImages(prompt: string, imageUrls: string | string[], numImages: number, onImageGenerated?: (imageUrl: string, index: number, total: number) => void | Promise<void>): Promise<string[]>;
10
10
  }
@@ -4,9 +4,10 @@ export interface ImageProvider {
4
4
  * @param prompt 提示词
5
5
  * @param imageUrls 输入图片 URL 数组
6
6
  * @param numImages 需要生成的图片数量
7
+ * @param onImageGenerated 可选的回调函数,每生成一张图片时调用(用于流式处理)
7
8
  * @returns 生成的图片 URL 数组(data: URL 或 http URL)
8
9
  */
9
- generateImages(prompt: string, imageUrls: string | string[], numImages: number): Promise<string[]>;
10
+ generateImages(prompt: string, imageUrls: string | string[], numImages: number, onImageGenerated?: (imageUrl: string, index: number, total: number) => void | Promise<void>): Promise<string[]>;
10
11
  }
11
12
  export interface ProviderConfig {
12
13
  apiTimeout: number;
@@ -74,6 +74,11 @@ export declare class UserManager {
74
74
  message?: string;
75
75
  isAdmin?: boolean;
76
76
  }>;
77
+ checkAndReserveQuota(userId: string, userName: string, numImages: number, config: Config): Promise<{
78
+ allowed: boolean;
79
+ message?: string;
80
+ reservationId?: string;
81
+ }>;
77
82
  consumeQuota(userId: string, userName: string, commandName: string, numImages: number, config: Config): Promise<{
78
83
  userData: UserData;
79
84
  consumptionType: 'free' | 'purchased' | 'mixed';
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.6.8",
4
+ "version": "0.6.10",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [