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