koishi-plugin-echo-cave 1.10.4 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
+ import { Config } from './index';
1
2
  import { CQCode } from '@pynickle/koishi-plugin-adapter-onebot';
2
3
  import { Message } from '@pynickle/koishi-plugin-adapter-onebot/lib/types';
3
4
  import { Context } from 'koishi';
4
- export declare function reconstructForwardMsg(ctx: Context, message: Message[]): Promise<CQCode[]>;
5
+ export declare function reconstructForwardMsg(ctx: Context, message: Message[], cfg: Config): Promise<CQCode[]>;
@@ -1,2 +1,5 @@
1
+ import { Config } from './index';
1
2
  import { Context } from 'koishi';
2
- export declare function saveImages(ctx: Context, imgElement: Record<string, any>): Promise<string>;
3
+ export declare function saveMedia(ctx: Context, mediaElement: Record<string, any>, type: 'image' | 'video' | 'file', cfg: Config): Promise<string>;
4
+ export declare function processMediaElement(ctx: Context, element: any, cfg: Config): Promise<any>;
5
+ export declare function checkAndCleanMediaFiles(ctx: Context, cfg: Config, type: 'image' | 'video' | 'file'): Promise<void>;
package/lib/index.cjs CHANGED
@@ -36,7 +36,11 @@ var require_zh_CN = __commonJS({
36
36
  _config: {
37
37
  adminMessageProtection: "\u5F00\u542F\u7BA1\u7406\u5458\u6D88\u606F\u4FDD\u62A4\uFF0C\u5F00\u542F\u540E\u7BA1\u7406\u5458\u53D1\u5E03\u7684\u6D88\u606F\u53EA\u80FD\u7531\u7BA1\u7406\u5458\u5220\u9664",
38
38
  allowContributorDelete: "\u5141\u8BB8\u6295\u7A3F\u8005\u5220\u9664\u81EA\u5DF1\u6295\u7A3F\u7684\u56DE\u58F0\u6D1E",
39
- allowSenderDelete: "\u5141\u8BB8\u539F\u59CB\u53D1\u9001\u8005\u5220\u9664\u88AB\u6295\u7A3F\u7684\u56DE\u58F0\u6D1E"
39
+ allowSenderDelete: "\u5141\u8BB8\u539F\u59CB\u53D1\u9001\u8005\u5220\u9664\u88AB\u6295\u7A3F\u7684\u56DE\u58F0\u6D1E",
40
+ enableSizeLimit: "\u662F\u5426\u542F\u7528\u5A92\u4F53\u6587\u4EF6\u5927\u5C0F\u9650\u5236",
41
+ maxImageSize: "\u6700\u5927\u56FE\u7247\u5927\u5C0F (MB)",
42
+ maxVideoSize: "\u6700\u5927\u89C6\u9891\u5927\u5C0F (MB)",
43
+ maxFileSize: "\u6700\u5927\u6587\u4EF6\u5927\u5C0F (MB)"
40
44
  },
41
45
  "echo-cave": {
42
46
  general: {
@@ -219,51 +223,143 @@ function formatDate(date) {
219
223
  var import_axios = __toESM(require("axios"), 1);
220
224
  var import_node_fs = require("node:fs");
221
225
  var import_node_path = __toESM(require("node:path"), 1);
222
- async function saveImages(ctx, imgElement) {
223
- const imgUrl = imgElement.url;
224
- const originalImgName = imgElement.file;
226
+ async function saveMedia(ctx, mediaElement, type, cfg) {
227
+ const mediaUrl = mediaElement.url;
228
+ const originalMediaName = mediaElement.file;
225
229
  const ext = (() => {
226
- const i = originalImgName.lastIndexOf(".");
227
- return i === -1 ? "png" : originalImgName.slice(i + 1).toLowerCase();
230
+ const i = originalMediaName.lastIndexOf(".");
231
+ return i === -1 ? type === "image" ? "png" : type === "video" ? "mp4" : "bin" : originalMediaName.slice(i + 1).toLowerCase();
228
232
  })();
229
- const imgDir = import_node_path.default.join(ctx.baseDir, "data", "cave", "images");
230
- const imgName = Date.now().toString();
231
- const fullImgPath = import_node_path.default.join(imgDir, `${imgName}.${ext}`);
232
- ctx.logger.info(`Saving image from ${imgUrl} -> ${fullImgPath}`);
233
+ const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
234
+ const mediaName = Date.now().toString();
235
+ const fullMediaPath = import_node_path.default.join(mediaDir, `${mediaName}.${ext}`);
236
+ ctx.logger.info(`Saving ${type} from ${mediaUrl} -> ${fullMediaPath}`);
233
237
  try {
234
- await import_node_fs.promises.mkdir(imgDir, { recursive: true });
235
- const res = await import_axios.default.get(imgUrl, {
238
+ await import_node_fs.promises.mkdir(mediaDir, { recursive: true });
239
+ const res = await import_axios.default.get(mediaUrl, {
236
240
  responseType: "arraybuffer",
237
241
  validateStatus: () => true
238
242
  });
239
243
  if (res.status < 200 || res.status >= 300) {
240
- ctx.logger.warn(`Image download failed: HTTP ${res.status}`);
241
- return imgUrl;
244
+ ctx.logger.warn(
245
+ `${type.charAt(0).toUpperCase() + type.slice(1)} download failed: HTTP ${res.status}`
246
+ );
247
+ return mediaUrl;
242
248
  }
243
- const type = res.headers["content-type"];
244
- if (!type || !type.startsWith("image/")) {
245
- ctx.logger.warn(`Invalid image content-type: ${type}`);
246
- return imgUrl;
249
+ const contentType = res.headers["content-type"];
250
+ if (contentType) {
251
+ if (type === "image" && !contentType.startsWith("image/")) {
252
+ ctx.logger.warn(`Invalid image content-type: ${contentType}`);
253
+ return mediaUrl;
254
+ }
255
+ if (type === "video" && !contentType.startsWith("video/")) {
256
+ ctx.logger.warn(`Invalid video content-type: ${contentType}`);
257
+ return mediaUrl;
258
+ }
247
259
  }
248
260
  const buffer = Buffer.from(res.data);
249
261
  if (!buffer || buffer.length === 0) {
250
- ctx.logger.warn("Downloaded image buffer is empty");
251
- return imgUrl;
262
+ ctx.logger.warn(`Downloaded ${type} buffer is empty`);
263
+ return mediaUrl;
264
+ }
265
+ await import_node_fs.promises.writeFile(fullMediaPath, buffer);
266
+ ctx.logger.info(
267
+ `${type.charAt(0).toUpperCase() + type.slice(1)} saved successfully: ${fullMediaPath}`
268
+ );
269
+ await checkAndCleanMediaFiles(ctx, cfg, type);
270
+ return fullMediaPath;
271
+ } catch (err) {
272
+ ctx.logger.error(`Failed to save ${type}: ${err}`);
273
+ return mediaUrl;
274
+ }
275
+ }
276
+ async function processMediaElement(ctx, element, cfg) {
277
+ if (element.type === "image" || element.type === "video" || element.type === "file") {
278
+ return {
279
+ ...element,
280
+ data: {
281
+ ...element.data,
282
+ url: await saveMedia(
283
+ ctx,
284
+ element.data,
285
+ element.type,
286
+ cfg
287
+ )
288
+ }
289
+ };
290
+ }
291
+ return element;
292
+ }
293
+ async function checkAndCleanMediaFiles(ctx, cfg, type) {
294
+ if (!cfg.enableSizeLimit) {
295
+ return;
296
+ }
297
+ const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
298
+ const maxSize = (() => {
299
+ switch (type) {
300
+ case "image":
301
+ return (cfg.maxImageSize || 100) * 1024 * 1024;
302
+ // 转换为字节
303
+ case "video":
304
+ return (cfg.maxVideoSize || 500) * 1024 * 1024;
305
+ case "file":
306
+ return (cfg.maxFileSize || 1e3) * 1024 * 1024;
307
+ }
308
+ })();
309
+ try {
310
+ const files = await import_node_fs.promises.readdir(mediaDir);
311
+ if (files.length === 0) {
312
+ return;
313
+ }
314
+ const fileInfos = await Promise.all(
315
+ files.map(async (file) => {
316
+ const filePath = import_node_path.default.join(mediaDir, file);
317
+ const stats = await import_node_fs.promises.stat(filePath);
318
+ return {
319
+ path: filePath,
320
+ size: stats.size,
321
+ mtime: stats.mtimeMs
322
+ };
323
+ })
324
+ );
325
+ const totalSize = fileInfos.reduce((sum, file) => sum + file.size, 0);
326
+ ctx.logger.info(
327
+ `${type} directory total size: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, max allowed: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
328
+ );
329
+ if (totalSize > maxSize) {
330
+ ctx.logger.warn(
331
+ `${type} directory size exceeds limit! Total: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, Max: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
332
+ );
333
+ fileInfos.sort((a, b) => a.mtime - b.mtime);
334
+ let currentSize = totalSize;
335
+ let filesToDelete = [];
336
+ for (const file of fileInfos) {
337
+ if (currentSize <= maxSize) {
338
+ break;
339
+ }
340
+ filesToDelete.push(file);
341
+ currentSize -= file.size;
342
+ }
343
+ for (const file of filesToDelete) {
344
+ await import_node_fs.promises.unlink(file.path);
345
+ ctx.logger.info(
346
+ `Deleted oldest ${type} file: ${import_node_path.default.basename(file.path)} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`
347
+ );
348
+ }
349
+ ctx.logger.info(
350
+ `Cleanup completed. ${type} directory new size: ${(currentSize / (1024 * 1024)).toFixed(2)} MB`
351
+ );
252
352
  }
253
- await import_node_fs.promises.writeFile(fullImgPath, buffer);
254
- ctx.logger.info(`Image saved successfully: ${fullImgPath}`);
255
- return fullImgPath;
256
353
  } catch (err) {
257
- ctx.logger.error(`Failed to save image: ${err}`);
258
- return imgUrl;
354
+ ctx.logger.error(`Failed to check and clean ${type} files: ${err}`);
259
355
  }
260
356
  }
261
357
 
262
358
  // src/forward-helper.ts
263
- async function reconstructForwardMsg(ctx, message) {
359
+ async function reconstructForwardMsg(ctx, message, cfg) {
264
360
  return Promise.all(
265
361
  message.map(async (msg) => {
266
- const content = await processForwardMessageContent(ctx, msg);
362
+ const content = await processForwardMessageContent(ctx, msg, cfg);
267
363
  return {
268
364
  type: "node",
269
365
  data: {
@@ -275,51 +371,31 @@ async function reconstructForwardMsg(ctx, message) {
275
371
  })
276
372
  );
277
373
  }
278
- async function processForwardMessageContent(ctx, msg) {
374
+ async function processForwardMessageContent(ctx, msg, cfg) {
279
375
  if (typeof msg.message === "string") {
280
376
  return msg.message;
281
377
  }
282
378
  const firstElement = msg.message[0];
283
379
  if (firstElement?.type === "forward") {
284
- return reconstructForwardMsg(ctx, firstElement.data.content);
380
+ return reconstructForwardMsg(ctx, firstElement.data.content, cfg);
285
381
  }
286
382
  return Promise.all(
287
383
  msg.message.map(async (element) => {
288
- if (element.type === "image") {
289
- return {
290
- ...element,
291
- data: {
292
- ...element.data,
293
- url: await saveImages(ctx, element.data)
294
- }
295
- };
296
- }
297
- return element;
384
+ return processMediaElement(ctx, element, cfg);
298
385
  })
299
386
  );
300
387
  }
301
388
 
302
389
  // src/msg-helper.ts
303
- async function processMessageContent(ctx, msg) {
304
- const result = [];
305
- for (const element of msg) {
306
- if (element.type === "reply") {
307
- continue;
308
- }
309
- if (element.type === "image") {
310
- const newUrl = await saveImages(ctx, element.data);
311
- result.push({
312
- ...element,
313
- data: {
314
- ...element.data,
315
- url: newUrl
316
- }
317
- });
318
- } else {
319
- result.push(element);
320
- }
321
- }
322
- return result;
390
+ async function processMessageContent(ctx, msg, cfg) {
391
+ return Promise.all(
392
+ msg.map(async (element) => {
393
+ if (element.type === "reply") {
394
+ return element;
395
+ }
396
+ return processMediaElement(ctx, element, cfg);
397
+ })
398
+ );
323
399
  }
324
400
 
325
401
  // src/index.ts
@@ -332,7 +408,11 @@ var inject = ["database"];
332
408
  var Config = import_koishi.Schema.object({
333
409
  adminMessageProtection: import_koishi.Schema.boolean().default(false),
334
410
  allowContributorDelete: import_koishi.Schema.boolean().default(true),
335
- allowSenderDelete: import_koishi.Schema.boolean().default(true)
411
+ allowSenderDelete: import_koishi.Schema.boolean().default(true),
412
+ enableSizeLimit: import_koishi.Schema.boolean().default(false),
413
+ maxImageSize: import_koishi.Schema.number().default(2048),
414
+ maxVideoSize: import_koishi.Schema.number().default(512),
415
+ maxFileSize: import_koishi.Schema.number().default(512)
336
416
  }).i18n({
337
417
  "zh-CN": require_zh_CN()._config
338
418
  });
@@ -361,7 +441,7 @@ function apply(ctx, cfg) {
361
441
  ctx.command("cave [id:number]").action(
362
442
  async ({ session }, id) => await getCave(ctx, session, id)
363
443
  );
364
- ctx.command("cave.echo").action(async ({ session }) => await addCave(ctx, session));
444
+ ctx.command("cave.echo").action(async ({ session }) => await addCave(ctx, session, cfg));
365
445
  ctx.command("cave.wipe <id:number>").action(
366
446
  async ({ session }, id) => await deleteCave(ctx, session, cfg, id)
367
447
  );
@@ -471,7 +551,7 @@ async function deleteCave(ctx, session, cfg, id) {
471
551
  await ctx.database.remove("echo_cave", id);
472
552
  return session.text(".msgDeleted", [id]);
473
553
  }
474
- async function addCave(ctx, session) {
554
+ async function addCave(ctx, session, cfg) {
475
555
  if (!session.guildId) {
476
556
  return session.text("echo-cave.general.privateChatReminder");
477
557
  }
@@ -486,7 +566,8 @@ async function addCave(ctx, session) {
486
566
  type = "forward";
487
567
  const message = await reconstructForwardMsg(
488
568
  ctx,
489
- await session.onebot.getForwardMsg(messageId)
569
+ await session.onebot.getForwardMsg(messageId),
570
+ cfg
490
571
  );
491
572
  content = JSON.stringify(message);
492
573
  } else {
@@ -496,9 +577,12 @@ async function addCave(ctx, session) {
496
577
  if (typeof message === "string") {
497
578
  msgJson = import_koishi_plugin_adapter_onebot2.CQCode.parse(message);
498
579
  } else {
580
+ if (message[0].type === "video" || message[0].type === "file") {
581
+ type = "forward";
582
+ }
499
583
  msgJson = message;
500
584
  }
501
- content = JSON.stringify(await processMessageContent(ctx, msgJson));
585
+ content = JSON.stringify(await processMessageContent(ctx, msgJson, cfg));
502
586
  }
503
587
  await ctx.database.get("echo_cave", { content }).then((existing) => {
504
588
  if (existing) {
package/lib/index.d.ts CHANGED
@@ -6,6 +6,10 @@ export interface Config {
6
6
  adminMessageProtection?: boolean;
7
7
  allowContributorDelete?: boolean;
8
8
  allowSenderDelete?: boolean;
9
+ enableSizeLimit?: boolean;
10
+ maxImageSize?: number;
11
+ maxVideoSize?: number;
12
+ maxFileSize?: number;
9
13
  }
10
14
  export declare const Config: Schema<Config>;
11
15
  export interface EchoCave {
@@ -1,3 +1,4 @@
1
+ import { Config } from './index';
1
2
  import { CQCode } from '@pynickle/koishi-plugin-adapter-onebot';
2
3
  import { Context } from 'koishi';
3
- export declare function processMessageContent(ctx: Context, msg: CQCode[]): Promise<CQCode[]>;
4
+ export declare function processMessageContent(ctx: Context, msg: CQCode[], cfg: Config): Promise<CQCode[]>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-echo-cave",
3
3
  "description": "Group echo cave",
4
- "version": "1.10.4",
4
+ "version": "1.12.0",
5
5
  "main": "lib/index.cjs",
6
6
  "typings": "lib/index.d.ts",
7
7
  "type": "module",