koishi-plugin-maple-drift-bottle 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/lib/index.js +332 -249
  2. package/package.json +1 -4
  3. package/lib/index.d.ts +0 -33
package/lib/index.js CHANGED
@@ -1,8 +1,6 @@
1
- var __create = Object.create;
2
1
  var __defProp = Object.defineProperty;
3
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
5
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
6
  var __export = (target, all) => {
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
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
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
@@ -37,34 +27,25 @@ __export(src_exports, {
37
27
  });
38
28
  module.exports = __toCommonJS(src_exports);
39
29
  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
+ var import_koishi2 = require("koishi");
45
31
  var name = "maple-drift-bottle";
46
- var using = ["database"];
32
+ var using = ["database", "http"];
47
33
  var Config = import_koishi.Schema.object({
48
34
  anonymousByDefault: import_koishi.Schema.boolean().default(false).description("默认是否匿名(true: 匿名,false: 不匿名)"),
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
+ maxContentLength: import_koishi.Schema.number().default(500).min(50).max(2e3).description("漂流瓶内容最大长度(字)(50-2000)"),
36
+ maxPreviewLength: import_koishi.Schema.number().default(8).min(3).max(50).description('指令"我的漂流瓶"最大预览字数(3-50)'),
37
+ maxCommentLength: import_koishi.Schema.number().default(50).min(10).max(500).description("漂流瓶评论最大长度(字)(10-500)"),
38
+ maxImageSize: import_koishi.Schema.number().default(1e3).min(0).max(1e4).description("漂流瓶图片大小限制(KB)(0-10000,0表示不允许图片)"),
58
39
  adminQQ: import_koishi.Schema.array(String).role("table").default([]).description("管理人QQ号(可添加多个)")
59
40
  });
60
41
  function getSenderDisplay(bottle) {
61
- if (bottle.anonymous) {
62
- return "匿名";
63
- } else {
64
- return `${bottle.authorName} (${bottle.author})`;
65
- }
42
+ return bottle.anonymous ? "匿名" : bottle.authorName;
66
43
  }
67
44
  __name(getSenderDisplay, "getSenderDisplay");
45
+ function getCommenterDisplay(comment) {
46
+ return comment.anonymous ? "匿名" : comment.authorName;
47
+ }
48
+ __name(getCommenterDisplay, "getCommenterDisplay");
68
49
  function formatTime(date) {
69
50
  return date.toLocaleString("zh-CN", {
70
51
  year: "numeric",
@@ -76,91 +57,109 @@ function formatTime(date) {
76
57
  });
77
58
  }
78
59
  __name(formatTime, "formatTime");
79
- function getContentPreview(content, maxLength) {
80
- if (!content || content.length === 0) {
81
- return "[无文本内容]";
60
+ function getContentPreview(content, hasImage, maxLength) {
61
+ let preview = content;
62
+ if (hasImage) {
63
+ preview += "[图片]";
82
64
  }
83
- if (content.length <= maxLength) {
84
- return content;
65
+ if (preview.length <= maxLength) {
66
+ return preview;
85
67
  }
86
- return content.substring(0, maxLength) + "...";
68
+ return preview.substring(0, maxLength) + "...";
87
69
  }
88
70
  __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}]`;
71
+ function getBase64ImageSize(base64) {
72
+ const stringLength = base64.length;
73
+ const sizeInBytes = 4 * Math.ceil(stringLength / 3) * 0.5624896334383812;
74
+ const sizeInKB = sizeInBytes / 1024;
75
+ return sizeInKB;
76
+ }
77
+ __name(getBase64ImageSize, "getBase64ImageSize");
78
+ function extractImageUrlFromCQCode(cqCode) {
79
+ const srcMatch = cqCode.match(/src="([^"]+)"/);
80
+ if (srcMatch && srcMatch[1]) {
81
+ const url = srcMatch[1];
82
+ if (url.startsWith("http://") || url.startsWith("https://")) {
83
+ return url;
106
84
  }
107
85
  }
108
- return { text: text.trim(), images };
86
+ return null;
109
87
  }
110
- __name(extractContentAndImages, "extractContentAndImages");
111
- async function downloadAndSaveImage(ctx, url, savePath, compress, maxWidth, maxHeight, quality) {
88
+ __name(extractImageUrlFromCQCode, "extractImageUrlFromCQCode");
89
+ async function downloadImageToBase64(url, maxSizeKB, ctx) {
112
90
  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
91
  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);
92
+ const buffer = Buffer.from(response);
93
+ const sizeKB = buffer.length / 1024;
94
+ if (sizeKB > maxSizeKB) {
95
+ throw new Error(`图片太大: ${sizeKB.toFixed(2)}KB (最大允许: ${maxSizeKB}KB)`);
96
+ }
97
+ const base64 = buffer.toString("base64");
98
+ const base64SizeKB = base64.length * 3 / (4 * 1024);
99
+ if (base64SizeKB > maxSizeKB) {
100
+ throw new Error(`转换后图片太大: ${base64SizeKB.toFixed(2)}KB (最大允许: ${maxSizeKB}KB)`);
144
101
  }
145
- return fileName;
102
+ return base64;
146
103
  } catch (error) {
147
- ctx.logger("maple-drift-bottle").error("下载或保存图片失败:", error);
148
- throw new Error(`图片保存失败: ${error.message}`);
104
+ ctx.logger("maple-drift-bottle").error("下载或转换图片失败:", error);
105
+ throw new Error(`图片处理失败: ${error.message}`);
149
106
  }
150
107
  }
151
- __name(downloadAndSaveImage, "downloadAndSaveImage");
152
- function getLocalImagePath(fileName, ctx, config) {
153
- if (!fileName || !config.saveImagesLocally) {
154
- return fileName;
108
+ __name(downloadImageToBase64, "downloadImageToBase64");
109
+ async function processImages(session, content, config, ctx) {
110
+ let processedContent = content || "";
111
+ let imageBase64 = null;
112
+ if (content && content.includes("<img")) {
113
+ const imgMatches = content.match(/<img[^>]+>/g);
114
+ if (imgMatches && imgMatches.length > 0) {
115
+ if (imgMatches.length > 1) {
116
+ throw new Error("最多只能发送一张图片!");
117
+ }
118
+ if (config.maxImageSize === 0) {
119
+ throw new Error("当前配置不允许发送图片!");
120
+ }
121
+ const imgTag = imgMatches[0];
122
+ const imageUrl = extractImageUrlFromCQCode(imgTag);
123
+ if (imageUrl) {
124
+ try {
125
+ imageBase64 = await downloadImageToBase64(imageUrl, config.maxImageSize, ctx);
126
+ } catch (error) {
127
+ ctx.logger("maple-drift-bottle").error("处理CQ码图片失败:", error);
128
+ throw new Error(`图片处理失败: ${error.message}`);
129
+ }
130
+ } else {
131
+ throw new Error("无法从图片消息中提取有效的图片URL,请尝试直接发送图片而不是复制图片链接。");
132
+ }
133
+ processedContent = content.replace(/<img[^>]*>/g, "").trim();
134
+ }
155
135
  }
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;
136
+ const hasImageInElements = session.elements?.some((el) => el.type === "image");
137
+ if (hasImageInElements) {
138
+ const images = session.elements.filter((el) => el.type === "image");
139
+ if (images.length > 1) {
140
+ throw new Error("最多只能发送一张图片!");
141
+ }
142
+ if (config.maxImageSize === 0) {
143
+ throw new Error("当前配置不允许发送图片!");
144
+ }
145
+ const imageElement = images[0];
146
+ let imageUrl = imageElement.attrs.url || imageElement.attrs.file;
147
+ if (imageUrl && imageUrl.startsWith("base64://")) {
148
+ const base64Data = imageUrl.substring(9);
149
+ const imageSize = getBase64ImageSize(base64Data);
150
+ if (imageSize > config.maxImageSize) {
151
+ throw new Error(`图片太大了!最大允许 ${config.maxImageSize}KB(当前: ${imageSize.toFixed(2)}KB)。`);
152
+ }
153
+ imageBase64 = base64Data;
154
+ } else if (imageUrl) {
155
+ imageBase64 = await downloadImageToBase64(imageUrl, config.maxImageSize, ctx);
156
+ }
157
+ }
158
+ if (hasImageInElements && processedContent !== content) {
160
159
  }
161
- return fullPath;
160
+ return { content: processedContent, imageBase64 };
162
161
  }
163
- __name(getLocalImagePath, "getLocalImagePath");
162
+ __name(processImages, "processImages");
164
163
  function apply(ctx, config) {
165
164
  console.log("maple-drift-bottle 漂流瓶插件加载中...");
166
165
  ctx.model.extend("maple-drift-bottles", {
@@ -172,15 +171,36 @@ function apply(ctx, config) {
172
171
  anonymous: "boolean",
173
172
  // 是否匿名
174
173
  content: "text",
175
- // 文本内容
176
- images: "json",
177
- // 图片本地文件名数组(JSON格式存储)
174
+ // 漂流瓶内容(text类型,支持长文本)
175
+ image: "text",
176
+ // 图片base64数据
178
177
  created: "timestamp"
179
178
  // 创建时间
180
179
  }, {
181
180
  primary: "id",
182
181
  autoInc: true
183
182
  });
183
+ ctx.model.extend("maple-drift-bottle-comments", {
184
+ id: "unsigned",
185
+ bottleId: "unsigned",
186
+ // 关联的漂流瓶ID
187
+ author: "string",
188
+ // 评论者ID
189
+ authorName: "string",
190
+ // 评论者昵称
191
+ anonymous: "boolean",
192
+ // 是否匿名评论
193
+ content: "text",
194
+ // 评论内容
195
+ created: "timestamp"
196
+ // 评论时间
197
+ }, {
198
+ primary: "id",
199
+ autoInc: true,
200
+ foreign: {
201
+ bottleId: ["maple-drift-bottles", "id"]
202
+ }
203
+ });
184
204
  ctx.command("漂流瓶", "漂流瓶相关指令").action(({ session }) => {
185
205
  return session.execute("help 漂流瓶");
186
206
  });
@@ -192,55 +212,65 @@ function apply(ctx, config) {
192
212
  }
193
213
  const randomIndex = Math.floor(Math.random() * allBottles.length);
194
214
  const bottle = allBottles[randomIndex];
215
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
216
+ bottleId: bottle.id
217
+ });
218
+ comments.sort((a, b) => b.created.getTime() - a.created.getTime());
195
219
  const senderDisplay = getSenderDisplay(bottle);
196
220
  const timeDisplay = formatTime(bottle.created);
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
- }
221
+ let textContent = bottle.content;
222
+ if (bottle.image) {
223
+ textContent += textContent ? "\n[图片]" : "[图片]";
224
+ }
225
+ let output = `【漂流瓶 #${bottle.id}】
226
+ 发送者:${senderDisplay}
227
+ 时间:${timeDisplay}
228
+ 内容:
229
+ ${textContent}`;
230
+ if (comments.length > 0) {
231
+ output += "\n──────────\n最新评论:\n";
232
+ const recentComments = comments.slice(0, 3);
233
+ recentComments.forEach((comment, index) => {
234
+ const commenterDisplay = getCommenterDisplay(comment);
235
+ output += `${index + 1}. ${commenterDisplay}:${comment.content}
236
+ `;
237
+ });
238
+ }
239
+ await session.send(output);
240
+ if (bottle.image) {
241
+ try {
242
+ await session.send(import_koishi2.segment.image(`base64://${bottle.image}`));
243
+ } catch (error) {
244
+ ctx.logger("maple-drift-bottle").error("发送图片时出错:", error);
245
+ await session.send("(图片发送失败)");
216
246
  }
217
- return "";
218
- } else {
219
- return message.join("\n");
220
247
  }
248
+ return "";
221
249
  } catch (error) {
222
250
  ctx.logger("maple-drift-bottle").error("捞漂流瓶时出错:", error);
223
251
  return "捞取漂流瓶时出错了,请稍后再试。";
224
252
  }
