koishi-plugin-aka-ai-generator 0.6.7 → 0.6.9

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
@@ -983,6 +983,36 @@ var UserManager = class {
983
983
  }
984
984
  return { allowed: true, isAdmin: false };
985
985
  }
986
+ // 原子性地检查并预留额度(防止并发绕过)
987
+ async checkAndReserveQuota(userId, userName, numImages, config) {
988
+ if (this.isAdmin(userId, config)) {
989
+ return { allowed: true, reservationId: "admin" };
990
+ }
991
+ const rateLimitCheck = this.checkRateLimit(userId, config);
992
+ if (!rateLimitCheck.allowed) {
993
+ return { ...rateLimitCheck };
994
+ }
995
+ this.updateRateLimit(userId);
996
+ return await this.dataLock.acquire(async () => {
997
+ const userData = await this.getUserData(userId, userName);
998
+ const today = (/* @__PURE__ */ new Date()).toDateString();
999
+ const lastReset = new Date(userData.lastDailyReset || userData.createdAt).toDateString();
1000
+ let dailyCount = userData.dailyUsageCount;
1001
+ if (today !== lastReset) {
1002
+ dailyCount = 0;
1003
+ }
1004
+ const remainingToday = Math.max(0, config.dailyFreeLimit - dailyCount);
1005
+ const totalAvailable = remainingToday + userData.remainingPurchasedCount;
1006
+ if (totalAvailable < numImages) {
1007
+ return {
1008
+ allowed: false,
1009
+ message: `生成 ${numImages} 张图片需要 ${numImages} 次可用次数,但您的可用次数不足(今日免费剩余:${remainingToday}次,充值剩余:${userData.remainingPurchasedCount}次,共${totalAvailable}次)`
1010
+ };
1011
+ }
1012
+ const reservationId = `${userId}_${Date.now()}_${Math.random()}`;
1013
+ return { allowed: true, reservationId };
1014
+ });
1015
+ }
986
1016
  // 扣减额度并记录使用
