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

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.d.ts +13 -8
  2. package/lib/index.js +274 -258
  3. package/package.json +1 -4
package/lib/index.d.ts CHANGED
@@ -7,25 +7,30 @@ interface DriftBottle {
7
7
  authorName: string;
8
8
  anonymous: boolean;
9
9
  content: string;
10
- images: string[];
10
+ image: string | null;
11
+ created: Date;
12
+ }
13
+ interface BottleComment {
14
+ id: number;
15
+ bottleId: number;
16
+ author: string;
17
+ authorName: string;
18
+ anonymous: boolean;
19
+ content: string;
11
20
  created: Date;
12
21
  }
13
22
  declare module 'koishi' {
14
23
  interface Tables {
15
24
  'maple-drift-bottles': DriftBottle;
25
+ 'maple-drift-bottle-comments': BottleComment;
16
26
  }
17
27
  }
18
28
  export interface Config {
19
29
  anonymousByDefault: boolean;
20
30
  maxContentLength: number;
21
31
  maxPreviewLength: number;
22
- maxImages: number;
23
- saveImagesLocally: boolean;
24
- imageSavePath: string;
25
- compressImages: boolean;
26
- maxImageWidth: number;
27
- maxImageHeight: number;
28
- imageQuality: number;
32
+ maxCommentLength: number;
33
+ maxImageSize: number;
29
34
  adminQQ: string[];
30
35
  }
31
36
  export declare const Config: Schema<Config>;
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,24 @@ __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"));
45
30
  var name = "maple-drift-bottle";
46
31
  var using = ["database"];
47
32
  var Config = import_koishi.Schema.object({
48
33
  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,越高图片质量越好但文件越大)"),
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)'),
36
+ maxCommentLength: import_koishi.Schema.number().default(50).min(10).max(500).description("漂流瓶评论最大长度(字)(10-500)"),
37
+ maxImageSize: import_koishi.Schema.number().default(1e3).min(0).max(1e4).description("漂流瓶图片大小限制(KB)(0-10000,0表示不允许图片)"),
58
38
  adminQQ: import_koishi.Schema.array(String).role("table").default([]).description("管理人QQ号(可添加多个)")
59
39
  });
60
40
  function getSenderDisplay(bottle) {
61
- if (bottle.anonymous) {
62
- return "匿名";
63
- } else {
64
- return `${bottle.authorName} (${bottle.author})`;
65
- }
41
+ return bottle.anonymous ? "匿名" : bottle.authorName;
66
42
  }
67
43
  __name(getSenderDisplay, "getSenderDisplay");