225
253
  });
226
- ctx.command("漂流瓶/扔漂流瓶", "扔一个漂流瓶到海里(支持文本和图片)").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }) => {
254
+ ctx.command("漂流瓶/扔漂流瓶 <content:text>", "扔一个漂流瓶到海里(可附带一张图片)").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }, content) => {
227
255
  try {
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 "漂流瓶内容不能为空!请发送文本或图片。";
256
+ if (options.invisible && options.visible) {
257
+ return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
235
258
  }
236
- if (text.length > config.maxContentLength) {
237
- return `漂流瓶文本内容太长了!最多只能输入${config.maxContentLength}个字符(当前: ${text.length})。`;
259
+ let processedContent = content || "";
260
+ let imageBase64 = null;
261
+ try {
262
+ const result = await processImages(session, content || "", config, ctx);
263
+ processedContent = result.content;
264
+ imageBase64 = result.imageBase64;
265
+ } catch (error) {
266
+ return error.message;
238
267
  }
239
- if (imageUrls.length > config.maxImages) {
240
- return `图片数量太多了!最多只能发送${config.maxImages}张图片(当前: ${imageUrls.length})。`;
268
+ if (!imageBase64 && (!processedContent || processedContent.trim().length === 0)) {
269
+ return "漂流瓶内容不能为空!\n格式:扔漂流瓶 [-i|-v] 内容\n示例:扔漂流瓶 -i 这是一条匿名漂流瓶";
241
270
  }
242
- if (options.invisible && options.visible) {
243
- return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
271
+ const trimmedContent = processedContent.trim();
272
+ if (trimmedContent.length > config.maxContentLength) {
273
+ return `漂流瓶内容太长了!最多只能输入${config.maxContentLength}个字(当前: ${trimmedContent.length})。`;
244
274
  }
245
275
  let anonymous = config.anonymousByDefault;
246
276
  if (options.invisible) {
@@ -250,40 +280,19 @@ ${bottle.content}`);
250
280
  }
251
281
  const userInfo = await session.getUser(session.userId);
252
282
  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
- }
273
283
  const newBottle = await ctx.database.create("maple-drift-bottles", {
274
284
  author: session.userId,
275
285
  authorName,
276
286
  anonymous,
277
- content: text,
278
- images: config.saveImagesLocally ? savedImageNames : imageUrls,
287
+ content: trimmedContent,
288
+ image: imageBase64,
279
289
  created: /* @__PURE__ */ new Date()
280
290
  });
281
291
  const anonymousText = anonymous ? "匿名" : "不匿名";
282
- const imageText = savedImageNames.length > 0 ? `,包含${savedImageNames.length}张图片` : "";
283
- const saveText = config.saveImagesLocally ? "(已本地保存)" : "";
292
+ const imageText = imageBase64 ? "(含图片)" : "";
284
293
  return `成功扔出一个漂流瓶!
285
294
  ID: #${newBottle.id}
286
- 状态: ${anonymousText}${imageText}${saveText}
295
+ 状态: ${anonymousText}${imageText}
287
296
  内容已保存到海中,等待有缘人捞取。`;
288
297
  } catch (error) {
289
298
  ctx.logger("maple-drift-bottle").error("扔漂流瓶时出错:", error);
@@ -317,10 +326,9 @@ ID: #${newBottle.id}
317
326
  const displayIndex = startIndex + index + 1;
318
327
  const timeDisplay = formatTime(bottle.created);
319
328
  const anonymousText = bottle.anonymous ? "(匿名)" : "";
320
- const contentPreview = getContentPreview(bottle.content, config.maxPreviewLength);
321
- const imageText = bottle.images && bottle.images.length > 0 ? ` [${bottle.images.length}图]` : "";
329
+ const contentPreview = getContentPreview(bottle.content, !!bottle.image, config.maxPreviewLength);
322
330
  output += `${displayIndex}. 漂流瓶 #${bottle.id} ${anonymousText} - ${timeDisplay}
323
- 预览: ${contentPreview}${imageText}
331
+ 预览: ${contentPreview}
324
332
  `;
325
333
  });
326
334
  output += "\n──────────\n";
@@ -354,33 +362,41 @@ ID: #${newBottle.id}
354
362
  return `仅可查看由你自己发送的漂流瓶。
355
363
  可以发送"我的漂流瓶"查看你发送的所有漂流瓶序号。`;
356
364
  }
365
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
366
+ bottleId: bottle.id
367
+ });
368
+ comments.sort((a, b) => b.created.getTime() - a.created.getTime());
357
369
  const senderDisplay = getSenderDisplay(bottle);
358
370
  const timeDisplay = formatTime(bottle.created);
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
- }
371
+ let textContent = bottle.content;
372
+ if (bottle.image) {
373
+ textContent += textContent ? "\n[图片]" : "[图片]";
374
+ }
375
+ let output = `【漂流瓶 #${bottle.id}
376
+ 发送者:${senderDisplay}
377
+ 时间:${timeDisplay}
378
+ 状态:${bottle.anonymous ? "匿名" : "公开"}
379
+ 内容:
380
+ ${textContent}`;
381
+ if (comments.length > 0) {
382
+ output += "\n──────────\n最新评论:\n";
383
+ const recentComments = comments.slice(0, 3);
384
+ recentComments.forEach((comment, index) => {
385
+ const commenterDisplay = getCommenterDisplay(comment);
386
+ output += `${index + 1}. ${commenterDisplay}:${comment.content}
387
+ `;
388
+ });
389
+ }
390
+ await session.send(output);
391
+ if (bottle.image) {
392
+ try {
393
+ await session.send(import_koishi2.segment.image(`base64://${bottle.image}`));
394
+ } catch (error) {
395
+ ctx.logger("maple-drift-bottle").error("发送图片时出错:", error);
396
+ await session.send("(图片发送失败)");
379
397
  }
380
- return "";
381
- } else {
382
- return message.join("\n");
383
398
  }
399
+ return "";
384
400
  } catch (error) {
385
401
  ctx.logger("maple-drift-bottle").error("查看漂流瓶时出错:", error);
386
402
  return "查看漂流瓶时出错了,请稍后再试。";
@@ -401,33 +417,130 @@ ${bottle.content}`);
401
417
  return `仅可删除由你自己发送的漂流瓶。
402
418
  可以发送"我的漂流瓶"查看你发送的所有漂流瓶序号。`;
403
419
  }
404
- const senderDisplay = getSenderDisplay(bottle);
405
- const timeDisplay = formatTime(bottle.created);
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
- }
420
+ await ctx.database.remove("maple-drift-bottle-comments", {
421
+ bottleId: id
422
+ });
421
423
  await ctx.database.remove("maple-drift-bottles", { id });
422
- return `已成功删除漂流瓶 #${id}
423
- 发送者:${senderDisplay}
424
- 时间:${timeDisplay}
425
- 预览:${contentPreview}${imageText}`;
424
+ return `已成功删除漂流瓶 #${id},并同时删除了该漂流瓶的所有评论。`;
426
425
  } catch (error) {
427
426
  ctx.logger("maple-drift-bottle").error("删除漂流瓶时出错:", error);
428
427
  return "删除漂流瓶时出错了,请稍后再试。";
429
428
  }
430
429
  });
430
+ ctx.command("漂流瓶/评论漂流瓶 <bottleId:number> <content:text>", "对指定的漂流瓶进行评论").alias("评论漂流瓶").option("invisible", "-i 匿名评论(不显示评论者)").option("visible", "-v 公开评论(显示评论者)").example("评论漂流瓶 1 这是一条评论").example("评论漂流瓶 2 -i 这是一条匿名评论").action(async ({ session, options }, bottleId, content) => {
431
+ try {
432
+ const bottles = await ctx.database.get("maple-drift-bottles", { id: bottleId });
433
+ if (bottles.length === 0) {
434
+ return `没有找到ID为 #${bottleId} 的漂流瓶。`;
435
+ }
436
+ if (!content || content.trim().length === 0) {
437
+ return "评论内容不能为空!";
438
+ }
439
+ const trimmedContent = content.trim();
440
+ if (trimmedContent.length > config.maxCommentLength) {
441
+ return `评论内容太长了!最多只能输入${config.maxCommentLength}个字(当前: ${trimmedContent.length})。`;
442
+ }
443
+ if (options.invisible && options.visible) {
444
+ return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
445
+ }
446
+ let anonymous = config.anonymousByDefault;
447
+ if (options.invisible) {
448
+ anonymous = true;
449
+ } else if (options.visible) {
450
+ anonymous = false;
451
+ }
452
+ const userInfo = await session.getUser(session.userId);
453
+ const authorName = userInfo?.name || session.username || `用户${session.userId}`;
454
+ await ctx.database.create("maple-drift-bottle-comments", {
455
+ bottleId,
456
+ author: session.userId,
457
+ authorName,
458
+ anonymous,
459
+ content: trimmedContent,
460
+ created: /* @__PURE__ */ new Date()
461
+ });
462
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
463
+ bottleId
464
+ });
465
+ if (comments.length > 3) {
466
+ comments.sort((a, b) => a.created.getTime() - b.created.getTime());
467
+ const commentsToDelete = comments.slice(0, comments.length - 3);
468
+ for (const comment of commentsToDelete) {
469
+ await ctx.database.remove("maple-drift-bottle-comments", { id: comment.id });
470
+ }
471
+ }
472
+ const anonymousText = anonymous ? "匿名" : "公开";
473
+ return `成功评论漂流瓶 #${bottleId}!
474
+ 状态: ${anonymousText}
475
+ 评论已保存。`;
476
+ } catch (error) {
477
+ ctx.logger("maple-drift-bottle").error("评论漂流瓶时出错:", error);
478
+ return "评论漂流瓶时出错了,请稍后再试。";
479
+ }
480
+ });
481
+ ctx.command("漂流瓶/删除漂流瓶评论 <bottleId:number>", "删除指定漂流瓶的所有评论").alias("删除漂流瓶评论").option("all", "-a 无视权限限制删除评论").example("删除漂流瓶评论 1").action(async ({ session, options }, bottleId) => {
482
+ try {
483
+ const bottles = await ctx.database.get("maple-drift-bottles", { id: bottleId });
484
+ if (bottles.length === 0) {
485
+ return `没有找到ID为 #${bottleId} 的漂流瓶。`;
486
+ }
487
+ const bottle = bottles[0];
488
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
489
+ bottleId
490
+ });
491
+ if (comments.length === 0) {
492
+ return `漂流瓶 #${bottleId} 还没有任何评论。`;
493
+ }
494
+ if (!options.all) {
495
+ const userId = session.userId;
496
+ const isBottleAuthor = bottle.author === userId;
497
+ const userComments = comments.filter((comment) => comment.author === userId);
498
+ const isCommentAuthor = userComments.length > 0;
499
+ if (!isBottleAuthor && !isCommentAuthor) {
500
+ return `仅可由漂流瓶发送者或评论发送者删除该漂流瓶的评论。
501
+ 可以发送"我的漂流瓶"查看你发送的漂流瓶序号。`;
502
+ }
503
+ if (!isBottleAuthor && isCommentAuthor) {
504
+ await ctx.database.remove("maple-drift-bottle-comments", {
505
+ bottleId,
506
+ author: userId
507
+ });
508
+ const deletedCount = userComments.length;
509
+ return `已成功删除你在漂流瓶 #${bottleId} 的评论(共 ${deletedCount} 条)。`;
510
+ }
511
+ }
512
+ await ctx.database.remove("maple-drift-bottle-comments", {
513
+ bottleId
514
+ });
515
+ return `已成功删除漂流瓶 #${bottleId} 的所有评论(共 ${comments.length} 条)。`;
516
+ } catch (error) {
517
+ ctx.logger("maple-drift-bottle").error("删除漂流瓶评论时出错:", error);
518
+ return "删除漂流瓶评论时出错了,请稍后再试。";
519
+ }
520
+ });
521
+ ctx.command("漂流瓶/清空漂流瓶评论 [options]", "清空漂流瓶评论").alias("清空漂流瓶评论").option("all", "-a 清空所有漂流瓶评论(默认)").option("user", "-u <userId:string> 清空指定用户的所有漂流瓶评论").example("清空漂流瓶评论").example("清空漂流瓶评论 -u 123456").action(async ({ session, options }) => {
522
+ try {
523
+ const query = {};
524
+ let description = "";
525
+ if (options.user) {
526
+ query.author = options.user;
527
+ description += `用户 ${options.user} 的`;
528
+ }
529
+ if (!options.user) {
530
+ description = "所有";
531
+ }
532
+ const comments = await ctx.database.get("maple-drift-bottle-comments", query);
533
+ const count = comments.length;
534
+ if (count === 0) {
535
+ return `没有找到${description}漂流瓶评论。`;
536
+ }
537
+ await ctx.database.remove("maple-drift-bottle-comments", query);
538
+ return `已成功清空 ${count} 条${description}漂流瓶评论。`;
539
+ } catch (error) {
540
+ ctx.logger("maple-drift-bottle").error("清空漂流瓶评论时出错:", error);
541
+ return "清空漂流瓶评论时出错了,请稍后再试。";
542
+ }
543
+ });
431
544
  ctx.command("漂流瓶/清空漂流瓶 [options]", "清空漂流瓶").alias("清空漂流瓶").alias("清空瓶子").option("all", "-a 清空所有漂流瓶(默认)").option("user", "-u <userId:string> 清空指定用户的所有漂流瓶").option("date", "-d <date:string> 清空指定日期之前的漂流瓶(格式: yyyy-mm-dd)").example("清空漂流瓶").example("清空漂流瓶 -d 2023-12-31").action(async ({ session, options }) => {
432
545
  try {
433
546
  const query = {};
@@ -458,29 +571,13 @@ ${bottle.content}`);
458
571
  if (count === 0) {
459
572
  return `没有找到${description}漂流瓶。`;
460
573
  }
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
- }
574
+ for (const bottle of bottles) {
575
+ await ctx.database.remove("maple-drift-bottle-comments", {
576
+ bottleId: bottle.id
577
+ });
481
578
  }
482
579
  await ctx.database.remove("maple-drift-bottles", query);
483
- return `已成功清空 ${count} 条${description}漂流瓶记录。`;
580
+ return `已成功清空 ${count} 条${description}漂流瓶记录,并同时删除了相关评论。`;
484
581
  } catch (error) {
485
582
  ctx.logger("maple-drift-bottle").error("清空漂流瓶时出错:", error);
486
583
  return "清空漂流瓶时出错了,请稍后再试。";
@@ -533,27 +630,13 @@ ${failDetails.join("\n")}`;
533
630
  return "联系漂流瓶管理人时出错了,请稍后再试。";
534
631
  }
535
632
  });
536
- ctx.on("ready", async () => {
633
+ ctx.on("ready", () => {
537
634
  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
- }
544
635
  ctx.logger("maple-drift-bottle").info(`默认匿名设置: ${config.anonymousByDefault ? "是" : "否"}`);
545
- ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字符`);
636
+ ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字`);
546
637
  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
- }
638
+ ctx.logger("maple-drift-bottle").info(`漂流瓶评论最大长度: ${config.maxCommentLength} 字`);
639
+ ctx.logger("maple-drift-bottle").info(`漂流瓶图片大小限制: ${config.maxImageSize} KB`);
557
640
  ctx.logger("maple-drift-bottle").info(`管理人QQ: ${config.adminQQ.length > 0 ? config.adminQQ.join(", ") : "未配置"}`);
558
641
  });
559
642
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-maple-drift-bottle",
3
3
  "description": "-",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -14,8 +14,5 @@
14
14
  ],
15
15
  "peerDependencies": {
16
16
  "koishi": "^4.18.7"
17
- },
18
- "dependencies": {
19
- "sharp": "^0.33.0"
20
17
  }
21
18
  }
package/lib/index.d.ts DELETED
@@ -1,33 +0,0 @@
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 {};