koishi-plugin-best-cave 2.0.8 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Utils.d.ts +29 -5
- package/lib/index.js +149 -165
- package/package.json +1 -1
- package/lib/DataManager.d.ts +0 -36
- package/lib/FileManager.d.ts +0 -48
- package/lib/ProfileManager.d.ts +0 -50
- package/lib/ReviewManager.d.ts +0 -38
- package/lib/index.d.ts +0 -48
package/lib/Utils.d.ts
CHANGED
|
@@ -39,11 +39,6 @@ export declare function getScopeQuery(session: Session, config: Config): object;
|
|
|
39
39
|
* @performance 在大数据集下,此函数可能存在性能瓶颈,因为它需要获取所有现有ID。
|
|
40
40
|
*/
|
|
41
41
|
export declare function getNextCaveId(ctx: Context, query?: object): Promise<number>;
|
|
42
|
-
/**
|
|
43
|
-
* @description 下载网络媒体资源并保存到文件存储中。
|
|
44
|
-
* @returns 保存后的文件名/标识符。
|
|
45
|
-
*/
|
|
46
|
-
export declare function downloadMedia(ctx: Context, fileManager: FileManager, url: string, originalName: string, type: string, caveId: number, index: number, channelId: string, userId: string): Promise<string>;
|
|
47
42
|
/**
|
|
48
43
|
* @description 检查用户是否处于指令冷却中。
|
|
49
44
|
* @returns 若在冷却中则返回提示字符串,否则返回 null。
|
|
@@ -53,3 +48,32 @@ export declare function checkCooldown(session: Session, config: Config, lastUsed
|
|
|
53
48
|
* @description 更新指定频道的指令使用时间戳。
|
|
54
49
|
*/
|
|
55
50
|
export declare function updateCooldownTimestamp(session: Session, config: Config, lastUsed: Map<string, number>): void;
|
|
51
|
+
/**
|
|
52
|
+
* @description 解析消息元素,分离出文本和待下载的媒体文件。
|
|
53
|
+
* @param sourceElements - 原始的 Koishi 消息元素数组。
|
|
54
|
+
* @param newId - 这条回声洞的新 ID。
|
|
55
|
+
* @param channelId - 频道 ID。
|
|
56
|
+
* @param userId - 用户 ID。
|
|
57
|
+
* @returns 一个包含数据库元素和待保存媒体列表的对象。
|
|
58
|
+
*/
|
|
59
|
+
export declare function processMessageElements(sourceElements: h[], newId: number, channelId: string, userId: string): Promise<{
|
|
60
|
+
finalElementsForDb: StoredElement[];
|
|
61
|
+
mediaToSave: {
|
|
62
|
+
sourceUrl: string;
|
|
63
|
+
fileName: string;
|
|
64
|
+
}[];
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* @description 异步处理文件上传和状态更新的后台任务。
|
|
68
|
+
* @param ctx - Koishi 上下文。
|
|
69
|
+
* @param config - 插件配置。
|
|
70
|
+
* @param fileManager - 文件管理器实例。
|
|
71
|
+
* @param logger - 日志记录器实例。
|
|
72
|
+
* @param reviewManager - 审核管理器实例 (可能为 null)。
|
|
73
|
+
* @param cave - 已创建的、状态为 'preload' 的回声洞对象。
|
|
74
|
+
* @param mediaToSave - 需要下载和保存的媒体文件列表。
|
|
75
|
+
*/
|
|
76
|
+
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reviewManager: any, cave: CaveObject, mediaToSave: {
|
|
77
|
+
sourceUrl: string;
|
|
78
|
+
fileName: string;
|
|
79
|
+
}[]): Promise<void>;
|
package/lib/index.js
CHANGED
|
@@ -38,7 +38,6 @@ __export(index_exports, {
|
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(index_exports);
|
|
40
40
|
var import_koishi3 = require("koishi");
|
|
41
|
-
var path3 = __toESM(require("path"));
|
|
42
41
|
|
|
43
42
|
// src/FileManager.ts
|
|
44
43
|
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
@@ -103,7 +102,6 @@ var FileManager = class {
|
|
|
103
102
|
Key: fileName,
|
|
104
103
|
Body: data,
|
|
105
104
|
ACL: "public-read"
|
|
106
|
-
// 默认为公开可读
|
|
107
105
|
});
|
|
108
106
|
await this.s3Client.send(command);
|
|
109
107
|
} else {
|
|
@@ -136,22 +134,18 @@ var FileManager = class {
|
|
|
136
134
|
* @param fileIdentifier 要删除的文件名/标识符。
|
|
137
135
|
*/
|
|
138
136
|
async deleteFile(fileIdentifier) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this.logger.warn(`删除本地文件 ${filePath} 失败:`, error);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
});
|
|
137
|
+
try {
|
|
138
|
+
if (this.s3Client) {
|
|
139
|
+
const command = new import_client_s3.DeleteObjectCommand({ Bucket: this.s3Bucket, Key: fileIdentifier });
|
|
140
|
+
await this.s3Client.send(command);
|
|
141
|
+
} else {
|
|
142
|
+
const filePath = path.join(this.resourceDir, fileIdentifier);
|
|
143
|
+
await this.withLock(filePath, () => fs.unlink(filePath));
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error.code !== "ENOENT" && error.name !== "NoSuchKey") {
|
|
147
|
+
this.logger.warn(`删除文件 ${fileIdentifier} 失败:`, error);
|
|
148
|
+
}
|
|
155
149
|
}
|
|
156
150
|
}
|
|
157
151
|
};
|
|
@@ -187,10 +181,9 @@ var ProfileManager = class {
|
|
|
187
181
|
if (trimmedNickname) {
|
|
188
182
|
await this.setNickname(session.userId, trimmedNickname);
|
|
189
183
|
return `昵称已更新为:${trimmedNickname}`;
|
|
190
|
-
} else {
|
|
191
|
-
await this.clearNickname(session.userId);
|
|
192
|
-
return "昵称已清除";
|
|
193
184
|
}
|
|
185
|
+
await this.clearNickname(session.userId);
|
|
186
|
+
return "昵称已清除";
|
|
194
187
|
});
|
|
195
188
|
}
|
|
196
189
|
/**
|
|
@@ -207,8 +200,8 @@ var ProfileManager = class {
|
|
|
207
200
|
* @returns 返回用户的昵称字符串,如果未设置则返回 null。
|
|
208
201
|
*/
|
|
209
202
|
async getNickname(userId) {
|
|
210
|
-
const profile = await this.ctx.database.get("cave_user", { userId });
|
|
211
|
-
return profile
|
|
203
|
+
const [profile] = await this.ctx.database.get("cave_user", { userId }, { fields: ["nickname"] });
|
|
204
|
+
return profile?.nickname ?? null;
|
|
212
205
|
}
|
|
213
206
|
/**
|
|
214
207
|
* @description 清除指定用户的昵称设置。
|
|
@@ -238,31 +231,28 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
|
|
|
238
231
|
const fileName = element.attrs.src;
|
|
239
232
|
if (!isMedia || !fileName) return element;
|
|
240
233
|
if (config.enableS3 && config.publicUrl) {
|
|
241
|
-
const fullUrl =
|
|
234
|
+
const fullUrl = new URL(fileName, config.publicUrl).href;
|
|
242
235
|
return (0, import_koishi.h)(element.type, { ...element.attrs, src: fullUrl });
|
|
243
236
|
}
|
|
244
237
|
if (config.localPath) {
|
|
245
|
-
|
|
246
|
-
return (0, import_koishi.h)(element.type, { ...element.attrs, src: fileUri });
|
|
238
|
+
return (0, import_koishi.h)(element.type, { ...element.attrs, src: `file://${path2.join(config.localPath, fileName)}` });
|
|
247
239
|
}
|
|
248
240
|
try {
|
|
249
241
|
const data = await fileManager.readFile(fileName);
|
|
250
|
-
const
|
|
251
|
-
const mimeType = mimeTypeMap[ext] || "application/octet-stream";
|
|
242
|
+
const mimeType = mimeTypeMap[path2.extname(fileName).toLowerCase()] || "application/octet-stream";
|
|
252
243
|
return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
|
|
253
244
|
} catch (error) {
|
|
254
245
|
logger2.warn(`转换文件 ${fileName} 为 Base64 失败:`, error);
|
|
255
246
|
return (0, import_koishi.h)("p", {}, `[${element.type}]`);
|
|
256
247
|
}
|
|
257
248
|
}));
|
|
258
|
-
const finalMessage = [];
|
|
259
|
-
const [headerFormat, footerFormat = ""] = config.caveFormat.split("|");
|
|
260
249
|
const replacements = { id: cave.id.toString(), name: cave.userName };
|
|
261
|
-
const
|
|
262
|
-
|
|
250
|
+
const formatPart = /* @__PURE__ */ __name((part) => part.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]).trim(), "formatPart");
|
|
251
|
+
const [header, footer] = config.caveFormat.split("|", 2).map(formatPart);
|
|
252
|
+
const finalMessage = [];
|
|
253
|
+
if (header) finalMessage.push(header + "\n");
|
|
263
254
|
finalMessage.push(...processedElements);
|
|
264
|
-
|
|
265
|
-
if (footerText.trim()) finalMessage.push(footerText);
|
|
255
|
+
if (footer) finalMessage.push("\n" + footer);
|
|
266
256
|
return finalMessage;
|
|
267
257
|
}
|
|
268
258
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
@@ -297,11 +287,10 @@ async function getNextCaveId(ctx, query = {}) {
|
|
|
297
287
|
__name(getNextCaveId, "getNextCaveId");
|
|
298
288
|
function checkCooldown(session, config, lastUsed) {
|
|
299
289
|
if (config.coolDown <= 0 || !session.channelId) return null;
|
|
300
|
-
const now = Date.now();
|
|
301
290
|
const lastTime = lastUsed.get(session.channelId) || 0;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return `指令冷却中,请在 ${
|
|
291
|
+
const remainingTime = lastTime + config.coolDown * 1e3 - Date.now();
|
|
292
|
+
if (remainingTime > 0) {
|
|
293
|
+
return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
|
|
305
294
|
}
|
|
306
295
|
return null;
|
|
307
296
|
}
|
|
@@ -312,6 +301,64 @@ function updateCooldownTimestamp(session, config, lastUsed) {
|
|
|
312
301
|
}
|
|
313
302
|
}
|
|
314
303
|
__name(updateCooldownTimestamp, "updateCooldownTimestamp");
|
|
304
|
+
async function processMessageElements(sourceElements, newId, channelId, userId) {
|
|
305
|
+
const finalElementsForDb = [];
|
|
306
|
+
const mediaToSave = [];
|
|
307
|
+
let mediaIndex = 0;
|
|
308
|
+
const typeMap = {
|
|
309
|
+
"img": "image",
|
|
310
|
+
"image": "image",
|
|
311
|
+
"video": "video",
|
|
312
|
+
"audio": "audio",
|
|
313
|
+
"file": "file",
|
|
314
|
+
"text": "text"
|
|
315
|
+
};
|
|
316
|
+
const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
|
|
317
|
+
async function traverse(elements) {
|
|
318
|
+
for (const el of elements) {
|
|
319
|
+
const normalizedType = typeMap[el.type];
|
|
320
|
+
if (normalizedType) {
|
|
321
|
+
if (["image", "video", "audio", "file"].includes(normalizedType) && el.attrs.src) {
|
|
322
|
+
let fileIdentifier = el.attrs.src;
|
|
323
|
+
if (fileIdentifier.startsWith("http")) {
|
|
324
|
+
const ext = path2.extname(el.attrs.file || "") || defaultExtMap[normalizedType];
|
|
325
|
+
const fileName = `${newId}_${++mediaIndex}_${channelId || "private"}_${userId}${ext}`;
|
|
326
|
+
mediaToSave.push({ sourceUrl: fileIdentifier, fileName });
|
|
327
|
+
fileIdentifier = fileName;
|
|
328
|
+
}
|
|
329
|
+
finalElementsForDb.push({ type: normalizedType, file: fileIdentifier });
|
|
330
|
+
} else if (normalizedType === "text" && el.attrs.content?.trim()) {
|
|
331
|
+
finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (el.children) await traverse(el.children);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
__name(traverse, "traverse");
|
|
338
|
+
await traverse(sourceElements);
|
|
339
|
+
return { finalElementsForDb, mediaToSave };
|
|
340
|
+
}
|
|
341
|
+
__name(processMessageElements, "processMessageElements");
|
|
342
|
+
async function handleFileUploads(ctx, config, fileManager, logger2, reviewManager, cave, mediaToSave) {
|
|
343
|
+
try {
|
|
344
|
+
const uploadPromises = mediaToSave.map(async (media) => {
|
|
345
|
+
const response = await ctx.http.get(media.sourceUrl, { responseType: "arraybuffer", timeout: 3e4 });
|
|
346
|
+
await fileManager.saveFile(media.fileName, Buffer.from(response));
|
|
347
|
+
});
|
|
348
|
+
await Promise.all(uploadPromises);
|
|
349
|
+
const finalStatus = config.enableReview ? "pending" : "active";
|
|
350
|
+
await ctx.database.upsert("cave", [{ id: cave.id, status: finalStatus }]);
|
|
351
|
+
if (finalStatus === "pending" && reviewManager) {
|
|
352
|
+
const [finalCave] = await ctx.database.get("cave", { id: cave.id });
|
|
353
|
+
if (finalCave) reviewManager.sendForReview(finalCave);
|
|
354
|
+
}
|
|
355
|
+
} catch (fileSaveError) {
|
|
356
|
+
logger2.error(`回声洞(${cave.id})文件保存失败:`, fileSaveError);
|
|
357
|
+
await ctx.database.upsert("cave", [{ id: cave.id, status: "delete" }]);
|
|
358
|
+
cleanupPendingDeletions(ctx, fileManager, logger2);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
__name(handleFileUploads, "handleFileUploads");
|
|
315
362
|
|
|
316
363
|
// src/DataManager.ts
|
|
317
364
|
var DataManager = class {
|
|
@@ -336,26 +383,19 @@ var DataManager = class {
|
|
|
336
383
|
* @param cave - 主 `cave` 命令实例。
|
|
337
384
|
*/
|
|
338
385
|
registerCommands(cave) {
|
|
339
|
-
|
|
340
|
-
|
|
386
|
+
const requireAdmin = /* @__PURE__ */ __name((action) => async ({ session }) => {
|
|
387
|
+
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
388
|
+
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
341
389
|
try {
|
|
342
|
-
await session.send("
|
|
343
|
-
return await
|
|
390
|
+
await session.send("正在处理,请稍候...");
|
|
391
|
+
return await action();
|
|
344
392
|
} catch (error) {
|
|
345
|
-
this.logger.error("
|
|
346
|
-
return
|
|
393
|
+
this.logger.error("数据操作时发生错误:", error);
|
|
394
|
+
return `操作失败: ${error.message || "未知错误"}`;
|
|
347
395
|
}
|
|
348
|
-
});
|
|
349
|
-
cave.subcommand(".
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
await session.send("正在导入数据,请稍候...");
|
|
353
|
-
return await this.importData();
|
|
354
|
-
} catch (error) {
|
|
355
|
-
this.logger.error("导入数据时发生错误:", error);
|
|
356
|
-
return `导入失败: ${error.message}`;
|
|
357
|
-
}
|
|
358
|
-
});
|
|
396
|
+
}, "requireAdmin");
|
|
397
|
+
cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(requireAdmin(() => this.exportData()));
|
|
398
|
+
cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(requireAdmin(() => this.importData()));
|
|
359
399
|
}
|
|
360
400
|
/**
|
|
361
401
|
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
@@ -382,18 +422,12 @@ var DataManager = class {
|
|
|
382
422
|
if (!Array.isArray(importedCaves)) throw new Error("导入文件格式无效");
|
|
383
423
|
} catch (error) {
|
|
384
424
|
this.logger.error(`读取导入文件失败:`, error);
|
|
385
|
-
|
|
425
|
+
throw new Error(`读取导入文件失败: ${error.message || "未知错误"}`);
|
|
386
426
|
}
|
|
387
427
|
let successCount = 0;
|
|
388
428
|
for (const cave of importedCaves) {
|
|
389
429
|
const newId = await getNextCaveId(this.ctx, {});
|
|
390
|
-
const newCave = {
|
|
391
|
-
...cave,
|
|
392
|
-
id: newId,
|
|
393
|
-
channelId: cave.channelId || null,
|
|
394
|
-
// 保证 channelId 存在
|
|
395
|
-
status: "active"
|
|
396
|
-
};
|
|
430
|
+
const newCave = { ...cave, id: newId, status: "active" };
|
|
397
431
|
await this.ctx.database.create("cave", newCave);
|
|
398
432
|
successCount++;
|
|
399
433
|
}
|
|
@@ -426,26 +460,29 @@ var ReviewManager = class {
|
|
|
426
460
|
*/
|
|
427
461
|
registerCommands(cave) {
|
|
428
462
|
cave.subcommand(".review [id:posint] [action:string]", "审核回声洞").usage("查看或审核回声洞,使用 <Y/N> 进行审核。").action(async ({ session }, id, action) => {
|
|
429
|
-
|
|
463
|
+
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
464
|
+
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
430
465
|
if (!id) {
|
|
431
|
-
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" });
|
|
432
|
-
if (pendingCaves.length
|
|
466
|
+
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" }, { fields: ["id"] });
|
|
467
|
+
if (!pendingCaves.length) return "当前没有需要审核的回声洞";
|
|
433
468
|
return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
|
|
434
469
|
${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
435
470
|
}
|
|
436
471
|
const [targetCave] = await this.ctx.database.get("cave", { id });
|
|
437
472
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
438
473
|
if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
|
|
439
|
-
if (
|
|
474
|
+
if (!action) {
|
|
440
475
|
return [`待审核:`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
|
|
441
476
|
}
|
|
442
477
|
const normalizedAction = action.toLowerCase();
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
478
|
+
if (["y", "yes", "pass", "approve"].includes(normalizedAction)) {
|
|
479
|
+
return this.processReview("approve", id);
|
|
480
|
+
}
|
|
481
|
+
if (["n", "no", "deny", "reject"].includes(normalizedAction)) {
|
|
482
|
+
return this.processReview("reject", id);
|
|
483
|
+
}
|
|
484
|
+
return `无效操作: "${action}"
|
|
447
485
|
请使用 "Y" (通过) 或 "N" (拒绝)`;
|
|
448
|
-
return this.processReview(reviewAction, id);
|
|
449
486
|
});
|
|
450
487
|
}
|
|
451
488
|
/**
|
|
@@ -453,13 +490,13 @@ ${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
|
453
490
|
* @param cave 新创建的、状态为 'pending' 的回声洞对象。
|
|
454
491
|
*/
|
|
455
492
|
async sendForReview(cave) {
|
|
456
|
-
if (!this.config.adminChannel) {
|
|
457
|
-
this.logger.warn(
|
|
493
|
+
if (!this.config.adminChannel?.includes(":")) {
|
|
494
|
+
this.logger.warn(`管理群组配置无效,已自动通过回声洞(${cave.id})`);
|
|
458
495
|
await this.ctx.database.upsert("cave", [{ id: cave.id, status: "active" }]);
|
|
459
496
|
return;
|
|
460
497
|
}
|
|
461
|
-
const reviewMessage = [`待审核:`, ...await buildCaveMessage(cave, this.config, this.fileManager, this.logger)];
|
|
462
498
|
try {
|
|
499
|
+
const reviewMessage = [`待审核:`, ...await buildCaveMessage(cave, this.config, this.fileManager, this.logger)];
|
|
463
500
|
await this.ctx.broadcast([this.config.adminChannel], import_koishi2.h.normalize(reviewMessage));
|
|
464
501
|
} catch (error) {
|
|
465
502
|
this.logger.error(`发送回声洞(${cave.id})审核消息失败:`, error);
|
|
@@ -474,16 +511,14 @@ ${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
|
474
511
|
async processReview(action, caveId) {
|
|
475
512
|
const [cave] = await this.ctx.database.get("cave", { id: caveId, status: "pending" });
|
|
476
513
|
if (!cave) return `回声洞(${caveId})无需审核`;
|
|
477
|
-
let resultMessage;
|
|
478
514
|
if (action === "approve") {
|
|
479
515
|
await this.ctx.database.upsert("cave", [{ id: caveId, status: "active" }]);
|
|
480
|
-
|
|
516
|
+
return `回声洞(${caveId})已通过`;
|
|
481
517
|
} else {
|
|
482
518
|
await this.ctx.database.upsert("cave", [{ id: caveId, status: "delete" }]);
|
|
483
|
-
resultMessage = `回声洞(${caveId})已拒绝`;
|
|
484
519
|
cleanupPendingDeletions(this.ctx, this.fileManager, this.logger);
|
|
520
|
+
return `回声洞(${caveId})已拒绝`;
|
|
485
521
|
}
|
|
486
|
-
return resultMessage;
|
|
487
522
|
}
|
|
488
523
|
};
|
|
489
524
|
|
|
@@ -509,7 +544,7 @@ var Config = import_koishi3.Schema.intersect([
|
|
|
509
544
|
perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
|
|
510
545
|
enableProfile: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
|
|
511
546
|
enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
|
|
512
|
-
adminChannel: import_koishi3.Schema.string().description("管理群组 ID"),
|
|
547
|
+
adminChannel: import_koishi3.Schema.string().default("onebot:").description("管理群组 ID"),
|
|
513
548
|
caveFormat: import_koishi3.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本")
|
|
514
549
|
}).description("基础配置"),
|
|
515
550
|
import_koishi3.Schema.object({
|
|
@@ -538,10 +573,10 @@ function apply(ctx, config) {
|
|
|
538
573
|
}, { primary: "id" });
|
|
539
574
|
const fileManager = new FileManager(ctx.baseDir, config, logger);
|
|
540
575
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const cave = ctx.command("cave", "回声洞").option("add", "-a <content:text>
|
|
576
|
+
const profileManager = config.enableProfile ? new ProfileManager(ctx) : null;
|
|
577
|
+
const dataManager = config.enableIO ? new DataManager(ctx, config, fileManager, logger) : null;
|
|
578
|
+
const reviewManager = config.enableReview ? new ReviewManager(ctx, config, fileManager, logger) : null;
|
|
579
|
+
const cave = ctx.command("cave", "回声洞").option("add", "-a <content:text>").option("view", "-g <id:posint>").option("delete", "-r <id:posint>").option("list", "-l").usage("随机抽取一条已添加的回声洞。").action(async ({ session, options }) => {
|
|
545
580
|
if (options.add) return session.execute(`cave.add ${options.add}`);
|
|
546
581
|
if (options.view) return session.execute(`cave.view ${options.view}`);
|
|
547
582
|
if (options.delete) return session.execute(`cave.del ${options.delete}`);
|
|
@@ -551,7 +586,7 @@ function apply(ctx, config) {
|
|
|
551
586
|
try {
|
|
552
587
|
const query = getScopeQuery(session, config);
|
|
553
588
|
const candidates = await ctx.database.get("cave", query, { fields: ["id"] });
|
|
554
|
-
if (candidates.length
|
|
589
|
+
if (!candidates.length) {
|
|
555
590
|
return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
|
|
556
591
|
}
|
|
557
592
|
const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
|
|
@@ -565,89 +600,44 @@ function apply(ctx, config) {
|
|
|
565
600
|
});
|
|
566
601
|
cave.subcommand(".add [content:text]", "添加回声洞").usage("添加一条回声洞。可以直接发送内容,也可以回复或引用一条消息。").action(async ({ session }, content) => {
|
|
567
602
|
try {
|
|
568
|
-
let sourceElements;
|
|
569
|
-
if (
|
|
570
|
-
sourceElements = session.quote.elements;
|
|
571
|
-
} else if (content?.trim()) {
|
|
603
|
+
let sourceElements = session.quote?.elements;
|
|
604
|
+
if (!sourceElements && content?.trim()) {
|
|
572
605
|
sourceElements = import_koishi3.h.parse(content);
|
|
573
|
-
}
|
|
606
|
+
}
|
|
607
|
+
if (!sourceElements) {
|
|
574
608
|
await session.send("请在一分钟内发送你要添加的内容");
|
|
575
609
|
const reply = await session.prompt(6e4);
|
|
576
610
|
if (!reply) return "操作超时,已取消添加";
|
|
577
611
|
sourceElements = import_koishi3.h.parse(reply);
|
|
578
612
|
}
|
|
579
|
-
const idScopeQuery = {};
|
|
580
|
-
if (config.perChannel && session.channelId) {
|
|
581
|
-
idScopeQuery["channelId"] = session.channelId;
|
|
582
|
-
}
|
|
613
|
+
const idScopeQuery = config.perChannel && session.channelId ? { channelId: session.channelId } : {};
|
|
583
614
|
const newId = await getNextCaveId(ctx, idScopeQuery);
|
|
584
|
-
const finalElementsForDb =
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
"video": "video",
|
|
591
|
-
"audio": "audio",
|
|
592
|
-
"file": "file",
|
|
593
|
-
"text": "text"
|
|
594
|
-
};
|
|
595
|
-
async function traverseAndProcess(elements) {
|
|
596
|
-
for (const el of elements) {
|
|
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) {
|
|
603
|
-
let fileIdentifier = el.attrs.src;
|
|
604
|
-
if (fileIdentifier.startsWith("http")) {
|
|
605
|
-
mediaIndex++;
|
|
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;
|
|
612
|
-
}
|
|
613
|
-
finalElementsForDb.push({ type: normalizedType, file: fileIdentifier });
|
|
614
|
-
} else if (normalizedType === "text" && el.attrs.content?.trim()) {
|
|
615
|
-
finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
|
|
616
|
-
}
|
|
617
|
-
if (el.children) {
|
|
618
|
-
await traverseAndProcess(el.children);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
__name(traverseAndProcess, "traverseAndProcess");
|
|
623
|
-
await traverseAndProcess(sourceElements);
|
|
615
|
+
const { finalElementsForDb, mediaToSave } = await processMessageElements(
|
|
616
|
+
sourceElements,
|
|
617
|
+
newId,
|
|
618
|
+
session.channelId,
|
|
619
|
+
session.userId
|
|
620
|
+
);
|
|
624
621
|
if (finalElementsForDb.length === 0) return "内容为空,已取消添加";
|
|
625
|
-
const
|
|
622
|
+
const userName = (config.enableProfile ? await profileManager.getNickname(session.userId) : null) || session.username;
|
|
623
|
+
const hasMedia = mediaToSave.length > 0;
|
|
624
|
+
const initialStatus = hasMedia ? "preload" : config.enableReview ? "pending" : "active";
|
|
626
625
|
const newCave = {
|
|
627
626
|
id: newId,
|
|
628
627
|
elements: finalElementsForDb,
|
|
629
628
|
channelId: session.channelId,
|
|
630
629
|
userId: session.userId,
|
|
631
|
-
userName
|
|
632
|
-
status:
|
|
630
|
+
userName,
|
|
631
|
+
status: initialStatus,
|
|
633
632
|
time: /* @__PURE__ */ new Date()
|
|
634
633
|
};
|
|
635
634
|
await ctx.database.create("cave", newCave);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
}
|
|
646
|
-
if (newCave.status === "pending") {
|
|
635
|
+
if (hasMedia) {
|
|
636
|
+
handleFileUploads(ctx, config, fileManager, logger, reviewManager, newCave, mediaToSave);
|
|
637
|
+
} else if (initialStatus === "pending") {
|
|
647
638
|
reviewManager.sendForReview(newCave);
|
|
648
|
-
return `提交成功,序号为(${newCave.id})`;
|
|
649
639
|
}
|
|
650
|
-
return `添加成功,序号为(${newId})`;
|
|
640
|
+
return initialStatus === "pending" || initialStatus === "preload" && config.enableReview ? `提交成功,序号为(${newId})` : `添加成功,序号为(${newId})`;
|
|
651
641
|
} catch (error) {
|
|
652
642
|
logger.error("添加回声洞失败:", error);
|
|
653
643
|
return "添加失败,请稍后再试";
|
|
@@ -673,7 +663,10 @@ function apply(ctx, config) {
|
|
|
673
663
|
try {
|
|
674
664
|
const [targetCave] = await ctx.database.get("cave", { id, status: "active" });
|
|
675
665
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
676
|
-
|
|
666
|
+
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
667
|
+
const isAuthor = targetCave.userId === session.userId;
|
|
668
|
+
const isAdmin = session.channelId === adminChannelId;
|
|
669
|
+
if (!isAuthor && !isAdmin) {
|
|
677
670
|
return "你没有权限删除这条回声洞";
|
|
678
671
|
}
|
|
679
672
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
@@ -688,8 +681,8 @@ function apply(ctx, config) {
|
|
|
688
681
|
cave.subcommand(".list", "查询我的投稿").usage("查询并列出你所有投稿的回声洞序号。").action(async ({ session }) => {
|
|
689
682
|
try {
|
|
690
683
|
const query = { ...getScopeQuery(session, config), userId: session.userId };
|
|
691
|
-
const userCaves = await ctx.database.get("cave", query);
|
|
692
|
-
if (userCaves.length
|
|
684
|
+
const userCaves = await ctx.database.get("cave", query, { fields: ["id"] });
|
|
685
|
+
if (!userCaves.length) return "你还没有投稿过回声洞";
|
|
693
686
|
const caveIds = userCaves.map((c) => c.id).sort((a, b) => a - b).join(", ");
|
|
694
687
|
return `你已投稿 ${userCaves.length} 条回声洞,序号为:
|
|
695
688
|
${caveIds}`;
|
|
@@ -698,18 +691,9 @@ ${caveIds}`;
|
|
|
698
691
|
return "查询失败,请稍后再试";
|
|
699
692
|
}
|
|
700
693
|
});
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
if (config.enableIO) {
|
|
706
|
-
dataManager = new DataManager(ctx, config, fileManager, logger);
|
|
707
|
-
dataManager.registerCommands(cave);
|
|
708
|
-
}
|
|
709
|
-
if (config.enableReview) {
|
|
710
|
-
reviewManager = new ReviewManager(ctx, config, fileManager, logger);
|
|
711
|
-
reviewManager.registerCommands(cave);
|
|
712
|
-
}
|
|
694
|
+
if (profileManager) profileManager.registerCommands(cave);
|
|
695
|
+
if (dataManager) dataManager.registerCommands(cave);
|
|
696
|
+
if (reviewManager) reviewManager.registerCommands(cave);
|
|
713
697
|
}
|
|
714
698
|
__name(apply, "apply");
|
|
715
699
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
package/lib/DataManager.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { Context, Logger } from 'koishi';
|
|
2
|
-
import { FileManager } from './FileManager';
|
|
3
|
-
import { Config } from './index';
|
|
4
|
-
/**
|
|
5
|
-
* @class DataManager
|
|
6
|
-
* @description 负责处理回声洞数据的导入和导出功能。
|
|
7
|
-
*/
|
|
8
|
-
export declare class DataManager {
|
|
9
|
-
private ctx;
|
|
10
|
-
private config;
|
|
11
|
-
private fileManager;
|
|
12
|
-
private logger;
|
|
13
|
-
/**
|
|
14
|
-
* @constructor
|
|
15
|
-
* @param ctx Koishi 上下文,用于数据库操作。
|
|
16
|
-
* @param config 插件配置。
|
|
17
|
-
* @param fileManager 文件管理器实例。
|
|
18
|
-
* @param logger 日志记录器实例。
|
|
19
|
-
*/
|
|
20
|
-
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
|
|
21
|
-
/**
|
|
22
|
-
* @description 注册 `.export` 和 `.import` 子命令。
|
|
23
|
-
* @param cave - 主 `cave` 命令实例。
|
|
24
|
-
*/
|
|
25
|
-
registerCommands(cave: any): void;
|
|
26
|
-
/**
|
|
27
|
-
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
28
|
-
* @returns 描述导出结果的消息字符串。
|
|
29
|
-
*/
|
|
30
|
-
exportData(): Promise<string>;
|
|
31
|
-
/**
|
|
32
|
-
* @description 从 `cave_import.json` 文件导入回声洞数据。
|
|
33
|
-
* @returns 描述导入结果的消息字符串。
|
|
34
|
-
*/
|
|
35
|
-
importData(): Promise<string>;
|
|
36
|
-
}
|
package/lib/FileManager.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Logger } from 'koishi';
|
|
2
|
-
import { Config } from './index';
|
|
3
|
-
/**
|
|
4
|
-
* @class FileManager
|
|
5
|
-
* @description 封装了对文件的存储、读取和删除操作。
|
|
6
|
-
* 能根据配置自动选择使用本地文件系统或 AWS S3 作为存储后端。
|
|
7
|
-
* 内置 Promise 文件锁,防止本地文件的并发写入冲突。
|
|
8
|
-
*/
|
|
9
|
-
export declare class FileManager {
|
|
10
|
-
private logger;
|
|
11
|
-
private resourceDir;
|
|
12
|
-
private locks;
|
|
13
|
-
private s3Client?;
|
|
14
|
-
private s3Bucket?;
|
|
15
|
-
/**
|
|
16
|
-
* @constructor
|
|
17
|
-
* @param baseDir Koishi 应用的基础数据目录 (ctx.baseDir)。
|
|
18
|
-
* @param config 插件的配置对象。
|
|
19
|
-
* @param logger 日志记录器实例。
|
|
20
|
-
*/
|
|
21
|
-
constructor(baseDir: string, config: Config, logger: Logger);
|
|
22
|
-
/**
|
|
23
|
-
* @description 使用文件锁安全地执行异步文件操作,防止并发读写冲突。
|
|
24
|
-
* @template T 异步操作的返回类型。
|
|
25
|
-
* @param fullPath 需要加锁的文件的完整路径。
|
|
26
|
-
* @param operation 要执行的异步函数。
|
|
27
|
-
* @returns 返回异步操作的结果。
|
|
28
|
-
*/
|
|
29
|
-
private withLock;
|
|
30
|
-
/**
|
|
31
|
-
* @description 保存文件,自动选择 S3 或本地存储。
|
|
32
|
-
* @param fileName 用作 S3 Key 或本地文件名。
|
|
33
|
-
* @param data 要写入的 Buffer 数据。
|
|
34
|
-
* @returns 返回保存时使用的文件名/标识符。
|
|
35
|
-
*/
|
|
36
|
-
saveFile(fileName: string, data: Buffer): Promise<string>;
|
|
37
|
-
/**
|
|
38
|
-
* @description 读取文件,自动从 S3 或本地存储读取。
|
|
39
|
-
* @param fileName 要读取的文件名/标识符。
|
|
40
|
-
* @returns 文件的 Buffer 数据。
|
|
41
|
-
*/
|
|
42
|
-
readFile(fileName: string): Promise<Buffer>;
|
|
43
|
-
/**
|
|
44
|
-
* @description 删除文件,自动从 S3 或本地删除。
|
|
45
|
-
* @param fileIdentifier 要删除的文件名/标识符。
|
|
46
|
-
*/
|
|
47
|
-
deleteFile(fileIdentifier: string): Promise<void>;
|
|
48
|
-
}
|
package/lib/ProfileManager.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
/**
|
|
3
|
-
* @description 数据库 `cave_user` 表的结构定义。
|
|
4
|
-
* @property userId 用户唯一ID,作为主键。
|
|
5
|
-
* @property nickname 用户自定义的昵称。
|
|
6
|
-
*/
|
|
7
|
-
export interface UserProfile {
|
|
8
|
-
userId: string;
|
|
9
|
-
nickname: string;
|
|
10
|
-
}
|
|
11
|
-
declare module 'koishi' {
|
|
12
|
-
interface Tables {
|
|
13
|
-
cave_user: UserProfile;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* @class ProfileManager
|
|
18
|
-
* @description 负责管理用户在回声洞中的自定义昵称。
|
|
19
|
-
* 当插件配置 `enableProfile` 为 true 时实例化。
|
|
20
|
-
*/
|
|
21
|
-
export declare class ProfileManager {
|
|
22
|
-
private ctx;
|
|
23
|
-
/**
|
|
24
|
-
* @constructor
|
|
25
|
-
* @param ctx - Koishi 上下文,用于初始化数据库模型。
|
|
26
|
-
*/
|
|
27
|
-
constructor(ctx: Context);
|
|
28
|
-
/**
|
|
29
|
-
* @description 注册 `.profile` 子命令,用于管理用户昵称。
|
|
30
|
-
* @param cave - 主 `cave` 命令实例。
|
|
31
|
-
*/
|
|
32
|
-
registerCommands(cave: any): void;
|
|
33
|
-
/**
|
|
34
|
-
* @description 设置或更新指定用户的昵称。
|
|
35
|
-
* @param userId - 目标用户的 ID。
|
|
36
|
-
* @param nickname - 要设置的新昵称。
|
|
37
|
-
*/
|
|
38
|
-
setNickname(userId: string, nickname: string): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* @description 获取指定用户的昵称。
|
|
41
|
-
* @param userId - 目标用户的 ID。
|
|
42
|
-
* @returns 返回用户的昵称字符串,如果未设置则返回 null。
|
|
43
|
-
*/
|
|
44
|
-
getNickname(userId: string): Promise<string | null>;
|
|
45
|
-
/**
|
|
46
|
-
* @description 清除指定用户的昵称设置。
|
|
47
|
-
* @param userId - 目标用户的 ID。
|
|
48
|
-
*/
|
|
49
|
-
clearNickname(userId: string): Promise<void>;
|
|
50
|
-
}
|
package/lib/ReviewManager.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { Context, Logger } from 'koishi';
|
|
2
|
-
import { CaveObject, Config } from './index';
|
|
3
|
-
import { FileManager } from './FileManager';
|
|
4
|
-
/**
|
|
5
|
-
* @class ReviewManager
|
|
6
|
-
* @description 负责处理回声洞的审核流程,处理新洞的提交、审核通知和审核操作。
|
|
7
|
-
*/
|
|
8
|
-
export declare class ReviewManager {
|
|
9
|
-
private ctx;
|
|
10
|
-
private config;
|
|
11
|
-
private fileManager;
|
|
12
|
-
private logger;
|
|
13
|
-
/**
|
|
14
|
-
* @constructor
|
|
15
|
-
* @param ctx Koishi 上下文。
|
|
16
|
-
* @param config 插件配置。
|
|
17
|
-
* @param fileManager 文件管理器实例。
|
|
18
|
-
* @param logger 日志记录器实例。
|
|
19
|
-
*/
|
|
20
|
-
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
|
|
21
|
-
/**
|
|
22
|
-
* @description 注册与审核相关的 `.review` 子命令。
|
|
23
|
-
* @param cave - 主 `cave` 命令实例。
|
|
24
|
-
*/
|
|
25
|
-
registerCommands(cave: any): void;
|
|
26
|
-
/**
|
|
27
|
-
* @description 将新回声洞提交到管理群组以供审核。
|
|
28
|
-
* @param cave 新创建的、状态为 'pending' 的回声洞对象。
|
|
29
|
-
*/
|
|
30
|
-
sendForReview(cave: CaveObject): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* @description 处理管理员的审核决定(通过或拒绝)。
|
|
33
|
-
* @param action 'approve' (通过) 或 'reject' (拒绝)。
|
|
34
|
-
* @param caveId 被审核的回声洞 ID。
|
|
35
|
-
* @returns 返回给操作者的确认消息。
|
|
36
|
-
*/
|
|
37
|
-
processReview(action: 'approve' | 'reject', caveId: number): Promise<string>;
|
|
38
|
-
}
|
package/lib/index.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Context, Schema } from 'koishi';
|
|
2
|
-
export declare const name = "best-cave";
|
|
3
|
-
export declare const inject: string[];
|
|
4
|
-
export declare const usage = "\n<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);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCCC \u63D2\u4EF6\u8BF4\u660E</h2>\n <p>\uD83D\uDCD6 <strong>\u4F7F\u7528\u6587\u6863</strong>\uFF1A\u8BF7\u70B9\u51FB\u5DE6\u4E0A\u89D2\u7684 <strong>\u63D2\u4EF6\u4E3B\u9875</strong> \u67E5\u770B\u63D2\u4EF6\u4F7F\u7528\u6587\u6863</p>\n <p>\uD83D\uDD0D <strong>\u66F4\u591A\u63D2\u4EF6</strong>\uFF1A\u53EF\u8BBF\u95EE <a href=\"https://github.com/YisRime\" style=\"color:#4a6ee0;text-decoration:none;\">\u82E1\u6DDE\u7684 GitHub</a> \u67E5\u770B\u672C\u4EBA\u7684\u6240\u6709\u63D2\u4EF6</p>\n</div>\n<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);\">\n <h2 style=\"margin-top: 0; color: #e0574a;\">\u2764\uFE0F \u652F\u6301\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u8BF7\u5728 <a href=\"https://github.com/YisRime\" style=\"color:#e0574a;text-decoration:none;\">GitHub</a> \u4E0A\u7ED9\u6211\u4E00\u4E2A Star\uFF01</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u8BF7\u901A\u8FC7 <strong>Issues</strong> \u63D0\u4EA4\u53CD\u9988\uFF0C\u6216\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/PdLMx9Jowq\" style=\"color:#e0574a;text-decoration:none;\"><strong>855571375</strong></a> \u8FDB\u884C\u4EA4\u6D41</p>\n</div>\n";
|
|
5
|
-
/**
|
|
6
|
-
* @description 存储在数据库中的单个消息元素。
|
|
7
|
-
*/
|
|
8
|
-
export interface StoredElement {
|
|
9
|
-
type: 'text' | 'image' | 'video' | 'audio' | 'file';
|
|
10
|
-
content?: string;
|
|
11
|
-
file?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* @description 数据库 `cave` 表的完整对象模型。
|
|
15
|
-
*/
|
|
16
|
-
export interface CaveObject {
|
|
17
|
-
id: number;
|
|
18
|
-
elements: StoredElement[];
|
|
19
|
-
channelId: string;
|
|
20
|
-
userId: string;
|
|
21
|
-
userName: string;
|
|
22
|
-
status: 'active' | 'delete' | 'pending';
|
|
23
|
-
time: Date;
|
|
24
|
-
}
|
|
25
|
-
declare module 'koishi' {
|
|
26
|
-
interface Tables {
|
|
27
|
-
cave: CaveObject;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export interface Config {
|
|
31
|
-
coolDown: number;
|
|
32
|
-
perChannel: boolean;
|
|
33
|
-
adminChannel: string;
|
|
34
|
-
enableProfile: boolean;
|
|
35
|
-
enableIO: boolean;
|
|
36
|
-
enableReview: boolean;
|
|
37
|
-
caveFormat: string;
|
|
38
|
-
localPath?: string;
|
|
39
|
-
enableS3: boolean;
|
|
40
|
-
endpoint?: string;
|
|
41
|
-
region?: string;
|
|
42
|
-
accessKeyId?: string;
|
|
43
|
-
secretAccessKey?: string;
|
|
44
|
-
bucket?: string;
|
|
45
|
-
publicUrl?: string;
|
|
46
|
-
}
|
|
47
|
-
export declare const Config: Schema<Config>;
|
|
48
|
-
export declare function apply(ctx: Context, config: Config): void;
|