987
1017
  async consumeQuota(userId, userName, commandName, numImages, config) {
988
1018
  return await this.dataLock.acquire(async () => {
@@ -1059,7 +1089,7 @@ var UserManager = class {
1059
1089
  if (blockCount >= config.securityBlockWarningThreshold && !hasWarning) {
1060
1090
  this.securityWarningMap.set(userId, true);
1061
1091
  shouldWarn = true;
1062
- } else if (hasWarning) {
1092
+ } else if (blockCount > config.securityBlockWarningThreshold) {
1063
1093
  shouldDeduct = true;
1064
1094
  }
1065
1095
  return { shouldWarn, shouldDeduct, blockCount };
@@ -1182,16 +1212,13 @@ var Config = import_koishi2.Schema.intersect([
1182
1212
  import_koishi2.Schema.const("gptgod").description("GPTGod 服务"),
1183
1213
  import_koishi2.Schema.const("gemini").description("Google Gemini 原生")
1184
1214
  ]).default("yunwu").description("图像生成供应商"),
1185
- // 所有字段都定义,但根据 provider 在运行时验证必填
1186
- yunwuApiKey: import_koishi2.Schema.string().description("云雾API密钥").role("secret").default(""),
1215
+ yunwuApiKey: import_koishi2.Schema.string().description("云雾API密钥").role("secret").required(),
1187
1216
  yunwuModelId: import_koishi2.Schema.string().default("gemini-2.5-flash-image").description("云雾图像生成模型ID"),
1188
1217
  gptgodApiKey: import_koishi2.Schema.string().description("GPTGod API 密钥").role("secret").default(""),
1189
1218
  gptgodModelId: import_koishi2.Schema.string().default("nano-banana").description("GPTGod 模型ID"),
1190
1219
  geminiApiKey: import_koishi2.Schema.string().description("Gemini API 密钥").role("secret").default(""),
1191
1220
  geminiModelId: import_koishi2.Schema.string().default("gemini-2.5-flash").description("Gemini 模型ID"),
1192
- geminiApiBase: import_koishi2.Schema.string().default("https://generativelanguage.googleapis.com").description("Gemini API 基础地址")
1193
- }),
1194
- import_koishi2.Schema.object({
1221
+ geminiApiBase: import_koishi2.Schema.string().default("https://generativelanguage.googleapis.com").description("Gemini API 基础地址"),
1195
1222
  modelMappings: import_koishi2.Schema.array(import_koishi2.Schema.object({
1196
1223
  suffix: import_koishi2.Schema.string().required().description("指令后缀(例如 4K,对应输入 -4K)"),
1197
1224
  provider: import_koishi2.Schema.union([
@@ -1242,15 +1269,6 @@ var Config = import_koishi2.Schema.intersect([
1242
1269
  ]);
1243
1270
  function apply(ctx, config) {
1244
1271
  const logger = ctx.logger("aka-ai-generator");
1245
- if (config.provider === "yunwu" && !config.yunwuApiKey) {
1246
- throw new Error('当选择"云雾 Gemini 服务"时,云雾API密钥为必填项');
1247
- }
1248
- if (config.provider === "gptgod" && !config.gptgodApiKey) {
1249
- throw new Error('当选择"GPTGod 服务"时,GPTGod API 密钥为必填项');
1250
- }
1251
- if (config.provider === "gemini" && !config.geminiApiKey) {
1252
- throw new Error('当选择"Google Gemini 原生"时,Gemini API 密钥为必填项');
1253
- }
1254
1272
  const userManager = new UserManager(ctx.baseDir, logger);
1255
1273
  function getProviderInstance(providerType, modelId) {
1256
1274
  return createImageProvider({
@@ -1330,34 +1348,36 @@ function apply(ctx, config) {
1330
1348
  return input || null;
1331
1349
  }
1332
1350
  __name(getPromptInput, "getPromptInput");
1333
- async function recordUserUsage(session, commandName, numImages = 1) {
1334
- const userId = session.userId;
1335
- const userName = session.username || session.userId || "未知用户";
1336
- if (!userId) return;
1337
- const { userData, consumptionType, freeUsed, purchasedUsed } = await userManager.consumeQuota(userId, userName, commandName, numImages, config);
1338
- if (userManager.isAdmin(userId, config)) {
1339
- await session.send(`📊 使用统计 [管理员]
1351
+ function buildStatsMessage(userData, numImages, consumptionType, freeUsed, purchasedUsed, config2) {
1352
+ if (userManager.isAdmin(userData.userId, config2)) {
1353
+ return `📊 使用统计 [管理员]
1340
1354
  用户:${userData.userName}
1341
1355
  总调用次数:${userData.totalUsageCount}次
1342
- 状态:无限制使用`);
1356
+ 状态:无限制使用`;
1357
+ }
1358
+ const remainingToday = Math.max(0, config2.dailyFreeLimit - userData.dailyUsageCount);
1359
+ let consumptionText = "";
1360
+ if (consumptionType === "mixed") {
1361
+ consumptionText = `每日免费次数 -${freeUsed},充值次数 -${purchasedUsed}`;
1362
+ } else if (consumptionType === "free") {
1363
+ consumptionText = `每日免费次数 -${freeUsed}`;
1343
1364
  } else {
1344
- const remainingToday = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
1345
- let consumptionText = "";
1346
- if (consumptionType === "mixed") {
1347
- consumptionText = `每日免费次数 -${freeUsed},充值次数 -${purchasedUsed}`;
1348
- } else if (consumptionType === "free") {
1349
- consumptionText = `每日免费次数 -${freeUsed}`;
1350
- } else {
1351
- consumptionText = `充值次数 -${purchasedUsed}`;
1352
- }
1353
- await session.send(`📊 使用统计
1365
+ consumptionText = `充值次数 -${purchasedUsed}`;
1366
+ }
1367
+ return `📊 使用统计
1354
1368
  用户:${userData.userName}
1355
1369
  本次生成:${numImages}张图片
1356
1370
  本次消费:${consumptionText}
1357
1371
  总调用次数:${userData.totalUsageCount}次
1358
1372
  今日剩余免费:${remainingToday}次
1359
- 充值剩余:${userData.remainingPurchasedCount}次`);
1360
- }
1373
+ 充值剩余:${userData.remainingPurchasedCount}次`;
1374
+ }
1375
+ __name(buildStatsMessage, "buildStatsMessage");
1376
+ async function recordUserUsage(session, commandName, numImages = 1) {
1377
+ const userId = session.userId;
1378
+ const userName = session.username || session.userId || "未知用户";
1379
+ if (!userId) return;
1380
+ const { userData, consumptionType, freeUsed, purchasedUsed } = await userManager.consumeQuota(userId, userName, commandName, numImages, config);
1361
1381
  logger.info("用户调用记录", {
1362
1382
  userId,
1363
1383
  userName: userData.userName,
@@ -1371,6 +1391,12 @@ function apply(ctx, config) {
1371
1391
  remainingPurchasedCount: userData.remainingPurchasedCount,
1372
1392
  isAdmin: userManager.isAdmin(userId, config)
1373
1393
  });
1394
+ try {
1395
+ const statsMessage = buildStatsMessage(userData, numImages, consumptionType, freeUsed, purchasedUsed, config);
1396
+ await session.send(statsMessage);
1397
+ } catch (error) {
1398
+ logger.warn("发送统计信息失败", { userId, error: sanitizeError(error) });
1399
+ }
1374
1400
  }
1375
1401
  __name(recordUserUsage, "recordUserUsage");
1376
1402
  async function recordSecurityBlock(session, numImages = 1) {
@@ -1505,7 +1531,6 @@ function apply(ctx, config) {
1505
1531
  }, config.commandTimeout * 1e3)
1506
1532
  )
1507
1533
  ]).catch(async (error) => {
1508
- if (userId) userManager.endTask(userId);
1509
1534
  const sanitizedError = sanitizeError(error);
1510
1535
  logger.error("图像处理超时或失败", { userId, error: sanitizedError });
1511
1536
  if (error?.message !== "命令执行超时") {
@@ -1526,18 +1551,13 @@ function apply(ctx, config) {
1526
1551
  if (!userManager.startTask(userId)) {
1527
1552
  return "您有一个图像处理任务正在进行中,请等待完成";
1528
1553
  }
1529
- let taskLockReleased = false;
1530
1554
  try {
1531
1555
  const imageCount = requestContext?.numImages || config.defaultNumImages;
1532
1556
  if (imageCount < 1 || imageCount > 4) {
1533
- userManager.endTask(userId);
1534
- taskLockReleased = true;
1535
1557
  return "生成数量必须在 1-4 之间";
1536
1558
  }
1537
1559
  const inputResult = await getInputData(session, img, mode);
1538
1560
  if ("error" in inputResult) {
1539
- userManager.endTask(userId);
1540
- taskLockReleased = true;
1541
1561
  return inputResult.error;
1542
1562
  }
1543
1563
  if (checkTimeout && checkTimeout()) throw new Error("命令执行超时");
@@ -1551,23 +1571,17 @@ function apply(ctx, config) {
1551
1571
  await session.send("请发送画面描述");
1552
1572
  const promptMsg = await session.prompt(3e4);
1553
1573
  if (!promptMsg) {
1554
- userManager.endTask(userId);
1555
- taskLockReleased = true;
1556
1574
  return "未检测到描述,操作已取消";
1557
1575
  }
1558
1576
  const elements = import_koishi2.h.parse(promptMsg);
1559
1577
  const images2 = import_koishi2.h.select(elements, "img");
1560
1578
  if (images2.length > 0) {
1561
- userManager.endTask(userId);
1562
- taskLockReleased = true;
1563
1579
  return "检测到图片,本功能仅支持文字输入";
1564
1580
  }
1565
1581
  const text = import_koishi2.h.select(elements, "text").map((e) => e.attrs.content).join(" ").trim();
1566
1582
  if (text) {
1567
1583
  finalPrompt = text;
1568
1584
  } else {
1569
- userManager.endTask(userId);
1570
- taskLockReleased = true;
1571
1585
  return "未检测到有效文字描述,操作已取消";
1572
1586
  }
1573
1587
  }
@@ -1601,55 +1615,20 @@ ${infoParts.join("\n")}`;
1601
1615
  const images = await requestProviderImages(finalPrompt, imageUrls, imageCount, requestContext);
1602
1616
  if (checkTimeout && checkTimeout()) throw new Error("命令执行超时");
1603
1617
  if (images.length === 0) {
1604
- userManager.endTask(userId);
1605
- taskLockReleased = true;
1606
1618
  return "图像处理失败:未能生成图片";
1607
1619
  }
1608
- try {
1609
- await recordUserUsage(session, styleName, images.length);
1610
- } catch (err) {
1611
- logger.error(`记录用户使用统计失败 (用户: ${userId}):`, err);
1612
- }
1613
- const resultMessage = "图像处理完成!";
1614
- taskLockReleased = true;
1615
- setImmediate(async () => {
1616
- try {
1617
- await session.send(resultMessage);
1618
- for (let i = 0; i < images.length; i++) {
1619
- try {
1620
- await Promise.race([
1621
- session.send(import_koishi2.h.image(images[i])),
1622
- new Promise((_, reject) => setTimeout(() => reject(new Error("SendTimeout")), 2e4))
1623
- ]);
1624
- } catch (err) {
1625
- logger.warn(`图片发送可能超时 (用户: ${userId}, 图片 ${i + 1}/${images.length}): ${err instanceof Error ? err.message : String(err)}`);
1626
- try {
1627
- await session.send(`⚠️ 图片 ${i + 1} 发送失败,请重试`);
1628
- } catch (sendErr) {
1629
- logger.error(`发送错误通知失败 (用户: ${userId}):`, sendErr);
1630
- }
1631
- }
1632
- if (images.length > 1 && i < images.length - 1) {
1633
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1634
- }
1635
- }
1636
- } catch (err) {
1637
- logger.error(`异步发送图片时出错 (用户: ${userId}):`, err);
1638
- try {
1639
- await session.send("⚠️ 图片发送过程中出现错误,但积分已扣除。如未收到图片,请联系管理员。");
1640
- } catch (sendErr) {
1641
- logger.error(`发送错误通知失败 (用户: ${userId}):`, sendErr);
1642
- }
1643
- } finally {
1644
- userManager.endTask(userId);
1620
+ await recordUserUsage(session, styleName, images.length);
1621
+ if (checkTimeout && checkTimeout()) throw new Error("命令执行超时");
1622
+ await session.send("图像处理完成!");
1623
+ for (let i = 0; i < images.length; i++) {
1624
+ if (checkTimeout && checkTimeout()) break;
1625
+ await session.send(import_koishi2.h.image(images[i]));
1626
+ if (images.length > 1 && i < images.length - 1) {
1627
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1645
1628
  }
1646
- });
1647
- return "";
1648
- } catch (error) {
1649
- if (!taskLockReleased) {
1650
- userManager.endTask(userId);
1651
1629
  }
1652
- throw error;
1630
+ } finally {
1631
+ userManager.endTask(userId);
1653
1632
  }
1654
1633
  }
1655
1634
  __name(processImage, "processImage");
@@ -1733,11 +1712,9 @@ ${infoParts.join("\n")}`;
1733
1712
  if (!userManager.startTask(userId)) {
1734
1713
  return "您有一个图像处理任务正在进行中,请等待完成";
1735
1714
  }
1736
- userManager.endTask(userId);
1737
1715
  let isTimeout = false;
1738
1716
  return Promise.race([
1739
1717
  (async () => {
1740
- if (!userManager.startTask(userId)) return "您有一个图像处理任务正在进行中";
1741
1718
  try {
1742
1719
  await session.send("多张图片+描述");
1743
1720
  const collectedImages = [];
@@ -1801,48 +1778,18 @@ Prompt: ${prompt}`);
1801
1778
  if (resultImages.length === 0) {
1802
1779
  return "图片合成失败:未能生成图片";
1803
1780
  }
1804
- try {
1805
- await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, resultImages.length);
1806
- } catch (err) {
1807
- logger.error(`记录用户使用统计失败 (用户: ${userId}):`, err);
1808
- }
1809
- const resultMessage = "图片合成完成!";
1810
- setImmediate(async () => {
1811
- try {
1812
- await session.send(resultMessage);
1813
- for (let i = 0; i < resultImages.length; i++) {
1814
- try {
1815
- await Promise.race([
1816
- session.send(import_koishi2.h.image(resultImages[i])),
1817
- new Promise((_, reject) => setTimeout(() => reject(new Error("SendTimeout")), 2e4))
1818
- ]);
1819
- } catch (err) {
1820
- logger.warn(`图片合成发送可能超时 (用户: ${userId}, 图片 ${i + 1}/${resultImages.length}): ${err instanceof Error ? err.message : String(err)}`);
1821
- try {
1822
- await session.send(`⚠️ 图片 ${i + 1} 发送失败,请重试`);
1823
- } catch (sendErr) {
1824
- logger.error(`发送错误通知失败 (用户: ${userId}):`, sendErr);
1825
- }
1826
- }
1827
- if (resultImages.length > 1 && i < resultImages.length - 1) {
1828
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1829
- }
1830
- }
1831
- } catch (err) {
1832
- logger.error(`异步发送合成图片时出错 (用户: ${userId}):`, err);
1833
- try {
1834
- await session.send("⚠️ 图片发送过程中出现错误,但积分已扣除。如未收到图片,请联系管理员。");
1835
- } catch (sendErr) {
1836
- logger.error(`发送错误通知失败 (用户: ${userId}):`, sendErr);
1837
- }
1838
- } finally {
1839
- userManager.endTask(userId);
1781
+ await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, resultImages.length);
1782
+ if (isTimeout) throw new Error("命令执行超时");
1783
+ await session.send("图片合成完成!");
1784
+ for (let i = 0; i < resultImages.length; i++) {
1785
+ if (isTimeout) break;
1786
+ await session.send(import_koishi2.h.image(resultImages[i]));
1787
+ if (resultImages.length > 1 && i < resultImages.length - 1) {
1788
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1840
1789
  }
1841
- });
1842
- return "";
1843
- } catch (error) {
1790
+ }
1791
+ } finally {
1844
1792
  userManager.endTask(userId);
1845
- throw error;
1846
1793
  }
1847
1794
  })(),
1848
1795
  new Promise(
@@ -1852,7 +1799,6 @@ Prompt: ${prompt}`);
1852
1799
  }, config.commandTimeout * 1e3)
1853
1800
  )
1854
1801
  ]).catch(async (error) => {
1855
- if (userId) userManager.endTask(userId);
1856
1802
  const sanitizedError = sanitizeError(error);
1857
1803
  logger.error("图片合成超时或失败", { userId, error: sanitizedError });
1858
1804
  if (error?.message !== "命令执行超时") {
@@ -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.7",
4
+ "version": "0.6.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [