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 +81 -135
- package/lib/services/UserManager.d.ts +5 -0
- package/package.json +1 -1
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
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';
|