koishi-plugin-memesluna 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +99 -16
- package/lib/service.d.ts +4 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -293,7 +293,6 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
293
293
|
}
|
|
294
294
|
if (!existingFilenames.has(currentName)) {
|
|
295
295
|
const index = ++maxIndex;
|
|
296
|
-
const fullPath = import_path.default.join(colDir, currentName);
|
|
297
296
|
try {
|
|
298
297
|
await this.ctx.database.create("memesluna_images", {
|
|
299
298
|
id: (0, import_crypto.randomUUID)(),
|
|
@@ -303,7 +302,8 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
303
302
|
type: "local",
|
|
304
303
|
value: currentName,
|
|
305
304
|
mime: this.getMimeByFilename(currentName),
|
|
306
|
-
|
|
305
|
+
hash: "",
|
|
306
|
+
perceptual_hash: "",
|
|
307
307
|
created_at: /* @__PURE__ */ new Date()
|
|
308
308
|
});
|
|
309
309
|
existingFilenames.add(currentName);
|
|
@@ -343,7 +343,9 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
343
343
|
await import_promises.default.mkdir(this.getStorageRoot(), { recursive: true });
|
|
344
344
|
await import_promises.default.mkdir(this.getStagingDir(), { recursive: true });
|
|
345
345
|
await this.syncExistingFilesToDatabase();
|
|
346
|
-
|
|
346
|
+
this.backfillImageFingerprints().catch((err) => {
|
|
347
|
+
this.ctx.logger("memesluna").warn("Failed to backfill image fingerprints in background:", err);
|
|
348
|
+
});
|
|
347
349
|
}
|
|
348
350
|
async getImagePerceptualHash(buffer) {
|
|
349
351
|
const photon = loadPhoton();
|
|
@@ -911,6 +913,78 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
911
913
|
const buffer = Buffer.from(base64, "base64");
|
|
912
914
|
return this.addLocalImageBuffer(collectionName, buffer, originalName, extHint);
|
|
913
915
|
}
|
|
916
|
+
async addLocalImagesBase64(collectionName, images) {
|
|
917
|
+
this.ensureCollectionName(collectionName);
|
|
918
|
+
if (!await this.collectionExists(collectionName)) {
|
|
919
|
+
throw new Error(`Collection not found: ${collectionName}`);
|
|
920
|
+
}
|
|
921
|
+
const maxImg = await this.ctx.database.get("memesluna_images", { collection: collectionName }, { limit: 1, sort: { index: "desc" } });
|
|
922
|
+
let nextIndex = maxImg.length ? maxImg[0].index + 1 : 1;
|
|
923
|
+
const backend = this.getStorageBackend();
|
|
924
|
+
const results = [];
|
|
925
|
+
const processPromises = images.map(async (img) => {
|
|
926
|
+
const { base64, extHint } = this.normalizeBase64(img.base64Data);
|
|
927
|
+
const buffer = Buffer.from(base64, "base64");
|
|
928
|
+
if (!buffer.length) {
|
|
929
|
+
throw new Error("Invalid image payload");
|
|
930
|
+
}
|
|
931
|
+
const fingerprints = await this.getImageFingerprints(buffer);
|
|
932
|
+
const duplicate = await this.getDuplicateImageByHash(fingerprints.hash, { includeStaged: false, includeImages: true });
|
|
933
|
+
if (duplicate) {
|
|
934
|
+
throw new Error("Duplicate image already exists: " + duplicate);
|
|
935
|
+
}
|
|
936
|
+
const rawExt = (import_path.default.parse(img.originalName ?? "").ext || (extHint ? `.${extHint}` : "") || ".png").toLowerCase();
|
|
937
|
+
const finalExt = rawExt === ".jpeg" ? ".jpg" : rawExt;
|
|
938
|
+
if (!IMAGE_EXTENSIONS.has(finalExt)) {
|
|
939
|
+
throw new Error("Unsupported image format");
|
|
940
|
+
}
|
|
941
|
+
const baseName = img.originalName ? import_path.default.basename(img.originalName, import_path.default.extname(img.originalName)) : `image-${Date.now()}`;
|
|
942
|
+
const safeBase = sanitizeFilename(`${baseName}${finalExt}`);
|
|
943
|
+
return {
|
|
944
|
+
buffer,
|
|
945
|
+
fingerprints,
|
|
946
|
+
safeBase
|
|
947
|
+
};
|
|
948
|
+
});
|
|
949
|
+
const processed = await Promise.all(processPromises);
|
|
950
|
+
const databaseRows = [];
|
|
951
|
+
for (const p of processed) {
|
|
952
|
+
const index = nextIndex++;
|
|
953
|
+
const finalName = await this.deduplicateDatabaseFilename(collectionName, p.safeBase);
|
|
954
|
+
if (backend) {
|
|
955
|
+
const result = await backend.upload(p.buffer, finalName);
|
|
956
|
+
databaseRows.push({
|
|
957
|
+
id: (0, import_crypto.randomUUID)(),
|
|
958
|
+
collection: collectionName,
|
|
959
|
+
index,
|
|
960
|
+
filename: finalName,
|
|
961
|
+
type: "storage",
|
|
962
|
+
value: result.key,
|
|
963
|
+
public_url: result.publicUrl || "",
|
|
964
|
+
mime: this.getMimeByFilename(finalName),
|
|
965
|
+
...p.fingerprints,
|
|
966
|
+
created_at: /* @__PURE__ */ new Date()
|
|
967
|
+
});
|
|
968
|
+
} else {
|
|
969
|
+
const dir = this.getCollectionDir(collectionName);
|
|
970
|
+
await import_promises.default.writeFile(import_path.default.join(dir, finalName), p.buffer);
|
|
971
|
+
databaseRows.push({
|
|
972
|
+
id: (0, import_crypto.randomUUID)(),
|
|
973
|
+
collection: collectionName,
|
|
974
|
+
index,
|
|
975
|
+
filename: finalName,
|
|
976
|
+
type: "local",
|
|
977
|
+
value: finalName,
|
|
978
|
+
mime: this.getMimeByFilename(finalName),
|
|
979
|
+
...p.fingerprints,
|
|
980
|
+
created_at: /* @__PURE__ */ new Date()
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
results.push(finalName);
|
|
984
|
+
}
|
|
985
|
+
await Promise.all(databaseRows.map((row) => this.ctx.database.create("memesluna_images", row)));
|
|
986
|
+
return results;
|
|
987
|
+
}
|
|
914
988
|
async addStagedImageBuffer(buffer, originalName, source = "filter", reason = "") {
|
|
915
989
|
if (!buffer.length) {
|
|
916
990
|
throw new Error("Invalid image payload");
|
|
@@ -1450,6 +1524,13 @@ function hitDailyAutoCollectLimit(groupId, limit) {
|
|
|
1450
1524
|
return false;
|
|
1451
1525
|
}
|
|
1452
1526
|
__name(hitDailyAutoCollectLimit, "hitDailyAutoCollectLimit");
|
|
1527
|
+
function isDailyAutoCollectLimitReached(groupId, limit) {
|
|
1528
|
+
const day = getDailyKey();
|
|
1529
|
+
const current = autoCollectDailyLimits.get(groupId);
|
|
1530
|
+
if (!current || current.day !== day) return false;
|
|
1531
|
+
return current.count >= limit;
|
|
1532
|
+
}
|
|
1533
|
+
__name(isDailyAutoCollectLimitReached, "isDailyAutoCollectLimitReached");
|
|
1453
1534
|
function trackImageFrequency(hash, groupId, windowMs) {
|
|
1454
1535
|
const now = Date.now();
|
|
1455
1536
|
const key = `${groupId}:${hash}`;
|
|
@@ -1652,6 +1733,7 @@ function applyAutoCollect(ctx, config) {
|
|
|
1652
1733
|
const groupId = getSessionGroupId(session);
|
|
1653
1734
|
if (!groupId) return;
|
|
1654
1735
|
if (whitelist.size && !whitelist.has(groupId)) return;
|
|
1736
|
+
if (isDailyAutoCollectLimitReached(groupId, dailyLimit)) return;
|
|
1655
1737
|
const imageUrls = getMessageImages(session);
|
|
1656
1738
|
if (!imageUrls.length) return;
|
|
1657
1739
|
const service = ctx.memesluna;
|
|
@@ -1834,18 +1916,20 @@ function applyServer(ctx, config, service) {
|
|
|
1834
1916
|
koa.body = { error: "No images provided" };
|
|
1835
1917
|
return;
|
|
1836
1918
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const
|
|
1843
|
-
|
|
1919
|
+
try {
|
|
1920
|
+
const imagesToUpload = items.map((item) => ({
|
|
1921
|
+
base64Data: toTrimmedString(item.base64),
|
|
1922
|
+
originalName: toTrimmedString(item.originalName) || void 0
|
|
1923
|
+
})).filter((img) => img.base64Data);
|
|
1924
|
+
const uploaded = await service.addLocalImagesBase64(collectionName, imagesToUpload);
|
|
1925
|
+
koa.body = {
|
|
1926
|
+
ok: true,
|
|
1927
|
+
uploaded
|
|
1928
|
+
};
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
koa.status = 400;
|
|
1931
|
+
koa.body = { error: error.message || "Failed to upload images" };
|
|
1844
1932
|
}
|
|
1845
|
-
koa.body = {
|
|
1846
|
-
ok: true,
|
|
1847
|
-
uploaded
|
|
1848
|
-
};
|
|
1849
1933
|
});
|
|
1850
1934
|
ctx.server.delete(`${basePath}/api/admin/collections/:name/images/:filename`, async (koa) => {
|
|
1851
1935
|
const collectionName = toTrimmedString(koa.params.name);
|
|
@@ -2119,8 +2203,7 @@ function apply(ctx, config) {
|
|
|
2119
2203
|
if (!ext) {
|
|
2120
2204
|
return "图片格式不兼容,仅支持 JPG/PNG/GIF/WEBP/BMP 格式图片(已拒绝 AVIF,且不会放入暂缓区)";
|
|
2121
2205
|
}
|
|
2122
|
-
const
|
|
2123
|
-
const filename = await service.addLocalImageBase64(name2, base64, `stole${ext}`);
|
|
2206
|
+
const filename = await service.addLocalImageBuffer(name2, buffer, `stole${ext}`);
|
|
2124
2207
|
savedFilenames.push(filename);
|
|
2125
2208
|
successCount++;
|
|
2126
2209
|
} catch (err) {
|
package/lib/service.d.ts
CHANGED
|
@@ -121,6 +121,10 @@ export declare class MemesLunaService extends Service {
|
|
|
121
121
|
private deduplicateDatabaseFilename;
|
|
122
122
|
addLocalImageBuffer(collectionName: string, buffer: Buffer, originalName?: string, extHint?: string): Promise<string>;
|
|
123
123
|
addLocalImageBase64(collectionName: string, base64Data: string, originalName?: string): Promise<string>;
|
|
124
|
+
addLocalImagesBase64(collectionName: string, images: Array<{
|
|
125
|
+
base64Data: string;
|
|
126
|
+
originalName?: string;
|
|
127
|
+
}>): Promise<string[]>;
|
|
124
128
|
addStagedImageBuffer(buffer: Buffer, originalName?: string, source?: string, reason?: string): Promise<StagedImageInfo>;
|
|
125
129
|
addStagedImageBase64(base64Data: string, originalName?: string, source?: string, reason?: string): Promise<StagedImageInfo>;
|
|
126
130
|
getStagedImages(): Promise<StagedImageInfo[]>;
|
package/package.json
CHANGED