44
+ function getCommenterDisplay(comment) {
45
+ return comment.anonymous ? "匿名" : comment.authorName;
46
+ }
47
+ __name(getCommenterDisplay, "getCommenterDisplay");
68
48
  function formatTime(date) {
69
49
  return date.toLocaleString("zh-CN", {
70
50
  year: "numeric",
@@ -76,91 +56,24 @@ function formatTime(date) {
76
56
  });
77
57
  }
78
58
  __name(formatTime, "formatTime");
79
- function getContentPreview(content, maxLength) {
80
- if (!content || content.length === 0) {
81
- return "[无文本内容]";
59
+ function getContentPreview(content, hasImage, maxLength) {
60
+ let preview = content;
61
+ if (hasImage) {
62
+ preview += "[图片]";
82
63
  }
83
- if (content.length <= maxLength) {
84
- return content;
64
+ if (preview.length <= maxLength) {
65
+ return preview;
85
66
  }
86
- return content.substring(0, maxLength) + "...";
67
+ return preview.substring(0, maxLength) + "...";
87
68
  }
88
69
  __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 };
70
+ function getBase64ImageSize(base64) {
71
+ const stringLength = base64.length;
72
+ const sizeInBytes = 4 * Math.ceil(stringLength / 3) * 0.5624896334383812;
73
+ const sizeInKB = sizeInBytes / 1024;
74
+ return sizeInKB;
109
75
  }
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");
76
+ __name(getBase64ImageSize, "getBase64ImageSize");
164
77
  function apply(ctx, config) {
165
78
  console.log("maple-drift-bottle 漂流瓶插件加载中...");
166
79
  ctx.model.extend("maple-drift-bottles", {
@@ -172,15 +85,36 @@ function apply(ctx, config) {
172
85
  anonymous: "boolean",
173
86
  // 是否匿名
174
87
  content: "text",
175
- // 文本内容
176
- images: "json",
177
- // 图片本地文件名数组(JSON格式存储)
88
+ // 漂流瓶内容(text类型,支持长文本)
89
+ image: "text",
90
+ // 图片base64数据
178
91
  created: "timestamp"
179
92
  // 创建时间
180
93
  }, {
181
94
  primary: "id",
182
95
  autoInc: true
183
96
  });
97
+ ctx.model.extend("maple-drift-bottle-comments", {
98
+ id: "unsigned",
99
+ bottleId: "unsigned",
100
+ // 关联的漂流瓶ID
101
+ author: "string",
102
+ // 评论者ID
103
+ authorName: "string",
104
+ // 评论者昵称
105
+ anonymous: "boolean",
106
+ // 是否匿名评论
107
+ content: "text",
108
+ // 评论内容
109
+ created: "timestamp"
110
+ // 评论时间
111
+ }, {
112
+ primary: "id",
113
+ autoInc: true,
114
+ foreign: {
115
+ bottleId: ["maple-drift-bottles", "id"]
116
+ }
117
+ });
184
118
  ctx.command("漂流瓶", "漂流瓶相关指令").action(({ session }) => {
185
119
  return session.execute("help 漂流瓶");
186
120
  });
@@ -192,56 +126,85 @@ function apply(ctx, config) {
192
126
  }
193
127
  const randomIndex = Math.floor(Math.random() * allBottles.length);
194
128
  const bottle = allBottles[randomIndex];
129
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
130
+ bottleId: bottle.id
131
+ });
132
+ comments.sort((a, b) => b.created.getTime() - a.created.getTime());
195
133
  const senderDisplay = getSenderDisplay(bottle);
196
134
  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
- }
135
+ let textContent = bottle.content;
136
+ if (bottle.image) {
137
+ textContent += "\n[图片]";
138
+ }
139
+ let output = `【漂流瓶 #${bottle.id}】
140
+ 发送者:${senderDisplay}
141
+ 时间:${timeDisplay}
142
+ 内容:
143
+ ${textContent}`;
144
+ if (comments.length > 0) {
145
+ output += "\n──────────\n最新评论:\n";
146
+ const recentComments = comments.slice(0, 3);
147
+ recentComments.forEach((comment, index) => {
148
+ const commenterDisplay = getCommenterDisplay(comment);
149
+ output += `${index + 1}. ${commenterDisplay}:${comment.content}
150
+ `;
151
+ });
152
+ }
153
+ await session.send(output);
154
+ if (bottle.image) {
155
+ try {
156
+ await session.send(`[CQ:image,file=base64://${bottle.image}]`);
157
+ } catch (error) {
158
+ ctx.logger("maple-drift-bottle").error("发送图片时出错:", error);
159
+ await session.send("(图片发送失败)");
216
160
  }
217
- return "";
218
- } else {
219
- return message.join("\n");
220
161
  }
162
+ return "";
221
163
  } catch (error) {
222
164
  ctx.logger("maple-drift-bottle").error("捞漂流瓶时出错:", error);
223
165
  return "捞取漂流瓶时出错了,请稍后再试。";
224
166
  }
225
167
  });
