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 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
- ...await this.getImageFingerprints(await import_promises.default.readFile(fullPath)),
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
- await this.backfillImageFingerprints();
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
- const uploaded = [];
1838
- for (const item of items) {
1839
- const base64 = toTrimmedString(item.base64);
1840
- const originalName = toTrimmedString(item.originalName);
1841
- if (!base64) continue;
1842
- const saved = await service.addLocalImageBase64(collectionName, base64, originalName || void 0);
1843
- uploaded.push(saved);
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 base64 = buffer.toString("base64");
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-memesluna",
3
3
  "description": "Image Forward service for Koishi with ChatLuna integration",
4
- "version": "0.5.1",
4
+ "version": "0.5.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",