koishi-plugin-best-cave 2.0.6 → 2.0.8
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/DataManager.d.ts +13 -15
- package/lib/FileManager.d.ts +19 -35
- package/lib/ProfileManager.d.ts +13 -15
- package/lib/ReviewManager.d.ts +16 -27
- package/lib/Utils.d.ts +26 -50
- package/lib/index.d.ts +4 -25
- package/lib/index.js +197 -322
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -37,7 +37,8 @@ __export(index_exports, {
|
|
|
37
37
|
usage: () => usage
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(index_exports);
|
|
40
|
-
var
|
|
40
|
+
var import_koishi3 = require("koishi");
|
|
41
|
+
var path3 = __toESM(require("path"));
|
|
41
42
|
|
|
42
43
|
// src/FileManager.ts
|
|
43
44
|
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
@@ -45,10 +46,10 @@ var fs = __toESM(require("fs/promises"));
|
|
|
45
46
|
var path = __toESM(require("path"));
|
|
46
47
|
var FileManager = class {
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
-
* @param baseDir
|
|
50
|
-
* @param config
|
|
51
|
-
* @param logger
|
|
49
|
+
* @constructor
|
|
50
|
+
* @param baseDir Koishi 应用的基础数据目录 (ctx.baseDir)。
|
|
51
|
+
* @param config 插件的配置对象。
|
|
52
|
+
* @param logger 日志记录器实例。
|
|
52
53
|
*/
|
|
53
54
|
constructor(baseDir, config, logger2) {
|
|
54
55
|
this.logger = logger2;
|
|
@@ -68,47 +69,18 @@ var FileManager = class {
|
|
|
68
69
|
static {
|
|
69
70
|
__name(this, "FileManager");
|
|
70
71
|
}
|
|
71
|
-
// 本地资源存储目录的绝对路径。
|
|
72
72
|
resourceDir;
|
|
73
|
-
// 本地文件锁,键为文件绝对路径,值为一个 Promise,用于防止对同一文件的并发访问。
|
|
74
73
|
locks = /* @__PURE__ */ new Map();
|
|
75
|
-
// S3 客户端实例,仅在启用 S3 时初始化。
|
|
76
74
|
s3Client;
|
|
77
|
-
// S3 存储桶名称。
|
|
78
75
|
s3Bucket;
|
|
79
76
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* @
|
|
83
|
-
|
|
84
|
-
async ensureDirectory() {
|
|
85
|
-
try {
|
|
86
|
-
await fs.mkdir(this.resourceDir, { recursive: true });
|
|
87
|
-
} catch (error) {
|
|
88
|
-
this.logger.error(`创建资源目录失败 ${this.resourceDir}:`, error);
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* 获取给定文件名的完整本地路径。
|
|
94
|
-
* @param fileName - 文件名。
|
|
95
|
-
* @returns 文件的绝对路径。
|
|
96
|
-
* @private
|
|
97
|
-
*/
|
|
98
|
-
getFullPath(fileName) {
|
|
99
|
-
return path.join(this.resourceDir, fileName);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* 使用文件锁来安全地执行一个异步文件操作。
|
|
103
|
-
* 这可以防止对同一文件的并发读写造成数据损坏。
|
|
104
|
-
* @template T - 异步操作的返回类型。
|
|
105
|
-
* @param fileName - 需要加锁的文件名。
|
|
106
|
-
* @param operation - 要执行的异步函数。
|
|
77
|
+
* @description 使用文件锁安全地执行异步文件操作,防止并发读写冲突。
|
|
78
|
+
* @template T 异步操作的返回类型。
|
|
79
|
+
* @param fullPath 需要加锁的文件的完整路径。
|
|
80
|
+
* @param operation 要执行的异步函数。
|
|
107
81
|
* @returns 返回异步操作的结果。
|
|
108
|
-
* @private
|
|
109
82
|
*/
|
|
110
|
-
async withLock(
|
|
111
|
-
const fullPath = this.getFullPath(fileName);
|
|
83
|
+
async withLock(fullPath, operation) {
|
|
112
84
|
while (this.locks.has(fullPath)) {
|
|
113
85
|
await this.locks.get(fullPath);
|
|
114
86
|
}
|
|
@@ -119,9 +91,9 @@ var FileManager = class {
|
|
|
119
91
|
return promise;
|
|
120
92
|
}
|
|
121
93
|
/**
|
|
122
|
-
* 保存文件,自动选择 S3 或本地存储。
|
|
123
|
-
* @param fileName
|
|
124
|
-
* @param data
|
|
94
|
+
* @description 保存文件,自动选择 S3 或本地存储。
|
|
95
|
+
* @param fileName 用作 S3 Key 或本地文件名。
|
|
96
|
+
* @param data 要写入的 Buffer 数据。
|
|
125
97
|
* @returns 返回保存时使用的文件名/标识符。
|
|
126
98
|
*/
|
|
127
99
|
async saveFile(fileName, data) {
|
|
@@ -131,57 +103,52 @@ var FileManager = class {
|
|
|
131
103
|
Key: fileName,
|
|
132
104
|
Body: data,
|
|
133
105
|
ACL: "public-read"
|
|
134
|
-
//
|
|
106
|
+
// 默认为公开可读
|
|
135
107
|
});
|
|
136
108
|
await this.s3Client.send(command);
|
|
137
|
-
return fileName;
|
|
138
109
|
} else {
|
|
139
|
-
await this.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
110
|
+
await fs.mkdir(this.resourceDir, { recursive: true }).catch((error) => {
|
|
111
|
+
this.logger.error(`创建资源目录失败 ${this.resourceDir}:`, error);
|
|
112
|
+
throw error;
|
|
113
|
+
});
|
|
114
|
+
const filePath = path.join(this.resourceDir, fileName);
|
|
115
|
+
await this.withLock(filePath, () => fs.writeFile(filePath, data));
|
|
143
116
|
}
|
|
117
|
+
return fileName;
|
|
144
118
|
}
|
|
145
119
|
/**
|
|
146
|
-
* 读取文件,自动从 S3 或本地存储读取。
|
|
147
|
-
* @param fileName
|
|
120
|
+
* @description 读取文件,自动从 S3 或本地存储读取。
|
|
121
|
+
* @param fileName 要读取的文件名/标识符。
|
|
148
122
|
* @returns 文件的 Buffer 数据。
|
|
149
123
|
*/
|
|
150
124
|
async readFile(fileName) {
|
|
151
125
|
if (this.s3Client) {
|
|
152
|
-
const command = new import_client_s3.GetObjectCommand({
|
|
153
|
-
Bucket: this.s3Bucket,
|
|
154
|
-
Key: fileName
|
|
155
|
-
});
|
|
126
|
+
const command = new import_client_s3.GetObjectCommand({ Bucket: this.s3Bucket, Key: fileName });
|
|
156
127
|
const response = await this.s3Client.send(command);
|
|
157
|
-
|
|
158
|
-
return Buffer.from(byteArray);
|
|
128
|
+
return Buffer.from(await response.Body.transformToByteArray());
|
|
159
129
|
} else {
|
|
160
|
-
const filePath = this.
|
|
161
|
-
return this.withLock(
|
|
130
|
+
const filePath = path.join(this.resourceDir, fileName);
|
|
131
|
+
return this.withLock(filePath, () => fs.readFile(filePath));
|
|
162
132
|
}
|
|
163
133
|
}
|
|
164
134
|
/**
|
|
165
|
-
* 删除文件,自动从 S3 或本地删除。
|
|
166
|
-
* @param fileIdentifier
|
|
135
|
+
* @description 删除文件,自动从 S3 或本地删除。
|
|
136
|
+
* @param fileIdentifier 要删除的文件名/标识符。
|
|
167
137
|
*/
|
|
168
138
|
async deleteFile(fileIdentifier) {
|
|
169
139
|
if (this.s3Client) {
|
|
170
|
-
const command = new import_client_s3.DeleteObjectCommand({
|
|
171
|
-
Bucket: this.s3Bucket,
|
|
172
|
-
Key: fileIdentifier
|
|
173
|
-
});
|
|
140
|
+
const command = new import_client_s3.DeleteObjectCommand({ Bucket: this.s3Bucket, Key: fileIdentifier });
|
|
174
141
|
await this.s3Client.send(command).catch((err) => {
|
|
175
|
-
this.logger.warn(
|
|
142
|
+
this.logger.warn(`删除 S3 文件 ${fileIdentifier} 失败:`, err);
|
|
176
143
|
});
|
|
177
144
|
} else {
|
|
178
|
-
const filePath = this.
|
|
179
|
-
await this.withLock(
|
|
145
|
+
const filePath = path.join(this.resourceDir, fileIdentifier);
|
|
146
|
+
await this.withLock(filePath, async () => {
|
|
180
147
|
try {
|
|
181
148
|
await fs.unlink(filePath);
|
|
182
149
|
} catch (error) {
|
|
183
150
|
if (error.code !== "ENOENT") {
|
|
184
|
-
this.logger.warn(
|
|
151
|
+
this.logger.warn(`删除本地文件 ${filePath} 失败:`, error);
|
|
185
152
|
}
|
|
186
153
|
}
|
|
187
154
|
});
|
|
@@ -192,7 +159,7 @@ var FileManager = class {
|
|
|
192
159
|
// src/ProfileManager.ts
|
|
193
160
|
var ProfileManager = class {
|
|
194
161
|
/**
|
|
195
|
-
*
|
|
162
|
+
* @constructor
|
|
196
163
|
* @param ctx - Koishi 上下文,用于初始化数据库模型。
|
|
197
164
|
*/
|
|
198
165
|
constructor(ctx) {
|
|
@@ -204,49 +171,47 @@ var ProfileManager = class {
|
|
|
204
171
|
// 用户自定义昵称
|
|
205
172
|
}, {
|
|
206
173
|
primary: "userId"
|
|
207
|
-
//
|
|
174
|
+
// 保证每个用户只有一条昵称记录。
|
|
208
175
|
});
|
|
209
176
|
}
|
|
210
177
|
static {
|
|
211
178
|
__name(this, "ProfileManager");
|
|
212
179
|
}
|
|
213
180
|
/**
|
|
214
|
-
*
|
|
215
|
-
* @param cave - 主 `cave`
|
|
181
|
+
* @description 注册 `.profile` 子命令,用于管理用户昵称。
|
|
182
|
+
* @param cave - 主 `cave` 命令实例。
|
|
216
183
|
*/
|
|
217
184
|
registerCommands(cave) {
|
|
218
|
-
cave.subcommand(".profile [nickname:text]", "设置显示昵称").usage("
|
|
185
|
+
cave.subcommand(".profile [nickname:text]", "设置显示昵称").usage("设置你在回声洞中显示的昵称。若不提供昵称,则清除现有昵称。").action(async ({ session }, nickname) => {
|
|
219
186
|
const trimmedNickname = nickname?.trim();
|
|
220
|
-
if (
|
|
187
|
+
if (trimmedNickname) {
|
|
188
|
+
await this.setNickname(session.userId, trimmedNickname);
|
|
189
|
+
return `昵称已更新为:${trimmedNickname}`;
|
|
190
|
+
} else {
|
|
221
191
|
await this.clearNickname(session.userId);
|
|
222
192
|
return "昵称已清除";
|
|
223
193
|
}
|
|
224
|
-
await this.setNickname(session.userId, trimmedNickname);
|
|
225
|
-
return `昵称已更新为:${trimmedNickname}`;
|
|
226
194
|
});
|
|
227
195
|
}
|
|
228
196
|
/**
|
|
229
|
-
* 设置或更新指定用户的昵称。
|
|
197
|
+
* @description 设置或更新指定用户的昵称。
|
|
230
198
|
* @param userId - 目标用户的 ID。
|
|
231
199
|
* @param nickname - 要设置的新昵称。
|
|
232
200
|
*/
|
|
233
201
|
async setNickname(userId, nickname) {
|
|
234
|
-
await this.ctx.database.upsert("cave_user", [{
|
|
235
|
-
userId,
|
|
236
|
-
nickname
|
|
237
|
-
}]);
|
|
202
|
+
await this.ctx.database.upsert("cave_user", [{ userId, nickname }]);
|
|
238
203
|
}
|
|
239
204
|
/**
|
|
240
|
-
* 获取指定用户的昵称。
|
|
205
|
+
* @description 获取指定用户的昵称。
|
|
241
206
|
* @param userId - 目标用户的 ID。
|
|
242
|
-
* @returns
|
|
207
|
+
* @returns 返回用户的昵称字符串,如果未设置则返回 null。
|
|
243
208
|
*/
|
|
244
209
|
async getNickname(userId) {
|
|
245
|
-
const
|
|
246
|
-
return
|
|
210
|
+
const profile = await this.ctx.database.get("cave_user", { userId });
|
|
211
|
+
return profile[0]?.nickname ?? null;
|
|
247
212
|
}
|
|
248
213
|
/**
|
|
249
|
-
* 清除指定用户的昵称设置。
|
|
214
|
+
* @description 清除指定用户的昵称设置。
|
|
250
215
|
* @param userId - 目标用户的 ID。
|
|
251
216
|
*/
|
|
252
217
|
async clearNickname(userId) {
|
|
@@ -257,78 +222,46 @@ var ProfileManager = class {
|
|
|
257
222
|
// src/Utils.ts
|
|
258
223
|
var import_koishi = require("koishi");
|
|
259
224
|
var path2 = __toESM(require("path"));
|
|
260
|
-
var mimeTypeMap = {
|
|
261
|
-
".png": "image/png",
|
|
262
|
-
".jpg": "image/jpeg",
|
|
263
|
-
".jpeg": "image/jpeg",
|
|
264
|
-
".gif": "image/gif",
|
|
265
|
-
".mp4": "video/mp4",
|
|
266
|
-
".mp3": "audio/mpeg",
|
|
267
|
-
".webp": "image/webp"
|
|
268
|
-
};
|
|
225
|
+
var mimeTypeMap = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".webp": "image/webp" };
|
|
269
226
|
function storedFormatToHElements(elements) {
|
|
270
227
|
return elements.map((el) => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
case "image":
|
|
275
|
-
case "video":
|
|
276
|
-
case "audio":
|
|
277
|
-
case "file":
|
|
278
|
-
return (0, import_koishi.h)(el.type, { src: el.file });
|
|
279
|
-
default:
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
228
|
+
if (el.type === "text") return import_koishi.h.text(el.content);
|
|
229
|
+
if (["image", "video", "audio", "file"].includes(el.type)) return (0, import_koishi.h)(el.type, { src: el.file });
|
|
230
|
+
return null;
|
|
282
231
|
}).filter(Boolean);
|
|
283
232
|
}
|
|
284
233
|
__name(storedFormatToHElements, "storedFormatToHElements");
|
|
285
|
-
async function mediaElementToBase64(element, fileManager, logger2) {
|
|
286
|
-
const fileName = element.attrs.src;
|
|
287
|
-
try {
|
|
288
|
-
const data = await fileManager.readFile(fileName);
|
|
289
|
-
const ext = path2.extname(fileName).toLowerCase();
|
|
290
|
-
const mimeType = mimeTypeMap[ext] || "application/octet-stream";
|
|
291
|
-
return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
|
|
292
|
-
} catch (error) {
|
|
293
|
-
logger2.warn(`转换本地文件 ${fileName} 为 Base64 失败:`, error);
|
|
294
|
-
return (0, import_koishi.h)("p", {}, `[${element.type}]`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
__name(mediaElementToBase64, "mediaElementToBase64");
|
|
298
234
|
async function buildCaveMessage(cave, config, fileManager, logger2) {
|
|
299
235
|
const caveHElements = storedFormatToHElements(cave.elements);
|
|
300
|
-
const processedElements = await Promise.all(caveHElements.map((element) => {
|
|
236
|
+
const processedElements = await Promise.all(caveHElements.map(async (element) => {
|
|
301
237
|
const isMedia = ["image", "video", "audio", "file"].includes(element.type);
|
|
302
238
|
const fileName = element.attrs.src;
|
|
303
|
-
if (!isMedia || !fileName)
|
|
304
|
-
return Promise.resolve(element);
|
|
305
|
-
}
|
|
239
|
+
if (!isMedia || !fileName) return element;
|
|
306
240
|
if (config.enableS3 && config.publicUrl) {
|
|
307
241
|
const fullUrl = config.publicUrl.endsWith("/") ? `${config.publicUrl}${fileName}` : `${config.publicUrl}/${fileName}`;
|
|
308
|
-
return
|
|
242
|
+
return (0, import_koishi.h)(element.type, { ...element.attrs, src: fullUrl });
|
|
309
243
|
}
|
|
310
244
|
if (config.localPath) {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
245
|
+
const fileUri = `file://${path2.join(config.localPath, fileName)}`;
|
|
246
|
+
return (0, import_koishi.h)(element.type, { ...element.attrs, src: fileUri });
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const data = await fileManager.readFile(fileName);
|
|
250
|
+
const ext = path2.extname(fileName).toLowerCase();
|
|
251
|
+
const mimeType = mimeTypeMap[ext] || "application/octet-stream";
|
|
252
|
+
return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
logger2.warn(`转换文件 ${fileName} 为 Base64 失败:`, error);
|
|
255
|
+
return (0, import_koishi.h)("p", {}, `[${element.type}]`);
|
|
314
256
|
}
|
|
315
|
-
return mediaElementToBase64(element, fileManager, logger2);
|
|
316
257
|
}));
|
|
317
258
|
const finalMessage = [];
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
if (separatorIndex === -1) {
|
|
322
|
-
headerFormat = formatString;
|
|
323
|
-
footerFormat = "";
|
|
324
|
-
} else {
|
|
325
|
-
headerFormat = formatString.substring(0, separatorIndex);
|
|
326
|
-
footerFormat = formatString.substring(separatorIndex + 1);
|
|
327
|
-
}
|
|
328
|
-
const headerText = headerFormat.replace("{id}", cave.id.toString()).replace("{name}", cave.userName);
|
|
259
|
+
const [headerFormat, footerFormat = ""] = config.caveFormat.split("|");
|
|
260
|
+
const replacements = { id: cave.id.toString(), name: cave.userName };
|
|
261
|
+
const headerText = headerFormat.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]);
|
|
329
262
|
if (headerText.trim()) finalMessage.push(headerText);
|
|
330
263
|
finalMessage.push(...processedElements);
|
|
331
|
-
const footerText = footerFormat.replace(
|
|
264
|
+
const footerText = footerFormat.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]);
|
|
332
265
|
if (footerText.trim()) finalMessage.push(footerText);
|
|
333
266
|
return finalMessage;
|
|
334
267
|
}
|
|
@@ -336,7 +269,7 @@ __name(buildCaveMessage, "buildCaveMessage");
|
|
|
336
269
|
async function cleanupPendingDeletions(ctx, fileManager, logger2) {
|
|
337
270
|
try {
|
|
338
271
|
const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
|
|
339
|
-
if (cavesToDelete.length
|
|
272
|
+
if (!cavesToDelete.length) return;
|
|
340
273
|
for (const cave of cavesToDelete) {
|
|
341
274
|
const deletePromises = cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file));
|
|
342
275
|
await Promise.all(deletePromises);
|
|
@@ -349,15 +282,12 @@ async function cleanupPendingDeletions(ctx, fileManager, logger2) {
|
|
|
349
282
|
__name(cleanupPendingDeletions, "cleanupPendingDeletions");
|
|
350
283
|
function getScopeQuery(session, config) {
|
|
351
284
|
const baseQuery = { status: "active" };
|
|
352
|
-
|
|
353
|
-
return { ...baseQuery, channelId: session.channelId };
|
|
354
|
-
}
|
|
355
|
-
return baseQuery;
|
|
285
|
+
return config.perChannel && session.channelId ? { ...baseQuery, channelId: session.channelId } : baseQuery;
|
|
356
286
|
}
|
|
357
287
|
__name(getScopeQuery, "getScopeQuery");
|
|
358
288
|
async function getNextCaveId(ctx, query = {}) {
|
|
359
|
-
const
|
|
360
|
-
const existingIds = new Set(
|
|
289
|
+
const allCaveIds = (await ctx.database.get("cave", query, { fields: ["id"] })).map((c) => c.id);
|
|
290
|
+
const existingIds = new Set(allCaveIds);
|
|
361
291
|
let newId = 1;
|
|
362
292
|
while (existingIds.has(newId)) {
|
|
363
293
|
newId++;
|
|
@@ -365,19 +295,8 @@ async function getNextCaveId(ctx, query = {}) {
|
|
|
365
295
|
return newId;
|
|
366
296
|
}
|
|
367
297
|
__name(getNextCaveId, "getNextCaveId");
|
|
368
|
-
async function downloadMedia(ctx, fileManager, url, originalName, type, caveId, index, channelId, userId) {
|
|
369
|
-
const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
|
|
370
|
-
const ext = originalName ? path2.extname(originalName) : "";
|
|
371
|
-
const finalExt = ext || defaultExtMap[type] || ".dat";
|
|
372
|
-
const fileName = `${caveId}_${index}_${channelId}_${userId}${finalExt}`;
|
|
373
|
-
const response = await ctx.http.get(url, { responseType: "arraybuffer", timeout: 3e4 });
|
|
374
|
-
return fileManager.saveFile(fileName, Buffer.from(response));
|
|
375
|
-
}
|
|
376
|
-
__name(downloadMedia, "downloadMedia");
|
|
377
298
|
function checkCooldown(session, config, lastUsed) {
|
|
378
|
-
if (config.coolDown <= 0 || !session.channelId
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
299
|
+
if (config.coolDown <= 0 || !session.channelId) return null;
|
|
381
300
|
const now = Date.now();
|
|
382
301
|
const lastTime = lastUsed.get(session.channelId) || 0;
|
|
383
302
|
if (now - lastTime < config.coolDown * 1e3) {
|
|
@@ -397,11 +316,11 @@ __name(updateCooldownTimestamp, "updateCooldownTimestamp");
|
|
|
397
316
|
// src/DataManager.ts
|
|
398
317
|
var DataManager = class {
|
|
399
318
|
/**
|
|
400
|
-
*
|
|
401
|
-
* @param ctx
|
|
402
|
-
* @param config
|
|
403
|
-
* @param fileManager
|
|
404
|
-
* @param logger
|
|
319
|
+
* @constructor
|
|
320
|
+
* @param ctx Koishi 上下文,用于数据库操作。
|
|
321
|
+
* @param config 插件配置。
|
|
322
|
+
* @param fileManager 文件管理器实例。
|
|
323
|
+
* @param logger 日志记录器实例。
|
|
405
324
|
*/
|
|
406
325
|
constructor(ctx, config, fileManager, logger2) {
|
|
407
326
|
this.ctx = ctx;
|
|
@@ -413,27 +332,25 @@ var DataManager = class {
|
|
|
413
332
|
__name(this, "DataManager");
|
|
414
333
|
}
|
|
415
334
|
/**
|
|
416
|
-
*
|
|
417
|
-
* @param cave - 主 `cave`
|
|
335
|
+
* @description 注册 `.export` 和 `.import` 子命令。
|
|
336
|
+
* @param cave - 主 `cave` 命令实例。
|
|
418
337
|
*/
|
|
419
338
|
registerCommands(cave) {
|
|
420
339
|
cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(async ({ session }) => {
|
|
421
|
-
if (
|
|
340
|
+
if (session.channelId !== this.config.adminChannel) return "此指令仅限在管理群组中使用";
|
|
422
341
|
try {
|
|
423
342
|
await session.send("正在导出数据,请稍候...");
|
|
424
|
-
|
|
425
|
-
return resultMessage;
|
|
343
|
+
return await this.exportData();
|
|
426
344
|
} catch (error) {
|
|
427
345
|
this.logger.error("导出数据时发生错误:", error);
|
|
428
346
|
return `导出失败: ${error.message}`;
|
|
429
347
|
}
|
|
430
348
|
});
|
|
431
349
|
cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(async ({ session }) => {
|
|
432
|
-
if (
|
|
350
|
+
if (session.channelId !== this.config.adminChannel) return "此指令仅限在管理群组中使用";
|
|
433
351
|
try {
|
|
434
352
|
await session.send("正在导入数据,请稍候...");
|
|
435
|
-
|
|
436
|
-
return resultMessage;
|
|
353
|
+
return await this.importData();
|
|
437
354
|
} catch (error) {
|
|
438
355
|
this.logger.error("导入数据时发生错误:", error);
|
|
439
356
|
return `导入失败: ${error.message}`;
|
|
@@ -441,9 +358,8 @@ var DataManager = class {
|
|
|
441
358
|
});
|
|
442
359
|
}
|
|
443
360
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* @returns 一个描述导出结果的字符串消息。
|
|
361
|
+
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
362
|
+
* @returns 描述导出结果的消息字符串。
|
|
447
363
|
*/
|
|
448
364
|
async exportData() {
|
|
449
365
|
const fileName = "cave_export.json";
|
|
@@ -454,8 +370,8 @@ var DataManager = class {
|
|
|
454
370
|
return `成功导出 ${portableCaves.length} 条数据`;
|
|
455
371
|
}
|
|
456
372
|
/**
|
|
457
|
-
* 从 `cave_import.json` 文件导入回声洞数据。
|
|
458
|
-
* @returns
|
|
373
|
+
* @description 从 `cave_import.json` 文件导入回声洞数据。
|
|
374
|
+
* @returns 描述导入结果的消息字符串。
|
|
459
375
|
*/
|
|
460
376
|
async importData() {
|
|
461
377
|
const fileName = "cave_import.json";
|
|
@@ -463,9 +379,7 @@ var DataManager = class {
|
|
|
463
379
|
try {
|
|
464
380
|
const fileContent = await this.fileManager.readFile(fileName);
|
|
465
381
|
importedCaves = JSON.parse(fileContent.toString("utf-8"));
|
|
466
|
-
if (!Array.isArray(importedCaves))
|
|
467
|
-
throw new Error("导入文件格式无效");
|
|
468
|
-
}
|
|
382
|
+
if (!Array.isArray(importedCaves)) throw new Error("导入文件格式无效");
|
|
469
383
|
} catch (error) {
|
|
470
384
|
this.logger.error(`读取导入文件失败:`, error);
|
|
471
385
|
return `读取导入文件失败: ${error.message || "未知错误"}`;
|
|
@@ -477,9 +391,8 @@ var DataManager = class {
|
|
|
477
391
|
...cave,
|
|
478
392
|
id: newId,
|
|
479
393
|
channelId: cave.channelId || null,
|
|
480
|
-
//
|
|
394
|
+
// 保证 channelId 存在
|
|
481
395
|
status: "active"
|
|
482
|
-
// 导入的数据直接设为 active 状态。
|
|
483
396
|
};
|
|
484
397
|
await this.ctx.database.create("cave", newCave);
|
|
485
398
|
successCount++;
|
|
@@ -489,13 +402,14 @@ var DataManager = class {
|
|
|
489
402
|
};
|
|
490
403
|
|
|
491
404
|
// src/ReviewManager.ts
|
|
405
|
+
var import_koishi2 = require("koishi");
|
|
492
406
|
var ReviewManager = class {
|
|
493
407
|
/**
|
|
494
|
-
*
|
|
495
|
-
* @param ctx
|
|
496
|
-
* @param config
|
|
497
|
-
* @param fileManager
|
|
498
|
-
* @param logger
|
|
408
|
+
* @constructor
|
|
409
|
+
* @param ctx Koishi 上下文。
|
|
410
|
+
* @param config 插件配置。
|
|
411
|
+
* @param fileManager 文件管理器实例。
|
|
412
|
+
* @param logger 日志记录器实例。
|
|
499
413
|
*/
|
|
500
414
|
constructor(ctx, config, fileManager, logger2) {
|
|
501
415
|
this.ctx = ctx;
|
|
@@ -507,111 +421,68 @@ var ReviewManager = class {
|
|
|
507
421
|
__name(this, "ReviewManager");
|
|
508
422
|
}
|
|
509
423
|
/**
|
|
510
|
-
* 注册与审核相关的 `.review` 子命令。
|
|
511
|
-
* @param cave - 主 `cave`
|
|
424
|
+
* @description 注册与审核相关的 `.review` 子命令。
|
|
425
|
+
* @param cave - 主 `cave` 命令实例。
|
|
512
426
|
*/
|
|
513
427
|
registerCommands(cave) {
|
|
514
428
|
cave.subcommand(".review [id:posint] [action:string]", "审核回声洞").usage("查看或审核回声洞,使用 <Y/N> 进行审核。").action(async ({ session }, id, action) => {
|
|
515
|
-
if (
|
|
516
|
-
return "抱歉,你没有权限执行审核";
|
|
517
|
-
}
|
|
429
|
+
if (session.channelId !== this.config.adminChannel) return "此指令仅限在管理群组中使用";
|
|
518
430
|
if (!id) {
|
|
519
431
|
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" });
|
|
520
|
-
if (pendingCaves.length === 0)
|
|
521
|
-
return "当前没有需要审核的回声洞";
|
|
522
|
-
}
|
|
523
|
-
const pendingIds = pendingCaves.map((c) => c.id).join(", ");
|
|
432
|
+
if (pendingCaves.length === 0) return "当前没有需要审核的回声洞";
|
|
524
433
|
return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
|
|
525
|
-
${
|
|
434
|
+
${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
526
435
|
}
|
|
527
436
|
const [targetCave] = await this.ctx.database.get("cave", { id });
|
|
528
|
-
if (!targetCave) {
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
if (targetCave.status !== "pending") {
|
|
532
|
-
return `回声洞(${id})无需审核`;
|
|
533
|
-
}
|
|
437
|
+
if (!targetCave) return `回声洞(${id})不存在`;
|
|
438
|
+
if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
|
|
534
439
|
if (id && !action) {
|
|
535
|
-
return this.
|
|
440
|
+
return [`待审核:`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
|
|
536
441
|
}
|
|
537
442
|
const normalizedAction = action.toLowerCase();
|
|
538
443
|
let reviewAction;
|
|
539
|
-
if (["y", "yes", "ok", "pass", "approve"].includes(normalizedAction))
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
reviewAction = "reject";
|
|
543
|
-
} else {
|
|
544
|
-
return `无效操作: "${action}"
|
|
444
|
+
if (["y", "yes", "ok", "pass", "approve"].includes(normalizedAction)) reviewAction = "approve";
|
|
445
|
+
else if (["n", "no", "deny", "reject"].includes(normalizedAction)) reviewAction = "reject";
|
|
446
|
+
else return `无效操作: "${action}"
|
|
545
447
|
请使用 "Y" (通过) 或 "N" (拒绝)`;
|
|
546
|
-
|
|
547
|
-
return this.processReview(reviewAction, id, session.username);
|
|
448
|
+
return this.processReview(reviewAction, id);
|
|
548
449
|
});
|
|
549
450
|
}
|
|
550
451
|
/**
|
|
551
|
-
*
|
|
552
|
-
* @param cave
|
|
452
|
+
* @description 将新回声洞提交到管理群组以供审核。
|
|
453
|
+
* @param cave 新创建的、状态为 'pending' 的回声洞对象。
|
|
553
454
|
*/
|
|
554
455
|
async sendForReview(cave) {
|
|
555
|
-
if (!this.config.
|
|
556
|
-
this.logger.warn(
|
|
456
|
+
if (!this.config.adminChannel) {
|
|
457
|
+
this.logger.warn(`未配置管理群组,已自动通过回声洞(${cave.id})`);
|
|
557
458
|
await this.ctx.database.upsert("cave", [{ id: cave.id, status: "active" }]);
|
|
558
459
|
return;
|
|
559
460
|
}
|
|
560
|
-
const reviewMessage = await this.
|
|
461
|
+
const reviewMessage = [`待审核:`, ...await buildCaveMessage(cave, this.config, this.fileManager, this.logger)];
|
|
561
462
|
try {
|
|
562
|
-
await this.ctx.broadcast(this.config.
|
|
463
|
+
await this.ctx.broadcast([this.config.adminChannel], import_koishi2.h.normalize(reviewMessage));
|
|
563
464
|
} catch (error) {
|
|
564
|
-
this.logger.error(
|
|
465
|
+
this.logger.error(`发送回声洞(${cave.id})审核消息失败:`, error);
|
|
565
466
|
}
|
|
566
467
|
}
|
|
567
468
|
/**
|
|
568
|
-
*
|
|
569
|
-
* @param
|
|
570
|
-
* @
|
|
571
|
-
* @private
|
|
572
|
-
*/
|
|
573
|
-
async buildReviewMessage(cave) {
|
|
574
|
-
const caveContent = await buildCaveMessage(cave, this.config, this.fileManager, this.logger);
|
|
575
|
-
return [
|
|
576
|
-
`以下内容待审核:`,
|
|
577
|
-
...caveContent
|
|
578
|
-
];
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* 处理管理员的审核决定(通过或拒绝)。
|
|
582
|
-
* @param action - 'approve' (通过) 或 'reject' (拒绝)。
|
|
583
|
-
* @param caveId - 被审核的回声洞 ID。
|
|
584
|
-
* @param adminUserName - 执行操作的管理员的昵称。
|
|
469
|
+
* @description 处理管理员的审核决定(通过或拒绝)。
|
|
470
|
+
* @param action 'approve' (通过) 或 'reject' (拒绝)。
|
|
471
|
+
* @param caveId 被审核的回声洞 ID。
|
|
585
472
|
* @returns 返回给操作者的确认消息。
|
|
586
473
|
*/
|
|
587
|
-
async processReview(action, caveId
|
|
588
|
-
const [cave] = await this.ctx.database.get("cave", { id: caveId });
|
|
589
|
-
if (!cave) return `回声洞(${caveId}
|
|
590
|
-
if (cave.status !== "pending") return `回声洞(${caveId})无需审核`;
|
|
474
|
+
async processReview(action, caveId) {
|
|
475
|
+
const [cave] = await this.ctx.database.get("cave", { id: caveId, status: "pending" });
|
|
476
|
+
if (!cave) return `回声洞(${caveId})无需审核`;
|
|
591
477
|
let resultMessage;
|
|
592
|
-
let broadcastMessage;
|
|
593
478
|
if (action === "approve") {
|
|
594
479
|
await this.ctx.database.upsert("cave", [{ id: caveId, status: "active" }]);
|
|
595
480
|
resultMessage = `回声洞(${caveId})已通过`;
|
|
596
|
-
broadcastMessage = `回声洞(${caveId})已由管理员 "${adminUserName}" 通过`;
|
|
597
481
|
} else {
|
|
598
482
|
await this.ctx.database.upsert("cave", [{ id: caveId, status: "delete" }]);
|
|
599
|
-
|
|
600
|
-
resultMessage = [
|
|
601
|
-
`回声洞(${caveId})已拒绝`,
|
|
602
|
-
...caveContent
|
|
603
|
-
];
|
|
604
|
-
broadcastMessage = [
|
|
605
|
-
`回声洞(${caveId})已由管理员 "${adminUserName}" 拒绝`,
|
|
606
|
-
...caveContent
|
|
607
|
-
];
|
|
483
|
+
resultMessage = `回声洞(${caveId})已拒绝`;
|
|
608
484
|
cleanupPendingDeletions(this.ctx, this.fileManager, this.logger);
|
|
609
485
|
}
|
|
610
|
-
if (broadcastMessage && this.config.adminUsers?.length) {
|
|
611
|
-
await this.ctx.broadcast(this.config.adminUsers, broadcastMessage).catch((err) => {
|
|
612
|
-
this.logger.error(`广播回声洞(${cave.id})审核结果失败:`, err);
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
486
|
return resultMessage;
|
|
616
487
|
}
|
|
617
488
|
};
|
|
@@ -625,57 +496,46 @@ var usage = `
|
|
|
625
496
|
<p>📖 <strong>使用文档</strong>:请点击左上角的 <strong>插件主页</strong> 查看插件使用文档</p>
|
|
626
497
|
<p>🔍 <strong>更多插件</strong>:可访问 <a href="https://github.com/YisRime" style="color:#4a6ee0;text-decoration:none;">苡淞的 GitHub</a> 查看本人的所有插件</p>
|
|
627
498
|
</div>
|
|
628
|
-
|
|
629
499
|
<div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
|
|
630
500
|
<h2 style="margin-top: 0; color: #e0574a;">❤️ 支持与反馈</h2>
|
|
631
501
|
<p>🌟 喜欢这个插件?请在 <a href="https://github.com/YisRime" style="color:#e0574a;text-decoration:none;">GitHub</a> 上给我一个 Star!</p>
|
|
632
502
|
<p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
|
|
633
503
|
</div>
|
|
634
504
|
`;
|
|
635
|
-
var logger = new
|
|
636
|
-
var Config =
|
|
637
|
-
|
|
638
|
-
coolDown:
|
|
639
|
-
perChannel:
|
|
640
|
-
enableProfile:
|
|
641
|
-
enableIO:
|
|
642
|
-
|
|
643
|
-
|
|
505
|
+
var logger = new import_koishi3.Logger("best-cave");
|
|
506
|
+
var Config = import_koishi3.Schema.intersect([
|
|
507
|
+
import_koishi3.Schema.object({
|
|
508
|
+
coolDown: import_koishi3.Schema.number().default(10).description("冷却时间(秒)"),
|
|
509
|
+
perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
|
|
510
|
+
enableProfile: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
|
|
511
|
+
enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
|
|
512
|
+
adminChannel: import_koishi3.Schema.string().description("管理群组 ID"),
|
|
513
|
+
caveFormat: import_koishi3.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本")
|
|
644
514
|
}).description("基础配置"),
|
|
645
|
-
|
|
646
|
-
enableReview:
|
|
515
|
+
import_koishi3.Schema.object({
|
|
516
|
+
enableReview: import_koishi3.Schema.boolean().default(false).description("启用审核")
|
|
647
517
|
}).description("审核配置"),
|
|
648
|
-
|
|
649
|
-
localPath:
|
|
650
|
-
enableS3:
|
|
651
|
-
publicUrl:
|
|
652
|
-
endpoint:
|
|
653
|
-
bucket:
|
|
654
|
-
region:
|
|
655
|
-
accessKeyId:
|
|
656
|
-
secretAccessKey:
|
|
518
|
+
import_koishi3.Schema.object({
|
|
519
|
+
localPath: import_koishi3.Schema.string().description("文件映射路径"),
|
|
520
|
+
enableS3: import_koishi3.Schema.boolean().default(false).description("启用 S3 存储"),
|
|
521
|
+
publicUrl: import_koishi3.Schema.string().description("公共访问 URL").role("link"),
|
|
522
|
+
endpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link"),
|
|
523
|
+
bucket: import_koishi3.Schema.string().description("存储桶 (Bucket)"),
|
|
524
|
+
region: import_koishi3.Schema.string().default("auto").description("区域 (Region)"),
|
|
525
|
+
accessKeyId: import_koishi3.Schema.string().description("Access Key ID").role("secret"),
|
|
526
|
+
secretAccessKey: import_koishi3.Schema.string().description("Secret Access Key").role("secret")
|
|
657
527
|
}).description("存储配置")
|
|
658
528
|
]);
|
|
659
529
|
function apply(ctx, config) {
|
|
660
530
|
ctx.model.extend("cave", {
|
|
661
531
|
id: "unsigned",
|
|
662
|
-
// 无符号整数,作为主键。
|
|
663
532
|
elements: "json",
|
|
664
|
-
// 存储为 JSON 字符串的元素数组。
|
|
665
533
|
channelId: "string",
|
|
666
|
-
// 频道 ID。
|
|
667
534
|
userId: "string",
|
|
668
|
-
// 用户 ID。
|
|
669
535
|
userName: "string",
|
|
670
|
-
// 用户昵称。
|
|
671
536
|
status: "string",
|
|
672
|
-
// 回声洞状态。
|
|
673
537
|
time: "timestamp"
|
|
674
|
-
|
|
675
|
-
}, {
|
|
676
|
-
primary: "id"
|
|
677
|
-
// 将 'id' 字段设置为主键。
|
|
678
|
-
});
|
|
538
|
+
}, { primary: "id" });
|
|
679
539
|
const fileManager = new FileManager(ctx.baseDir, config, logger);
|
|
680
540
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
681
541
|
let profileManager;
|
|
@@ -704,59 +564,85 @@ function apply(ctx, config) {
|
|
|
704
564
|
}
|
|
705
565
|
});
|
|
706
566
|
cave.subcommand(".add [content:text]", "添加回声洞").usage("添加一条回声洞。可以直接发送内容,也可以回复或引用一条消息。").action(async ({ session }, content) => {
|
|
707
|
-
const savedFileIdentifiers = [];
|
|
708
567
|
try {
|
|
709
568
|
let sourceElements;
|
|
710
569
|
if (session.quote?.elements) {
|
|
711
570
|
sourceElements = session.quote.elements;
|
|
712
571
|
} else if (content?.trim()) {
|
|
713
|
-
sourceElements =
|
|
572
|
+
sourceElements = import_koishi3.h.parse(content);
|
|
714
573
|
} else {
|
|
715
574
|
await session.send("请在一分钟内发送你要添加的内容");
|
|
716
575
|
const reply = await session.prompt(6e4);
|
|
717
576
|
if (!reply) return "操作超时,已取消添加";
|
|
718
|
-
sourceElements =
|
|
577
|
+
sourceElements = import_koishi3.h.parse(reply);
|
|
719
578
|
}
|
|
720
|
-
const
|
|
721
|
-
|
|
579
|
+
const idScopeQuery = {};
|
|
580
|
+
if (config.perChannel && session.channelId) {
|
|
581
|
+
idScopeQuery["channelId"] = session.channelId;
|
|
582
|
+
}
|
|
583
|
+
const newId = await getNextCaveId(ctx, idScopeQuery);
|
|
722
584
|
const finalElementsForDb = [];
|
|
585
|
+
const mediaToSave = [];
|
|
723
586
|
let mediaIndex = 0;
|
|
587
|
+
const typeMap = {
|
|
588
|
+
"img": "image",
|
|
589
|
+
"image": "image",
|
|
590
|
+
"video": "video",
|
|
591
|
+
"audio": "audio",
|
|
592
|
+
"file": "file",
|
|
593
|
+
"text": "text"
|
|
594
|
+
};
|
|
724
595
|
async function traverseAndProcess(elements) {
|
|
725
596
|
for (const el of elements) {
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
597
|
+
const normalizedType = typeMap[el.type];
|
|
598
|
+
if (!normalizedType) {
|
|
599
|
+
if (el.children) await traverseAndProcess(el.children);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (["image", "video", "audio", "file"].includes(normalizedType) && el.attrs.src) {
|
|
728
603
|
let fileIdentifier = el.attrs.src;
|
|
729
604
|
if (fileIdentifier.startsWith("http")) {
|
|
730
605
|
mediaIndex++;
|
|
731
|
-
const
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
606
|
+
const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
|
|
607
|
+
const ext = el.attrs.file && path3.extname(el.attrs.file) ? path3.extname(el.attrs.file) : defaultExtMap[normalizedType] || ".dat";
|
|
608
|
+
const channelIdentifier = session.channelId || "private";
|
|
609
|
+
const fileName = `${newId}_${mediaIndex}_${channelIdentifier}_${session.userId}${ext}`;
|
|
610
|
+
mediaToSave.push({ sourceUrl: fileIdentifier, fileName });
|
|
611
|
+
fileIdentifier = fileName;
|
|
735
612
|
}
|
|
736
|
-
finalElementsForDb.push({ type:
|
|
737
|
-
} else if (
|
|
613
|
+
finalElementsForDb.push({ type: normalizedType, file: fileIdentifier });
|
|
614
|
+
} else if (normalizedType === "text" && el.attrs.content?.trim()) {
|
|
738
615
|
finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
|
|
739
616
|
}
|
|
740
|
-
if (el.children)
|
|
617
|
+
if (el.children) {
|
|
618
|
+
await traverseAndProcess(el.children);
|
|
619
|
+
}
|
|
741
620
|
}
|
|
742
621
|
}
|
|
743
622
|
__name(traverseAndProcess, "traverseAndProcess");
|
|
744
623
|
await traverseAndProcess(sourceElements);
|
|
745
624
|
if (finalElementsForDb.length === 0) return "内容为空,已取消添加";
|
|
746
|
-
|
|
747
|
-
if (config.enableProfile) {
|
|
748
|
-
userName = await profileManager.getNickname(session.userId) || userName;
|
|
749
|
-
}
|
|
625
|
+
const customNickname = config.enableProfile ? await profileManager.getNickname(session.userId) : null;
|
|
750
626
|
const newCave = {
|
|
751
627
|
id: newId,
|
|
752
628
|
elements: finalElementsForDb,
|
|
753
629
|
channelId: session.channelId,
|
|
754
630
|
userId: session.userId,
|
|
755
|
-
userName,
|
|
631
|
+
userName: customNickname || session.username,
|
|
756
632
|
status: config.enableReview ? "pending" : "active",
|
|
757
633
|
time: /* @__PURE__ */ new Date()
|
|
758
634
|
};
|
|
759
635
|
await ctx.database.create("cave", newCave);
|
|
636
|
+
try {
|
|
637
|
+
await Promise.all(mediaToSave.map(async (media) => {
|
|
638
|
+
const response = await ctx.http.get(media.sourceUrl, { responseType: "arraybuffer", timeout: 3e4 });
|
|
639
|
+
await fileManager.saveFile(media.fileName, Buffer.from(response));
|
|
640
|
+
}));
|
|
641
|
+
} catch (fileSaveError) {
|
|
642
|
+
logger.error(`文件保存失败:`, fileSaveError);
|
|
643
|
+
await ctx.database.remove("cave", { id: newId });
|
|
644
|
+
throw fileSaveError;
|
|
645
|
+
}
|
|
760
646
|
if (newCave.status === "pending") {
|
|
761
647
|
reviewManager.sendForReview(newCave);
|
|
762
648
|
return `提交成功,序号为(${newCave.id})`;
|
|
@@ -764,10 +650,6 @@ function apply(ctx, config) {
|
|
|
764
650
|
return `添加成功,序号为(${newId})`;
|
|
765
651
|
} catch (error) {
|
|
766
652
|
logger.error("添加回声洞失败:", error);
|
|
767
|
-
if (savedFileIdentifiers.length > 0) {
|
|
768
|
-
logger.info(`添加失败,回滚并删除 ${savedFileIdentifiers.length} 个文件...`);
|
|
769
|
-
await Promise.all(savedFileIdentifiers.map((fileId) => fileManager.deleteFile(fileId)));
|
|
770
|
-
}
|
|
771
653
|
return "添加失败,请稍后再试";
|
|
772
654
|
}
|
|
773
655
|
});
|
|
@@ -778,9 +660,7 @@ function apply(ctx, config) {
|
|
|
778
660
|
try {
|
|
779
661
|
const query = { ...getScopeQuery(session, config), id };
|
|
780
662
|
const [targetCave] = await ctx.database.get("cave", query);
|
|
781
|
-
if (!targetCave) {
|
|
782
|
-
return `回声洞(${id})不存在`;
|
|
783
|
-
}
|
|
663
|
+
if (!targetCave) return `回声洞(${id})不存在`;
|
|
784
664
|
updateCooldownTimestamp(session, config, lastUsed);
|
|
785
665
|
return buildCaveMessage(targetCave, config, fileManager, logger);
|
|
786
666
|
} catch (error) {
|
|
@@ -793,18 +673,13 @@ function apply(ctx, config) {
|
|
|
793
673
|
try {
|
|
794
674
|
const [targetCave] = await ctx.database.get("cave", { id, status: "active" });
|
|
795
675
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (!isOwner && !isAdmin) {
|
|
799
|
-
return "抱歉,你没有权限删除这条回声洞";
|
|
676
|
+
if (targetCave.userId !== session.userId && session.channelId !== config.adminChannel) {
|
|
677
|
+
return "你没有权限删除这条回声洞";
|
|
800
678
|
}
|
|
801
679
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
802
680
|
const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
|
|
803
681
|
cleanupPendingDeletions(ctx, fileManager, logger);
|
|
804
|
-
return [
|
|
805
|
-
`以下内容已删除`,
|
|
806
|
-
...caveMessage
|
|
807
|
-
];
|
|
682
|
+
return [`已删除`, ...caveMessage];
|
|
808
683
|
} catch (error) {
|
|
809
684
|
logger.error(`标记回声洞(${id})失败:`, error);
|
|
810
685
|
return "删除失败,请稍后再试";
|