226
- ctx.command("漂流瓶/扔漂流瓶", "扔一个漂流瓶到海里(支持文本和图片)").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }) => {
168
+ ctx.command("漂流瓶/扔漂流瓶 <content:text>", "扔一个漂流瓶到海里(可附带一张图片)").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }, content) => {
227
169
  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 "漂流瓶内容不能为空!请发送文本或图片。";
235
- }
236
- if (text.length > config.maxContentLength) {
237
- return `漂流瓶文本内容太长了!最多只能输入${config.maxContentLength}个字符(当前: ${text.length})。`;
170
+ const hasImage = session.elements?.some((el) => el.type === "image");
171
+ if (!content || content.trim().length === 0) {
172
+ if (!hasImage) {
173
+ return "漂流瓶内容不能为空!\n格式:扔漂流瓶 [-i|-v] 内容\n示例:扔漂流瓶 -i 这是一条匿名漂流瓶";
174
+ }
238
175
  }
239
- if (imageUrls.length > config.maxImages) {
240
- return `图片数量太多了!最多只能发送${config.maxImages}张图片(当前: ${imageUrls.length})。`;
176
+ const trimmedContent = content ? content.trim() : "";
177
+ if (trimmedContent.length > config.maxContentLength) {
178
+ return `漂流瓶内容太长了!最多只能输入${config.maxContentLength}个字(当前: ${trimmedContent.length})。`;
241
179
  }
242
180
  if (options.invisible && options.visible) {
243
181
  return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
244
182
  }
183
+ let imageBase64 = null;
184
+ if (hasImage) {
185
+ const images = session.elements.filter((el) => el.type === "image");
186
+ if (images.length > 1) {
187
+ return "最多只能发送一张图片!";
188
+ }
189
+ if (config.maxImageSize === 0) {
190
+ return "当前配置不允许发送图片!";
191
+ }
192
+ const imageElement = images[0];
193
+ let imageUrl = imageElement.attrs.url || imageElement.attrs.file;
194
+ if (imageUrl && imageUrl.startsWith("base64://")) {
195
+ imageBase64 = imageUrl.substring(9);
196
+ const imageSize = getBase64ImageSize(imageBase64);
197
+ if (imageSize > config.maxImageSize) {
198
+ return `图片太大了!最大允许 ${config.maxImageSize}KB(当前: ${imageSize.toFixed(2)}KB)。`;
199
+ }
200
+ } else if (imageUrl) {
201
+ try {
202
+ await session.send("正在处理图片,请稍候...");
203
+ } catch (error) {
204
+ return "图片处理失败,请稍后再试。";
205
+ }
206
+ }
207
+ }
245
208
  let anonymous = config.anonymousByDefault;
246
209
  if (options.invisible) {
247
210
  anonymous = true;
@@ -250,40 +213,19 @@ ${bottle.content}`);
250
213
  }
251
214
  const userInfo = await session.getUser(session.userId);
252
215
  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
216
  const newBottle = await ctx.database.create("maple-drift-bottles", {
274
217
  author: session.userId,
275
218
  authorName,
276
219
  anonymous,
277
- content: text,
278
- images: config.saveImagesLocally ? savedImageNames : imageUrls,
220
+ content: trimmedContent,
221
+ image: imageBase64,
279
222
  created: /* @__PURE__ */ new Date()
280
223
  });
281
224
  const anonymousText = anonymous ? "匿名" : "不匿名";
282
- const imageText = savedImageNames.length > 0 ? `,包含${savedImageNames.length}张图片` : "";
283
- const saveText = config.saveImagesLocally ? "(已本地保存)" : "";
225
+ const imageText = imageBase64 ? "(含图片)" : "";
284
226
  return `成功扔出一个漂流瓶!
285
227
  ID: #${newBottle.id}
286
- 状态: ${anonymousText}${imageText}${saveText}
228
+ 状态: ${anonymousText}${imageText}
287
229
  内容已保存到海中,等待有缘人捞取。`;
288
230
  } catch (error) {
289
231
  ctx.logger("maple-drift-bottle").error("扔漂流瓶时出错:", error);
@@ -317,10 +259,9 @@ ID: #${newBottle.id}
317
259
  const displayIndex = startIndex + index + 1;
318
260
  const timeDisplay = formatTime(bottle.created);
319
261
  const anonymousText = bottle.anonymous ? "(匿名)" : "";
320
- const contentPreview = getContentPreview(bottle.content, config.maxPreviewLength);
321
- const imageText = bottle.images && bottle.images.length > 0 ? ` [${bottle.images.length}图]` : "";
262
+ const contentPreview = getContentPreview(bottle.content, !!bottle.image, config.maxPreviewLength);
322
263
  output += `${displayIndex}. 漂流瓶 #${bottle.id} ${anonymousText} - ${timeDisplay}
323
- 预览: ${contentPreview}${imageText}
264
+ 预览: ${contentPreview}
324
265
  `;
325
266
  });
326
267
  output += "\n──────────\n";
@@ -354,33 +295,41 @@ ID: #${newBottle.id}
354
295
  return `仅可查看由你自己发送的漂流瓶。
355
296
  可以发送"我的漂流瓶"查看你发送的所有漂流瓶序号。`;
356
297
  }
298
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
299
+ bottleId: bottle.id
300
+ });
301
+ comments.sort((a, b) => b.created.getTime() - a.created.getTime());
357
302
  const senderDisplay = getSenderDisplay(bottle);
358
303
  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
- }
304
+ let textContent = bottle.content;
305
+ if (bottle.image) {
306
+ textContent += "\n[图片]";
307
+ }
308
+ let output = `【漂流瓶 #${bottle.id}
309
+ 发送者:${senderDisplay}
310
+ 时间:${timeDisplay}
311
+ 状态:${bottle.anonymous ? "匿名" : "公开"}
312
+ 内容:
313
+ ${textContent}`;
314
+ if (comments.length > 0) {
315
+ output += "\n──────────\n最新评论:\n";
316
+ const recentComments = comments.slice(0, 3);
317
+ recentComments.forEach((comment, index) => {
318
+ const commenterDisplay = getCommenterDisplay(comment);
319
+ output += `${index + 1}. ${commenterDisplay}:${comment.content}
320
+ `;
321
+ });
322
+ }
323
+ await session.send(output);
324
+ if (bottle.image) {
325
+ try {
326
+ await session.send(`[CQ:image,file=base64://${bottle.image}]`);
327
+ } catch (error) {
328
+ ctx.logger("maple-drift-bottle").error("发送图片时出错:", error);
329
+ await session.send("(图片发送失败)");
379
330
  }
380
- return "";
381
- } else {
382
- return message.join("\n");
383
331
  }
332
+ return "";
384
333
  } catch (error) {
385
334
  ctx.logger("maple-drift-bottle").error("查看漂流瓶时出错:", error);
386
335
  return "查看漂流瓶时出错了,请稍后再试。";
@@ -401,33 +350,130 @@ ${bottle.content}`);
401
350
  return `仅可删除由你自己发送的漂流瓶。
402
351
  可以发送"我的漂流瓶"查看你发送的所有漂流瓶序号。`;
403
352
  }
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
- }
353
+ await ctx.database.remove("maple-drift-bottle-comments", {
354
+ bottleId: id
355
+ });
421
356
  await ctx.database.remove("maple-drift-bottles", { id });
422
- return `已成功删除漂流瓶 #${id}
423
- 发送者:${senderDisplay}
424
- 时间:${timeDisplay}
425
- 预览:${contentPreview}${imageText}`;
357
+ return `已成功删除漂流瓶 #${id},并同时删除了该漂流瓶的所有评论。`;
426
358
  } catch (error) {
427
359
  ctx.logger("maple-drift-bottle").error("删除漂流瓶时出错:", error);
428
360
  return "删除漂流瓶时出错了,请稍后再试。";
429
361
  }
430
362
  });
