koishi-plugin-aka-ai-generator 0.5.5 → 0.6.1
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 +0 -40
- package/lib/index.js +785 -843
- package/lib/providers/index.d.ts +0 -1
- package/lib/providers/types.d.ts +1 -8
- package/lib/providers/utils.d.ts +17 -0
- package/lib/services/UserManager.d.ts +88 -0
- package/lib/utils/parser.d.ts +20 -0
- package/package.json +1 -1
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
|
|
29
|
-
var import_fs = require("fs");
|
|
30
|
-
var import_path = require("path");
|
|
28
|
+
var import_koishi2 = require("koishi");
|
|
31
29
|
|
|
32
|
-
// src/providers/
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
|
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 =
|
|
905
|
-
commandName:
|
|
906
|
-
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 =
|
|
909
|
-
|
|
910
|
-
provider:
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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:
|
|
916
|
-
yunwuModelId:
|
|
917
|
-
gptgodApiKey:
|
|
918
|
-
gptgodModelId:
|
|
919
|
-
geminiApiKey:
|
|
920
|
-
geminiModelId:
|
|
921
|
-
geminiApiBase:
|
|
922
|
-
modelMappings:
|
|
923
|
-
suffix:
|
|
924
|
-
provider:
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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:
|
|
1199
|
+
modelId: import_koishi2.Schema.string().required().description("触发该后缀时使用的模型 ID")
|
|
930
1200
|
})).role("table").default([]).description("根据 -后缀切换模型/供应商"),
|
|
931
|
-
apiTimeout:
|
|
932
|
-
commandTimeout:
|
|
1201
|
+
apiTimeout: import_koishi2.Schema.number().default(120).description("API请求超时时间(秒)"),
|
|
1202
|
+
commandTimeout: import_koishi2.Schema.number().default(180).description("命令执行总超时时间(秒)"),
|
|
933
1203
|
// 默认设置
|
|
934
|
-
defaultNumImages:
|
|
1204
|
+
defaultNumImages: import_koishi2.Schema.number().default(1).min(1).max(4).description("默认生成图片数量"),
|
|
935
1205
|
// 配额设置
|
|
936
|
-
dailyFreeLimit:
|
|
1206
|
+
dailyFreeLimit: import_koishi2.Schema.number().default(5).min(1).max(100).description("每日免费调用次数"),
|
|
937
1207
|
// 限流设置
|
|
938
|
-
rateLimitWindow:
|
|
939
|
-
rateLimitMax:
|
|
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:
|
|
1211
|
+
adminUsers: import_koishi2.Schema.array(import_koishi2.Schema.string()).default([]).description("管理员用户ID列表(不受每日使用限制)"),
|
|
942
1212
|
// 日志级别设置
|
|
943
|
-
logLevel:
|
|
944
|
-
|
|
945
|
-
|
|
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:
|
|
949
|
-
securityBlockWarningThreshold:
|
|
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
|
-
|
|
953
|
-
styles:
|
|
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
|
-
|
|
965
|
-
styleGroups:
|
|
966
|
-
prompts:
|
|
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
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1372
|
+
shouldWarn,
|
|
1373
|
+
shouldDeduct,
|
|
1421
1374
|
numImages
|
|
1422
1375
|
});
|
|
1423
|
-
if (
|
|
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 (
|
|
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 =
|
|
1446
|
-
const images =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1486
|
-
const images =
|
|
1487
|
-
const textElements =
|
|
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(() =>
|
|
1490
|
+
(_, reject) => setTimeout(() => {
|
|
1491
|
+
isTimeout = true;
|
|
1492
|
+
reject(new Error("命令执行超时"));
|
|
1493
|
+
}, config.commandTimeout * 1e3)
|
|
1537
1494
|
)
|
|
1538
1495
|
]).catch(async (error) => {
|
|
1539
|
-
|
|
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,95 @@ 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 (
|
|
1514
|
+
if (!userManager.startTask(userId)) {
|
|
1559
1515
|
return "您有一个图像处理任务正在进行中,请等待完成";
|
|
1560
1516
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
return "检测到图片,本功能仅支持文字输入";
|
|
1522
|
+
const inputResult = await getInputData(session, img, mode);
|
|
1523
|
+
if ("error" in inputResult) {
|
|
1524
|
+
return inputResult.error;
|
|
1585
1525
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
-
|
|
1618
|
-
|
|
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
|
}
|
|
1583
|
+
await recordUserUsage(session, styleName, images.length);
|
|
1626
1584
|
await session.send("图像处理完成!");
|
|
1627
1585
|
for (let i = 0; i < images.length; i++) {
|
|
1628
|
-
|
|
1586
|
+
if (checkTimeout && checkTimeout()) break;
|
|
1587
|
+
try {
|
|
1588
|
+
await Promise.race([
|
|
1589
|
+
session.send(import_koishi2.h.image(images[i])),
|
|
1590
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("SendTimeout")), 2e4))
|
|
1591
|
+
]);
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
logger.warn(`图片发送可能超时 (用户: ${userId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1594
|
+
}
|
|
1629
1595
|
if (images.length > 1 && i < images.length - 1) {
|
|
1630
1596
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1631
1597
|
}
|
|
1632
1598
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
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 "图像处理失败,请稍后重试";
|
|
1599
|
+
} finally {
|
|
1600
|
+
userManager.endTask(userId);
|
|
1649
1601
|
}
|
|
1650
1602
|
}
|
|
1651
1603
|
__name(processImage, "processImage");
|
|
@@ -1655,7 +1607,7 @@ ${infoParts.join("\n")}`;
|
|
|
1655
1607
|
ctx.command(`${style.commandName} [img:text]`, "图像风格转换").option("num", "-n <num:number> 生成图片数量 (1-4)").option("multiple", "-m 允许多图输入").action(async (argv, img) => {
|
|
1656
1608
|
const { session, options } = argv;
|
|
1657
1609
|
if (!session?.userId) return "会话无效";
|
|
1658
|
-
const modifiers = parseStyleCommandModifiers(argv, img);
|
|
1610
|
+
const modifiers = parseStyleCommandModifiers(argv, img, modelMappingIndex);
|
|
1659
1611
|
let userPromptParts = [];
|
|
1660
1612
|
if (modifiers.customAdditions?.length) {
|
|
1661
1613
|
userPromptParts.push(...modifiers.customAdditions);
|
|
@@ -1665,7 +1617,7 @@ ${infoParts.join("\n")}`;
|
|
|
1665
1617
|
}
|
|
1666
1618
|
const userPromptText = userPromptParts.join(" - ");
|
|
1667
1619
|
const numImages = options?.num || config.defaultNumImages;
|
|
1668
|
-
const limitCheck = await checkDailyLimit(session.userId, numImages);
|
|
1620
|
+
const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
|
|
1669
1621
|
if (!limitCheck.allowed) {
|
|
1670
1622
|
return limitCheck.message;
|
|
1671
1623
|
}
|
|
@@ -1701,7 +1653,7 @@ ${infoParts.join("\n")}`;
|
|
|
1701
1653
|
ctx.command(`${COMMANDS.TXT_TO_IMG} [prompt:text]`, "根据文字描述生成图像").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async ({ session, options }, prompt) => {
|
|
1702
1654
|
if (!session?.userId) return "会话无效";
|
|
1703
1655
|
const numImages = options?.num || config.defaultNumImages;
|
|
1704
|
-
const limitCheck = await checkDailyLimit(session.userId, numImages);
|
|
1656
|
+
const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
|
|
1705
1657
|
if (!limitCheck.allowed) {
|
|
1706
1658
|
return limitCheck.message;
|
|
1707
1659
|
}
|
|
@@ -1714,7 +1666,7 @@ ${infoParts.join("\n")}`;
|
|
|
1714
1666
|
if (!session?.userId) return "会话无效";
|
|
1715
1667
|
const numImages = options?.num || config.defaultNumImages;
|
|
1716
1668
|
const mode = options?.multiple ? "multiple" : "single";
|
|
1717
|
-
const limitCheck = await checkDailyLimit(session.userId, numImages);
|
|
1669
|
+
const limitCheck = await userManager.checkDailyLimit(session.userId, config, numImages);
|
|
1718
1670
|
if (!limitCheck.allowed) {
|
|
1719
1671
|
return limitCheck.message;
|
|
1720
1672
|
}
|
|
@@ -1725,106 +1677,106 @@ ${infoParts.join("\n")}`;
|
|
|
1725
1677
|
});
|
|
1726
1678
|
ctx.command(COMMANDS.COMPOSE_IMAGE, "合成多张图片,使用自定义prompt控制合成效果").option("num", "-n <num:number> 生成图片数量 (1-4)").action(async ({ session, options }) => {
|
|
1727
1679
|
if (!session?.userId) return "会话无效";
|
|
1680
|
+
const userId = session.userId;
|
|
1681
|
+
if (!userManager.startTask(userId)) {
|
|
1682
|
+
return "您有一个图像处理任务正在进行中,请等待完成";
|
|
1683
|
+
}
|
|
1684
|
+
userManager.endTask(userId);
|
|
1685
|
+
let isTimeout = false;
|
|
1728
1686
|
return Promise.race([
|
|
1729
1687
|
(async () => {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1688
|
+
if (!userManager.startTask(userId)) return "您有一个图像处理任务正在进行中";
|
|
1689
|
+
try {
|
|
1690
|
+
await session.send("多张图片+描述");
|
|
1691
|
+
const collectedImages = [];
|
|
1692
|
+
let prompt = "";
|
|
1693
|
+
while (true) {
|
|
1694
|
+
const msg = await session.prompt(6e4);
|
|
1695
|
+
if (!msg) {
|
|
1696
|
+
return "等待超时,请重试";
|
|
1697
|
+
}
|
|
1698
|
+
if (isTimeout) throw new Error("命令执行超时");
|
|
1699
|
+
const elements = import_koishi2.h.parse(msg);
|
|
1700
|
+
const images = import_koishi2.h.select(elements, "img");
|
|
1701
|
+
const textElements = import_koishi2.h.select(elements, "text");
|
|
1702
|
+
const text = textElements.map((el) => el.attrs.content).join(" ").trim();
|
|
1703
|
+
if (images.length > 0) {
|
|
1704
|
+
for (const img of images) {
|
|
1705
|
+
collectedImages.push(img.attrs.src);
|
|
1706
|
+
}
|
|
1707
|
+
if (text) {
|
|
1708
|
+
prompt = text;
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
await session.send(`已收到 ${collectedImages.length} 张图片,继续发送或输入描述`);
|
|
1712
|
+
continue;
|
|
1750
1713
|
}
|
|
1751
1714
|
if (text) {
|
|
1715
|
+
if (collectedImages.length < 2) {
|
|
1716
|
+
return `需要至少两张图片进行合成,当前只有 ${collectedImages.length} 张图片`;
|
|
1717
|
+
}
|
|
1752
1718
|
prompt = text;
|
|
1753
1719
|
break;
|
|
1754
1720
|
}
|
|
1755
|
-
|
|
1756
|
-
continue;
|
|
1721
|
+
return "未检测到有效内容,操作已取消";
|
|
1757
1722
|
}
|
|
1758
|
-
if (
|
|
1759
|
-
|
|
1760
|
-
return `需要至少两张图片进行合成,当前只有 ${collectedImages.length} 张图片`;
|
|
1761
|
-
}
|
|
1762
|
-
prompt = text;
|
|
1763
|
-
break;
|
|
1723
|
+
if (collectedImages.length < 2) {
|
|
1724
|
+
return "需要至少两张图片进行合成,请重新发送";
|
|
1764
1725
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
numImages: imageCount,
|
|
1786
|
-
imageCount: collectedImages.length
|
|
1787
|
-
});
|
|
1788
|
-
await session.send(`开始合成图(${collectedImages.length}张)...
|
|
1726
|
+
if (!prompt) {
|
|
1727
|
+
return "未检测到prompt描述,请重新发送";
|
|
1728
|
+
}
|
|
1729
|
+
const imageCount = options?.num || config.defaultNumImages;
|
|
1730
|
+
if (imageCount < 1 || imageCount > 4) {
|
|
1731
|
+
return "生成数量必须在 1-4 之间";
|
|
1732
|
+
}
|
|
1733
|
+
const limitCheck = await userManager.checkDailyLimit(userId, config, imageCount);
|
|
1734
|
+
if (!limitCheck.allowed) {
|
|
1735
|
+
return limitCheck.message;
|
|
1736
|
+
}
|
|
1737
|
+
if (isTimeout) throw new Error("命令执行超时");
|
|
1738
|
+
logger.info("开始图片合成处理", {
|
|
1739
|
+
userId,
|
|
1740
|
+
imageUrls: collectedImages,
|
|
1741
|
+
prompt,
|
|
1742
|
+
numImages: imageCount,
|
|
1743
|
+
imageCount: collectedImages.length
|
|
1744
|
+
});
|
|
1745
|
+
await session.send(`开始合成图(${collectedImages.length}张)...
|
|
1789
1746
|
Prompt: ${prompt}`);
|
|
1790
|
-
try {
|
|
1791
|
-
activeTasks.set(userId, "processing");
|
|
1792
1747
|
const resultImages = await requestProviderImages(prompt, collectedImages, imageCount);
|
|
1748
|
+
if (isTimeout) throw new Error("命令执行超时");
|
|
1793
1749
|
if (resultImages.length === 0) {
|
|
1794
|
-
activeTasks.delete(userId);
|
|
1795
1750
|
return "图片合成失败:未能生成图片";
|
|
1796
1751
|
}
|
|
1752
|
+
await recordUserUsage(session, COMMANDS.COMPOSE_IMAGE, resultImages.length);
|
|
1797
1753
|
await session.send("图片合成完成!");
|
|
1798
1754
|
for (let i = 0; i < resultImages.length; i++) {
|
|
1799
|
-
|
|
1755
|
+
if (isTimeout) break;
|
|
1756
|
+
try {
|
|
1757
|
+
await Promise.race([
|
|
1758
|
+
session.send(import_koishi2.h.image(resultImages[i])),
|
|
1759
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("SendTimeout")), 2e4))
|
|
1760
|
+
]);
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
logger.warn(`图片合成发送可能超时 (用户: ${userId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1763
|
+
}
|
|
1800
1764
|
if (resultImages.length > 1 && i < resultImages.length - 1) {
|
|
1801
1765
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1802
1766
|
}
|
|
1803
1767
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
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 "图片合成失败,请稍后重试";
|
|
1768
|
+
} finally {
|
|
1769
|
+
userManager.endTask(userId);
|
|
1820
1770
|
}
|
|
1821
1771
|
})(),
|
|
1822
1772
|
new Promise(
|
|
1823
|
-
(_, reject) => setTimeout(() =>
|
|
1773
|
+
(_, reject) => setTimeout(() => {
|
|
1774
|
+
isTimeout = true;
|
|
1775
|
+
reject(new Error("命令执行超时"));
|
|
1776
|
+
}, config.commandTimeout * 1e3)
|
|
1824
1777
|
)
|
|
1825
1778
|
]).catch(async (error) => {
|
|
1826
|
-
|
|
1827
|
-
if (userId) activeTasks.delete(userId);
|
|
1779
|
+
if (userId) userManager.endTask(userId);
|
|
1828
1780
|
const sanitizedError = sanitizeError(error);
|
|
1829
1781
|
logger.error("图片合成超时或失败", { userId, error: sanitizedError });
|
|
1830
1782
|
if (error?.message !== "命令执行超时") {
|
|
@@ -1841,14 +1793,14 @@ Prompt: ${prompt}`);
|
|
|
1841
1793
|
});
|
|
1842
1794
|
ctx.command(`${COMMANDS.RECHARGE} [content:text]`, "为用户充值次数(仅管理员)").action(async ({ session }, content) => {
|
|
1843
1795
|
if (!session?.userId) return "会话无效";
|
|
1844
|
-
if (!isAdmin(session.userId)) {
|
|
1796
|
+
if (!userManager.isAdmin(session.userId, config)) {
|
|
1845
1797
|
return "权限不足,仅管理员可操作";
|
|
1846
1798
|
}
|
|
1847
1799
|
const inputContent = content || await getPromptInput(session, "请输入充值信息,格式:\n@用户1 @用户2 充值次数 [备注]");
|
|
1848
1800
|
if (!inputContent) return "输入超时或无效";
|
|
1849
|
-
const elements =
|
|
1850
|
-
const atElements =
|
|
1851
|
-
const textElements =
|
|
1801
|
+
const elements = import_koishi2.h.parse(inputContent);
|
|
1802
|
+
const atElements = import_koishi2.h.select(elements, "at");
|
|
1803
|
+
const textElements = import_koishi2.h.select(elements, "text");
|
|
1852
1804
|
const text = textElements.map((el) => el.attrs.content).join(" ").trim();
|
|
1853
1805
|
if (atElements.length === 0) {
|
|
1854
1806
|
return "未找到@用户,请使用@用户的方式";
|
|
@@ -1867,66 +1819,64 @@ Prompt: ${prompt}`);
|
|
|
1867
1819
|
return "未找到有效的用户,请使用@用户的方式";
|
|
1868
1820
|
}
|
|
1869
1821
|
try {
|
|
1870
|
-
const usersData = await loadUsersData();
|
|
1871
|
-
const rechargeHistory = await loadRechargeHistory();
|
|
1872
1822
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1873
1823
|
const recordId = `recharge_${now.replace(/[-:T.]/g, "").slice(0, 14)}_${Math.random().toString(36).substr(2, 3)}`;
|
|
1874
1824
|
const targets = [];
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
userName =
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1825
|
+
let totalAmount = 0;
|
|
1826
|
+
await userManager.updateUsersBatch((usersData) => {
|
|
1827
|
+
for (const userId of userIds) {
|
|
1828
|
+
if (!userId) continue;
|
|
1829
|
+
let userName = userId;
|
|
1830
|
+
if (usersData[userId]) {
|
|
1831
|
+
userName = usersData[userId].userName || userId;
|
|
1832
|
+
} else {
|
|
1833
|
+
usersData[userId] = {
|
|
1834
|
+
userId,
|
|
1835
|
+
userName: userId,
|
|
1836
|
+
totalUsageCount: 0,
|
|
1837
|
+
dailyUsageCount: 0,
|
|
1838
|
+
lastDailyReset: now,
|
|
1839
|
+
purchasedCount: 0,
|
|
1840
|
+
remainingPurchasedCount: 0,
|
|
1841
|
+
donationCount: 0,
|
|
1842
|
+
donationAmount: 0,
|
|
1843
|
+
lastUsed: now,
|
|
1844
|
+
createdAt: now
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const beforeBalance = usersData[userId].remainingPurchasedCount;
|
|
1848
|
+
usersData[userId].purchasedCount += amount;
|
|
1849
|
+
usersData[userId].remainingPurchasedCount += amount;
|
|
1850
|
+
targets.push({
|
|
1883
1851
|
userId,
|
|
1884
|
-
userName
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
remainingPurchasedCount: 0,
|
|
1890
|
-
donationCount: 0,
|
|
1891
|
-
donationAmount: 0,
|
|
1892
|
-
lastUsed: now,
|
|
1893
|
-
createdAt: now
|
|
1894
|
-
};
|
|
1852
|
+
userName,
|
|
1853
|
+
amount,
|
|
1854
|
+
beforeBalance,
|
|
1855
|
+
afterBalance: usersData[userId].remainingPurchasedCount
|
|
1856
|
+
});
|
|
1895
1857
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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 = {
|
|
1858
|
+
totalAmount = amount * targets.length;
|
|
1859
|
+
});
|
|
1860
|
+
await userManager.addRechargeRecord({
|
|
1909
1861
|
id: recordId,
|
|
1910
1862
|
timestamp: now,
|
|
1911
|
-
type:
|
|
1863
|
+
type: targets.length > 1 ? "batch" : "single",
|
|
1912
1864
|
operator: {
|
|
1913
1865
|
userId: session.userId,
|
|
1914
1866
|
userName: session.username || session.userId
|
|
1915
1867
|
},
|
|
1916
1868
|
targets,
|
|
1917
|
-
totalAmount
|
|
1918
|
-
note
|
|
1869
|
+
totalAmount,
|
|
1870
|
+
note,
|
|
1919
1871
|
metadata: {}
|
|
1920
|
-
};
|
|
1921
|
-
rechargeHistory.records.push(record);
|
|
1922
|
-
await saveRechargeHistory(rechargeHistory);
|
|
1872
|
+
});
|
|
1923
1873
|
const userList = targets.map((t) => `${t.userName}(${t.afterBalance}次)`).join(", ");
|
|
1924
1874
|
return `✅ 充值成功
|
|
1925
1875
|
目标用户:${userList}
|
|
1926
1876
|
充值次数:${amount}次/人
|
|
1927
|
-
总充值:${
|
|
1928
|
-
操作员:${
|
|
1929
|
-
备注:${
|
|
1877
|
+
总充值:${totalAmount}次
|
|
1878
|
+
操作员:${session.username}
|
|
1879
|
+
备注:${note}`;
|
|
1930
1880
|
} catch (error) {
|
|
1931
1881
|
logger.error("充值操作失败", error);
|
|
1932
1882
|
return "充值失败,请稍后重试";
|
|
@@ -1934,13 +1884,13 @@ Prompt: ${prompt}`);
|
|
|
1934
1884
|
});
|
|
1935
1885
|
ctx.command(`${COMMANDS.RECHARGE_ALL} [content:text]`, "为所有用户充值次数(活动派发,仅管理员)").action(async ({ session }, content) => {
|
|
1936
1886
|
if (!session?.userId) return "会话无效";
|
|
1937
|
-
if (!isAdmin(session.userId)) {
|
|
1887
|
+
if (!userManager.isAdmin(session.userId, config)) {
|
|
1938
1888
|
return "权限不足,仅管理员可操作";
|
|
1939
1889
|
}
|
|
1940
1890
|
const inputContent = content || await getPromptInput(session, "请输入活动充值信息,格式:\n充值次数 [备注]\n例如:20 或 20 春节活动奖励");
|
|
1941
1891
|
if (!inputContent) return "输入超时或无效";
|
|
1942
|
-
const elements =
|
|
1943
|
-
const textElements =
|
|
1892
|
+
const elements = import_koishi2.h.parse(inputContent);
|
|
1893
|
+
const textElements = import_koishi2.h.select(elements, "text");
|
|
1944
1894
|
const text = textElements.map((el) => el.attrs.content).join(" ").trim();
|
|
1945
1895
|
const parts = text.split(/\s+/).filter((p) => p);
|
|
1946
1896
|
if (parts.length === 0) {
|
|
@@ -1952,32 +1902,34 @@ Prompt: ${prompt}`);
|
|
|
1952
1902
|
return "充值次数必须大于0";
|
|
1953
1903
|
}
|
|
1954
1904
|
try {
|
|
1955
|
-
const usersData = await loadUsersData();
|
|
1956
|
-
const rechargeHistory = await loadRechargeHistory();
|
|
1957
1905
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1958
1906
|
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
1907
|
const targets = [];
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
const
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1908
|
+
let totalAmount = 0;
|
|
1909
|
+
let successCount = 0;
|
|
1910
|
+
await userManager.updateUsersBatch((usersData) => {
|
|
1911
|
+
const allUserIds = Object.keys(usersData);
|
|
1912
|
+
for (const userId of allUserIds) {
|
|
1913
|
+
if (!userId || !usersData[userId]) continue;
|
|
1914
|
+
const userData = usersData[userId];
|
|
1915
|
+
const beforeBalance = userData.remainingPurchasedCount;
|
|
1916
|
+
userData.purchasedCount += amount;
|
|
1917
|
+
userData.remainingPurchasedCount += amount;
|
|
1918
|
+
targets.push({
|
|
1919
|
+
userId,
|
|
1920
|
+
userName: userData.userName || userId,
|
|
1921
|
+
amount,
|
|
1922
|
+
beforeBalance,
|
|
1923
|
+
afterBalance: userData.remainingPurchasedCount
|
|
1924
|
+
});
|
|
1925
|
+
successCount++;
|
|
1926
|
+
}
|
|
1927
|
+
totalAmount = amount * successCount;
|
|
1928
|
+
});
|
|
1929
|
+
if (successCount === 0) {
|
|
1930
|
+
return "当前没有使用过插件的用户,无法进行活动充值";
|
|
1978
1931
|
}
|
|
1979
|
-
await
|
|
1980
|
-
const record = {
|
|
1932
|
+
await userManager.addRechargeRecord({
|
|
1981
1933
|
id: recordId,
|
|
1982
1934
|
timestamp: now,
|
|
1983
1935
|
type: "all",
|
|
@@ -1986,18 +1938,16 @@ Prompt: ${prompt}`);
|
|
|
1986
1938
|
userName: session.username || session.userId
|
|
1987
1939
|
},
|
|
1988
1940
|
targets,
|
|
1989
|
-
totalAmount
|
|
1990
|
-
note
|
|
1941
|
+
totalAmount,
|
|
1942
|
+
note,
|
|
1991
1943
|
metadata: { all: true }
|
|
1992
|
-
};
|
|
1993
|
-
rechargeHistory.records.push(record);
|
|
1994
|
-
await saveRechargeHistory(rechargeHistory);
|
|
1944
|
+
});
|
|
1995
1945
|
return `✅ 活动充值成功
|
|
1996
|
-
目标用户数:${
|
|
1946
|
+
目标用户数:${successCount}人
|
|
1997
1947
|
充值次数:${amount}次/人
|
|
1998
|
-
总充值:${
|
|
1999
|
-
操作员:${
|
|
2000
|
-
备注:${
|
|
1948
|
+
总充值:${totalAmount}次
|
|
1949
|
+
操作员:${session.username}
|
|
1950
|
+
备注:${note}`;
|
|
2001
1951
|
} catch (error) {
|
|
2002
1952
|
logger.error("活动充值操作失败", error);
|
|
2003
1953
|
return "活动充值失败,请稍后重试";
|
|
@@ -2005,7 +1955,7 @@ Prompt: ${prompt}`);
|
|
|
2005
1955
|
});
|
|
2006
1956
|
ctx.command(`${COMMANDS.QUERY_QUOTA} [target:text]`, "查询用户额度信息").action(async ({ session }, target) => {
|
|
2007
1957
|
if (!session?.userId) return "会话无效";
|
|
2008
|
-
const userIsAdmin = isAdmin(session.userId);
|
|
1958
|
+
const userIsAdmin = userManager.isAdmin(session.userId, config);
|
|
2009
1959
|
let targetUserId = session.userId;
|
|
2010
1960
|
let targetUserName = session.username || session.userId;
|
|
2011
1961
|
if (target && userIsAdmin) {
|
|
@@ -2018,15 +1968,7 @@ Prompt: ${prompt}`);
|
|
|
2018
1968
|
return "权限不足,仅管理员可查询其他用户";
|
|
2019
1969
|
}
|
|
2020
1970
|
try {
|
|
2021
|
-
const
|
|
2022
|
-
const userData = usersData[targetUserId];
|
|
2023
|
-
if (!userData) {
|
|
2024
|
-
return `👤 用户信息
|
|
2025
|
-
用户:${targetUserName}
|
|
2026
|
-
状态:新用户
|
|
2027
|
-
今日剩余免费:${config.dailyFreeLimit}次
|
|
2028
|
-
充值剩余:0次`;
|
|
2029
|
-
}
|
|
1971
|
+
const userData = await userManager.getUserData(targetUserId, targetUserName);
|
|
2030
1972
|
const remainingToday = Math.max(0, config.dailyFreeLimit - userData.dailyUsageCount);
|
|
2031
1973
|
const totalAvailable = remainingToday + userData.remainingPurchasedCount;
|
|
2032
1974
|
return `👤 用户额度信息
|
|
@@ -2043,11 +1985,11 @@ Prompt: ${prompt}`);
|
|
|
2043
1985
|
});
|
|
2044
1986
|
ctx.command(`${COMMANDS.RECHARGE_HISTORY} [page:number]`, "查看充值历史记录(仅管理员)").action(async ({ session }, page = 1) => {
|
|
2045
1987
|
if (!session?.userId) return "会话无效";
|
|
2046
|
-
if (!isAdmin(session.userId)) {
|
|
1988
|
+
if (!userManager.isAdmin(session.userId, config)) {
|
|
2047
1989
|
return "权限不足,仅管理员可查看充值记录";
|
|
2048
1990
|
}
|
|
2049
1991
|
try {
|
|
2050
|
-
const history = await loadRechargeHistory();
|
|
1992
|
+
const history = await userManager.loadRechargeHistory();
|
|
2051
1993
|
const pageSize = 10;
|
|
2052
1994
|
const totalPages = Math.ceil(history.records.length / pageSize);
|
|
2053
1995
|
const startIndex = (page - 1) * pageSize;
|
|
@@ -2081,7 +2023,7 @@ Prompt: ${prompt}`);
|
|
|
2081
2023
|
ctx.command(COMMANDS.FUNCTION_LIST, "查看所有可用的图像处理功能").action(async ({ session }) => {
|
|
2082
2024
|
if (!session?.userId) return "会话无效";
|
|
2083
2025
|
try {
|
|
2084
|
-
const userIsAdmin = isAdmin(session.userId);
|
|
2026
|
+
const userIsAdmin = userManager.isAdmin(session.userId, config);
|
|
2085
2027
|
let result = "🎨 图像处理功能列表\n\n";
|
|
2086
2028
|
result += "📝 用户指令:\n";
|
|
2087
2029
|
commandRegistry.userCommands.forEach((cmd) => {
|