koishi-plugin-aka-ai-generator 0.5.5 → 0.6.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.js CHANGED
@@ -25,11 +25,9 @@ __export(src_exports, {
25
25
  name: () => name
26
26
  });
27
27
  module.exports = __toCommonJS(src_exports);
28
- var import_koishi = require("koishi");
29
- var import_fs = require("fs");
30
- var import_path = require("path");
28
+ var import_koishi2 = require("koishi");
31
29
 
32
- // src/providers/types.ts
30
+ // src/providers/utils.ts
33
31
  function sanitizeError(error) {
34
32
  if (!error) return error;
35
33
  if (typeof error === "string") {
@@ -58,176 +56,58 @@ function sanitizeString(str) {
58
56
  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
57
  }
60
58
  __name(sanitizeString, "sanitizeString");
61
-
62
- // src/providers/yunwu.ts
63
- async function downloadImageAsBase64(ctx, url, timeout, logger) {
59
+ async function downloadImageAsBase64(ctx, url, timeout, logger, maxSize = 10 * 1024 * 1024) {
64
60
  try {
65
61
  const response = await ctx.http.get(url, {
66
62
  responseType: "arraybuffer",
67
- timeout: timeout * 1e3
63
+ timeout: timeout * 1e3,
64
+ headers: {
65
+ "Accept": "image/*"
66
+ }
68
67
  });
69
68
  const buffer = Buffer.from(response);
69
+ if (buffer.length > maxSize) {
70
+ throw new Error(`图片大小超过限制 (${(maxSize / 1024 / 1024).toFixed(1)}MB)`);
71
+ }
70
72
  const base64 = buffer.toString("base64");
71
73
  let mimeType = "image/jpeg";
72
- const urlLower = url.toLowerCase();
73
- if (urlLower.endsWith(".png")) {
74
- mimeType = "image/png";
75
- } else if (urlLower.endsWith(".webp")) {
76
- mimeType = "image/webp";
77
- } else if (urlLower.endsWith(".gif")) {
78
- mimeType = "image/gif";
74
+ if (buffer.length > 4) {
75
+ if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
76
+ mimeType = "image/png";
77
+ } else if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
78
+ mimeType = "image/jpeg";
79
+ } else if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) {
80
+ mimeType = "image/gif";
81
+ } else if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80) {
82
+ mimeType = "image/webp";
83
+ }
84
+ }
85
+ if (mimeType === "image/jpeg") {
86
+ const urlLower = url.toLowerCase();
87
+ if (urlLower.endsWith(".png")) {
88
+ mimeType = "image/png";
89
+ } else if (urlLower.endsWith(".webp")) {
90
+ mimeType = "image/webp";
91
+ } else if (urlLower.endsWith(".gif")) {
92
+ mimeType = "image/gif";
93
+ }
79
94
  }
80
95
  logger.debug("图片下载并转换为Base64", { url, mimeType, size: base64.length });
81
96
  return { data: base64, mimeType };
82
97
  } catch (error) {
83
- logger.error("下载图片失败", { url, error });
98
+ logger.error("下载图片失败", { url, error: sanitizeError(error) });
99
+ if (error?.message?.includes("图片大小超过限制")) {
100
+ throw error;
101
+ }
84
102
  throw new Error("下载图片失败,请检查图片链接是否有效");
85
103
  }
86
104
  }
87
105
  __name(downloadImageAsBase64, "downloadImageAsBase64");
88
- function parseYunwuResponse(response) {
89
- try {
90
- const images = [];
91
- if (response.candidates && response.candidates.length > 0) {
92
- for (const candidate of response.candidates) {
93
- if (candidate.content && candidate.content.parts) {
94
- for (const part of candidate.content.parts) {
95
- if (part.inlineData && part.inlineData.data) {
96
- const base64Data = part.inlineData.data;
97
- const mimeType = part.inlineData.mimeType || "image/jpeg";
98
- const dataUrl = `data:${mimeType};base64,${base64Data}`;
99
- images.push(dataUrl);
100
- } else if (part.inline_data && part.inline_data.data) {
101
- const base64Data = part.inline_data.data;
102
- const mimeType = part.inline_data.mime_type || "image/jpeg";
103
- const dataUrl = `data:${mimeType};base64,${base64Data}`;
104
- images.push(dataUrl);
105
- } else if (part.fileData && part.fileData.fileUri) {
106
- images.push(part.fileData.fileUri);
107
- }
108
- }
109
- }
110
- }
111
- }
112
- return images;
113
- } catch (error) {
114
- return [];
115
- }
116
- }
117
- __name(parseYunwuResponse, "parseYunwuResponse");
118
- var YunwuProvider = class {
119
- static {
120
- __name(this, "YunwuProvider");
121
- }
122
- config;
123
- constructor(config) {
124
- this.config = config;
125
- }
126
- async generateImages(prompt, imageUrls, numImages) {
127
- const urls = Array.isArray(imageUrls) ? imageUrls : [imageUrls];
128
- const logger = this.config.logger;
129
- const ctx = this.config.ctx;
130
- logger.debug("开始下载图片并转换为Base64", { urls });
131
- const imageParts = [];
132
- for (const url of urls) {
133
- const { data, mimeType } = await downloadImageAsBase64(
134
- ctx,
135
- url,
136
- this.config.apiTimeout,
137
- logger
138
- );
139
- imageParts.push({
140
- inline_data: {
141
- mime_type: mimeType,
142
- data
143
- }
144
- });
145
- }
146
- const allImages = [];
147
- for (let i = 0; i < numImages; i++) {
148
- const requestData = {
149
- contents: [
150
- {
151
- role: "user",
152
- parts: [
153
- { text: prompt },
154
- ...imageParts
155
- ]
156
- }
157
- ],
158
- generationConfig: {
159
- responseModalities: ["IMAGE"]
160
- }
161
- };
162
- logger.debug("调用云雾图像编辑 API", { prompt, imageCount: urls.length, numImages, current: i + 1 });
163
- try {
164
- const response = await ctx.http.post(
165
- `https://yunwu.ai/v1beta/models/${this.config.modelId}:generateContent`,
166
- requestData,
167
- {
168
- headers: {
169
- "Content-Type": "application/json"
170
- },
171
- params: {
172
- key: this.config.apiKey
173
- },
174
- timeout: this.config.apiTimeout * 1e3
175
- }
176
- );
177
- const images = parseYunwuResponse(response);
178
- allImages.push(...images);
179
- logger.success("云雾图像编辑 API 调用成功", { current: i + 1, total: numImages });
180
- } catch (error) {
181
- const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
182
- logger.error("云雾图像编辑 API 调用失败", {
183
- message: safeMessage,
184
- code: error?.code,
185
- status: error?.response?.status,
186
- current: i + 1,
187
- total: numImages
188
- });
189
- if (allImages.length > 0) {
190
- logger.warn("部分图片生成失败,返回已生成的图片", { generated: allImages.length, requested: numImages });
191
- break;
192
- }
193
- throw new Error("图像处理API调用失败");
194
- }
195
- }
196
- return allImages;
197
- }
198
- };
199
106
 
200
107
  // src/providers/gptgod.ts
201
108
  var GPTGOD_DEFAULT_API_URL = "https://api.gptgod.online/v1/chat/completions";
202
109
  var HTTP_URL_REGEX = /^https?:\/\//i;
203
110
  var DATA_URL_REGEX = /^data:image\//i;
204
- async function downloadImageAsBase642(ctx, url, timeout, logger) {
205
- try {
206
- const response = await ctx.http.get(url, {
207
- responseType: "arraybuffer",
208
- timeout: timeout * 1e3
209
- });
210
- const buffer = Buffer.from(response);
211
- const base64 = buffer.toString("base64");
212
- let mimeType = "image/jpeg";
213
- const urlLower = url.toLowerCase();
214
- if (urlLower.endsWith(".png")) {
215
- mimeType = "image/png";
216
- } else if (urlLower.endsWith(".webp")) {
217
- mimeType = "image/webp";
218
- } else if (urlLower.endsWith(".gif")) {
219
- mimeType = "image/gif";
220
- }
221
- if (logger) {
222
- logger.debug("图片下载并转换为Base64", { url, mimeType, size: base64.length });
223
- }
224
- return { data: base64, mimeType };
225
- } catch (error) {
226
- logger.error("下载图片失败", { url, error });
227
- throw new Error("下载图片失败,请检查图片链接是否有效");
228
- }
229
- }
230
- __name(downloadImageAsBase642, "downloadImageAsBase64");
231
111
  function isHttpImage(url) {
232
112
  return HTTP_URL_REGEX.test(url);
233
113
  }
@@ -252,7 +132,7 @@ async function buildImageContentPart(ctx, url, timeout, logger) {
252
132
  image_url: { url }
253
133
  };
254
134
  }
255
- const { data, mimeType } = await downloadImageAsBase642(ctx, url, timeout, logger);
135
+ const { data, mimeType } = await downloadImageAsBase64(ctx, url, timeout, logger);
256
136
  return {
257
137
  type: "image_url",
258
138
  image_url: {
@@ -402,7 +282,7 @@ function parseGptGodResponse(response, logger) {
402
282
  }
403
283
  return images;
404
284
  } catch (error) {
405
- logger?.error("解析响应时出错", { error });
285
+ logger?.error("解析响应时出错", { error: sanitizeError(error) });
406
286
  return [];
407
287
  }
408
288
  }
@@ -427,13 +307,17 @@ var GptGodProvider = class {
427
307
  }
428
308
  const imageParts = [];
429
309
  for (const url of urls) {
430
- const imagePart = await buildImageContentPart(
431
- ctx,
432
- url,
433
- this.config.apiTimeout,
434
- logger
435
- );
436
- imageParts.push(imagePart);
310
+ try {
311
+ const imagePart = await buildImageContentPart(
312
+ ctx,
313
+ url,
314
+ this.config.apiTimeout,
315
+ logger
316
+ );
317
+ imageParts.push(imagePart);
318
+ } catch (error) {
319
+ logger.error("构建图片部分失败,跳过该图片", { url, error: sanitizeError(error) });
320
+ }
437
321
  }
438
322
  const allImages = [];
