koishi-plugin-aka-ai-generator 0.4.2 → 0.5.0

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 CHANGED
@@ -25,6 +25,8 @@ export interface UserData {
25
25
  donationAmount: number;
26
26
  lastUsed: string;
27
27
  createdAt: string;
28
+ safetyBlockCount: number;
29
+ safetyBlockHistory: string[];
28
30
  }
29
31
  export interface UsersData {
30
32
  [userId: string]: UserData;
package/lib/index.js CHANGED
@@ -29,6 +29,36 @@ var import_koishi = require("koishi");
29
29
  var import_fs = require("fs");
30
30
  var import_path = require("path");
31
31
 
32
+ // src/providers/types.ts
33
+ function sanitizeError(error) {
34
+ if (!error) return error;
35
+ if (typeof error === "string") {
36
+ return sanitizeString(error);
37
+ }
38
+ if (Array.isArray(error)) {
39
+ return error.map((item) => sanitizeError(item));
40
+ }
41
+ if (typeof error === "object") {
42
+ const sanitized = {};
43
+ for (const key in error) {
44
+ const lowerKey = key.toLowerCase();
45
+ if (lowerKey.includes("apikey") || lowerKey.includes("api_key") || lowerKey.includes("apikey") || lowerKey === "key" || lowerKey === "authorization" || lowerKey === "token" || lowerKey === "secret" || lowerKey === "password") {
46
+ sanitized[key] = "[REDACTED]";
47
+ continue;
48
+ }
49
+ sanitized[key] = sanitizeError(error[key]);
50
+ }
51
+ return sanitized;
52
+ }
53
+ return error;
54
+ }
55
+ __name(sanitizeError, "sanitizeError");
56
+ function sanitizeString(str) {
57
+ if (typeof str !== "string") return str;
58
+ return str.replace(/key["\s:=]+([a-zA-Z0-9_-]{20,})/gi, 'key="[REDACTED]"').replace(/apikey["\s:=]+([a-zA-Z0-9_-]{20,})/gi, 'apikey="[REDACTED]"').replace(/api_key["\s:=]+([a-zA-Z0-9_-]{20,})/gi, 'api_key="[REDACTED]"').replace(/authorization["\s:=]+(Bearer\s+)?([a-zA-Z0-9_-]{20,})/gi, 'authorization="[REDACTED]"').replace(/Bearer\s+([a-zA-Z0-9_-]{20,})/gi, "Bearer [REDACTED]");
59
+ }
60
+ __name(sanitizeString, "sanitizeString");
61
+
32
62
  // src/providers/yunwu.ts
33
63
  async function downloadImageAsBase64(ctx, url, timeout, logger) {
34
64
  try {
@@ -148,8 +178,9 @@ var YunwuProvider = class {
148
178
  allImages.push(...images);
149
179
  logger.success("云雾图像编辑 API 调用成功", { current: i + 1, total: numImages });
150
180
  } catch (error) {
181
+ const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
151
182
  logger.error("云雾图像编辑 API 调用失败", {
152
- message: error?.message || "未知错误",
183
+ message: safeMessage,
153
184
  code: error?.code,
154
185
  status: error?.response?.status,
155
186
  current: i + 1,
@@ -516,23 +547,25 @@ var GptGodProvider = class {
516
547
  await new Promise((resolve) => setTimeout(resolve, delay));
517
548
  continue;
518
549
  }
550
+ const sanitizedError = sanitizeError(error);
551
+ const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
519
552
  logger.error("GPTGod 图像编辑 API 调用失败", {
520
- message: error?.message || "未知错误",
553
+ message: safeMessage,
521
554
  name: error?.name,
522
555
  code: error?.code,
523
556
  status: error?.response?.status,
524
557
  statusText: error?.response?.statusText,
525
- data: error?.response?.data,
526
- stack: error?.stack,
527
- cause: error?.cause,
558
+ data: sanitizeError(error?.response?.data),
559
+ stack: sanitizeString(error?.stack || ""),
560
+ cause: sanitizeError(error?.cause),
528
561
  attempt,
529
562
  maxRetries,
530
563
  current: i + 1,
531
564
  total: numImages,
532
- // 如果是 axios 错误,通常会有 config 和 request 信息
565
+ // 如果是 axios 错误,通常会有 config 和 request 信息(清理敏感信息)
533
566
  url: error?.config?.url,
534
567
  method: error?.config?.method,
535
- headers: error?.config?.headers
568
+ headers: sanitizeError(error?.config?.headers)
536
569
  });
537
570
  if (error?.cause?.code === "UND_ERR_SOCKET" || error?.message?.includes("other side closed")) {
538
571
  throw new Error("图像处理失败:服务器连接中断,可能是服务器负载过高或网络不稳定,请稍后重试");
@@ -595,8 +628,11 @@ function parseGeminiResponse(response, logger) {
595
628
  return [];
596
629
  }
597
630
  if (response.error) {
598
- logger?.error("Gemini API 返回错误", { error: response.error });
599
- throw new Error(`Gemini API 错误: ${response.error.message || JSON.stringify(response.error)}`);
631
+ const sanitizedError = sanitizeError(response.error);
632
+ logger?.error("Gemini API 返回错误", { error: sanitizedError });
633
+ const errorMessage = response.error.message || JSON.stringify(sanitizedError);
634
+ const safeMessage = sanitizeString(errorMessage);
635
+ throw new Error(`Gemini API 错误: ${safeMessage}`);
600
636
  }
601
637
  if (response.promptFeedback) {
602
638
  const blockReason = response.promptFeedback.blockReason;
@@ -698,8 +734,12 @@ function parseGeminiResponse(response, logger) {
698
734
  }
699
735
  return images;
700
736
  } catch (error) {
701
- logger?.error("解析 Gemini 响应时出错", { error: error.message, stack: error.stack });
702
- throw error;
737
+ const safeMessage = sanitizeString(error?.message || "未知错误");
738
+ const safeStack = sanitizeString(error?.stack || "");
739
+ logger?.error("解析 Gemini 响应时出错", { error: safeMessage, stack: safeStack });
740
+ const sanitizedError = new Error(safeMessage);
741
+ sanitizedError.name = error?.name || "Error";
742
+ throw sanitizedError;
703
743
  }
704
744
  }
705
745
  __name(parseGeminiResponse, "parseGeminiResponse");
@@ -783,11 +823,13 @@ var GeminiProvider = class {
783
823
  }
784
824
  allImages.push(...images);
785
825
  } catch (error) {
826
+ const sanitizedError = sanitizeError(error);
827
+ const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
786
828
  logger.error("Gemini API 调用失败", {
787
- message: error?.message || "未知错误",
829
+ message: safeMessage,
788
830
  code: error?.code,
789
831
  status: error?.response?.status,
790
- responseData: error?.response?.data ? JSON.stringify(error.response.data).substring(0, 500) : void 0,
832
+ responseData: error?.response?.data ? sanitizeString(JSON.stringify(error.response.data).substring(0, 500)) : void 0,
791
833
  current: i + 1,
792
834
  total: numImages
793
835
  });
@@ -795,7 +837,7 @@ var GeminiProvider = class {
795
837
  logger.warn("部分图片生成失败,返回已生成的图片", { generated: allImages.length, requested: numImages });
796
838
  break;
797
839
  }
798
- throw new Error(`图像处理API调用失败: ${error?.message || "未知错误"}`);
840
+ throw new Error(`图像处理API调用失败: ${safeMessage}`);
799
841
  }
800
842
  }
801
843
  if (allImages.length === 0) {
@@ -1122,7 +1164,7 @@ function apply(ctx, config) {
1122
1164
  rateLimitMap.set(userId, userTimestamps);
1123
1165
  }
1124
1166
  __name(updateRateLimit, "updateRateLimit");
1125
- async function checkDailyLimit(userId, numImages = 1) {
1167
+ async function checkDailyLimit(userId, numImages = 1, updateRateLimitImmediately = true) {
1126
1168
  if (isAdmin(userId)) {
1127
1169
  return { allowed: true, isAdmin: true };
1128
1170
  }
@@ -1130,6 +1172,9 @@ function apply(ctx, config) {
1130
1172
  if (!rateLimitCheck.allowed) {
1131
1173
  return { ...rateLimitCheck, isAdmin: false };
1132
1174
  }
1175
+ if (updateRateLimitImmediately) {
1176
+ updateRateLimit(userId);
1177
+ }
1133
1178
  const usersData = await loadUsersData();
1134
1179
  const userData = usersData[userId];
1135
1180
  if (!userData) {
@@ -1162,6 +1207,46 @@ function apply(ctx, config) {
1162
1207
  return { allowed: true, isAdmin: false };
1163
1208
  }
1164
1209
  __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");
1165
1250
  async function getPromptInput(session, message) {
1166
1251
  await session.send(message);
1167
1252
  const input = await session.prompt(3e4);
@@ -1241,15 +1326,24 @@ function apply(ctx, config) {
1241
1326
  donationCount: 0,
1242
1327
  donationAmount: 0,
1243
1328
  lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
1244
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1329
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1330
+ safetyBlockCount: 0,
1331
+ safetyBlockHistory: []
1245
1332
  };
1246
1333
  await saveUsersData(usersData);
1247
1334
  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
+ }
1248
1342
  }
1249
1343
  return usersData[userId];
1250
1344
  }
1251
1345
  __name(getUserData, "getUserData");
1252
- async function updateUserData(userId, userName, commandName, numImages = 1) {
1346
+ async function updateUserData(userId, userName, commandName, numImages = 1, isSafetyBlock = false) {
1253
1347
  const usersData = await loadUsersData();
1254
1348
  const now = (/* @__PURE__ */ new Date()).toISOString();
1255
1349
  const today = (/* @__PURE__ */ new Date()).toDateString();
@@ -1265,11 +1359,19 @@ function apply(ctx, config) {
1265
1359
  donationCount: 0,
1266
1360
  donationAmount: 0,
1267
1361
  lastUsed: now,
1268
- createdAt: now
1362
+ createdAt: now,
1363
+ safetyBlockCount: isSafetyBlock ? numImages : 0,
1364
+ safetyBlockHistory: isSafetyBlock ? [now] : []
1269
1365
  };
1270
1366
  await saveUsersData(usersData);
1271
1367
  return { userData: usersData[userId], consumptionType: "free", freeUsed: numImages, purchasedUsed: 0 };
1272
1368
  }
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
+ }
1273
1375
  usersData[userId].totalUsageCount += numImages;
1274
1376
  usersData[userId].lastUsed = now;
1275
1377
  const lastReset = new Date(usersData[userId].lastDailyReset || usersData[userId].createdAt).toDateString();
@@ -1293,6 +1395,13 @@ function apply(ctx, config) {
1293
1395
  purchasedUsed = purchasedToUse;
1294
1396
  remainingToConsume -= purchasedToUse;
1295
1397
  }
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
+ }
1296
1405
  await saveUsersData(usersData);
1297
1406
  let consumptionType;
1298
1407
  if (freeUsed > 0 && purchasedUsed > 0) {
@@ -1305,17 +1414,17 @@ function apply(ctx, config) {
1305
1414
  return { userData: usersData[userId], consumptionType, freeUsed, purchasedUsed };
1306
1415
  }
1307
1416
  __name(updateUserData, "updateUserData");
1308
- async function recordUserUsage(session, commandName, numImages = 1) {
1417
+ async function recordUserUsage(session, commandName, numImages = 1, isSafetyBlock = false) {
1309
1418
  const userId = session.userId;
1310
1419
  const userName = session.username || session.userId || "未知用户";
1311
1420
  if (!userId) return;
1312
- updateRateLimit(userId);
1313
- const { userData, consumptionType, freeUsed, purchasedUsed } = await updateUserData(userId, userName, commandName, numImages);
1421
+ const { userData, consumptionType, freeUsed, purchasedUsed } = await updateUserData(userId, userName, commandName, numImages, isSafetyBlock);
1314
1422
  if (isAdmin(userId)) {
1423
+ const blockInfo = isSafetyBlock ? "\n⚠️ 本次请求被安全策略拦截,已扣除使用次数" : "";
1315
1424
  await session.send(`📊 使用统计 [管理员]
1316
1425
  用户:${userData.userName}
1317
1426
  总调用次数:${userData.totalUsageCount}次
1318
- 状态:无限制使用`);
1427
+ 状态:无限制使用${blockInfo}`);
1319
1428
  } else {
1320
1429
  const remainingToday = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
1321
1430
  let consumptionText = "";
@@ -1326,13 +1435,14 @@ function apply(ctx, config) {
1326
1435
  } else {
1327
1436
  consumptionText = `充值次数 -${purchasedUsed}`;
1328
1437
  }
1438
+ const blockInfo = isSafetyBlock ? "\n⚠️ 本次请求被安全策略拦截,已扣除使用次数。请检查内容是否符合使用规范。" : "";
1329
1439
  await session.send(`📊 使用统计
1330
1440
  用户:${userData.userName}
1331
1441
  本次生成:${numImages}张图片
1332
1442
  本次消费:${consumptionText}
1333
1443
  总调用次数:${userData.totalUsageCount}次
1334
1444
  今日剩余免费:${remainingToday}次
1335
- 充值剩余:${userData.remainingPurchasedCount}次`);
1445
+ 充值剩余:${userData.remainingPurchasedCount}次${blockInfo}`);
1336
1446
  }
1337
1447
  logger.info("用户调用记录", {
1338
1448
  userId,
@@ -1345,6 +1455,8 @@ function apply(ctx, config) {
1345
1455
  totalUsageCount: userData.totalUsageCount,
1346
1456
  dailyUsageCount: userData.dailyUsageCount,
1347
1457
  remainingPurchasedCount: userData.remainingPurchasedCount,
1458
+ isSafetyBlock,
1459
+ safetyBlockCount: userData.safetyBlockCount,
1348
1460
  isAdmin: isAdmin(userId)
1349
1461
  });
1350
1462
  }
@@ -1455,8 +1567,10 @@ function apply(ctx, config) {
1455
1567
  ]).catch((error) => {
1456
1568
  const userId = session.userId;
1457
1569
  if (userId) activeTasks.delete(userId);
1458
- logger.error("图像处理超时或失败", { userId, error });
1459
- return error.message === "命令执行超时" ? "图像处理超时,请重试" : `图像处理失败:${error.message}`;
1570
+ const sanitizedError = sanitizeError(error);
1571
+ logger.error("图像处理超时或失败", { userId, error: sanitizedError });
1572
+ const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
1573
+ return error.message === "命令执行超时" ? "图像处理超时,请重试" : `图像处理失败:${safeMessage}`;
1460
1574
  });
1461
1575
  }
1462
1576
  __name(processImageWithTimeout, "processImageWithTimeout");
@@ -1541,9 +1655,23 @@ ${infoParts.join("\n")}`;
1541
1655
  activeTasks.delete(userId);
1542
1656
  } catch (error) {
1543
1657
  activeTasks.delete(userId);
1544
- logger.error("图像处理失败", { userId, error });
1658
+ const sanitizedError = sanitizeError(error);
1659
+ logger.error("图像处理失败", { userId, error: sanitizedError });
1660
+ const isSafetyBlock = isSafetyBlockError(error);
1661
+ if (isSafetyBlock) {
1662
+ try {
1663
+ await recordUserUsage(session, styleName, imageCount, true);
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
+ }
1671
+ }
1545
1672
  if (error?.message) {
1546
- return `图像处理失败:${error.message}`;
1673
+ const safeMessage = sanitizeString(error.message);
1674
+ return `图像处理失败:${safeMessage}`;
1547
1675
  }
1548
1676
  return "图像处理失败,请稍后重试";
1549
1677
  }
@@ -1705,9 +1833,23 @@ Prompt: ${prompt}`);
1705
1833
  activeTasks.delete(userId);
1706
1834
  } catch (error) {
1707
1835
  activeTasks.delete(userId);
1708
- logger.error("图片合成失败", { userId, error });
1836
+ const sanitizedError = sanitizeError(error);
1837
+ logger.error("图片合成失败", { userId, error: sanitizedError });
1838
+ const isSafetyBlock = isSafetyBlockError(error);
1839
+ if (isSafetyBlock) {
1840
+ try {
1841
+ await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, imageCount, true);
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
+ }
1849
+ }
1709
1850
  if (error?.message) {
1710
- return `图片合成失败:${error.message}`;
1851
+ const safeMessage = sanitizeString(error.message);
1852
+ return `图片合成失败:${safeMessage}`;
1711
1853
  }
1712
1854
  return "图片合成失败,请稍后重试";
1713
1855
  }
@@ -1718,8 +1860,10 @@ Prompt: ${prompt}`);
1718
1860
  ]).catch((error) => {
1719
1861
  const userId = session.userId;
1720
1862
  if (userId) activeTasks.delete(userId);
1721
- logger.error("图片合成超时或失败", { userId, error });
1722
- return error.message === "命令执行超时" ? "图片合成超时,请重试" : `图片合成失败:${error.message}`;
1863
+ const sanitizedError = sanitizeError(error);
1864
+ logger.error("图片合成超时或失败", { userId, error: sanitizedError });
1865
+ const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
1866
+ return error.message === "命令执行超时" ? "图片合成超时,请重试" : `图片合成失败:${safeMessage}`;
1723
1867
  });
1724
1868
  });
1725
1869
  ctx.command(`${COMMANDS.RECHARGE} [content:text]`, "为用户充值次数(仅管理员)").action(async ({ session }, content) => {
@@ -1773,7 +1917,9 @@ Prompt: ${prompt}`);
1773
1917
  donationCount: 0,
1774
1918
  donationAmount: 0,
1775
1919
  lastUsed: now,
1776
- createdAt: now
1920
+ createdAt: now,
1921
+ safetyBlockCount: 0,
1922
+ safetyBlockHistory: []
1777
1923
  };
1778
1924
  }
1779
1925
  const beforeBalance = usersData[userId].remainingPurchasedCount;
@@ -14,3 +14,11 @@ export interface ProviderConfig {
14
14
  logger: any;
15
15
  ctx: any;
16
16
  }
17
+ /**
18
+ * 清理对象中的敏感信息(API KEY、密钥等)
19
+ */
20
+ export declare function sanitizeError(error: any): any;
21
+ /**
22
+ * 清理字符串中的 API KEY 模式
23
+ */
24
+ export declare function sanitizeString(str: string): string;
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.4.2",
4
+ "version": "0.5.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -9,11 +9,19 @@
9
9
  "dist"
10
10
  ],
11
11
  "license": "MIT",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch"
15
+ },
12
16
  "keywords": [
13
17
  "chatbot",
14
18
  "koishi",
15
19
  "plugin"
16
20
  ],
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "typescript": "^5.0.0"
24
+ },
17
25
  "peerDependencies": {
18
26
  "koishi": "^4.18.9"
19
27
  }