363
+ ctx.command("漂流瓶/评论漂流瓶 <bottleId:number> <content:text>", "对指定的漂流瓶进行评论").alias("评论漂流瓶").option("invisible", "-i 匿名评论(不显示评论者)").option("visible", "-v 公开评论(显示评论者)").example("评论漂流瓶 1 这是一条评论").example("评论漂流瓶 2 -i 这是一条匿名评论").action(async ({ session, options }, bottleId, content) => {
364
+ try {
365
+ const bottles = await ctx.database.get("maple-drift-bottles", { id: bottleId });
366
+ if (bottles.length === 0) {
367
+ return `没有找到ID为 #${bottleId} 的漂流瓶。`;
368
+ }
369
+ if (!content || content.trim().length === 0) {
370
+ return "评论内容不能为空!";
371
+ }
372
+ const trimmedContent = content.trim();
373
+ if (trimmedContent.length > config.maxCommentLength) {
374
+ return `评论内容太长了!最多只能输入${config.maxCommentLength}个字(当前: ${trimmedContent.length})。`;
375
+ }
376
+ if (options.invisible && options.visible) {
377
+ return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
378
+ }
379
+ let anonymous = config.anonymousByDefault;
380
+ if (options.invisible) {
381
+ anonymous = true;
382
+ } else if (options.visible) {
383
+ anonymous = false;
384
+ }
385
+ const userInfo = await session.getUser(session.userId);
386
+ const authorName = userInfo?.name || session.username || `用户${session.userId}`;
387
+ await ctx.database.create("maple-drift-bottle-comments", {
388
+ bottleId,
389
+ author: session.userId,
390
+ authorName,
391
+ anonymous,
392
+ content: trimmedContent,
393
+ created: /* @__PURE__ */ new Date()
394
+ });
395
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
396
+ bottleId
397
+ });
398
+ if (comments.length > 3) {
399
+ comments.sort((a, b) => a.created.getTime() - b.created.getTime());
400
+ const commentsToDelete = comments.slice(0, comments.length - 3);
401
+ for (const comment of commentsToDelete) {
402
+ await ctx.database.remove("maple-drift-bottle-comments", { id: comment.id });
403
+ }
404
+ }
405
+ const anonymousText = anonymous ? "匿名" : "公开";
406
+ return `成功评论漂流瓶 #${bottleId}!
407
+ 状态: ${anonymousText}
408
+ 评论已保存。`;
409
+ } catch (error) {
410
+ ctx.logger("maple-drift-bottle").error("评论漂流瓶时出错:", error);
411
+ return "评论漂流瓶时出错了,请稍后再试。";
412
+ }
413
+ });
414
+ ctx.command("漂流瓶/删除漂流瓶评论 <bottleId:number>", "删除指定漂流瓶的所有评论").alias("删除漂流瓶评论").option("all", "-a 无视权限限制删除评论").example("删除漂流瓶评论 1").action(async ({ session, options }, bottleId) => {
415
+ try {
416
+ const bottles = await ctx.database.get("maple-drift-bottles", { id: bottleId });
417
+ if (bottles.length === 0) {
418
+ return `没有找到ID为 #${bottleId} 的漂流瓶。`;
419
+ }
420
+ const bottle = bottles[0];
421
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
422
+ bottleId
423
+ });
424
+ if (comments.length === 0) {
425
+ return `漂流瓶 #${bottleId} 还没有任何评论。`;
426
+ }
427
+ if (!options.all) {
428
+ const userId = session.userId;
429
+ const isBottleAuthor = bottle.author === userId;
430
+ const userComments = comments.filter((comment) => comment.author === userId);
431
+ const isCommentAuthor = userComments.length > 0;
432
+ if (!isBottleAuthor && !isCommentAuthor) {
433
+ return `仅可由漂流瓶发送者或评论发送者删除该漂流瓶的评论。
434
+ 可以发送"我的漂流瓶"查看你发送的漂流瓶序号。`;
435
+ }
436
+ if (!isBottleAuthor && isCommentAuthor) {
437
+ await ctx.database.remove("maple-drift-bottle-comments", {
438
+ bottleId,
439
+ author: userId
440
+ });
441
+ const deletedCount = userComments.length;
442
+ return `已成功删除你在漂流瓶 #${bottleId} 的评论(共 ${deletedCount} 条)。`;
443
+ }
444
+ }
445
+ await ctx.database.remove("maple-drift-bottle-comments", {
446
+ bottleId
447
+ });
448
+ return `已成功删除漂流瓶 #${bottleId} 的所有评论(共 ${comments.length} 条)。`;
449
+ } catch (error) {
450
+ ctx.logger("maple-drift-bottle").error("删除漂流瓶评论时出错:", error);
451
+ return "删除漂流瓶评论时出错了,请稍后再试。";
452
+ }
453
+ });
454
+ ctx.command("漂流瓶/清空漂流瓶评论 [options]", "清空漂流瓶评论").alias("清空漂流瓶评论").option("all", "-a 清空所有漂流瓶评论(默认)").option("user", "-u <userId:string> 清空指定用户的所有漂流瓶评论").example("清空漂流瓶评论").example("清空漂流瓶评论 -u 123456").action(async ({ session, options }) => {
455
+ try {
456
+ const query = {};
457
+ let description = "";
458
+ if (options.user) {
459
+ query.author = options.user;
460
+ description += `用户 ${options.user} 的`;
461
+ }
462
+ if (!options.user) {
463
+ description = "所有";
464
+ }
465
+ const comments = await ctx.database.get("maple-drift-bottle-comments", query);
466
+ const count = comments.length;
467
+ if (count === 0) {
468
+ return `没有找到${description}漂流瓶评论。`;
469
+ }
470
+ await ctx.database.remove("maple-drift-bottle-comments", query);
471
+ return `已成功清空 ${count} 条${description}漂流瓶评论。`;
472
+ } catch (error) {
473
+ ctx.logger("maple-drift-bottle").error("清空漂流瓶评论时出错:", error);
474
+ return "清空漂流瓶评论时出错了,请稍后再试。";
475
+ }
476
+ });
431
477
  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
