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.
- package/lib/index.js +332 -249
- package/package.json +1 -4
- 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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
60
|
+
function getContentPreview(content, hasImage, maxLength) {
|
|
61
|
+
let preview = content;
|
|
62
|
+
if (hasImage) {
|
|
63
|
+
preview += "[图片]";
|
|
82
64
|
}
|
|
83
|
-
if (
|
|
84
|
-
return
|
|
65
|
+
if (preview.length <= maxLength) {
|
|
66
|
+
return preview;
|
|
85
67
|
}
|
|
86
|
-
return
|
|
68
|
+
return preview.substring(0, maxLength) + "...";
|
|
87
69
|
}
|
|
88
70
|
__name(getContentPreview, "getContentPreview");
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
86
|
+
return null;
|
|
109
87
|
}
|
|
110
|
-
__name(
|
|
111
|
-
async function
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
102
|
+
return base64;
|
|
146
103
|
} catch (error) {
|
|
147
|
-
ctx.logger("maple-drift-bottle").error("
|
|
148
|
-
throw new Error(
|
|
104
|
+
ctx.logger("maple-drift-bottle").error("下载或转换图片失败:", error);
|
|
105
|
+
throw new Error(`图片处理失败: ${error.message}`);
|
|
149
106
|
}
|
|
150
107
|
}
|
|
151
|
-
__name(
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
160
|
+
return { content: processedContent, imageBase64 };
|
|
162
161
|
}
|
|
163
|
-
__name(
|
|
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
|
-
|
|
177
|
-
//
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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("漂流瓶/扔漂流瓶", "
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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 (
|
|
240
|
-
return
|
|
268
|
+
if (!imageBase64 && (!processedContent || processedContent.trim().length === 0)) {
|
|
269
|
+
return "漂流瓶内容不能为空!\n格式:扔漂流瓶 [-i|-v] 内容\n示例:扔漂流瓶 -i 这是一条匿名漂流瓶";
|
|
241
270
|
}
|
|
242
|
-
|
|
243
|
-
|
|
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:
|
|
278
|
-
|
|
287
|
+
content: trimmedContent,
|
|
288
|
+
image: imageBase64,
|
|
279
289
|
created: /* @__PURE__ */ new Date()
|
|
280
290
|
});
|
|
281
291
|
const anonymousText = anonymous ? "匿名" : "不匿名";
|
|
282
|
-
const imageText =
|
|
283
|
-
const saveText = config.saveImagesLocally ? "(已本地保存)" : "";
|
|
292
|
+
const imageText = imageBase64 ? "(含图片)" : "";
|
|
284
293
|
return `成功扔出一个漂流瓶!
|
|
285
294
|
ID: #${newBottle.id}
|
|
286
|
-
状态: ${anonymousText}${imageText}
|
|
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}
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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",
|
|
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(
|
|
548
|
-
ctx.logger("maple-drift-bottle").info(
|
|
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.
|
|
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 {};
|