koishi-plugin-maple-drift-bottle 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "maple-drift-bottle";
3
+ export declare const using: readonly ["database"];
4
+ interface DriftBottle {
5
+ id: number;
6
+ author: string;
7
+ authorName: string;
8
+ anonymous: boolean;
9
+ content: string;
10
+ images: string[];
11
+ created: Date;
12
+ }
13
+ declare module 'koishi' {
14
+ interface Tables {
15
+ 'maple-drift-bottles': DriftBottle;
16
+ }
17
+ }
18
+ export interface Config {
19
+ anonymousByDefault: boolean;
20
+ maxContentLength: number;
21
+ maxPreviewLength: number;
22
+ maxImages: number;
23
+ saveImagesLocally: boolean;
24
+ imageSavePath: string;
25
+ compressImages: boolean;
26
+ maxImageWidth: number;
27
+ maxImageHeight: number;
28
+ imageQuality: number;
29
+ adminQQ: string[];
30
+ }
31
+ export declare const Config: Schema<Config>;
32
+ export declare function apply(ctx: Context, config: Config): void;
33
+ export {};
package/lib/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ var __create = Object.create;
1
2
  var __defProp = Object.defineProperty;
2
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
4
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
7
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
8
  var __export = (target, all) => {
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -27,12 +37,24 @@ __export(src_exports, {
27
37
  });
28
38
  module.exports = __toCommonJS(src_exports);
29
39
  var import_koishi = require("koishi");
40
+ var import_promises = require("fs/promises");
41
+ var import_path = require("path");
42
+ var import_crypto = require("crypto");
43
+ var import_fs = require("fs");
44
+ var import_sharp = __toESM(require("sharp"));
30
45
  var name = "maple-drift-bottle";
31
46
  var using = ["database"];
32
47
  var Config = import_koishi.Schema.object({
33
48
  anonymousByDefault: import_koishi.Schema.boolean().default(false).description("默认是否匿名(true: 匿名,false: 不匿名)"),
34
- maxContentLength: import_koishi.Schema.number().default(500).min(50).max(2e3).description("漂流瓶内容最大长度(字)(50-2000)"),
35
- maxPreviewLength: import_koishi.Schema.number().default(8).min(3).max(50).description('指令"我的漂流瓶"最大预览长度(字)(3-50)'),
49
+ maxContentLength: import_koishi.Schema.number().default(500).min(50).max(2e3).description("漂流瓶文本内容最大长度(字符)"),
50
+ maxPreviewLength: import_koishi.Schema.number().default(8).min(3).max(50).description('指令"我的漂流瓶"最大预览字数'),
51
+ maxImages: import_koishi.Schema.number().default(3).min(0).max(10).description("每一条漂流瓶最多可包含的图片数量(0为不允许发送图片)"),
52
+ saveImagesLocally: import_koishi.Schema.boolean().default(true).description("是否本地保存图片(启用后图片永久保存,不依赖外部URL)"),
53
+ imageSavePath: import_koishi.Schema.string().default("./data/maple-drift-bottle/images").description("图片本地保存路径(相对Koishi根目录)"),
54
+ compressImages: import_koishi.Schema.boolean().default(true).description("是否压缩图片以节省空间"),
55
+ maxImageWidth: import_koishi.Schema.number().default(1200).min(100).max(3840).description("图片最大宽度(像素)"),
56
+ maxImageHeight: import_koishi.Schema.number().default(1200).min(100).max(3840).description("图片最大高度(像素)"),
57
+ imageQuality: import_koishi.Schema.number().default(85).min(10).max(100).description("图片质量 (0-100,越高图片质量越好但文件越大)"),
36
58
  adminQQ: import_koishi.Schema.array(String).role("table").default([]).description("管理人QQ号(可添加多个)")
37
59
  });
38
60
  function getSenderDisplay(bottle) {
@@ -55,12 +77,90 @@ function formatTime(date) {
55
77
  }
56
78
  __name(formatTime, "formatTime");
57
79
  function getContentPreview(content, maxLength) {
80
+ if (!content || content.length === 0) {
81
+ return "[无文本内容]";
82
+ }
58
83
  if (content.length <= maxLength) {
59
84
  return content;
60
85
  }
61
86
  return content.substring(0, maxLength) + "...";
62
87
  }
63
88
  __name(getContentPreview, "getContentPreview");
89
+ function extractContentAndImages(elements) {
90
+ let text = "";
91
+ const images = [];
92
+ for (const element of elements) {
93
+ if (element.type === "text") {
94
+ text += element.attrs?.content || "";
95
+ } else if (element.type === "image" || element.type === "img") {
96
+ const url = element.attrs?.url || element.attrs?.src;
97
+ if (url) {
98
+ images.push(url);
99
+ }
100
+ } else if (element.type === "at") {
101
+ continue;
102
+ } else if (element.type === "face") {
103
+ continue;
104
+ } else {
105
+ text += `[${element.type}]`;
106
+ }
107
+ }
108
+ return { text: text.trim(), images };
109
+ }
110
+ __name(extractContentAndImages, "extractContentAndImages");
111
+ async function downloadAndSaveImage(ctx, url, savePath, compress, maxWidth, maxHeight, quality) {
112
+ try {
113
+ if (!(0, import_fs.existsSync)(savePath)) {
114
+ await (0, import_promises.mkdir)(savePath, { recursive: true });
115
+ }
116
+ const hash = (0, import_crypto.createHash)("md5").update(url + Date.now()).digest("hex");
117
+ const originalExt = (0, import_path.extname)(new URL(url).pathname) || ".jpg";
118
+ const fileName = `${hash}${originalExt}`;
119
+ const filePath = (0, import_path.join)(savePath, fileName);
120
+ const response = await ctx.http.get(url, { responseType: "arraybuffer" });
121
+ const imageBuffer = Buffer.from(response);
122
+ if (compress) {
123
+ try {
124
+ let sharpInstance = (0, import_sharp.default)(imageBuffer);
125
+ const metadata = await sharpInstance.metadata();
126
+ if (metadata.width > maxWidth || metadata.height > maxHeight) {
127
+ sharpInstance = sharpInstance.resize({
128
+ width: maxWidth,
129
+ height: maxHeight,
130
+ fit: "inside",
131
+ // 保持宽高比,不超过指定尺寸
132
+ withoutEnlargement: true
133
+ // 不放大比指定尺寸小的图片
134
+ });
135
+ }
136
+ await sharpInstance.jpeg({ quality }).toFile(filePath);
137
+ ctx.logger("maple-drift-bottle").info(`图片压缩保存: ${fileName} (${imageBuffer.length}字节 -> ${(await (0, import_promises.readFile)(filePath)).length}字节)`);
138
+ } catch (sharpError) {
139
+ ctx.logger("maple-drift-bottle").warn("sharp压缩失败,保存原始图片:", sharpError);
140
+ await (0, import_promises.writeFile)(filePath, imageBuffer);
141
+ }
142
+ } else {
143
+ await (0, import_promises.writeFile)(filePath, imageBuffer);
144
+ }
145
+ return fileName;
146
+ } catch (error) {
147
+ ctx.logger("maple-drift-bottle").error("下载或保存图片失败:", error);
148
+ throw new Error(`图片保存失败: ${error.message}`);
149
+ }
150
+ }
151
+ __name(downloadAndSaveImage, "downloadAndSaveImage");
152
+ function getLocalImagePath(fileName, ctx, config) {
153
+ if (!fileName || !config.saveImagesLocally) {
154
+ return fileName;
155
+ }
156
+ const fullPath = (0, import_path.join)(config.imageSavePath, fileName);
157
+ if (!(0, import_fs.existsSync)(fullPath)) {
158
+ ctx.logger("maple-drift-bottle").warn(`图片文件不存在: ${fullPath}`);
159
+ return fileName;
160
+ }
161
+ return fullPath;
162
+ }
163
+ __name(getLocalImagePath, "getLocalImagePath");
64
164
  function apply(ctx, config) {
65
165
  console.log("maple-drift-bottle 漂流瓶插件加载中...");
66
166
  ctx.model.extend("maple-drift-bottles", {
@@ -72,7 +172,9 @@ function apply(ctx, config) {
72
172
  anonymous: "boolean",
73
173
  // 是否匿名
74
174
  content: "text",
75
- // 漂流瓶内容(text类型,支持长文本)
175
+ // 文本内容
176
+ images: "json",
177
+ // 图片本地文件名数组(JSON格式存储)
76
178
  created: "timestamp"
77
179
  // 创建时间
78
180
  }, {
@@ -92,24 +194,50 @@ function apply(ctx, config) {
92
194
  const bottle = allBottles[randomIndex];
93
195
  const senderDisplay = getSenderDisplay(bottle);
94
196
  const timeDisplay = formatTime(bottle.created);
95
- return `【漂流瓶 #${bottle.id}】
96
- 发送者:${senderDisplay}
97
- 时间:${timeDisplay}
98
- 内容:
99
- ${bottle.content}`;
197
+ const message = [
198
+ `【漂流瓶 #${bottle.id}】`,
199
+ `发送者:${senderDisplay}`,
200
+ `时间:${timeDisplay}`
201
+ ];
202
+ if (bottle.content) {
203
+ message.push(`内容:
204
+ ${bottle.content}`);
205
+ }
206
+ if (bottle.images && bottle.images.length > 0) {
207
+ await session.send(message.join("\n"));
208
+ for (const imageName of bottle.images) {
209
+ try {
210
+ const imagePath = getLocalImagePath(imageName, ctx, config);
211
+ await session.send(import_koishi.h.image(imagePath));
212
+ } catch (error) {
213
+ ctx.logger("maple-drift-bottle").warn(`发送图片失败: ${imageName}`, error);
214
+ await session.send(`[图片加载失败]`);
215
+ }
216
+ }
217
+ return "";
218
+ } else {
219
+ return message.join("\n");
220
+ }
100
221
  } catch (error) {
101
222
  ctx.logger("maple-drift-bottle").error("捞漂流瓶时出错:", error);
102
223
  return "捞取漂流瓶时出错了,请稍后再试。";
103
224
  }
104
225
  });
105
- ctx.command("漂流瓶/扔漂流瓶 <content:text>", "扔一个漂流瓶到海里").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }, content) => {
226
+ ctx.command("漂流瓶/扔漂流瓶", "扔一个漂流瓶到海里(支持文本和图片)").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }) => {
106
227
  try {
107
- if (!content || content.trim().length === 0) {
108
- return "漂流瓶内容不能为空!\n格式:扔漂流瓶 [-i|-v] 内容\n示例:扔漂流瓶 -i 这是一条匿名漂流瓶";
228
+ const elements = session.elements;
229
+ if (elements.length === 0) {
230
+ return "漂流瓶内容不能为空!请发送文本或图片。";
231
+ }
232
+ const { text, images: imageUrls } = extractContentAndImages(elements);
233
+ if (!text && imageUrls.length === 0) {
234
+ return "漂流瓶内容不能为空!请发送文本或图片。";
235
+ }
236
+ if (text.length > config.maxContentLength) {
237
+ return `漂流瓶文本内容太长了!最多只能输入${config.maxContentLength}个字符(当前: ${text.length})。`;
109
238
  }
110
- const trimmedContent = content.trim();
111
- if (trimmedContent.length > config.maxContentLength) {
112
- return `漂流瓶内容太长了!最多只能输入${config.maxContentLength}个字(当前: ${trimmedContent.length})。`;
239
+ if (imageUrls.length > config.maxImages) {
240
+ return `图片数量太多了!最多只能发送${config.maxImages}张图片(当前: ${imageUrls.length})。`;
113
241
  }
114
242
  if (options.invisible && options.visible) {
115
243
  return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
@@ -122,17 +250,40 @@ ${bottle.content}`;
122
250
  }
123
251
  const userInfo = await session.getUser(session.userId);
124
252
  const authorName = userInfo?.name || session.username || `用户${session.userId}`;
253
+ const savedImageNames = [];
254
+ if (config.saveImagesLocally && imageUrls.length > 0) {
255
+ for (const imageUrl of imageUrls) {
256
+ try {
257
+ const savedName = await downloadAndSaveImage(
258
+ ctx,
259
+ imageUrl,
260
+ config.imageSavePath,
261
+ config.compressImages,
262
+ config.maxImageWidth,
263
+ config.maxImageHeight,
264
+ config.imageQuality
265
+ );
266
+ savedImageNames.push(savedName);
267
+ } catch (error) {
268
+ ctx.logger("maple-drift-bottle").error("图片处理失败:", error);
269
+ return `图片处理失败: ${error.message}`;
270
+ }
271
+ }
272
+ }
125
273
  const newBottle = await ctx.database.create("maple-drift-bottles", {
126
274
  author: session.userId,
127
275
  authorName,
128
276
  anonymous,
129
- content: trimmedContent,
277
+ content: text,
278
+ images: config.saveImagesLocally ? savedImageNames : imageUrls,
130
279
  created: /* @__PURE__ */ new Date()
131
280
  });
132
281
  const anonymousText = anonymous ? "匿名" : "不匿名";
282
+ const imageText = savedImageNames.length > 0 ? `,包含${savedImageNames.length}张图片` : "";
283
+ const saveText = config.saveImagesLocally ? "(已本地保存)" : "";
133
284
  return `成功扔出一个漂流瓶!
134
285
  ID: #${newBottle.id}
135
- 状态: ${anonymousText}
286
+ 状态: ${anonymousText}${imageText}${saveText}
136
287
  内容已保存到海中,等待有缘人捞取。`;
137
288
  } catch (error) {
138
289
  ctx.logger("maple-drift-bottle").error("扔漂流瓶时出错:", error);
@@ -167,8 +318,9 @@ ID: #${newBottle.id}
167
318
  const timeDisplay = formatTime(bottle.created);
168
319
  const anonymousText = bottle.anonymous ? "(匿名)" : "";
169
320
  const contentPreview = getContentPreview(bottle.content, config.maxPreviewLength);
321
+ const imageText = bottle.images && bottle.images.length > 0 ? ` [${bottle.images.length}图]` : "";
170
322
  output += `${displayIndex}. 漂流瓶 #${bottle.id} ${anonymousText} - ${timeDisplay}
171
- 预览: ${contentPreview}
323
+ 预览: ${contentPreview}${imageText}
172
324
  `;
173
325
  });
174
326
  output += "\n──────────\n";
@@ -204,12 +356,31 @@ ID: #${newBottle.id}
204
356
  }
205
357
  const senderDisplay = getSenderDisplay(bottle);
206
358
  const timeDisplay = formatTime(bottle.created);
207
- return `【漂流瓶 #${bottle.id}】
208
- 发送者:${senderDisplay}
209
- 时间:${timeDisplay}
210
- 状态:${bottle.anonymous ? "匿名" : "公开"}
211
- 内容:
212
- ${bottle.content}`;
359
+ const message = [
360
+ `【漂流瓶 #${bottle.id}】`,
361
+ `发送者:${senderDisplay}`,
362
+ `时间:${timeDisplay}`,
363
+ `状态:${bottle.anonymous ? "匿名" : "公开"}`
364
+ ];
365
+ if (bottle.content) {
366
+ message.push(`内容:
367
+ ${bottle.content}`);
368
+ }
369
+ if (bottle.images && bottle.images.length > 0) {
370
+ await session.send(message.join("\n"));
371
+ for (const imageName of bottle.images) {
372
+ try {
373
+ const imagePath = getLocalImagePath(imageName, ctx, config);
374
+ await session.send(import_koishi.h.image(imagePath));
375
+ } catch (error) {
376
+ ctx.logger("maple-drift-bottle").warn(`发送图片失败: ${imageName}`, error);
377
+ await session.send(`[图片加载失败]`);
378
+ }
379
+ }
380
+ return "";
381
+ } else {
382
+ return message.join("\n");
383
+ }
213
384
  } catch (error) {
214
385
  ctx.logger("maple-drift-bottle").error("查看漂流瓶时出错:", error);
215
386
  return "查看漂流瓶时出错了,请稍后再试。";
@@ -232,12 +403,26 @@ ${bottle.content}`;
232
403
  }
233
404
  const senderDisplay = getSenderDisplay(bottle);
234
405
  const timeDisplay = formatTime(bottle.created);
235
- const contentPreview = bottle.content.length > 50 ? bottle.content.substring(0, 50) + "..." : bottle.content;
406
+ const contentPreview = bottle.content ? bottle.content.length > 50 ? bottle.content.substring(0, 50) + "..." : bottle.content : "[无文本内容]";
407
+ const imageText = bottle.images && bottle.images.length > 0 ? ` (${bottle.images.length}张图片)` : "";
408
+ if (config.saveImagesLocally && bottle.images && bottle.images.length > 0) {
409
+ for (const imageName of bottle.images) {
410
+ try {
411
+ const imagePath = (0, import_path.join)(config.imageSavePath, imageName);
412
+ if ((0, import_fs.existsSync)(imagePath)) {
413
+ await (0, import_promises.rm)(imagePath);
414
+ ctx.logger("maple-drift-bottle").info(`删除图片文件: ${imagePath}`);
415
+ }
416
+ } catch (error) {
417
+ ctx.logger("maple-drift-bottle").warn(`删除图片文件失败: ${imageName}`, error);
418
+ }
419
+ }
420
+ }
236
421
  await ctx.database.remove("maple-drift-bottles", { id });
237
422
  return `已成功删除漂流瓶 #${id}
238
423
  发送者:${senderDisplay}
239
424
  时间:${timeDisplay}
240
- 预览:${contentPreview}`;
425
+ 预览:${contentPreview}${imageText}`;
241
426
  } catch (error) {
242
427
  ctx.logger("maple-drift-bottle").error("删除漂流瓶时出错:", error);
243
428
  return "删除漂流瓶时出错了,请稍后再试。";
@@ -273,6 +458,27 @@ ${bottle.content}`;
273
458
  if (count === 0) {
274
459
  return `没有找到${description}漂流瓶。`;
275
460
  }
461
+ if (config.saveImagesLocally) {
462
+ let deletedImages = 0;
463
+ for (const bottle of bottles) {
464
+ if (bottle.images && bottle.images.length > 0) {
465
+ for (const imageName of bottle.images) {
466
+ try {
467
+ const imagePath = (0, import_path.join)(config.imageSavePath, imageName);
468
+ if ((0, import_fs.existsSync)(imagePath)) {
469
+ await (0, import_promises.rm)(imagePath);
470
+ deletedImages++;
471
+ }
472
+ } catch (error) {
473
+ ctx.logger("maple-drift-bottle").warn(`删除图片文件失败: ${imageName}`, error);
474
+ }
475
+ }
476
+ }
477
+ }
478
+ if (deletedImages > 0) {
479
+ ctx.logger("maple-drift-bottle").info(`清空漂流瓶时删除了 ${deletedImages} 张图片文件`);
480
+ }
481
+ }
276
482
  await ctx.database.remove("maple-drift-bottles", query);
277
483
  return `已成功清空 ${count} 条${description}漂流瓶记录。`;
278
484
  } catch (error) {
@@ -327,11 +533,27 @@ ${failDetails.join("\n")}`;
327
533
  return "联系漂流瓶管理人时出错了,请稍后再试。";
328
534
  }
329
535
  });
330
- ctx.on("ready", () => {
536
+ ctx.on("ready", async () => {
331
537
  console.log("maple-drift-bottle 漂流瓶插件已加载完成");
538
+ if (config.saveImagesLocally) {
539
+ if (!(0, import_fs.existsSync)(config.imageSavePath)) {
540
+ await (0, import_promises.mkdir)(config.imageSavePath, { recursive: true });
541
+ ctx.logger("maple-drift-bottle").info(`创建图片保存目录: ${config.imageSavePath}`);
542
+ }
543
+ }
332
544
  ctx.logger("maple-drift-bottle").info(`默认匿名设置: ${config.anonymousByDefault ? "是" : "否"}`);
333
- ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字`);
545
+ ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字符`);
334
546
  ctx.logger("maple-drift-bottle").info(`我的漂流瓶最大预览字数: ${config.maxPreviewLength} 字`);
547
+ ctx.logger("maple-drift-bottle").info(`最大图片数量: ${config.maxImages} 张`);
548
+ ctx.logger("maple-drift-bottle").info(`本地保存图片: ${config.saveImagesLocally ? "是" : "否"}`);
549
+ if (config.saveImagesLocally) {
550
+ ctx.logger("maple-drift-bottle").info(`图片保存路径: ${config.imageSavePath}`);
551
+ ctx.logger("maple-drift-bottle").info(`压缩图片: ${config.compressImages ? "是" : "否"}`);
552
+ if (config.compressImages) {
553
+ ctx.logger("maple-drift-bottle").info(`图片最大尺寸: ${config.maxImageWidth}x${config.maxImageHeight} 像素`);
554
+ ctx.logger("maple-drift-bottle").info(`图片质量: ${config.imageQuality}%`);
555
+ }
556
+ }
335
557
  ctx.logger("maple-drift-bottle").info(`管理人QQ: ${config.adminQQ.length > 0 ? config.adminQQ.join(", ") : "未配置"}`);
336
558
  });
337
559
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-maple-drift-bottle",
3
3
  "description": "-",
4
- "version": "0.0.3",
4
+ "version": "0.1.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -14,5 +14,8 @@
14
14
  ],
15
15
  "peerDependencies": {
16
16
  "koishi": "^4.18.7"
17
+ },
18
+ "dependencies": {
19
+ "sharp": "^0.33.0"
17
20
  }
18
21
  }