478
  try {
433
479
  const query = {};
@@ -458,29 +504,13 @@ ${bottle.content}`);
458
504
  if (count === 0) {
459
505
  return `没有找到${description}漂流瓶。`;
460
506
  }
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
- }
507
+ for (const bottle of bottles) {
508
+ await ctx.database.remove("maple-drift-bottle-comments", {
509
+ bottleId: bottle.id
510
+ });
481
511
  }
482
512
  await ctx.database.remove("maple-drift-bottles", query);
483
- return `已成功清空 ${count} 条${description}漂流瓶记录。`;
513
+ return `已成功清空 ${count} 条${description}漂流瓶记录,并同时删除了相关评论。`;
484
514
  } catch (error) {
485
515
  ctx.logger("maple-drift-bottle").error("清空漂流瓶时出错:", error);
486
516
  return "清空漂流瓶时出错了,请稍后再试。";
@@ -533,27 +563,13 @@ ${failDetails.join("\n")}`;
533
563
  return "联系漂流瓶管理人时出错了,请稍后再试。";
534
564
  }
535
565
  });
536
- ctx.on("ready", async () => {
566
+ ctx.on("ready", () => {
537
567
  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
568
  ctx.logger("maple-drift-bottle").info(`默认匿名设置: ${config.anonymousByDefault ? "是" : "否"}`);
545
- ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字符`);
569
+ ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字`);
546
570
  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
- }
571
+ ctx.logger("maple-drift-bottle").info(`漂流瓶评论最大长度: ${config.maxCommentLength} 字`);
572
+ ctx.logger("maple-drift-bottle").info(`漂流瓶图片大小限制: ${config.maxImageSize} KB`);
557
573
  ctx.logger("maple-drift-bottle").info(`管理人QQ: ${config.adminQQ.length > 0 ? config.adminQQ.join(", ") : "未配置"}`);
558
574
  });
559
575
  }
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.1",
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
  }