439
323
  for (let i = 0; i < numImages; i++) {
@@ -547,7 +431,6 @@ var GptGodProvider = class {
547
431
  await new Promise((resolve) => setTimeout(resolve, delay));
548
432
  continue;
549
433
  }
550
- const sanitizedError = sanitizeError(error);
551
434
  const safeMessage = typeof error?.message === "string" ? sanitizeString(error.message) : "未知错误";
552
435
  logger.error("GPTGod 图像编辑 API 调用失败", {
553
436
  message: safeMessage,
@@ -595,31 +478,6 @@ var GptGodProvider = class {
595
478
  };
596
479
 
597
480
  // src/providers/gemini.ts
598
- async function downloadImageAsBase643(ctx, url, timeout, logger) {
599
- try {
600
- const response = await ctx.http.get(url, {
601
- responseType: "arraybuffer",
602
- timeout: timeout * 1e3
603
- });
604
- const buffer = Buffer.from(response);
605
- const base64 = buffer.toString("base64");
606
- let mimeType = "image/jpeg";
607
- const urlLower = url.toLowerCase();
608
- if (urlLower.endsWith(".png")) {
609
- mimeType = "image/png";
610
- } else if (urlLower.endsWith(".webp")) {
611
- mimeType = "image/webp";
612
- } else if (urlLower.endsWith(".gif")) {
613
- mimeType = "image/gif";
614
- }
615
- logger.debug("图片下载并转换为Base64", { url, mimeType, size: base64.length });
616
- return { data: base64, mimeType };
617
- } catch (error) {
618
- logger.error("下载图片失败", { url, error });
619
- throw new Error("下载图片失败,请检查图片链接是否有效");
620
- }
621
- }
622
- __name(downloadImageAsBase643, "downloadImageAsBase64");
623
481
  function parseGeminiResponse(response, logger) {
624
482
  try {
625
483
  const images = [];
@@ -764,18 +622,22 @@ var GeminiProvider = class {
764
622
  const imageParts = [];
765
623
  for (const url of urls) {
766
624
  if (!url || !url.trim()) continue;
767
- const { data, mimeType } = await downloadImageAsBase643(
768
- ctx,
769
- url,
770
- this.config.apiTimeout,
771
- logger
772
- );
773
- imageParts.push({
774
- inline_data: {
775
- mime_type: mimeType,
776
- data
777
- }
778
- });
625
+ try {
626
+ const { data, mimeType } = await downloadImageAsBase64(
627
+ ctx,
628
+ url,
629
+ this.config.apiTimeout,
630
+ logger
631
+ );
632
+ imageParts.push({
633
+ inline_data: {
634
+ mime_type: mimeType,
635
+ data
636
+ }
637
+ });
638
+ } catch (error) {
639
+ logger.error("处理输入图片失败,跳过该图片", { url, error: sanitizeError(error) });
640
+ }
779
641
  }
780
642
  const apiBase = this.config.apiBase?.replace(/\/$/, "") || "https://generativelanguage.googleapis.com";
781
643
  const endpoint = `${apiBase}/v1beta/models/${this.config.modelId}:generateContent`;
@@ -852,9 +714,11 @@ var GeminiProvider = class {
852
714
  function createImageProvider(config) {
853
715
  switch (config.provider) {
854
716
  case "yunwu":
855
- return new YunwuProvider({
717
+ return new GeminiProvider({
856
718
  apiKey: config.yunwuApiKey,
857
719
  modelId: config.yunwuModelId,
720
+ apiBase: "https://yunwu.ai",
721
+ // 指定云雾 API 地址
858
722
  apiTimeout: config.apiTimeout,
859
723
  logLevel: config.logLevel,
860
724
  logger: config.logger,
@@ -885,6 +749,412 @@ function createImageProvider(config) {
885
749
  }
886
750
  __name(createImageProvider, "createImageProvider");
887
751
 
752
+ // src/services/UserManager.ts
753
+ var import_fs = require("fs");
754
+ var import_path = require("path");
755
+ var AsyncLock = class {
756
+ static {
757
+ __name(this, "AsyncLock");
758
+ }
759
+ promise = Promise.resolve();
760
+ async acquire(fn) {
761
+ const previousPromise = this.promise;
762
+ let release;
763
+ const nextPromise = new Promise((resolve) => {
764
+ release = resolve;
765
+ });
766
+ this.promise = nextPromise;
767
+ await previousPromise;
768
+ try {
769
+ return await fn();
770
+ } finally {
771
+ release();
772
+ }
773
+ }
774
+ };
775
+ var UserManager = class {
776
+ static {
777
+ __name(this, "UserManager");
778
+ }
779
+ dataDir;
780
+ dataFile;
781
+ backupFile;
782
+ rechargeHistoryFile;
783
+ logger;
784
+ dataLock = new AsyncLock();
785
+ historyLock = new AsyncLock();
786
+ // 内存缓存
787
+ usersCache = null;
788
+ activeTasks = /* @__PURE__ */ new Map();
789
+ // userId -> requestId
790
+ rateLimitMap = /* @__PURE__ */ new Map();
791
+ // userId -> timestamps
792
+ securityBlockMap = /* @__PURE__ */ new Map();
793
+ // userId -> 拦截时间戳数组
794
+ securityWarningMap = /* @__PURE__ */ new Map();
795
+ // userId -> 是否已收到警示
796
+ constructor(baseDir, logger) {
797
+ this.logger = logger;
798
+ this.dataDir = (0, import_path.join)(baseDir, "data/aka-ai-generator");
799
+ this.dataFile = (0, import_path.join)(this.dataDir, "users_data.json");
800
+ this.backupFile = (0, import_path.join)(this.dataDir, "users_data.json.backup");
801
+ this.rechargeHistoryFile = (0, import_path.join)(this.dataDir, "recharge_history.json");
802
+ if (!(0, import_fs.existsSync)(this.dataDir)) {
803
+ (0, import_fs.mkdirSync)(this.dataDir, { recursive: true });
804
+ }
805
+ }
806
+ // --- 任务管理 ---
807
+ startTask(userId) {
808
+ if (this.activeTasks.has(userId)) return false;
809
+ this.activeTasks.set(userId, "processing");
810
+ return true;
811
+ }
812
+ endTask(userId) {
813
+ this.activeTasks.delete(userId);
814
+ }
815
+ isTaskActive(userId) {
816
+ return this.activeTasks.has(userId);
817
+ }
818
+ // --- 权限管理 ---
819
+ isAdmin(userId, config) {
820
+ return config.adminUsers && config.adminUsers.includes(userId);
821
+ }
822
+ // --- 数据持久化 ---
823
+ async loadUsersData() {
824
+ if (this.usersCache) return this.usersCache;
825
+ return await this.dataLock.acquire(async () => {
826
+ if (this.usersCache) return this.usersCache;
827
+ try {
828
+ if ((0, import_fs.existsSync)(this.dataFile)) {
829
+ const data = await import_fs.promises.readFile(this.dataFile, "utf-8");
830
+ this.usersCache = JSON.parse(data);
831
+ return this.usersCache;
832
+ }
833
+ } catch (error) {
834
+ this.logger.error("读取用户数据失败", error);
835
+ if ((0, import_fs.existsSync)(this.backupFile)) {
836
+ try {
837
+ const backupData = await import_fs.promises.readFile(this.backupFile, "utf-8");
838
+ this.logger.warn("从备份文件恢复用户数据");
839
+ this.usersCache = JSON.parse(backupData);
840
+ return this.usersCache;
841
+ } catch (backupError) {
842
+ this.logger.error("备份文件也损坏,使用空数据", backupError);
843
+ }
844
+ }
845
+ }
846
+ this.usersCache = {};
847
+ return this.usersCache;
848
+ });
849
+ }
850
+ // 保存所有用户数据(内部使用)
851
+ async saveUsersDataInternal() {
852
+ if (!this.usersCache) return;
853
+ try {
854
+ if ((0, import_fs.existsSync)(this.dataFile)) {
855
+ await import_fs.promises.copyFile(this.dataFile, this.backupFile);
856
+ }
857
+ await import_fs.promises.writeFile(this.dataFile, JSON.stringify(this.usersCache, null, 2), "utf-8");
858
+ } catch (error) {
859
+ this.logger.error("保存用户数据失败", error);
860
+ throw error;
861
+ }
862
+ }
863
+ // 获取特定用户数据
864
+ async getUserData(userId, userName) {
865
+ await this.loadUsersData();
866
+ if (!this.usersCache[userId]) {
867
+ await this.dataLock.acquire(async () => {
868
+ if (this.usersCache[userId]) return;
869
+ this.usersCache[userId] = {
870
+ userId,
871
+ userName,
872
+ totalUsageCount: 0,
873
+ dailyUsageCount: 0,
874
+ lastDailyReset: (/* @__PURE__ */ new Date()).toISOString(),
875
+ purchasedCount: 0,
876
+ remainingPurchasedCount: 0,
877
+ donationCount: 0,
878
+ donationAmount: 0,
879
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
880
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
881
+ };
882
+ await this.saveUsersDataInternal();
883
+ this.logger.info("创建新用户数据", { userId, userName });
884
+ });
885
+ }
886
+ return this.usersCache[userId];
887
+ }
888
+ // 获取所有用户数据 (用于充值等批量操作)
889
+ async getAllUsers() {
890
+ return await this.loadUsersData();
891
+ }
892
+ // 批量更新用户数据 (用于充值)
893
+ async updateUsersBatch(updates) {
894
+ await this.dataLock.acquire(async () => {
895
+ await this.loadUsersData();
896
+ updates(this.usersCache);
897
+ await this.saveUsersDataInternal();
898
+ });
899
+ }
900
+ // --- 充值历史 ---
901
+ async loadRechargeHistory() {
902
+ return await this.historyLock.acquire(async () => {
903
+ try {
904
+ if ((0, import_fs.existsSync)(this.rechargeHistoryFile)) {
905
+ const data = await import_fs.promises.readFile(this.rechargeHistoryFile, "utf-8");
906
+ return JSON.parse(data);
907
+ }
908
+ } catch (error) {
909
+ this.logger.error("读取充值历史失败", error);
910
+ }
911
+ return {
912
+ version: "1.0.0",
913
+ lastUpdate: (/* @__PURE__ */ new Date()).toISOString(),
914
+ records: []
915
+ };
916
+ });
917
+ }
918
+ async addRechargeRecord(record) {
919
+ await this.historyLock.acquire(async () => {
920
+ let history;
921
+ try {
922
+ if ((0, import_fs.existsSync)(this.rechargeHistoryFile)) {
923
+ history = JSON.parse(await import_fs.promises.readFile(this.rechargeHistoryFile, "utf-8"));
924
+ } else {
925
+ history = { version: "1.0.0", lastUpdate: "", records: [] };
926
+ }
927
+ } catch (e) {
928
+ history = { version: "1.0.0", lastUpdate: "", records: [] };
929
+ }
930
+ history.records.push(record);
931
+ history.lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
932
+ await import_fs.promises.writeFile(this.rechargeHistoryFile, JSON.stringify(history, null, 2), "utf-8");
933
+ });
934
+ }
935
+ // --- 限流逻辑 ---
936
+ checkRateLimit(userId, config) {
937
+ const now = Date.now();
938
+ const userTimestamps = this.rateLimitMap.get(userId) || [];
939
+ const windowStart = now - config.rateLimitWindow * 1e3;
940
+ const validTimestamps = userTimestamps.filter((timestamp) => timestamp > windowStart);
941
+ this.rateLimitMap.set(userId, validTimestamps);
942
+ if (validTimestamps.length >= config.rateLimitMax) {
943
+ return {
944
+ allowed: false,
945
+ message: `操作过于频繁,请${Math.ceil((validTimestamps[0] + config.rateLimitWindow * 1e3 - now) / 1e3)}秒后再试`
946
+ };
947
+ }
948
+ return { allowed: true };
949
+ }
950
+ updateRateLimit(userId) {
951
+ const now = Date.now();
952
+ const userTimestamps = this.rateLimitMap.get(userId) || [];
953
+ userTimestamps.push(now);
954
+ this.rateLimitMap.set(userId, userTimestamps);
955
+ }
956
+ // --- 核心业务逻辑 ---
957
+ async checkDailyLimit(userId, config, numImages = 1) {
958
+ if (this.isAdmin(userId, config)) {
959
+ return { allowed: true, isAdmin: true };
960
+ }
961
+ const rateLimitCheck = this.checkRateLimit(userId, config);
962
+ if (!rateLimitCheck.allowed) {
963
+ return { ...rateLimitCheck, isAdmin: false };
964
+ }
965
+ this.updateRateLimit(userId);
966
+ const userData = await this.getUserData(userId, userId);
967
+ const today = (/* @__PURE__ */ new Date()).toDateString();
968
+ const lastReset = new Date(userData.lastDailyReset || userData.createdAt).toDateString();
969
+ let dailyCount = userData.dailyUsageCount;
970
+ if (today !== lastReset) {
971
+ dailyCount = 0;
972
+ }
973
+ if (numImages > config.dailyFreeLimit && userData.totalUsageCount === 0 && userData.purchasedCount === 0) {
974
+ }
975
+ const remainingToday = Math.max(0, config.dailyFreeLimit - dailyCount);
976
+ const totalAvailable = remainingToday + userData.remainingPurchasedCount;
977
+ if (totalAvailable < numImages) {
978
+ return {
979
+ allowed: false,
980
+ message: `生成 ${numImages} 张图片需要 ${numImages} 次可用次数,但您的可用次数不足(今日免费剩余:${remainingToday}次,充值剩余:${userData.remainingPurchasedCount}次,共${totalAvailable}次)`,
981
+ isAdmin: false
982
+ };
983
+ }
984
+ return { allowed: true, isAdmin: false };
985
+ }
986
+ // 扣减额度并记录使用
987
+ async consumeQuota(userId, userName, commandName, numImages, config) {
988
+ return await this.dataLock.acquire(async () => {
989
+ await this.loadUsersData();
990
+ let userData = this.usersCache[userId];
991
+ const now = (/* @__PURE__ */ new Date()).toISOString();
992
+ const today = (/* @__PURE__ */ new Date()).toDateString();
993
+ if (!userData) {
994
+ userData = {
995
+ userId,
996
+ userName: userName || userId,
997
+ totalUsageCount: 0,
998
+ dailyUsageCount: 0,
999
+ lastDailyReset: now,
1000
+ purchasedCount: 0,
1001
+ remainingPurchasedCount: 0,
1002
+ donationCount: 0,
1003
+ donationAmount: 0,
1004
+ lastUsed: now,
1005
+ createdAt: now
1006
+ };
1007
+ this.usersCache[userId] = userData;
1008
+ }
1009
+ userData.totalUsageCount += numImages;
1010
+ userData.lastUsed = now;
1011
+ const lastReset = new Date(userData.lastDailyReset || userData.createdAt).toDateString();
1012
+ if (today !== lastReset) {
1013
+ userData.dailyUsageCount = 0;
1014
+ userData.lastDailyReset = now;
1015
+ }
1016
+ let remainingToConsume = numImages;
1017
+ let freeUsed = 0;
1018
+ let purchasedUsed = 0;
1019
+ const availableFree = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
1020
+ if (availableFree > 0) {
1021
+ const freeToUse = Math.min(availableFree, remainingToConsume);
1022
+ userData.dailyUsageCount += freeToUse;
1023
+ freeUsed = freeToUse;
1024
+ remainingToConsume -= freeToUse;
1025
+ }
1026
+ if (remainingToConsume > 0) {
1027
+ const purchasedToUse = Math.min(userData.remainingPurchasedCount, remainingToConsume);
1028
+ userData.remainingPurchasedCount -= purchasedToUse;
1029
+ purchasedUsed = purchasedToUse;
1030
+ remainingToConsume -= purchasedToUse;
1031
+ }
1032
+ await this.saveUsersDataInternal();
1033
+ let consumptionType;
1034
+ if (freeUsed > 0 && purchasedUsed > 0) {
1035
+ consumptionType = "mixed";
1036
+ } else if (freeUsed > 0) {
1037
+ consumptionType = "free";
1038
+ } else {
1039
+ consumptionType = "purchased";
1040
+ }
1041
+ return { userData, consumptionType, freeUsed, purchasedUsed };
1042
+ });
1043
+ }
1044
+ // 记录安全拦截
1045
+ async recordSecurityBlock(userId, config) {
1046
+ if (!userId) return { shouldWarn: false, shouldDeduct: false, blockCount: 0 };
1047
+ if (this.isAdmin(userId, config)) return { shouldWarn: false, shouldDeduct: false, blockCount: 0 };
1048
+ const now = Date.now();
1049
+ const windowMs = config.securityBlockWindow * 1e3;
1050
+ const windowStart = now - windowMs;
1051
+ let blockTimestamps = this.securityBlockMap.get(userId) || [];
1052
+ blockTimestamps = blockTimestamps.filter((timestamp) => timestamp > windowStart);
1053
+ blockTimestamps.push(now);
1054
+ this.securityBlockMap.set(userId, blockTimestamps);
1055
+ const blockCount = blockTimestamps.length;
1056
+ const hasWarning = this.securityWarningMap.get(userId) || false;
1057
+ let shouldWarn = false;
1058
+ let shouldDeduct = false;
1059
+ if (blockCount >= config.securityBlockWarningThreshold && !hasWarning) {
1060
+ this.securityWarningMap.set(userId, true);
1061
+ shouldWarn = true;
1062
+ } else if (hasWarning) {
1063
+ shouldDeduct = true;
1064
+ }
1065
+ return { shouldWarn, shouldDeduct, blockCount };
1066
+ }
1067
+ };
1068
+
1069
+ // src/utils/parser.ts
1070
+ var import_koishi = require("koishi");
1071
+ function normalizeSuffix(value) {
1072
+ return value?.replace(/^\-+/, "").trim().toLowerCase();
1073
+ }
1074
+ __name(normalizeSuffix, "normalizeSuffix");
1075
+ function buildModelMappingIndex(mappings) {
1076
+ const map = /* @__PURE__ */ new Map();
1077
+ if (!Array.isArray(mappings)) return map;
1078
+ for (const mapping of mappings) {
1079
+ const key = normalizeSuffix(mapping?.suffix);
1080
+ if (!key || !mapping?.modelId) continue;
1081
+ map.set(key, mapping);
1082
+ }
1083
+ return map;
1084
+ }
1085
+ __name(buildModelMappingIndex, "buildModelMappingIndex");
1086
+ function parseStyleCommandModifiers(argv, imgParam, modelMappingIndex) {
1087
+ const session = argv.session;
1088
+ let rawText = "";
1089
+ if (session?.content) {
1090
+ const elements = import_koishi.h.parse(session.content);
1091
+ rawText = import_koishi.h.select(elements, "text").map((e) => e.attrs.content).join(" ");
1092
+ }
1093
+ const argsList = rawText ? rawText.split(/\s+/).filter(Boolean) : [...argv.args || []].map((arg) => typeof arg === "string" ? arg.trim() : "").filter(Boolean);
1094
+ if (!rawText) {
1095
+ const restStr = typeof argv.rest === "string" ? argv.rest.trim() : "";
1096
+ if (restStr) {
1097
+ const restParts = restStr.split(/\s+/).filter(Boolean);
1098
+ argsList.push(...restParts);
1099
+ }
1100
+ if (imgParam && typeof imgParam === "string" && !imgParam.startsWith("http") && !imgParam.startsWith("data:")) {
1101
+ const imgParts = imgParam.split(/\s+/).filter(Boolean);
1102
+ argsList.push(...imgParts);
1103
+ }
1104
+ }
1105
+ if (!argsList.length) return {};
1106
+ const modifiers = { customAdditions: [] };
1107
+ const flagCandidates = [];
1108
+ let index = 0;
1109
+ while (index < argsList.length) {
1110
+ const token = argsList[index];
1111
+ if (!token) {
1112
+ index++;
1113
+ continue;
1114
+ }
1115
+ const lower = token.toLowerCase();
1116
+ if (lower.startsWith("-prompt:")) {
1117
+ const promptHead = token.substring(token.indexOf(":") + 1);
1118
+ const restTokens = argsList.slice(index + 1);
1119
+ modifiers.customPromptSuffix = [promptHead, ...restTokens].join(" ").trim();
1120
+ break;
1121
+ }
1122
+ if (lower === "-add") {
1123
+ index++;
1124
+ const additionTokens = [];
1125
+ while (index < argsList.length) {
1126
+ const nextToken = argsList[index];
1127
+ if (nextToken.startsWith("-")) {
1128
+ const key = normalizeSuffix(nextToken);
1129
+ if (key && modelMappingIndex.has(key)) break;
1130
+ if (nextToken.toLowerCase().startsWith("-prompt:")) break;
1131
+ if (nextToken.toLowerCase() === "-add") break;
1132
+ }
1133
+ additionTokens.push(nextToken);
1134
+ index++;
1135
+ }
1136
+ if (additionTokens.length) {
1137
+ modifiers.customAdditions.push(additionTokens.join(" "));
1138
+ }
1139
+ continue;
1140
+ }
1141
+ flagCandidates.push(token);
1142
+ index++;
1143
+ }
1144
+ for (const arg of flagCandidates) {
1145
+ if (!arg.startsWith("-")) continue;
1146
+ const key = normalizeSuffix(arg);
1147
+ if (!key) continue;
1148
+ const mapping = modelMappingIndex.get(key);
1149
+ if (mapping) {
1150
+ modifiers.modelMapping = mapping;
1151
+ break;
1152
+ }
1153
+ }
1154
+ return modifiers;
1155
+ }
1156
+ __name(parseStyleCommandModifiers, "parseStyleCommandModifiers");
1157
+
888
1158
  // src/index.ts
889
1159
  var name = "aka-ai-generator";
890
1160
  var COMMANDS = {
@@ -901,56 +1171,56 @@ var COMMANDS = {
901
1171
  FUNCTION_LIST: "图像功能",
902
1172
  IMAGE_COMMANDS: "图像指令"
903
1173
  };
904
- var StyleItemSchema = import_koishi.Schema.object({
905
- commandName: import_koishi.Schema.string().required().description("命令名称").role("table-cell", { width: 100 }),
906
- prompt: import_koishi.Schema.string().role("textarea", { rows: 4 }).required().description("生成 prompt")
1174
+ var StyleItemSchema = import_koishi2.Schema.object({
1175
+ commandName: import_koishi2.Schema.string().required().description("命令名称").role("table-cell", { width: 100 }),
1176
+ prompt: import_koishi2.Schema.string().role("textarea", { rows: 4 }).required().description("生成 prompt")
907
1177
  });
908
- var Config = import_koishi.Schema.intersect([
909
- import_koishi.Schema.object({
910
- provider: import_koishi.Schema.union([
911
- import_koishi.Schema.const("yunwu").description("云雾 Gemini 服务"),
912
- import_koishi.Schema.const("gptgod").description("GPTGod 服务"),
913
- import_koishi.Schema.const("gemini").description("Google Gemini 原生")
1178
+ var Config = import_koishi2.Schema.intersect([
1179
+ import_koishi2.Schema.object({
1180
+ provider: import_koishi2.Schema.union([
1181
+ import_koishi2.Schema.const("yunwu").description("云雾 Gemini 服务"),
1182
+ import_koishi2.Schema.const("gptgod").description("GPTGod 服务"),
1183
+ import_koishi2.Schema.const("gemini").description("Google Gemini 原生")
914
1184
  ]).default("yunwu").description("图像生成供应商"),
915
- yunwuApiKey: import_koishi.Schema.string().description("云雾API密钥").role("secret").required(),
916
- yunwuModelId: import_koishi.Schema.string().default("gemini-2.5-flash-image").description("云雾图像生成模型ID"),
917
- gptgodApiKey: import_koishi.Schema.string().description("GPTGod API 密钥").role("secret").default(""),
918
- gptgodModelId: import_koishi.Schema.string().default("nano-banana").description("GPTGod 模型ID"),
919
- geminiApiKey: import_koishi.Schema.string().description("Gemini API 密钥").role("secret").default(""),
920
- geminiModelId: import_koishi.Schema.string().default("gemini-2.5-flash").description("Gemini 模型ID"),
921
- geminiApiBase: import_koishi.Schema.string().default("https://generativelanguage.googleapis.com").description("Gemini API 基础地址"),
922
- modelMappings: import_koishi.Schema.array(import_koishi.Schema.object({
923
- suffix: import_koishi.Schema.string().required().description("指令后缀(例如 4K,对应输入 -4K)"),
924
- provider: import_koishi.Schema.union([
925
- import_koishi.Schema.const("yunwu").description("云雾 Gemini 服务"),
926
- import_koishi.Schema.const("gptgod").description("GPTGod 服务"),
927
- import_koishi.Schema.const("gemini").description("Google Gemini 原生")
1185
+ yunwuApiKey: import_koishi2.Schema.string().description("云雾API密钥").role("secret").required(),
1186
+ yunwuModelId: import_koishi2.Schema.string().default("gemini-2.5-flash-image").description("云雾图像生成模型ID"),
1187
+ gptgodApiKey: import_koishi2.Schema.string().description("GPTGod API 密钥").role("secret").default(""),
1188
+ gptgodModelId: import_koishi2.Schema.string().default("nano-banana").description("GPTGod 模型ID"),
1189
+ geminiApiKey: import_koishi2.Schema.string().description("Gemini API 密钥").role("secret").default(""),
1190
+ geminiModelId: import_koishi2.Schema.string().default("gemini-2.5-flash").description("Gemini 模型ID"),
1191
+ geminiApiBase: import_koishi2.Schema.string().default("https://generativelanguage.googleapis.com").description("Gemini API 基础地址"),
1192
+ modelMappings: import_koishi2.Schema.array(import_koishi2.Schema.object({
1193
+ suffix: import_koishi2.Schema.string().required().description("指令后缀(例如 4K,对应输入 -4K)"),
1194
+ provider: import_koishi2.Schema.union([
1195
+ import_koishi2.Schema.const("yunwu").description("云雾 Gemini 服务"),
1196
+ import_koishi2.Schema.const("gptgod").description("GPTGod 服务"),
1197
+ import_koishi2.Schema.const("gemini").description("Google Gemini 原生")
928
1198
  ]).description("可选:覆盖供应商"),
929
- modelId: import_koishi.Schema.string().required().description("触发该后缀时使用的模型 ID")
1199
+ modelId: import_koishi2.Schema.string().required().description("触发该后缀时使用的模型 ID")
930
1200
  })).role("table").default([]).description("根据 -后缀切换模型/供应商"),
931
- apiTimeout: import_koishi.Schema.number().default(120).description("API请求超时时间(秒)"),
932
- commandTimeout: import_koishi.Schema.number().default(180).description("命令执行总超时时间(秒)"),
1201
+ apiTimeout: import_koishi2.Schema.number().default(120).description("API请求超时时间(秒)"),
1202
+ commandTimeout: import_koishi2.Schema.number().default(180).description("命令执行总超时时间(秒)"),
933
1203
  // 默认设置
934
- defaultNumImages: import_koishi.Schema.number().default(1).min(1).max(4).description("默认生成图片数量"),
1204
+ defaultNumImages: import_koishi2.Schema.number().default(1).min(1).max(4).description("默认生成图片数量"),
935
1205
  // 配额设置
936
- dailyFreeLimit: import_koishi.Schema.number().default(5).min(1).max(100).description("每日免费调用次数"),
1206
+ dailyFreeLimit: import_koishi2.Schema.number().default(5).min(1).max(100).description("每日免费调用次数"),
937
1207
  // 限流设置
938
- rateLimitWindow: import_koishi.Schema.number().default(300).min(60).max(3600).description("限流时间窗口(秒)"),
939
- rateLimitMax: import_koishi.Schema.number().default(3).min(1).max(20).description("限流窗口内最大调用次数"),
1208
+ rateLimitWindow: import_koishi2.Schema.number().default(300).min(60).max(3600).description("限流时间窗口(秒)"),
1209
+ rateLimitMax: import_koishi2.Schema.number().default(3).min(1).max(20).description("限流窗口内最大调用次数"),
940
1210
  // 管理员设置
941
- adminUsers: import_koishi.Schema.array(import_koishi.Schema.string()).default([]).description("管理员用户ID列表(不受每日使用限制)"),
1211
+ adminUsers: import_koishi2.Schema.array(import_koishi2.Schema.string()).default([]).description("管理员用户ID列表(不受每日使用限制)"),
942
1212
  // 日志级别设置
943
- logLevel: import_koishi.Schema.union([
944
- import_koishi.Schema.const("info").description("普通信息"),
945
- import_koishi.Schema.const("debug").description("完整的debug信息")
1213
+ logLevel: import_koishi2.Schema.union([
1214
+ import_koishi2.Schema.const("info").description("普通信息"),
1215
+ import_koishi2.Schema.const("debug").description("完整的debug信息")
946
1216
  ]).default("info").description("日志输出详细程度"),
947
1217
  // 安全策略拦截设置
948
- securityBlockWindow: import_koishi.Schema.number().default(600).min(60).max(3600).description("安全策略拦截追踪时间窗口(秒),在此时间窗口内连续触发拦截会被记录"),
949
- securityBlockWarningThreshold: import_koishi.Schema.number().default(3).min(1).max(10).description("安全策略拦截警示阈值,连续触发此次数拦截后将发送警示消息,再次触发将被扣除积分")
1218
+ securityBlockWindow: import_koishi2.Schema.number().default(600).min(60).max(3600).description("安全策略拦截追踪时间窗口(秒),在此时间窗口内连续触发拦截会被记录"),
1219
+ securityBlockWarningThreshold: import_koishi2.Schema.number().default(3).min(1).max(10).description("安全策略拦截警示阈值,连续触发此次数拦截后将发送警示消息,再次触发将被扣除积分")
950
1220
  }),
951
1221
  // 自定义风格命令配置
952
- import_koishi.Schema.object({
953
- styles: import_koishi.Schema.array(StyleItemSchema).role("table").default([
1222
+ import_koishi2.Schema.object({
1223
+ styles: import_koishi2.Schema.array(StyleItemSchema).role("table").default([
954
1224
  {
955
1225
  commandName: "变手办",
956
1226
  prompt: "将这张照片变成手办模型。在它后面放置一个印有图像主体的盒子,桌子上有一台电脑显示Blender建模过程。在盒子前面添加一个圆形塑料底座,角色手办站在上面。如果可能的话,将场景设置在室内"
@@ -961,128 +1231,33 @@ var Config = import_koishi.Schema.intersect([
961
1231
  }
962
1232
  ]).description("自定义风格命令配置")
963
1233
  }),
964
- import_koishi.Schema.object({
965
- styleGroups: import_koishi.Schema.dict(import_koishi.Schema.object({
966
- prompts: import_koishi.Schema.array(StyleItemSchema).role("table").default([]).description("属于该类型的 prompt 列表")
1234
+ import_koishi2.Schema.object({
1235
+ styleGroups: import_koishi2.Schema.dict(import_koishi2.Schema.object({
1236
+ prompts: import_koishi2.Schema.array(StyleItemSchema).role("table").default([]).description("属于该类型的 prompt 列表")
967
1237
  })).role("table").default({}).description("按类型管理的 prompt 组,键名即为分组名称")
968
1238
  })
969
1239
  ]);
970
1240
  function apply(ctx, config) {
971
1241
  const logger = ctx.logger("aka-ai-generator");
972
- const activeTasks = /* @__PURE__ */ new Map();
973
- const rateLimitMap = /* @__PURE__ */ new Map();
974
- const securityBlockMap = /* @__PURE__ */ new Map();
975
- const securityWarningMap = /* @__PURE__ */ new Map();
976
- const providerCache = /* @__PURE__ */ new Map();
1242
+ const userManager = new UserManager(ctx.baseDir, logger);
977
1243
  function getProviderInstance(providerType, modelId) {
978
- const cacheKey = `${providerType}:${modelId || "default"}`;
979
- if (!providerCache.has(cacheKey)) {
980
- providerCache.set(cacheKey, createImageProvider({
981
- provider: providerType,
982
- yunwuApiKey: config.yunwuApiKey,
983
- yunwuModelId: providerType === "yunwu" ? modelId || config.yunwuModelId : config.yunwuModelId,
984
- gptgodApiKey: config.gptgodApiKey,
985
- gptgodModelId: providerType === "gptgod" ? modelId || config.gptgodModelId : config.gptgodModelId,
986
- geminiApiKey: config.geminiApiKey,
987
- geminiModelId: providerType === "gemini" ? modelId || config.geminiModelId : config.geminiModelId,
988
- geminiApiBase: config.geminiApiBase,
989
- apiTimeout: config.apiTimeout,
990
- logLevel: config.logLevel,
991
- logger,
992
- ctx
993
- }));
994
- }
995
- return providerCache.get(cacheKey);
1244
+ return createImageProvider({
1245
+ provider: providerType,
1246
+ yunwuApiKey: config.yunwuApiKey,
1247
+ yunwuModelId: providerType === "yunwu" ? modelId || config.yunwuModelId : config.yunwuModelId,
1248
+ gptgodApiKey: config.gptgodApiKey,
1249
+ gptgodModelId: providerType === "gptgod" ? modelId || config.gptgodModelId : config.gptgodModelId,
1250
+ geminiApiKey: config.geminiApiKey,
1251
+ geminiModelId: providerType === "gemini" ? modelId || config.geminiModelId : config.geminiModelId,
1252
+ geminiApiBase: config.geminiApiBase,
1253
+ apiTimeout: config.apiTimeout,
1254
+ logLevel: config.logLevel,
1255
+ logger,
1256
+ ctx
1257
+ });
996
1258
  }
997
1259
  __name(getProviderInstance, "getProviderInstance");
998
- getProviderInstance(config.provider);
999
1260
  const modelMappingIndex = buildModelMappingIndex(config.modelMappings);
1000
- function normalizeSuffix(value) {
1001
- return value?.replace(/^\-+/, "").trim().toLowerCase();
1002
- }
1003
- __name(normalizeSuffix, "normalizeSuffix");
1004
- function buildModelMappingIndex(mappings) {
1005
- const map = /* @__PURE__ */ new Map();
1006
- if (!Array.isArray(mappings)) return map;
1007
- for (const mapping of mappings) {
1008
- const key = normalizeSuffix(mapping?.suffix);
1009
- if (!key || !mapping?.modelId) continue;
1010
- map.set(key, mapping);
1011
- }
1012
- return map;
1013
- }
1014
- __name(buildModelMappingIndex, "buildModelMappingIndex");
1015
- function parseStyleCommandModifiers(argv, imgParam) {
1016
- const session = argv.session;
1017
- let rawText = "";
1018
- if (session?.content) {
1019
- const elements = import_koishi.h.parse(session.content);
1020
- rawText = import_koishi.h.select(elements, "text").map((e) => e.attrs.content).join(" ");
1021
- }
1022
- const argsList = rawText ? rawText.split(/\s+/).filter(Boolean) : [...argv.args || []].map((arg) => typeof arg === "string" ? arg.trim() : "").filter(Boolean);
1023
- if (!rawText) {
1024
- const restStr = typeof argv.rest === "string" ? argv.rest.trim() : "";
1025
- if (restStr) {
1026
- const restParts = restStr.split(/\s+/).filter(Boolean);
1027
- argsList.push(...restParts);
1028
- }
1029
- if (imgParam && typeof imgParam === "string" && !imgParam.startsWith("http") && !imgParam.startsWith("data:")) {
1030
- const imgParts = imgParam.split(/\s+/).filter(Boolean);
1031
- argsList.push(...imgParts);
1032
- }
1033
- }
1034
- if (!argsList.length) return {};
1035
- const modifiers = { customAdditions: [] };
1036
- const flagCandidates = [];
1037
- let index = 0;
1038
- while (index < argsList.length) {
1039
- const token = argsList[index];
1040
- if (!token) {
1041
- index++;
1042
- continue;
1043
- }
1044
- const lower = token.toLowerCase();
1045
- if (lower.startsWith("-prompt:")) {
1046
- const promptHead = token.substring(token.indexOf(":") + 1);
1047
- const restTokens = argsList.slice(index + 1);
1048
- modifiers.customPromptSuffix = [promptHead, ...restTokens].join(" ").trim();
1049
- break;
1050
- }
1051
- if (lower === "-add") {
1052
- index++;
1053
- const additionTokens = [];
1054
- while (index < argsList.length) {
1055
- const nextToken = argsList[index];
1056
- if (nextToken.startsWith("-")) {
1057
- const key = normalizeSuffix(nextToken);
1058
- if (key && modelMappingIndex.has(key)) break;
1059
- if (nextToken.toLowerCase().startsWith("-prompt:")) break;
1060
- if (nextToken.toLowerCase() === "-add") break;
1061
- }
1062
- additionTokens.push(nextToken);
1063
- index++;
1064
- }
1065
- if (additionTokens.length) {
1066
- modifiers.customAdditions.push(additionTokens.join(" "));
1067
- }
1068
- continue;
1069
- }
1070
- flagCandidates.push(token);
1071
- index++;
1072
- }
1073
- for (const arg of flagCandidates) {
1074
- if (!arg.startsWith("-")) continue;
1075
- const key = normalizeSuffix(arg);
1076
- if (!key) continue;
1077
- const mapping = modelMappingIndex.get(key);
1078
- if (mapping) {
1079
- modifiers.modelMapping = mapping;
1080
- break;
1081
- }
1082
- }
1083
- return modifiers;
1084
- }
1085
- __name(parseStyleCommandModifiers, "parseStyleCommandModifiers");
1086
1261
  const styleDefinitions = collectStyleDefinitions();
1087
1262
  function collectStyleDefinitions() {
1088
1263
  const unique = /* @__PURE__ */ new Map();
@@ -1137,230 +1312,18 @@ function apply(ctx, config) {
1137
1312
  { name: COMMANDS.RECHARGE_HISTORY, description: "查看充值历史记录(仅管理员)" }
1138
1313
  ]
1139
1314
  };
1140
- const dataDir = "./data/aka-ai-generator";
1141
- const dataFile = (0, import_path.join)(dataDir, "users_data.json");
1142
- const backupFile = (0, import_path.join)(dataDir, "users_data.json.backup");
1143
- const rechargeHistoryFile = (0, import_path.join)(dataDir, "recharge_history.json");
1144
- if (!(0, import_fs.existsSync)(dataDir)) {
1145
- (0, import_fs.mkdirSync)(dataDir, { recursive: true });
1146
- }
1147
- function isAdmin(userId) {
1148
- return config.adminUsers && config.adminUsers.includes(userId);
1149
- }
1150
- __name(isAdmin, "isAdmin");
1151
- function checkRateLimit(userId) {
1152
- const now = Date.now();
1153
- const userTimestamps = rateLimitMap.get(userId) || [];
1154
- const windowStart = now - config.rateLimitWindow * 1e3;
1155
- const validTimestamps = userTimestamps.filter((timestamp) => timestamp > windowStart);
1156
- if (validTimestamps.length >= config.rateLimitMax) {
1157
- return {
1158
- allowed: false,
1159
- message: `操作过于频繁,请${Math.ceil((validTimestamps[0] + config.rateLimitWindow * 1e3 - now) / 1e3)}秒后再试`
1160
- };
1161
- }
1162
- return { allowed: true };
1163
- }
1164
- __name(checkRateLimit, "checkRateLimit");
1165
- function updateRateLimit(userId) {
1166
- const now = Date.now();
1167
- const userTimestamps = rateLimitMap.get(userId) || [];
1168
- userTimestamps.push(now);
1169
- rateLimitMap.set(userId, userTimestamps);
1170
- }
1171
- __name(updateRateLimit, "updateRateLimit");
1172
- async function checkDailyLimit(userId, numImages = 1, updateRateLimitImmediately = true) {
1173
- if (isAdmin(userId)) {
1174
- return { allowed: true, isAdmin: true };
1175
- }
1176
- const rateLimitCheck = checkRateLimit(userId);
1177
- if (!rateLimitCheck.allowed) {
1178
- return { ...rateLimitCheck, isAdmin: false };
1179
- }
1180
- if (updateRateLimitImmediately) {
1181
- updateRateLimit(userId);
1182
- }
1183
- const usersData = await loadUsersData();
1184
- const userData = usersData[userId];
1185
- if (!userData) {
1186
- if (numImages > config.dailyFreeLimit) {
1187
- return {
1188
- allowed: false,
1189
- message: `生成 ${numImages} 张图片需要 ${numImages} 次可用次数,但您的可用次数不足(今日免费:${config.dailyFreeLimit}次,充值:0次)`,
1190
- isAdmin: false
1191
- };
1192
- }
1193
- return { allowed: true, isAdmin: false };
1194
- }
1195
- const today = (/* @__PURE__ */ new Date()).toDateString();
1196
- const lastReset = new Date(userData.lastDailyReset || userData.createdAt).toDateString();
1197
- let dailyCount = userData.dailyUsageCount;
1198
- if (today !== lastReset) {
1199
- dailyCount = 0;
1200
- userData.dailyUsageCount = 0;
1201
- userData.lastDailyReset = (/* @__PURE__ */ new Date()).toISOString();
1202
- }
1203
- const remainingToday = Math.max(0, config.dailyFreeLimit - dailyCount);
1204
- const totalAvailable = remainingToday + userData.remainingPurchasedCount;
1205
- if (totalAvailable < numImages) {
1206
- return {
1207
- allowed: false,
1208
- message: `生成 ${numImages} 张图片需要 ${numImages} 次可用次数,但您的可用次数不足(今日免费剩余:${remainingToday}次,充值剩余:${userData.remainingPurchasedCount}次,共${totalAvailable}次)`,
1209
- isAdmin: false
1210
- };
1211
- }
1212
- return { allowed: true, isAdmin: false };
1213
- }
1214
- __name(checkDailyLimit, "checkDailyLimit");
1215
1315
  async function getPromptInput(session, message) {
1216
1316
  await session.send(message);
1217
1317
  const input = await session.prompt(3e4);
1218
1318
  return input || null;
1219
1319
  }
1220
1320
  __name(getPromptInput, "getPromptInput");
1221
- async function loadUsersData() {
1222
- try {
1223
- if ((0, import_fs.existsSync)(dataFile)) {
1224
- const data = await import_fs.promises.readFile(dataFile, "utf-8");
1225
- return JSON.parse(data);
1226
- }
1227
- } catch (error) {
1228
- logger.error("读取用户数据失败", error);
1229
- if ((0, import_fs.existsSync)(backupFile)) {
1230
- try {
1231
- const backupData = await import_fs.promises.readFile(backupFile, "utf-8");
1232
- logger.warn("从备份文件恢复用户数据");
1233
- return JSON.parse(backupData);
1234
- } catch (backupError) {
1235
- logger.error("备份文件也损坏,使用空数据", backupError);
1236
- }
1237
- }
1238
- }
1239
- return {};
1240
- }
1241
- __name(loadUsersData, "loadUsersData");
1242
- async function saveUsersData(data) {
1243
- try {
1244
- if ((0, import_fs.existsSync)(dataFile)) {
1245
- await import_fs.promises.copyFile(dataFile, backupFile);
1246
- }
1247
- await import_fs.promises.writeFile(dataFile, JSON.stringify(data, null, 2), "utf-8");
1248
- } catch (error) {
1249
- logger.error("保存用户数据失败", error);
1250
- throw error;
1251
- }
1252
- }
1253
- __name(saveUsersData, "saveUsersData");
1254
- async function loadRechargeHistory() {
1255
- try {
1256
- if ((0, import_fs.existsSync)(rechargeHistoryFile)) {
1257
- const data = await import_fs.promises.readFile(rechargeHistoryFile, "utf-8");
1258
- return JSON.parse(data);
1259
- }
1260
- } catch (error) {
1261
- logger.error("读取充值历史失败", error);
1262
- }
1263
- return {
1264
- version: "1.0.0",
1265
- lastUpdate: (/* @__PURE__ */ new Date()).toISOString(),
1266
- records: []
1267
- };
1268
- }
1269
- __name(loadRechargeHistory, "loadRechargeHistory");
1270
- async function saveRechargeHistory(history) {
1271
- try {
1272
- history.lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
1273
- await import_fs.promises.writeFile(rechargeHistoryFile, JSON.stringify(history, null, 2), "utf-8");
1274
- } catch (error) {
1275
- logger.error("保存充值历史失败", error);
1276
- throw error;
1277
- }
1278
- }
1279
- __name(saveRechargeHistory, "saveRechargeHistory");
1280
- async function getUserData(userId, userName) {
1281
- const usersData = await loadUsersData();
1282
- if (!usersData[userId]) {
1283
- usersData[userId] = {
1284
- userId,
1285
- userName,
1286
- totalUsageCount: 0,
1287
- dailyUsageCount: 0,
1288
- lastDailyReset: (/* @__PURE__ */ new Date()).toISOString(),
1289
- purchasedCount: 0,
1290
- remainingPurchasedCount: 0,
1291
- donationCount: 0,
1292
- donationAmount: 0,
1293
- lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
1294
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1295
- };
1296
- await saveUsersData(usersData);
1297
- logger.info("创建新用户数据", { userId, userName });
1298
- }
1299
- return usersData[userId];
1300
- }
1301
- __name(getUserData, "getUserData");
1302
- async function updateUserData(userId, userName, commandName, numImages = 1) {
1303
- const usersData = await loadUsersData();
1304
- const now = (/* @__PURE__ */ new Date()).toISOString();
1305
- const today = (/* @__PURE__ */ new Date()).toDateString();
1306
- if (!usersData[userId]) {
1307
- usersData[userId] = {
1308
- userId,
1309
- userName: userId,
1310
- totalUsageCount: numImages,
1311
- dailyUsageCount: numImages,
1312
- lastDailyReset: now,
1313
- purchasedCount: 0,
1314
- remainingPurchasedCount: 0,
1315
- donationCount: 0,
1316
- donationAmount: 0,
1317
- lastUsed: now,
1318
- createdAt: now
1319
- };
1320
- await saveUsersData(usersData);
1321
- return { userData: usersData[userId], consumptionType: "free", freeUsed: numImages, purchasedUsed: 0 };
1322
- }
1323
- usersData[userId].totalUsageCount += numImages;
1324
- usersData[userId].lastUsed = now;
1325
- const lastReset = new Date(usersData[userId].lastDailyReset || usersData[userId].createdAt).toDateString();
1326
- if (today !== lastReset) {
1327
- usersData[userId].dailyUsageCount = 0;
1328
- usersData[userId].lastDailyReset = now;
1329
- }
1330
- let remainingToConsume = numImages;
1331
- let freeUsed = 0;
1332
- let purchasedUsed = 0;
1333
- const availableFree = Math.max(0, config.dailyFreeLimit - usersData[userId].dailyUsageCount);
1334
- if (availableFree > 0) {
1335
- const freeToUse = Math.min(availableFree, remainingToConsume);
1336
- usersData[userId].dailyUsageCount += freeToUse;
1337
- freeUsed = freeToUse;
1338
- remainingToConsume -= freeToUse;
1339
- }
1340
- if (remainingToConsume > 0) {
1341
- const purchasedToUse = Math.min(usersData[userId].remainingPurchasedCount, remainingToConsume);
1342
- usersData[userId].remainingPurchasedCount -= purchasedToUse;
1343
- purchasedUsed = purchasedToUse;
1344
- remainingToConsume -= purchasedToUse;
1345
- }
1346
- await saveUsersData(usersData);
1347
- let consumptionType;
1348
- if (freeUsed > 0 && purchasedUsed > 0) {
1349
- consumptionType = "mixed";
1350
- } else if (freeUsed > 0) {
1351
- consumptionType = "free";
1352
- } else {
1353
- consumptionType = "purchased";
1354
- }
1355
- return { userData: usersData[userId], consumptionType, freeUsed, purchasedUsed };
1356
- }
1357
- __name(updateUserData, "updateUserData");
1358
1321
  async function recordUserUsage(session, commandName, numImages = 1) {
1359
1322
  const userId = session.userId;
1360
1323
  const userName = session.username || session.userId || "未知用户";
1361
1324
  if (!userId) return;
1362
- const { userData, consumptionType, freeUsed, purchasedUsed } = await updateUserData(userId, userName, commandName, numImages);
1363
- if (isAdmin(userId)) {
1325
+ const { userData, consumptionType, freeUsed, purchasedUsed } = await userManager.consumeQuota(userId, userName, commandName, numImages, config);
1326
+ if (userManager.isAdmin(userId, config)) {
1364
1327
  await session.send(`📊 使用统计 [管理员]
1365
1328
  用户:${userData.userName}
1366
1329
  总调用次数:${userData.totalUsageCount}次
@@ -1394,38 +1357,27 @@ function apply(ctx, config) {
1394
1357
  totalUsageCount: userData.totalUsageCount,
1395
1358
  dailyUsageCount: userData.dailyUsageCount,
1396
1359
  remainingPurchasedCount: userData.remainingPurchasedCount,
1397
- isAdmin: isAdmin(userId)
1360
+ isAdmin: userManager.isAdmin(userId, config)
1398
1361
  });
1399
1362
  }
1400
1363
  __name(recordUserUsage, "recordUserUsage");
1401
1364
  async function recordSecurityBlock(session, numImages = 1) {
1402
1365
  const userId = session.userId;
1403
1366
  if (!userId) return;
1404
- if (isAdmin(userId)) {
1405
- return;
1406
- }
1407
- const now = Date.now();
1408
- const windowMs = config.securityBlockWindow * 1e3;
1409
- const windowStart = now - windowMs;
1410
- let blockTimestamps = securityBlockMap.get(userId) || [];
1411
- blockTimestamps = blockTimestamps.filter((timestamp) => timestamp > windowStart);
1412
- blockTimestamps.push(now);
1413
- securityBlockMap.set(userId, blockTimestamps);
1414
- const blockCount = blockTimestamps.length;
1415
- const hasWarning = securityWarningMap.get(userId) || false;
1367
+ const { shouldWarn, shouldDeduct, blockCount } = await userManager.recordSecurityBlock(userId, config);
1416
1368
  logger.info("安全策略拦截记录", {
1417
1369
  userId,
1418
1370
  blockCount,
1419
1371
  threshold: config.securityBlockWarningThreshold,
1420
- hasWarning,
1372
+ shouldWarn,
1373
+ shouldDeduct,
1421
1374
  numImages
1422
1375
  });
1423
- if (blockCount >= config.securityBlockWarningThreshold && !hasWarning) {
1424
- securityWarningMap.set(userId, true);
1376
+ if (shouldWarn) {
1425
1377
  await session.send(`⚠️ 安全策略警示
1426
1378
  您已连续${config.securityBlockWarningThreshold}次触发安全策略拦截,再次发送被拦截内容将被扣除积分`);
1427
1379
  logger.warn("用户收到安全策略警示", { userId, blockCount, threshold: config.securityBlockWarningThreshold });
1428
- } else if (hasWarning) {
1380
+ } else if (shouldDeduct) {
1429
1381
  const commandName = "安全策略拦截";
1430
1382
  await recordUserUsage(session, commandName, numImages);
1431
1383
  logger.warn("用户因安全策略拦截被扣除积分", { userId, numImages });
@@ -1442,12 +1394,12 @@ function apply(ctx, config) {
1442
1394
  await session.send("请输入画面描述");
1443
1395
  const msg = await session.prompt(3e4);
1444
1396
  if (!msg) return { error: "等待超时" };
1445
- const elements = import_koishi.h.parse(msg);
1446
- const images = import_koishi.h.select(elements, "img");
1397
+ const elements = import_koishi2.h.parse(msg);
1398
+ const images = import_koishi2.h.select(elements, "img");
1447
1399
  if (images.length > 0) {
1448
1400
  return { error: "检测到图片,本功能仅支持文字输入" };
1449
1401
  }
1450
- const text = import_koishi.h.select(elements, "text").map((e) => e.attrs.content).join(" ").trim();
1402
+ const text = import_koishi2.h.select(elements, "text").map((e) => e.attrs.content).join(" ").trim();
1451
1403
  if (!text) {
1452
1404
  return { error: "未检测到描述,操作已取消" };
1453
1405
  }
@@ -1463,7 +1415,7 @@ function apply(ctx, config) {
1463
1415
  }
1464
1416
  }
1465
1417
  if (session.quote?.elements) {
1466
- const quoteImages = import_koishi.h.select(session.quote.elements, "img");
1418
+ const quoteImages = import_koishi2.h.select(session.quote.elements, "img");
1467
1419
  for (const img of quoteImages) {
1468
1420
  if (img.attrs.src) collectedImages.push(img.attrs.src);
1469
1421
  }
@@ -1482,9 +1434,9 @@ function apply(ctx, config) {
1482
1434
  while (true) {
1483
1435
  const msg = await session.prompt(mode === "multiple" ? 6e4 : 3e4);
1484
1436
  if (!msg) return { error: "等待超时" };
1485
- const elements = import_koishi.h.parse(msg);
1486
- const images = import_koishi.h.select(elements, "img");
1487
- const textElements = import_koishi.h.select(elements, "text");
1437
+ const elements = import_koishi2.h.parse(msg);
1438
+ const images = import_koishi2.h.select(elements, "img");
1439
+ const textElements = import_koishi2.h.select(elements, "text");
1488
1440
  const text = textElements.map((el) => el.attrs.content).join(" ").trim();
1489
1441
  if (images.length > 0) {
1490
1442
  for (const img of images) {
@@ -1530,14 +1482,18 @@ function apply(ctx, config) {
1530
1482
  }
1531
1483
  __name(requestProviderImages, "requestProviderImages");
1532
1484
  async function processImageWithTimeout(session, img, prompt, styleName, requestContext, displayInfo, mode = "single") {
1485
+ const userId = session.userId;
1486
+ let isTimeout = false;
1533
1487
  return Promise.race([
1534
- processImage(session, img, prompt, styleName, requestContext, displayInfo, mode),
1488
+ processImage(session, img, prompt, styleName, requestContext, displayInfo, mode, () => isTimeout),
1535
1489
  new Promise(
1536
- (_, reject) => setTimeout(() => reject(new Error("命令执行超时")), config.commandTimeout * 1e3)
1490
+ (_, reject) => setTimeout(() => {
1491
+ isTimeout = true;
1492
+ reject(new Error("命令执行超时"));
1493
+ }, config.commandTimeout * 1e3)
1537
1494
  )
1538
1495
  ]).catch(async (error) => {
1539
- const userId = session.userId;
1540
- if (userId) activeTasks.delete(userId);
1496
+ if (userId) userManager.endTask(userId);
1541
1497
  const sanitizedError = sanitizeError(error);
1542
1498
  logger.error("图像处理超时或失败", { userId, error: sanitizedError });
1543
1499
  if (error?.message !== "命令执行超时") {
@@ -1553,99 +1509,88 @@ function apply(ctx, config) {
1553
1509
  });
1554
1510
  }
1555
1511
  __name(processImageWithTimeout, "processImageWithTimeout");
1556
- async function processImage(session, img, prompt, styleName, requestContext, displayInfo, mode = "single") {
1512
+ async function processImage(session, img, prompt, styleName, requestContext, displayInfo, mode = "single", checkTimeout) {
1557
1513
  const userId = session.userId;
1558
- if (activeTasks.has(userId)) {
1514
+ if (!userManager.startTask(userId)) {
1559
1515
  return "您有一个图像处理任务正在进行中,请等待完成";
1560
1516
  }
1561
- const imageCount = requestContext?.numImages || config.defaultNumImages;
1562
- if (imageCount < 1 || imageCount > 4) {
1563
- return "生成数量必须在 1-4 之间";
1564
- }
1565
- const inputResult = await getInputData(session, img, mode);
1566
- if ("error" in inputResult) {
1567
- return inputResult.error;
1568
- }
1569
- const { images: imageUrls, text: extraText } = inputResult;
1570
- let finalPrompt = prompt;
1571
- if (extraText) {
1572
- finalPrompt += " " + extraText;
1573
- }
1574
- finalPrompt = finalPrompt.trim();
1575
- if (!finalPrompt) {
1576
- await session.send("请发送画面描述");
1577
- const promptMsg = await session.prompt(3e4);
1578
- if (!promptMsg) {
1579
- return "未检测到描述,操作已取消";
1517
+ try {
1518
+ const imageCount = requestContext?.numImages || config.defaultNumImages;
1519
+ if (imageCount < 1 || imageCount > 4) {
1520
+ return "生成数量必须在 1-4 之间";
1580
1521
  }
1581
- const elements = import_koishi.h.parse(promptMsg);
1582
- const images = import_koishi.h.select(elements, "img");
1583
- if (images.length > 0) {
1584
- return "检测到图片,本功能仅支持文字输入";
1522
+ const inputResult = await getInputData(session, img, mode);
1523
+ if ("error" in inputResult) {
1524
+ return inputResult.error;
1585
1525
  }
1586
- const text = import_koishi.h.select(elements, "text").map((e) => e.attrs.content).join(" ").trim();
1587
- if (text) {
1588
- finalPrompt = text;
1589
- } else {
1590
- return "未检测到有效文字描述,操作已取消";
1526
+ if (checkTimeout && checkTimeout()) throw new Error("命令执行超时");
1527
+ const { images: imageUrls, text: extraText } = inputResult;
1528
+ let finalPrompt = prompt;
1529
+ if (extraText) {
1530
+ finalPrompt += " " + extraText;
1591
1531
  }
1592
- }
1593
- const providerType = requestContext?.provider || config.provider;
1594
- const providerModelId = requestContext?.modelId || (providerType === "yunwu" ? config.yunwuModelId : config.gptgodModelId);
1595
- logger.info("开始图像处理", {
1596
- userId,
1597
- imageUrls,
1598
- styleName,
1599
- prompt: finalPrompt,
1600
- numImages: imageCount,
1601
- provider: providerType,
1602
- modelId: providerModelId
1603
- });
1604
- let statusMessage = `开始处理图片(${styleName})`;
1605
- const infoParts = [];
1606
- if (displayInfo?.customAdditions && displayInfo.customAdditions.length > 0) {
1607
- infoParts.push(`自定义内容:${displayInfo.customAdditions.join(";")}`);
1608
- }
1609
- if (displayInfo?.modelId) {
1610
- const modelDesc = displayInfo.modelDescription || displayInfo.modelId;
1611
- infoParts.push(`使用模型:${modelDesc}`);
1612
- }
1613
- if (infoParts.length > 0) {
1614
- statusMessage += `
1532
+ finalPrompt = finalPrompt.trim();
1533
+ if (!finalPrompt) {
1534
+ await session.send("请发送画面描述");
1535
+ const promptMsg = await session.prompt(3e4);
1536
+ if (!promptMsg) {
1537
+ return "未检测到描述,操作已取消";
1538
+ }
1539
+ const elements = import_koishi2.h.parse(promptMsg);
1540
+ const images2 = import_koishi2.h.select(elements, "img");
1541
+ if (images2.length > 0) {
1542
+ return "检测到图片,本功能仅支持文字输入";
1543
+ }
1544
+ const text = import_koishi2.h.select(elements, "text").map((e) => e.attrs.content).join(" ").trim();
1545
+ if (text) {
1546
+ finalPrompt = text;
1547
+ } else {
1548
+ return "未检测到有效文字描述,操作已取消";
1549
+ }
1550
+ }
1551
+ if (checkTimeout && checkTimeout()) throw new Error("命令执行超时");
1552
+ const providerType = requestContext?.provider || config.provider;
1553
+ const providerModelId = requestContext?.modelId || (providerType === "yunwu" ? config.yunwuModelId : config.gptgodModelId);
1554
+ logger.info("开始图像处理", {
1555
+ userId,
1556
+ imageUrls,
1557
+ styleName,
1558
+ prompt: finalPrompt,
1559
+ numImages: imageCount,
1560
+ provider: providerType,
1561
+ modelId: providerModelId
1562
+ });
1563
+ let statusMessage = `开始处理图片(${styleName})`;
1564
+ const infoParts = [];
1565
+ if (displayInfo?.customAdditions && displayInfo.customAdditions.length > 0) {
1566
+ infoParts.push(`自定义内容:${displayInfo.customAdditions.join(";")}`);
1567
+ }
1568
+ if (displayInfo?.modelId) {
1569
+ const modelDesc = displayInfo.modelDescription || displayInfo.modelId;
1570
+ infoParts.push(`使用模型:${modelDesc}`);
1571
+ }
1572
+ if (infoParts.length > 0) {
1573
+ statusMessage += `
1615
1574
  ${infoParts.join("\n")}`;
1616
- }
1617
- statusMessage += "...";
1618
- await session.send(statusMessage);
1619
- try {
1620
- activeTasks.set(userId, "processing");
1575
+ }
1576
+ statusMessage += "...";
1577
+ await session.send(statusMessage);
1621
1578
  const images = await requestProviderImages(finalPrompt, imageUrls, imageCount, requestContext);
1579
+ if (checkTimeout && checkTimeout()) throw new Error("命令执行超时");
1622
1580
  if (images.length === 0) {
1623
- activeTasks.delete(userId);
1624
1581
  return "图像处理失败:未能生成图片";
1625
1582
  }
1626
1583
  await session.send("图像处理完成!");
1627
1584
  for (let i = 0; i < images.length; i++) {
1628
- await session.send(import_koishi.h.image(images[i]));
1585
+ if (checkTimeout && checkTimeout()) break;
1586
+ await session.send(import_koishi2.h.image(images[i]));
1629
1587
  if (images.length > 1 && i < images.length - 1) {
1630
1588
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1631
1589
  }
1632
1590
  }
1633
1591
  await recordUserUsage(session, styleName, images.length);
1634
- activeTasks.delete(userId);
1635
- } catch (error) {
1636
- activeTasks.delete(userId);
1637
- const sanitizedError = sanitizeError(error);
1638
- logger.error("图像处理失败", { userId, error: sanitizedError });
1639
- const errorMessage = error?.message || "";
1640
- const isSecurityBlock = errorMessage.includes("内容被安全策略拦截") || errorMessage.includes("内容被安全策略阻止") || errorMessage.includes("内容被阻止") || errorMessage.includes("被阻止") || errorMessage.includes("SAFETY") || errorMessage.includes("RECITATION");
1641
- if (isSecurityBlock) {
1642
- await recordSecurityBlock(session, imageCount);
1643
- }
1644
- if (error?.message) {
1645
- const safeMessage = sanitizeString(error.message);
1646
- return `图像处理失败:${safeMessage}`;
1647
- }
1648
- return "图像处理失败,请稍后重试";
1592
+ } finally {
1593
+ userManager.endTask(userId);
1649
1594
  }
1650
1595
  }
1651
1596
  __name(processImage, "processImage");
@@ -1655,7 +1600,7 @@ ${infoParts.join("\n")}`;
1655
1600
  ctx.command(`${style.commandName} [img:text]`, "图像风格转换").option("num", "-n <num:number> 生成图片数量 (1-4)").option("multiple", "-m 允许多图输入").action(async (argv, img) => {
1656
1601
  const { session, options } = argv;
1657
1602
  if (!session?.userId) return "会话无效";
1658
- const modifiers = parseStyleCommandModifiers(argv, img);
1603
+ const modifiers = parseStyleCommandModifiers(argv, img, modelMappingIndex);
1659
1604
  let userPromptParts = [];
1660
1605
  if (modifiers.customAdditions?.length) {
1661
1606
  userPromptParts.push(...modifiers.customAdditions);
@@ -1665,7 +1610,7 @@ ${infoParts.join("\n")}`;
1665
1610
  }
1666
1611
  const userPromptText = userPromptParts.join(" - ");
1667
1612
  const numImages = options?.num || config.defaultNumImages;
1668
- const limitCheck = await checkDailyLimit(session.userId, numImages);
1613
+ const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
1669
1614
  if (!limitCheck.allowed) {
1670
1615
  return limitCheck.message;
1671
1616
  }
@@ -1701,7 +1646,7 @@ ${infoParts.join("\n")}`;
1701
1646
  ctx.command(`${COMMANDS.TXT_TO_IMG} [prompt:text]`, "根据文字描述生成图像").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async ({ session, options }, prompt) => {
1702
1647
  if (!session?.userId) return "会话无效";
1703
1648
  const numImages = options?.num || config.defaultNumImages;
1704
- const limitCheck = await checkDailyLimit(session.userId, numImages);
1649
+ const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
1705
1650
  if (!limitCheck.allowed) {
1706
1651
  return limitCheck.message;
1707
1652
  }
@@ -1714,7 +1659,7 @@ ${infoParts.join("\n")}`;
1714
1659
  if (!session?.userId) return "会话无效";
1715
1660
  const numImages = options?.num || config.defaultNumImages;
1716
1661
  const mode = options?.multiple ? "multiple" : "single";
1717
- const limitCheck = await checkDailyLimit(session.userId, numImages);
1662
+ const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
1718
1663
  if (!limitCheck.allowed) {
1719
1664
  return limitCheck.message;
1720
1665
  }
@@ -1725,106 +1670,99 @@ ${infoParts.join("\n")}`;
1725
1670
  });
1726
1671
  ctx.command(COMMANDS.COMPOSE_IMAGE, "合成多张图片,使用自定义prompt控制合成效果").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async ({ session, options }) => {
1727
1672
  if (!session?.userId) return "会话无效";
1673
+ const userId = session.userId;
1674
+ if (!userManager.startTask(userId)) {
1675
+ return "您有一个图像处理任务正在进行中,请等待完成";
1676
+ }
1677
+ userManager.endTask(userId);
1678
+ let isTimeout = false;
1728
1679
  return Promise.race([
1729
1680
  (async () => {
1730
- const userId = session.userId;
1731
- if (!userId) return "会话无效";
1732
- if (activeTasks.has(userId)) {
1733
- return "您有一个图像处理任务正在进行中,请等待完成";
1734
- }
1735
- await session.send("多张图片+描述");
1736
- const collectedImages = [];
1737
- let prompt = "";
1738
- while (true) {
1739
- const msg = await session.prompt(6e4);
1740
- if (!msg) {
1741
- return "等待超时,请重试";
1742
- }
1743
- const elements = import_koishi.h.parse(msg);
1744
- const images = import_koishi.h.select(elements, "img");
1745
- const textElements = import_koishi.h.select(elements, "text");
1746
- const text = textElements.map((el) => el.attrs.content).join(" ").trim();
1747
- if (images.length > 0) {
1748
- for (const img of images) {
1749
- collectedImages.push(img.attrs.src);
1681
+ if (!userManager.startTask(userId)) return "您有一个图像处理任务正在进行中";
1682
+ try {
1683
+ await session.send("多张图片+描述");
1684
+ const collectedImages = [];
1685
+ let prompt = "";
1686
+ while (true) {
1687
+ const msg = await session.prompt(6e4);
1688
+ if (!msg) {
1689
+ return "等待超时,请重试";
1690
+ }
1691
+ if (isTimeout) throw new Error("命令执行超时");
1692
+ const elements = import_koishi2.h.parse(msg);
1693
+ const images = import_koishi2.h.select(elements, "img");
1694
+ const textElements = import_koishi2.h.select(elements, "text");
1695
+ const text = textElements.map((el) => el.attrs.content).join(" ").trim();
1696
+ if (images.length > 0) {
1697
+ for (const img of images) {
1698
+ collectedImages.push(img.attrs.src);
1699
+ }
1700
+ if (text) {
1701
+ prompt = text;
1702
+ break;
1703
+ }
1704
+ await session.send(`已收到 ${collectedImages.length} 张图片,继续发送或输入描述`);
1705
+ continue;
1750
1706
  }
1751
1707
  if (text) {
1708
+ if (collectedImages.length < 2) {
1709
+ return `需要至少两张图片进行合成,当前只有 ${collectedImages.length} 张图片`;
1710
+ }
1752
1711
  prompt = text;
1753
1712
  break;
1754
1713
  }
1755
- await session.send(`已收到 ${collectedImages.length} 张图片,继续发送或输入描述`);
1756
- continue;
1714
+ return "未检测到有效内容,操作已取消";
1757
1715
  }
1758
- if (text) {
1759
- if (collectedImages.length < 2) {
1760
- return `需要至少两张图片进行合成,当前只有 ${collectedImages.length} 张图片`;
1761
- }
1762
- prompt = text;
1763
- break;
1716
+ if (collectedImages.length < 2) {
1717
+ return "需要至少两张图片进行合成,请重新发送";
1764
1718
  }
1765
- return "未检测到有效内容,操作已取消";
1766
- }
1767
- if (collectedImages.length < 2) {
1768
- return "需要至少两张图片进行合成,请重新发送";
1769
- }
1770
- if (!prompt) {
1771
- return "未检测到prompt描述,请重新发送";
1772
- }
1773
- const imageCount = options?.num || config.defaultNumImages;
1774
- if (imageCount < 1 || imageCount > 4) {
1775
- return "生成数量必须在 1-4 之间";
1776
- }
1777
- const limitCheck = await checkDailyLimit(userId, imageCount);
1778
- if (!limitCheck.allowed) {
1779
- return limitCheck.message;
1780
- }
1781
- logger.info("开始图片合成处理", {
1782
- userId,
1783
- imageUrls: collectedImages,
1784
- prompt,
1785
- numImages: imageCount,
1786
- imageCount: collectedImages.length
1787
- });
1788
- await session.send(`开始合成图(${collectedImages.length}张)...
1719
+ if (!prompt) {
1720
+ return "未检测到prompt描述,请重新发送";
1721
+ }
1722
+ const imageCount = options?.num || config.defaultNumImages;
1723
+ if (imageCount < 1 || imageCount > 4) {
1724
+ return "生成数量必须在 1-4 之间";
1725
+ }
1726
+ const limitCheck = await userManager.checkDailyLimit(userId, config, imageCount);
1727
+ if (!limitCheck.allowed) {
1728
+ return limitCheck.message;
1729
+ }
1730
+ if (isTimeout) throw new Error("命令执行超时");
1731
+ logger.info("开始图片合成处理", {
1732
+ userId,
1733
+ imageUrls: collectedImages,
1734
+ prompt,
1735
+ numImages: imageCount,
1736
+ imageCount: collectedImages.length
1737
+ });
1738
+ await session.send(`开始合成图(${collectedImages.length}张)...
1789
1739
  Prompt: ${prompt}`);
1790
- try {
1791
- activeTasks.set(userId, "processing");
1792
1740
  const resultImages = await requestProviderImages(prompt, collectedImages, imageCount);
1741
+ if (isTimeout) throw new Error("命令执行超时");
1793
1742
  if (resultImages.length === 0) {
1794
- activeTasks.delete(userId);
1795
1743
  return "图片合成失败:未能生成图片";
1796
1744
  }
1797
1745
  await session.send("图片合成完成!");
1798
1746
  for (let i = 0; i < resultImages.length; i++) {
1799
- await session.send(import_koishi.h.image(resultImages[i]));
1747
+ if (isTimeout) break;
1748
+ await session.send(import_koishi2.h.image(resultImages[i]));
1800
1749
  if (resultImages.length > 1 && i < resultImages.length - 1) {
1801
1750
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1802
1751
  }
1803
1752
  }
1804
1753
  await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, resultImages.length);
1805
- activeTasks.delete(userId);
1806
- } catch (error) {
1807
- activeTasks.delete(userId);
1808
- const sanitizedError = sanitizeError(error);
1809
- logger.error("图片合成失败", { userId, error: sanitizedError });
1810
- const errorMessage = error?.message || "";
1811
- const isSecurityBlock = errorMessage.includes("内容被安全策略拦截") || errorMessage.includes("内容被安全策略阻止") || errorMessage.includes("内容被阻止") || errorMessage.includes("被阻止") || errorMessage.includes("SAFETY") || errorMessage.includes("RECITATION");
1812
- if (isSecurityBlock) {
1813
- await recordSecurityBlock(session, imageCount);
1814
- }
1815
- if (error?.message) {
1816
- const safeMessage = sanitizeString(error.message);
1817
- return `图片合成失败:${safeMessage}`;
1818
- }
1819
- return "图片合成失败,请稍后重试";
1754
+ } finally {
1755
+ userManager.endTask(userId);
1820
1756
  }
1821
1757
  })(),
1822
1758
  new Promise(
1823
- (_, reject) => setTimeout(() => reject(new Error("命令执行超时")), config.commandTimeout * 1e3)
1759
+ (_, reject) => setTimeout(() => {
1760
+ isTimeout = true;
1761
+ reject(new Error("命令执行超时"));
1762
+ }, config.commandTimeout * 1e3)
1824
1763
  )
1825
1764
  ]).catch(async (error) => {
1826
- const userId = session.userId;
1827
- if (userId) activeTasks.delete(userId);
1765
+ if (userId) userManager.endTask(userId);
1828
1766
  const sanitizedError = sanitizeError(error);
1829
1767
  logger.error("图片合成超时或失败", { userId, error: sanitizedError });
1830
1768
  if (error?.message !== "命令执行超时") {
@@ -1841,14 +1779,14 @@ Prompt: ${prompt}`);
1841
1779
  });
1842
1780
  ctx.command(`${COMMANDS.RECHARGE} [content:text]`, "为用户充值次数(仅管理员)").action(async ({ session }, content) => {
1843
1781
  if (!session?.userId) return "会话无效";
1844
- if (!isAdmin(session.userId)) {
1782
+ if (!userManager.isAdmin(session.userId, config)) {
1845
1783
  return "权限不足,仅管理员可操作";
1846
1784
  }
1847
1785
  const inputContent = content || await getPromptInput(session, "请输入充值信息,格式:\n@用户1 @用户2 充值次数 [备注]");
1848
1786
  if (!inputContent) return "输入超时或无效";
1849
- const elements = import_koishi.h.parse(inputContent);
1850
- const atElements = import_koishi.h.select(elements, "at");
1851
- const textElements = import_koishi.h.select(elements, "text");
1787
+ const elements = import_koishi2.h.parse(inputContent);
1788
+ const atElements = import_koishi2.h.select(elements, "at");
1789
+ const textElements = import_koishi2.h.select(elements, "text");
1852
1790
  const text = textElements.map((el) => el.attrs.content).join(" ").trim();
1853
1791
  if (atElements.length === 0) {
1854
1792
  return "未找到@用户,请使用@用户的方式";
@@ -1867,66 +1805,64 @@ Prompt: ${prompt}`);
1867
1805
  return "未找到有效的用户,请使用@用户的方式";
1868
1806
  }
1869
1807
  try {
1870
- const usersData = await loadUsersData();
1871
- const rechargeHistory = await loadRechargeHistory();
1872
1808
  const now = (/* @__PURE__ */ new Date()).toISOString();
1873
1809
  const recordId = `recharge_${now.replace(/[-:T.]/g, "").slice(0, 14)}_${Math.random().toString(36).substr(2, 3)}`;
1874
1810
  const targets = [];
1875
- for (const userId of userIds) {
1876
- if (!userId) continue;
1877
- let userName = userId;
1878
- if (usersData[userId]) {
1879
- userName = usersData[userId].userName || userId;
1880
- }
1881
- if (!usersData[userId]) {
1882
- usersData[userId] = {
1811
+ let totalAmount = 0;
1812
+ await userManager.updateUsersBatch((usersData) => {
1813
+ for (const userId of userIds) {
1814
+ if (!userId) continue;
1815
+ let userName = userId;
1816
+ if (usersData[userId]) {
1817
+ userName = usersData[userId].userName || userId;
1818
+ } else {
1819
+ usersData[userId] = {
1820
+ userId,
1821
+ userName: userId,
1822
+ totalUsageCount: 0,
1823
+ dailyUsageCount: 0,
1824
+ lastDailyReset: now,
1825
+ purchasedCount: 0,
1826
+ remainingPurchasedCount: 0,
1827
+ donationCount: 0,
1828
+ donationAmount: 0,
1829
+ lastUsed: now,
1830
+ createdAt: now
1831
+ };
1832
+ }
1833
+ const beforeBalance = usersData[userId].remainingPurchasedCount;
1834
+ usersData[userId].purchasedCount += amount;
1835
+ usersData[userId].remainingPurchasedCount += amount;
1836
+ targets.push({
1883
1837
  userId,
1884
- userName: userId,
1885
- totalUsageCount: 0,
1886
- dailyUsageCount: 0,
1887
- lastDailyReset: now,
1888
- purchasedCount: 0,
1889
- remainingPurchasedCount: 0,
1890
- donationCount: 0,
1891
- donationAmount: 0,
1892
- lastUsed: now,
1893
- createdAt: now
1894
- };
1838
+ userName,
1839
+ amount,
1840
+ beforeBalance,
1841
+ afterBalance: usersData[userId].remainingPurchasedCount
1842
+ });
1895
1843
  }
1896
- const beforeBalance = usersData[userId].remainingPurchasedCount;
1897
- usersData[userId].purchasedCount += amount;
1898
- usersData[userId].remainingPurchasedCount += amount;
1899
- targets.push({
1900
- userId,
1901
- userName,
1902
- amount,
1903
- beforeBalance,
1904
- afterBalance: usersData[userId].remainingPurchasedCount
1905
- });
1906
- }
1907
- await saveUsersData(usersData);
1908
- const record = {
1844
+ totalAmount = amount * targets.length;
1845
+ });
1846
+ await userManager.addRechargeRecord({
1909
1847
  id: recordId,
1910
1848
  timestamp: now,
1911
- type: userIds.length > 1 ? "batch" : "single",
1849
+ type: targets.length > 1 ? "batch" : "single",
1912
1850
  operator: {
1913
1851
  userId: session.userId,
1914
1852
  userName: session.username || session.userId
1915
1853
  },
1916
1854
  targets,
1917
- totalAmount: amount * userIds.length,
1918
- note: note || "管理员充值",
1855
+ totalAmount,
1856
+ note,
1919
1857
  metadata: {}
1920
- };
1921
- rechargeHistory.records.push(record);
1922
- await saveRechargeHistory(rechargeHistory);
1858
+ });
1923
1859
  const userList = targets.map((t) => `${t.userName}(${t.afterBalance}次)`).join(", ");
1924
1860
  return `✅ 充值成功
1925
1861
  目标用户:${userList}
1926
1862
  充值次数:${amount}次/人
1927
- 总充值:${record.totalAmount}次
1928
- 操作员:${record.operator.userName}
1929
- 备注:${record.note}`;
1863
+ 总充值:${totalAmount}次
1864
+ 操作员:${session.username}
1865
+ 备注:${note}`;
1930
1866
  } catch (error) {
1931
1867
  logger.error("充值操作失败", error);
1932
1868
  return "充值失败,请稍后重试";
@@ -1934,13 +1870,13 @@ Prompt: ${prompt}`);
1934
1870
  });
1935
1871
  ctx.command(`${COMMANDS.RECHARGE_ALL} [content:text]`, "为所有用户充值次数(活动派发,仅管理员)").action(async ({ session }, content) => {
1936
1872
  if (!session?.userId) return "会话无效";
1937
- if (!isAdmin(session.userId)) {
1873
+ if (!userManager.isAdmin(session.userId, config)) {
1938
1874
  return "权限不足,仅管理员可操作";
1939
1875
  }
1940
1876
  const inputContent = content || await getPromptInput(session, "请输入活动充值信息,格式:\n充值次数 [备注]\n例如:20 或 20 春节活动奖励");
1941
1877
  if (!inputContent) return "输入超时或无效";
1942
- const elements = import_koishi.h.parse(inputContent);
1943
- const textElements = import_koishi.h.select(elements, "text");
1878
+ const elements = import_koishi2.h.parse(inputContent);
1879
+ const textElements = import_koishi2.h.select(elements, "text");
1944
1880
  const text = textElements.map((el) => el.attrs.content).join(" ").trim();
1945
1881
  const parts = text.split(/\s+/).filter((p) => p);
1946
1882
  if (parts.length === 0) {
@@ -1952,32 +1888,34 @@ Prompt: ${prompt}`);
1952
1888
  return "充值次数必须大于0";
1953
1889
  }
1954
1890
  try {
1955
- const usersData = await loadUsersData();
1956
- const rechargeHistory = await loadRechargeHistory();
1957
1891
  const now = (/* @__PURE__ */ new Date()).toISOString();
1958
1892
  const recordId = `recharge_all_${now.replace(/[-:T.]/g, "").slice(0, 14)}_${Math.random().toString(36).substr(2, 3)}`;
1959
- const allUserIds = Object.keys(usersData).filter((userId) => userId && usersData[userId]);
1960
- if (allUserIds.length === 0) {
1961
- return "当前没有使用过插件的用户,无法进行活动充值";
1962
- }
1963
1893
  const targets = [];
1964
- for (const userId of allUserIds) {
1965
- if (!userId) continue;
1966
- const userData = usersData[userId];
1967
- const userName = userData.userName || userId;
1968
- const beforeBalance = userData.remainingPurchasedCount;
1969
- userData.purchasedCount += amount;
1970
- userData.remainingPurchasedCount += amount;
1971
- targets.push({
1972
- userId,
1973
- userName,
1974
- amount,
1975
- beforeBalance,
1976
- afterBalance: userData.remainingPurchasedCount
1977
- });
1894
+ let totalAmount = 0;
1895
+ let successCount = 0;
1896
+ await userManager.updateUsersBatch((usersData) => {
1897
+ const allUserIds = Object.keys(usersData);
1898
+ for (const userId of allUserIds) {
1899
+ if (!userId || !usersData[userId]) continue;
1900
+ const userData = usersData[userId];
1901
+ const beforeBalance = userData.remainingPurchasedCount;
1902
+ userData.purchasedCount += amount;
1903
+ userData.remainingPurchasedCount += amount;
1904
+ targets.push({
1905
+ userId,
1906
+ userName: userData.userName || userId,
1907
+ amount,
1908
+ beforeBalance,
1909
+ afterBalance: userData.remainingPurchasedCount
1910
+ });
1911
+ successCount++;
1912
+ }
1913
+ totalAmount = amount * successCount;
1914
+ });
1915
+ if (successCount === 0) {
1916
+ return "当前没有使用过插件的用户,无法进行活动充值";
1978
1917
  }
1979
- await saveUsersData(usersData);
1980
- const record = {
1918
+ await userManager.addRechargeRecord({
1981
1919
  id: recordId,
1982
1920
  timestamp: now,
1983
1921
  type: "all",
@@ -1986,18 +1924,16 @@ Prompt: ${prompt}`);
1986
1924
  userName: session.username || session.userId
1987
1925
  },
1988
1926
  targets,
1989
- totalAmount: amount * allUserIds.length,
1990
- note: note || "活动充值",
1927
+ totalAmount,
1928
+ note,
1991
1929
  metadata: { all: true }
1992
- };
1993
- rechargeHistory.records.push(record);
1994
- await saveRechargeHistory(rechargeHistory);
1930
+ });
1995
1931
  return `✅ 活动充值成功
1996
- 目标用户数:${allUserIds.length}人
1932
+ 目标用户数:${successCount}人
1997
1933
  充值次数:${amount}次/人
1998
- 总充值:${record.totalAmount}次
1999
- 操作员:${record.operator.userName}
2000
- 备注:${record.note}`;
1934
+ 总充值:${totalAmount}次
1935
+ 操作员:${session.username}
1936
+ 备注:${note}`;
2001
1937
  } catch (error) {
2002
1938
  logger.error("活动充值操作失败", error);
2003
1939
  return "活动充值失败,请稍后重试";
@@ -2005,7 +1941,7 @@ Prompt: ${prompt}`);
2005
1941
  });
2006
1942
  ctx.command(`${COMMANDS.QUERY_QUOTA} [target:text]`, "查询用户额度信息").action(async ({ session }, target) => {
2007
1943
  if (!session?.userId) return "会话无效";
2008
- const userIsAdmin = isAdmin(session.userId);
1944
+ const userIsAdmin = userManager.isAdmin(session.userId, config);
2009
1945
  let targetUserId = session.userId;
2010
1946
  let targetUserName = session.username || session.userId;
2011
1947
  if (target && userIsAdmin) {
@@ -2018,15 +1954,7 @@ Prompt: ${prompt}`);
2018
1954
  return "权限不足,仅管理员可查询其他用户";
2019
1955
  }
2020
1956
  try {
2021
- const usersData = await loadUsersData();
2022
- const userData = usersData[targetUserId];
2023
- if (!userData) {
2024
- return `👤 用户信息
2025
- 用户:${targetUserName}
2026
- 状态:新用户
2027
- 今日剩余免费:${config.dailyFreeLimit}次
2028
- 充值剩余:0次`;
2029
- }
1957
+ const userData = await userManager.getUserData(targetUserId, targetUserName);
2030
1958
  const remainingToday = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
2031
1959
  const totalAvailable = remainingToday + userData.remainingPurchasedCount;
2032
1960
  return `👤 用户额度信息
@@ -2043,11 +1971,11 @@ Prompt: ${prompt}`);
2043
1971
  });
2044
1972
  ctx.command(`${COMMANDS.RECHARGE_HISTORY} [page:number]`, "查看充值历史记录(仅管理员)").action(async ({ session }, page = 1) => {
2045
1973
  if (!session?.userId) return "会话无效";
2046
- if (!isAdmin(session.userId)) {
1974
+ if (!userManager.isAdmin(session.userId, config)) {
2047
1975
  return "权限不足,仅管理员可查看充值记录";
2048
1976
  }
2049
1977
  try {
2050
- const history = await loadRechargeHistory();
1978
+ const history = await userManager.loadRechargeHistory();
2051
1979
  const pageSize = 10;
2052
1980
  const totalPages = Math.ceil(history.records.length / pageSize);
2053
1981
  const startIndex = (page - 1) * pageSize;
@@ -2081,7 +2009,7 @@ Prompt: ${prompt}`);
2081
2009
  ctx.command(COMMANDS.FUNCTION_LIST, "查看所有可用的图像处理功能").action(async ({ session }) => {
2082
2010
  if (!session?.userId) return "会话无效";
2083
2011
  try {
2084
- const userIsAdmin = isAdmin(session.userId);
2012
+ const userIsAdmin = userManager.isAdmin(session.userId, config);
2085
2013
  let result = "🎨 图像处理功能列表\n\n";
2086
2014
  result += "📝 用户指令:\n";
2087
2015
  commandRegistry.userCommands.forEach((cmd) => {