koishi-plugin-maple-drift-bottle 0.0.2 → 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 CHANGED
@@ -7,6 +7,7 @@ interface DriftBottle {
7
7
  authorName: string;
8
8
  anonymous: boolean;
9
9
  content: string;
10
+ images: string[];
10
11
  created: Date;
11
12
  }
12
13
  declare module 'koishi' {
@@ -17,6 +18,14 @@ declare module 'koishi' {
17
18
  export interface Config {
18
19
  anonymousByDefault: boolean;
19
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;
20
29
  adminQQ: string[];
21
30
  }
22
31
  export declare const Config: Schema<Config>;
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,11 +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("漂流瓶内容最大长度(字符)"),
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,越高图片质量越好但文件越大)"),
35
58
  adminQQ: import_koishi.Schema.array(String).role("table").default([]).description("管理人QQ号(可添加多个)")
36
59
  });
37
60
  function getSenderDisplay(bottle) {
@@ -53,13 +76,91 @@ function formatTime(date) {
53
76
  });
54
77
  }
55
78
  __name(formatTime, "formatTime");
56
- function getContentPreview(content) {
57
- if (content.length <= 10) {
79
+ function getContentPreview(content, maxLength) {
80
+ if (!content || content.length === 0) {
81
+ return "[无文本内容]";
82
+ }
83
+ if (content.length <= maxLength) {
58
84
  return content;
59
85
  }
60
- return content.substring(0, 10) + "...";
86
+ return content.substring(0, maxLength) + "...";
61
87
  }
62
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");
63
164
  function apply(ctx, config) {
64
165
  console.log("maple-drift-bottle 漂流瓶插件加载中...");
65
166
  ctx.model.extend("maple-drift-bottles", {
@@ -71,7 +172,9 @@ function apply(ctx, config) {
71
172
  anonymous: "boolean",
72
173
  // 是否匿名
73
174
  content: "text",
74
- // 漂流瓶内容(text类型,支持长文本)
175
+ // 文本内容
176
+ images: "json",
177
+ // 图片本地文件名数组(JSON格式存储)
75
178
  created: "timestamp"
76
179
  // 创建时间
77
180
  }, {
@@ -91,24 +194,50 @@ function apply(ctx, config) {
91
194
  const bottle = allBottles[randomIndex];
92
195
  const senderDisplay = getSenderDisplay(bottle);
93
196
  const timeDisplay = formatTime(bottle.created);
94
- return `【漂流瓶 #${bottle.id}】
95
- 发送者:${senderDisplay}
96
- 时间:${timeDisplay}
97
- 内容:
98
- ${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
+ }
99
221
  } catch (error) {
100
222
  ctx.logger("maple-drift-bottle").error("捞漂流瓶时出错:", error);
101
223
  return "捞取漂流瓶时出错了,请稍后再试。";
102
224
  }
103
225
  });
104
- 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 }) => {
105
227
  try {
106
- if (!content || content.trim().length === 0) {
107
- 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})。`;
108
238
  }
109
- const trimmedContent = content.trim();
110
- if (trimmedContent.length > config.maxContentLength) {
111
- return `漂流瓶内容太长了!最多只能输入${config.maxContentLength}个字符(当前: ${trimmedContent.length})。`;
239
+ if (imageUrls.length > config.maxImages) {
240
+ return `图片数量太多了!最多只能发送${config.maxImages}张图片(当前: ${imageUrls.length})。`;
112
241
  }
113
242
  if (options.invisible && options.visible) {
114
243
  return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
@@ -121,17 +250,40 @@ ${bottle.content}`;
121
250
  }
122
251
  const userInfo = await session.getUser(session.userId);
123
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
+ }
124
273
  const newBottle = await ctx.database.create("maple-drift-bottles", {
125
274
  author: session.userId,
126
275
  authorName,
127
276
  anonymous,
128
- content: trimmedContent,
277
+ content: text,
278
+ images: config.saveImagesLocally ? savedImageNames : imageUrls,
129
279
  created: /* @__PURE__ */ new Date()
130
280
  });
131
281
  const anonymousText = anonymous ? "匿名" : "不匿名";
282
+ const imageText = savedImageNames.length > 0 ? `,包含${savedImageNames.length}张图片` : "";
283
+ const saveText = config.saveImagesLocally ? "(已本地保存)" : "";
132
284
  return `成功扔出一个漂流瓶!
133
285
  ID: #${newBottle.id}
134
- 状态: ${anonymousText}
286
+ 状态: ${anonymousText}${imageText}${saveText}
135
287
  内容已保存到海中,等待有缘人捞取。`;
136
288
  } catch (error) {
137
289
  ctx.logger("maple-drift-bottle").error("扔漂流瓶时出错:", error);
@@ -165,12 +317,13 @@ ID: #${newBottle.id}
165
317
  const displayIndex = startIndex + index + 1;
166
318
  const timeDisplay = formatTime(bottle.created);
167
319
  const anonymousText = bottle.anonymous ? "(匿名)" : "";
168
- const contentPreview = getContentPreview(bottle.content);
320
+ const contentPreview = getContentPreview(bottle.content, config.maxPreviewLength);
321
+ const imageText = bottle.images && bottle.images.length > 0 ? ` [${bottle.images.length}图]` : "";
169
322
  output += `${displayIndex}. 漂流瓶 #${bottle.id} ${anonymousText} - ${timeDisplay}
170
- 预览: ${contentPreview}
323
+ 预览: ${contentPreview}${imageText}
171
324
  `;
172
325
  });
173
- output += "\n──────────────\n";
326
+ output += "\n──────────\n";
174
327
  if (page > 1) {
175
328
  output += `输入"我的漂流瓶 ${page - 1}"查看上一页
176
329
  `;
@@ -203,12 +356,31 @@ ID: #${newBottle.id}
203
356
  }
204
357
  const senderDisplay = getSenderDisplay(bottle);
205
358
  const timeDisplay = formatTime(bottle.created);
206
- return `【漂流瓶 #${bottle.id}】
207
- 发送者:${senderDisplay}
208
- 时间:${timeDisplay}
209
- 状态:${bottle.anonymous ? "匿名" : "公开"}
210
- 内容:
211
- ${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
+ }
212
384
  } catch (error) {
213
385
  ctx.logger("maple-drift-bottle").error("查看漂流瓶时出错:", error);
214
386
  return "查看漂流瓶时出错了,请稍后再试。";
@@ -231,12 +403,26 @@ ${bottle.content}`;
231
403
  }
232
404
  const senderDisplay = getSenderDisplay(bottle);
233
405
  const timeDisplay = formatTime(bottle.created);
234
- 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
+ }
235
421
  await ctx.database.remove("maple-drift-bottles", { id });
236
422
  return `已成功删除漂流瓶 #${id}
237
423
  发送者:${senderDisplay}
238
424
  时间:${timeDisplay}
239
- 预览:${contentPreview}`;
425
+ 预览:${contentPreview}${imageText}`;
240
426
  } catch (error) {
241
427
  ctx.logger("maple-drift-bottle").error("删除漂流瓶时出错:", error);
242
428
  return "删除漂流瓶时出错了,请稍后再试。";
@@ -272,6 +458,27 @@ ${bottle.content}`;
272
458
  if (count === 0) {
273
459
  return `没有找到${description}漂流瓶。`;
274
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
+ }
275
482
  await ctx.database.remove("maple-drift-bottles", query);
276
483
  return `已成功清空 ${count} 条${description}漂流瓶记录。`;
277
484
  } catch (error) {
@@ -326,10 +533,27 @@ ${failDetails.join("\n")}`;
326
533
  return "联系漂流瓶管理人时出错了,请稍后再试。";
327
534
  }
328
535
  });
329
- ctx.on("ready", () => {
536
+ ctx.on("ready", async () => {
330
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
+ }
331
544
  ctx.logger("maple-drift-bottle").info(`默认匿名设置: ${config.anonymousByDefault ? "是" : "否"}`);
332
545
  ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字符`);
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
+ }
333
557
  ctx.logger("maple-drift-bottle").info(`管理人QQ: ${config.adminQQ.length > 0 ? config.adminQQ.join(", ") : "未配置"}`);
334
558
  });
335
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.2",
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
  }