koishi-plugin-aka-ai-generator 0.5.0 → 0.5.2
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.d.ts +2 -2
- package/lib/index.js +83 -110
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -25,8 +25,6 @@ export interface UserData {
|
|
|
25
25
|
donationAmount: number;
|
|
26
26
|
lastUsed: string;
|
|
27
27
|
createdAt: string;
|
|
28
|
-
safetyBlockCount: number;
|
|
29
|
-
safetyBlockHistory: string[];
|
|
30
28
|
}
|
|
31
29
|
export interface UsersData {
|
|
32
30
|
[userId: string]: UserData;
|
|
@@ -51,6 +49,8 @@ export interface Config {
|
|
|
51
49
|
styles: StyleConfig[];
|
|
52
50
|
styleGroups?: Record<string, StyleGroupConfig>;
|
|
53
51
|
logLevel: 'info' | 'debug';
|
|
52
|
+
securityBlockWindow: number;
|
|
53
|
+
securityBlockWarningThreshold: number;
|
|
54
54
|
}
|
|
55
55
|
export interface RechargeRecord {
|
|
56
56
|
id: string;
|
package/lib/index.js
CHANGED
|
@@ -771,8 +771,8 @@ var GeminiProvider = class {
|
|
|
771
771
|
logger
|
|
772
772
|
);
|
|
773
773
|
imageParts.push({
|
|
774
|
-
|
|
775
|
-
|
|
774
|
+
inlineData: {
|
|
775
|
+
mimeType,
|
|
776
776
|
data
|
|
777
777
|
}
|
|
778
778
|
});
|
|
@@ -792,7 +792,11 @@ var GeminiProvider = class {
|
|
|
792
792
|
}
|
|
793
793
|
],
|
|
794
794
|
generationConfig: {
|
|
795
|
-
responseModalities: ["IMAGE"]
|
|
795
|
+
responseModalities: ["IMAGE"],
|
|
796
|
+
imageConfig: {
|
|
797
|
+
aspectRatio: "16:9",
|
|
798
|
+
imageSize: "4K"
|
|
799
|
+
}
|
|
796
800
|
}
|
|
797
801
|
};
|
|
798
802
|
logger.debug("调用 Gemini API", { prompt, imageCount: urls.length, numImages, current: i + 1, endpoint });
|
|
@@ -802,10 +806,8 @@ var GeminiProvider = class {
|
|
|
802
806
|
requestData,
|
|
803
807
|
{
|
|
804
808
|
headers: {
|
|
805
|
-
"Content-Type": "application/json"
|
|
806
|
-
|
|
807
|
-
params: {
|
|
808
|
-
key: this.config.apiKey
|
|
809
|
+
"Content-Type": "application/json",
|
|
810
|
+
"x-goog-api-key": this.config.apiKey
|
|
809
811
|
},
|
|
810
812
|
timeout: this.config.apiTimeout * 1e3
|
|
811
813
|
}
|
|
@@ -943,7 +945,10 @@ var Config = import_koishi.Schema.intersect([
|
|
|
943
945
|
logLevel: import_koishi.Schema.union([
|
|
944
946
|
import_koishi.Schema.const("info").description("普通信息"),
|
|
945
947
|
import_koishi.Schema.const("debug").description("完整的debug信息")
|
|
946
|
-
]).default("info").description("日志输出详细程度")
|
|
948
|
+
]).default("info").description("日志输出详细程度"),
|
|
949
|
+
// 安全策略拦截设置
|
|
950
|
+
securityBlockWindow: import_koishi.Schema.number().default(600).min(60).max(3600).description("安全策略拦截追踪时间窗口(秒),在此时间窗口内连续触发拦截会被记录"),
|
|
951
|
+
securityBlockWarningThreshold: import_koishi.Schema.number().default(3).min(1).max(10).description("安全策略拦截警示阈值,连续触发此次数拦截后将发送警示消息,再次触发将被扣除积分")
|
|
947
952
|
}),
|
|
948
953
|
// 自定义风格命令配置
|
|
949
954
|
import_koishi.Schema.object({
|
|
@@ -968,6 +973,8 @@ function apply(ctx, config) {
|
|
|
968
973
|
const logger = ctx.logger("aka-ai-generator");
|
|
969
974
|
const activeTasks = /* @__PURE__ */ new Map();
|
|
970
975
|
const rateLimitMap = /* @__PURE__ */ new Map();
|
|
976
|
+
const securityBlockMap = /* @__PURE__ */ new Map();
|
|
977
|
+
const securityWarningMap = /* @__PURE__ */ new Map();
|
|
971
978
|
const providerCache = /* @__PURE__ */ new Map();
|
|
972
979
|
function getProviderInstance(providerType, modelId) {
|
|
973
980
|
const cacheKey = `${providerType}:${modelId || "default"}`;
|
|
@@ -1207,46 +1214,6 @@ function apply(ctx, config) {
|
|
|
1207
1214
|
return { allowed: true, isAdmin: false };
|
|
1208
1215
|
}
|
|
1209
1216
|
__name(checkDailyLimit, "checkDailyLimit");
|
|
1210
|
-
function isSafetyBlockError(error) {
|
|
1211
|
-
if (!error) return false;
|
|
1212
|
-
const errorMessage = typeof error === "string" ? error : error?.message || "";
|
|
1213
|
-
if (!errorMessage) return false;
|
|
1214
|
-
const lowerMessage = errorMessage.toLowerCase();
|
|
1215
|
-
const safetyKeywords = [
|
|
1216
|
-
"安全策略",
|
|
1217
|
-
"被阻止",
|
|
1218
|
-
"被拦截",
|
|
1219
|
-
"blocked",
|
|
1220
|
-
"safety",
|
|
1221
|
-
"prohibited",
|
|
1222
|
-
"recitation",
|
|
1223
|
-
"内容被阻止",
|
|
1224
|
-
"内容被拦截",
|
|
1225
|
-
"安全策略阻止",
|
|
1226
|
-
"安全策略拦截"
|
|
1227
|
-
];
|
|
1228
|
-
return safetyKeywords.some((keyword) => lowerMessage.includes(keyword.toLowerCase()));
|
|
1229
|
-
}
|
|
1230
|
-
__name(isSafetyBlockError, "isSafetyBlockError");
|
|
1231
|
-
async function checkSafetyBlockLimit(userId) {
|
|
1232
|
-
const usersData = await loadUsersData();
|
|
1233
|
-
const userData = usersData[userId];
|
|
1234
|
-
if (!userData || !userData.safetyBlockHistory || userData.safetyBlockHistory.length === 0) {
|
|
1235
|
-
return { allowed: true };
|
|
1236
|
-
}
|
|
1237
|
-
const windowSize = 10;
|
|
1238
|
-
const recentBlocks = userData.safetyBlockHistory.slice(-windowSize);
|
|
1239
|
-
const blockRate = recentBlocks.length / windowSize;
|
|
1240
|
-
const threshold = 0.5;
|
|
1241
|
-
if (blockRate > threshold) {
|
|
1242
|
-
return {
|
|
1243
|
-
allowed: false,
|
|
1244
|
-
message: `检测到您最近多次发送被安全策略拦截的内容(拦截率:${(blockRate * 100).toFixed(0)}%),请检查您的内容是否符合使用规范。如继续发送违规内容,可能会被进一步限制。`
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
return { allowed: true };
|
|
1248
|
-
}
|
|
1249
|
-
__name(checkSafetyBlockLimit, "checkSafetyBlockLimit");
|
|
1250
1217
|
async function getPromptInput(session, message) {
|
|
1251
1218
|
await session.send(message);
|
|
1252
1219
|
const input = await session.prompt(3e4);
|
|
@@ -1326,24 +1293,15 @@ function apply(ctx, config) {
|
|
|
1326
1293
|
donationCount: 0,
|
|
1327
1294
|
donationAmount: 0,
|
|
1328
1295
|
lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1329
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1330
|
-
safetyBlockCount: 0,
|
|
1331
|
-
safetyBlockHistory: []
|
|
1296
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1332
1297
|
};
|
|
1333
1298
|
await saveUsersData(usersData);
|
|
1334
1299
|
logger.info("创建新用户数据", { userId, userName });
|
|
1335
|
-
} else {
|
|
1336
|
-
if (usersData[userId].safetyBlockCount === void 0) {
|
|
1337
|
-
usersData[userId].safetyBlockCount = 0;
|
|
1338
|
-
}
|
|
1339
|
-
if (usersData[userId].safetyBlockHistory === void 0) {
|
|
1340
|
-
usersData[userId].safetyBlockHistory = [];
|
|
1341
|
-
}
|
|
1342
1300
|
}
|
|
1343
1301
|
return usersData[userId];
|
|
1344
1302
|
}
|
|
1345
1303
|
__name(getUserData, "getUserData");
|
|
1346
|
-
async function updateUserData(userId, userName, commandName, numImages = 1
|
|
1304
|
+
async function updateUserData(userId, userName, commandName, numImages = 1) {
|
|
1347
1305
|
const usersData = await loadUsersData();
|
|
1348
1306
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1349
1307
|
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
@@ -1359,19 +1317,11 @@ function apply(ctx, config) {
|
|
|
1359
1317
|
donationCount: 0,
|
|
1360
1318
|
donationAmount: 0,
|
|
1361
1319
|
lastUsed: now,
|
|
1362
|
-
createdAt: now
|
|
1363
|
-
safetyBlockCount: isSafetyBlock ? numImages : 0,
|
|
1364
|
-
safetyBlockHistory: isSafetyBlock ? [now] : []
|
|
1320
|
+
createdAt: now
|
|
1365
1321
|
};
|
|
1366
1322
|
await saveUsersData(usersData);
|
|
1367
1323
|
return { userData: usersData[userId], consumptionType: "free", freeUsed: numImages, purchasedUsed: 0 };
|
|
1368
1324
|
}
|
|
1369
|
-
if (usersData[userId].safetyBlockCount === void 0) {
|
|
1370
|
-
usersData[userId].safetyBlockCount = 0;
|
|
1371
|
-
}
|
|
1372
|
-
if (usersData[userId].safetyBlockHistory === void 0) {
|
|
1373
|
-
usersData[userId].safetyBlockHistory = [];
|
|
1374
|
-
}
|
|
1375
1325
|
usersData[userId].totalUsageCount += numImages;
|
|
1376
1326
|
usersData[userId].lastUsed = now;
|
|
1377
1327
|
const lastReset = new Date(usersData[userId].lastDailyReset || usersData[userId].createdAt).toDateString();
|
|
@@ -1395,13 +1345,6 @@ function apply(ctx, config) {
|
|
|
1395
1345
|
purchasedUsed = purchasedToUse;
|
|
1396
1346
|
remainingToConsume -= purchasedToUse;
|
|
1397
1347
|
}
|
|
1398
|
-
if (isSafetyBlock) {
|
|
1399
|
-
usersData[userId].safetyBlockCount += numImages;
|
|
1400
|
-
usersData[userId].safetyBlockHistory.push(now);
|
|
1401
|
-
if (usersData[userId].safetyBlockHistory.length > 20) {
|
|
1402
|
-
usersData[userId].safetyBlockHistory = usersData[userId].safetyBlockHistory.slice(-20);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
1348
|
await saveUsersData(usersData);
|
|
1406
1349
|
let consumptionType;
|
|
1407
1350
|
if (freeUsed > 0 && purchasedUsed > 0) {
|
|
@@ -1414,17 +1357,16 @@ function apply(ctx, config) {
|
|
|
1414
1357
|
return { userData: usersData[userId], consumptionType, freeUsed, purchasedUsed };
|
|
1415
1358
|
}
|
|
1416
1359
|
__name(updateUserData, "updateUserData");
|
|
1417
|
-
async function recordUserUsage(session, commandName, numImages = 1
|
|
1360
|
+
async function recordUserUsage(session, commandName, numImages = 1) {
|
|
1418
1361
|
const userId = session.userId;
|
|
1419
1362
|
const userName = session.username || session.userId || "未知用户";
|
|
1420
1363
|
if (!userId) return;
|
|
1421
|
-
const { userData, consumptionType, freeUsed, purchasedUsed } = await updateUserData(userId, userName, commandName, numImages
|
|
1364
|
+
const { userData, consumptionType, freeUsed, purchasedUsed } = await updateUserData(userId, userName, commandName, numImages);
|
|
1422
1365
|
if (isAdmin(userId)) {
|
|
1423
|
-
const blockInfo = isSafetyBlock ? "\n⚠️ 本次请求被安全策略拦截,已扣除使用次数" : "";
|
|
1424
1366
|
await session.send(`📊 使用统计 [管理员]
|
|
1425
1367
|
用户:${userData.userName}
|
|
1426
1368
|
总调用次数:${userData.totalUsageCount}次
|
|
1427
|
-
|
|
1369
|
+
状态:无限制使用`);
|
|
1428
1370
|
} else {
|
|
1429
1371
|
const remainingToday = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
|
|
1430
1372
|
let consumptionText = "";
|
|
@@ -1435,14 +1377,13 @@ function apply(ctx, config) {
|
|
|
1435
1377
|
} else {
|
|
1436
1378
|
consumptionText = `充值次数 -${purchasedUsed}`;
|
|
1437
1379
|
}
|
|
1438
|
-
const blockInfo = isSafetyBlock ? "\n⚠️ 本次请求被安全策略拦截,已扣除使用次数。请检查内容是否符合使用规范。" : "";
|
|
1439
1380
|
await session.send(`📊 使用统计
|
|
1440
1381
|
用户:${userData.userName}
|
|
1441
1382
|
本次生成:${numImages}张图片
|
|
1442
1383
|
本次消费:${consumptionText}
|
|
1443
1384
|
总调用次数:${userData.totalUsageCount}次
|
|
1444
1385
|
今日剩余免费:${remainingToday}次
|
|
1445
|
-
充值剩余:${userData.remainingPurchasedCount}
|
|
1386
|
+
充值剩余:${userData.remainingPurchasedCount}次`);
|
|
1446
1387
|
}
|
|
1447
1388
|
logger.info("用户调用记录", {
|
|
1448
1389
|
userId,
|
|
@@ -1455,12 +1396,44 @@ function apply(ctx, config) {
|
|
|
1455
1396
|
totalUsageCount: userData.totalUsageCount,
|
|
1456
1397
|
dailyUsageCount: userData.dailyUsageCount,
|
|
1457
1398
|
remainingPurchasedCount: userData.remainingPurchasedCount,
|
|
1458
|
-
isSafetyBlock,
|
|
1459
|
-
safetyBlockCount: userData.safetyBlockCount,
|
|
1460
1399
|
isAdmin: isAdmin(userId)
|
|
1461
1400
|
});
|
|
1462
1401
|
}
|
|
1463
1402
|
__name(recordUserUsage, "recordUserUsage");
|
|
1403
|
+
async function recordSecurityBlock(session, numImages = 1) {
|
|
1404
|
+
const userId = session.userId;
|
|
1405
|
+
if (!userId) return;
|
|
1406
|
+
if (isAdmin(userId)) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const now = Date.now();
|
|
1410
|
+
const windowMs = config.securityBlockWindow * 1e3;
|
|
1411
|
+
const windowStart = now - windowMs;
|
|
1412
|
+
let blockTimestamps = securityBlockMap.get(userId) || [];
|
|
1413
|
+
blockTimestamps = blockTimestamps.filter((timestamp) => timestamp > windowStart);
|
|
1414
|
+
blockTimestamps.push(now);
|
|
1415
|
+
securityBlockMap.set(userId, blockTimestamps);
|
|
1416
|
+
const blockCount = blockTimestamps.length;
|
|
1417
|
+
const hasWarning = securityWarningMap.get(userId) || false;
|
|
1418
|
+
logger.info("安全策略拦截记录", {
|
|
1419
|
+
userId,
|
|
1420
|
+
blockCount,
|
|
1421
|
+
threshold: config.securityBlockWarningThreshold,
|
|
1422
|
+
hasWarning,
|
|
1423
|
+
numImages
|
|
1424
|
+
});
|
|
1425
|
+
if (blockCount >= config.securityBlockWarningThreshold && !hasWarning) {
|
|
1426
|
+
securityWarningMap.set(userId, true);
|
|
1427
|
+
await session.send(`⚠️ 安全策略警示
|
|
1428
|
+
您已连续${config.securityBlockWarningThreshold}次触发安全策略拦截,再次发送被拦截内容将被扣除积分`);
|
|
1429
|
+
logger.warn("用户收到安全策略警示", { userId, blockCount, threshold: config.securityBlockWarningThreshold });
|
|
1430
|
+
} else if (hasWarning) {
|
|
1431
|
+
const commandName = "安全策略拦截";
|
|
1432
|
+
await recordUserUsage(session, commandName, numImages);
|
|
1433
|
+
logger.warn("用户因安全策略拦截被扣除积分", { userId, numImages });
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
__name(recordSecurityBlock, "recordSecurityBlock");
|
|
1464
1437
|
async function getInputData(session, imgParam, mode) {
|
|
1465
1438
|
const collectedImages = [];
|
|
1466
1439
|
let collectedText = "";
|
|
@@ -1564,11 +1537,19 @@ function apply(ctx, config) {
|
|
|
1564
1537
|
new Promise(
|
|
1565
1538
|
(_, reject) => setTimeout(() => reject(new Error("命令执行超时")), config.commandTimeout * 1e3)
|
|
1566
1539
|
)
|
|
1567
|
-
]).catch((error) => {
|
|
1540
|
+
]).catch(async (error) => {
|
|
1568
1541
|
const userId = session.userId;
|
|
1569
1542
|
if (userId) activeTasks.delete(userId);
|
|
1570
1543
|
const sanitizedError = sanitizeError(error);
|
|
1571
1544
|
logger.error("图像处理超时或失败", { userId, error: sanitizedError });
|
|
1545
|
+
if (error?.message !== "命令执行超时") {
|
|
1546
|
+
const errorMessage = error?.message || "";
|
|
1547
|
+
const isSecurityBlock = errorMessage.includes("内容被安全策略拦截") || errorMessage.includes("内容被安全策略阻止") || errorMessage.includes("内容被阻止") || errorMessage.includes("被阻止") || errorMessage.includes("SAFETY") || errorMessage.includes("RECITATION");
|
|
1548
|
+
if (isSecurityBlock) {
|
|
1549
|
+
const imageCount = requestContext?.numImages || config.defaultNumImages;
|
|
1550
|
+
await recordSecurityBlock(session, imageCount);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1572
1553
|
const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
|
|
1573
1554
|
return error.message === "命令执行超时" ? "图像处理超时,请重试" : `图像处理失败:${safeMessage}`;
|
|
1574
1555
|
});
|
|
@@ -1657,17 +1638,10 @@ ${infoParts.join("\n")}`;
|
|
|
1657
1638
|
activeTasks.delete(userId);
|
|
1658
1639
|
const sanitizedError = sanitizeError(error);
|
|
1659
1640
|
logger.error("图像处理失败", { userId, error: sanitizedError });
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
const blockLimitCheck = await checkSafetyBlockLimit(userId);
|
|
1665
|
-
if (!blockLimitCheck.allowed) {
|
|
1666
|
-
return `图像处理失败:内容被安全策略拦截。${blockLimitCheck.message}`;
|
|
1667
|
-
}
|
|
1668
|
-
} catch (recordError) {
|
|
1669
|
-
logger.error("记录安全拦截使用次数失败", { userId, error: recordError });
|
|
1670
|
-
}
|
|
1641
|
+
const errorMessage = error?.message || "";
|
|
1642
|
+
const isSecurityBlock = errorMessage.includes("内容被安全策略拦截") || errorMessage.includes("内容被安全策略阻止") || errorMessage.includes("内容被阻止") || errorMessage.includes("被阻止") || errorMessage.includes("SAFETY") || errorMessage.includes("RECITATION");
|
|
1643
|
+
if (isSecurityBlock) {
|
|
1644
|
+
await recordSecurityBlock(session, imageCount);
|
|
1671
1645
|
}
|
|
1672
1646
|
if (error?.message) {
|
|
1673
1647
|
const safeMessage = sanitizeString(error.message);
|
|
@@ -1835,17 +1809,10 @@ Prompt: ${prompt}`);
|
|
|
1835
1809
|
activeTasks.delete(userId);
|
|
1836
1810
|
const sanitizedError = sanitizeError(error);
|
|
1837
1811
|
logger.error("图片合成失败", { userId, error: sanitizedError });
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const blockLimitCheck = await checkSafetyBlockLimit(userId);
|
|
1843
|
-
if (!blockLimitCheck.allowed) {
|
|
1844
|
-
return `图片合成失败:内容被安全策略拦截。${blockLimitCheck.message}`;
|
|
1845
|
-
}
|
|
1846
|
-
} catch (recordError) {
|
|
1847
|
-
logger.error("记录安全拦截使用次数失败", { userId, error: recordError });
|
|
1848
|
-
}
|
|
1812
|
+
const errorMessage = error?.message || "";
|
|
1813
|
+
const isSecurityBlock = errorMessage.includes("内容被安全策略拦截") || errorMessage.includes("内容被安全策略阻止") || errorMessage.includes("内容被阻止") || errorMessage.includes("被阻止") || errorMessage.includes("SAFETY") || errorMessage.includes("RECITATION");
|
|
1814
|
+
if (isSecurityBlock) {
|
|
1815
|
+
await recordSecurityBlock(session, imageCount);
|
|
1849
1816
|
}
|
|
1850
1817
|
if (error?.message) {
|
|
1851
1818
|
const safeMessage = sanitizeString(error.message);
|
|
@@ -1857,11 +1824,19 @@ Prompt: ${prompt}`);
|
|
|
1857
1824
|
new Promise(
|
|
1858
1825
|
(_, reject) => setTimeout(() => reject(new Error("命令执行超时")), config.commandTimeout * 1e3)
|
|
1859
1826
|
)
|
|
1860
|
-
]).catch((error) => {
|
|
1827
|
+
]).catch(async (error) => {
|
|
1861
1828
|
const userId = session.userId;
|
|
1862
1829
|
if (userId) activeTasks.delete(userId);
|
|
1863
1830
|
const sanitizedError = sanitizeError(error);
|
|
1864
1831
|
logger.error("图片合成超时或失败", { userId, error: sanitizedError });
|
|
1832
|
+
if (error?.message !== "命令执行超时") {
|
|
1833
|
+
const errorMessage = error?.message || "";
|
|
1834
|
+
const isSecurityBlock = errorMessage.includes("内容被安全策略拦截") || errorMessage.includes("内容被安全策略阻止") || errorMessage.includes("内容被阻止") || errorMessage.includes("被阻止") || errorMessage.includes("SAFETY") || errorMessage.includes("RECITATION");
|
|
1835
|
+
if (isSecurityBlock) {
|
|
1836
|
+
const imageCount = options?.num || config.defaultNumImages;
|
|
1837
|
+
await recordSecurityBlock(session, imageCount);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1865
1840
|
const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
|
|
1866
1841
|
return error.message === "命令执行超时" ? "图片合成超时,请重试" : `图片合成失败:${safeMessage}`;
|
|
1867
1842
|
});
|
|
@@ -1917,9 +1892,7 @@ Prompt: ${prompt}`);
|
|
|
1917
1892
|
donationCount: 0,
|
|
1918
1893
|
donationAmount: 0,
|
|
1919
1894
|
lastUsed: now,
|
|
1920
|
-
createdAt: now
|
|
1921
|
-
safetyBlockCount: 0,
|
|
1922
|
-
safetyBlockHistory: []
|
|
1895
|
+
createdAt: now
|
|
1923
1896
|
};
|
|
1924
1897
|
}
|
|
1925
1898
|
const beforeBalance = usersData[userId].remainingPurchasedCount;
|