koishi-plugin-best-cave 1.1.5 → 1.2.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.
Files changed (3) hide show
  1. package/lib/index.d.ts +25 -0
  2. package/lib/index.js +690 -474
  3. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -33,14 +33,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
33
33
  // src/locales/zh-CN.yml
34
34
  var require_zh_CN = __commonJS({
35
35
  "src/locales/zh-CN.yml"(exports2, module2) {
36
- module2.exports = { _config: { manager: "管理员", blacklist: "黑名单(用户)", whitelist: "审核白名单(用户/群组/频道)", number: "冷却时间(秒)", enableAudit: "启用审核", allowVideo: "允许视频上传", videoMaxSize: "视频最大大小(MB)", imageMaxSize: "图片最大大小(MB)", enablePagination: "启用统计分页", itemsPerPage: "每页显示数目" }, commands: { cave: { description: "回声洞", usage: "支持添加、抽取、查看、管理回声洞", examples: "使用 cave 随机抽取回声洞\n使用 -a 直接添加或引用添加\n使用 -g 查看指定回声洞\n使用 -r 删除指定回声洞", options: { a: "添加回声洞", g: "查看回声洞", r: "删除回声洞", p: "通过审核(批量)", d: "拒绝审核(批量)", l: "查询投稿统计" }, add: { noContent: "请在一分钟内发送内容", operationTimeout: "操作超时,添加取消", videoDisabled: "不允许上传视频", submitPending: "提交成功,序号为({0})", addSuccess: "添加成功,序号为({0})", mediaSizeExceeded: "{0}文件大小超过限制", mediaFormatUnsupported: "{0}格式不支持", videoSendFailed: "视频发送失败", localFileNotAllowed: "检测到本地文件路径,无法保存" }, remove: { noPermission: "你无权删除他人添加的回声洞", deletePending: "删除(待审核)", deleted: "已删除" }, list: { pageInfo: "第 {0} / {1} 页", header: "当前共有 {0} 项回声洞:", totalItems: "用户 {0} 共计投稿 {1} 项:", idsLine: "{0}" }, audit: { noPending: "暂无待审核回声洞", pendingNotFound: "未找到待审核回声洞", pendingResult: "{0},剩余 {1} 个待审核回声洞:[{2}]", auditPassed: "已通过", auditRejected: "已拒绝", batchAuditResult: "已{0} {1}/{2} 项回声洞", title: "待审核回声洞:", from: "投稿人:", sendFailed: "发送审核消息失败,无法联系管理员 {0}" }, error: { commandProcess: "命令处理失败:{0}", getCave: "获取回声洞失败", noCave: "暂无回声洞", saveFailed: "保存数据失败", mediaLoadFailed: "[加载{0}失败]", videoSendFailed: "视频发送失败", uploadImageFailed: "图片上传失败", uploadVideoFailed: "视频上传失败", fileRead: "读取 JSON 数据失败:{0}", fileWrite: "写入 JSON 数据失败:{0}", notFound: "未找到", userInfo: "获取用户信息失败:{0}", auditProcess: "审核处理失败:{0}", invalidId: "请输入有效的回声洞序号" }, message: { blacklisted: "你已被列入黑名单", managerOnly: "此操作仅限管理员可用", cooldown: "群聊冷却中...请在 {0} 秒后重试", caveTitle: "回声洞 —— ({0})", videoSending: "[视频发送中]", mediaInvalid: "[无效的{0}]", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0}文件大小超过限制" } } } };
36
+ module2.exports = { _config: { manager: "管理员", blacklist: "黑名单(用户)", whitelist: "审核白名单(用户/群组/频道)", number: "冷却时间(秒)", enableAudit: "启用审核", allowVideo: "允许视频上传", videoMaxSize: "视频最大大小(MB)", imageMaxSize: "图片最大大小(MB)", enablePagination: "启用统计分页", itemsPerPage: "每页显示数目" }, commands: { cave: { description: "回声洞", usage: "支持添加、抽取、查看、管理回声洞", examples: "使用 cave 随机抽取回声洞\n使用 -a 直接添加或引用添加\n使用 -g 查看指定回声洞\n使用 -r 删除指定回声洞", options: { a: "添加回声洞", g: "查看回声洞", r: "删除回声洞", p: "通过审核(批量)", d: "拒绝审核(批量)", l: "查询投稿统计" }, add: { noContent: "请在一分钟内发送内容", operationTimeout: "操作超时,添加取消", videoDisabled: "不允许上传视频", submitPending: "提交成功,序号为({0})", addSuccess: "添加成功,序号为({0})", mediaSizeExceeded: "{0}文件大小超过限制", localFileNotAllowed: "检测到本地文件路径,无法保存" }, remove: { noPermission: "你无权删除他人添加的回声洞", deletePending: "删除(待审核)", deleted: "已删除" }, list: { pageInfo: "第 {0} / {1} 页", header: "当前共有 {0} 项回声洞:", totalItems: "用户 {0} 共计投稿 {1} 项:", idsLine: "{0}" }, audit: { noPending: "暂无待审核回声洞", pendingNotFound: "未找到待审核回声洞", pendingResult: "{0},剩余 {1} 个待审核回声洞:[{2}]", auditPassed: "已通过", auditRejected: "已拒绝", batchAuditResult: "已{0} {1}/{2} 项回声洞", title: "待审核回声洞:", from: "投稿人:", sendFailed: "发送审核消息失败,无法联系管理员 {0}" }, error: { noContent: "回声洞内容为空", getCave: "获取回声洞失败", noCave: "没有回声洞", invalidId: "请输入有效的回声洞ID", notFound: "未找到该回声洞" }, message: { blacklisted: "你已被列入黑名单", managerOnly: "此操作仅限管理员可用", cooldown: "群聊冷却中...请在 {0} 秒后重试", caveTitle: "回声洞 —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0}文件大小超过限制" } } } };
37
37
  }
38
38
  });
39
39
 
40
40
  // src/locales/en-US.yml
41
41
  var require_en_US = __commonJS({
42
42
  "src/locales/en-US.yml"(exports2, module2) {
43
- module2.exports = { _config: { manager: "Manager", blacklist: "Blacklist (User)", whitelist: "Whitelist (User/Group/Channel)", number: "Cooldown Time (Seconds)", enableAudit: "Enable Audit", allowVideo: "Allow Video Upload", videoMaxSize: "Max Video Size (MB)", imageMaxSize: "Max Image Size (MB)", enablePagination: "Enable Pagination", itemsPerPage: "Items Per Page" }, commands: { cave: { description: "Echo Cave", usage: "Support adding, drawing, viewing, and managing Echo Cave", examples: "Use cave to randomly draw Echo Cave\nUse -a to add or reference directly\nUse -g to view specified Echo Cave\nUse -r to delete specified Echo Cave", options: { a: "Add Echo Cave", g: "View Echo Cave", r: "Delete Echo Cave", p: "Approve Audit (Batch)", d: "Reject Audit (Batch)", l: "Query Submission Statistics" }, add: { noContent: "Please send content within one minute", operationTimeout: "Operation timed out, addition canceled", videoDisabled: "Video upload not allowed", submitPending: "Submission successful, ID is ({0})", addSuccess: "Addition successful, ID is ({0})", mediaSizeExceeded: "{0} file size exceeds limit", mediaFormatUnsupported: "{0} format not supported", videoSendFailed: "Video send failed", localFileNotAllowed: "Local file path detected, cannot save" }, remove: { noPermission: "You do not have permission to delete others' Echo Cave", deletePending: "Delete (Pending Audit)", deleted: "Deleted" }, list: { pageInfo: "Page {0} / {1}", header: "There are currently {0} Echo Caves:", totalItems: "User {0} has submitted a total of {1} items:", idsLine: "{0}" }, audit: { noPending: "No pending Echo Caves", pendingNotFound: "Pending Echo Cave not found", pendingResult: "{0}, {1} pending Echo Caves remaining: [{2}]", auditPassed: "Approved", auditRejected: "Rejected", batchAuditResult: "{0} {1}/{2} Echo Caves", title: "Pending Echo Caves:", from: "Contributor:", sendFailed: "Failed to send audit message, unable to contact manager {0}" }, error: { commandProcess: "Command processing failed: {0}", getCave: "Failed to get Echo Cave", noCave: "No Echo Caves", saveFailed: "Failed to save data", mediaLoadFailed: "[Failed to load {0}]", videoSendFailed: "Video send failed", uploadImageFailed: "Image upload failed", uploadVideoFailed: "Video upload failed", fileRead: "Failed to read JSON data: {0}", fileWrite: "Failed to write JSON data: {0}", notFound: "Not found", userInfo: "Failed to get user information: {0}", auditProcess: "Audit processing failed: {0}", invalidId: "Please enter a valid Echo Cave ID" }, message: { blacklisted: "You have been blacklisted", managerOnly: "This operation is for managers only", cooldown: "Group chat cooldown... Please try again in {0} seconds", caveTitle: "Echo Cave —— ({0})", videoSending: "[Video sending]", mediaInvalid: "[Invalid {0}]", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0} file size exceeds limit" } } } };
43
+ module2.exports = { _config: { manager: "Administrators", blacklist: "Blacklist (Users)", whitelist: "Audit Whitelist (Users/Groups/Channels)", number: "Cooldown Time (seconds)", enableAudit: "Enable Audit", allowVideo: "Allow Video Upload", videoMaxSize: "Maximum Video Size (MB)", imageMaxSize: "Maximum Image Size (MB)", enablePagination: "Enable Statistics Pagination", itemsPerPage: "Items Per Page" }, commands: { cave: { description: "Echo Cave", usage: "Supports adding, drawing, viewing, and managing echo cave entries", examples: "Use cave to randomly draw an entry\nUse -a to add directly or add by reference\nUse -g to view specific entry\nUse -r to delete specific entry", options: { a: "Add entry", g: "View entry", r: "Delete entry", p: "Approve audit (batch)", d: "Reject audit (batch)", l: "Query submission statistics" }, add: { noContent: "Please send content within one minute", operationTimeout: "Operation timed out, addition cancelled", videoDisabled: "Video upload is not allowed", submitPending: "Submission successful, ID is ({0})", addSuccess: "Addition successful, ID is ({0})", mediaSizeExceeded: "{0} file size exceeds limit", localFileNotAllowed: "Local file path detected, cannot save" }, remove: { noPermission: "You don't have permission to delete others' entries", deletePending: "Delete (pending audit)", deleted: "Deleted" }, list: { pageInfo: "Page {0} / {1}", header: "Currently there are {0} entries:", totalItems: "User {0} has submitted {1} entries:", idsLine: "{0}" }, audit: { noPending: "No pending entries", pendingNotFound: "Pending entry not found", pendingResult: "{0}, {1} pending entries remaining: [{2}]", auditPassed: "Approved", auditRejected: "Rejected", batchAuditResult: "Has {0} {1}/{2} entries", title: "Pending Entries:", from: "Submitted by:", sendFailed: "Failed to send audit message, cannot contact administrator {0}" }, error: { noContent: "Entry content is empty", getCave: "Failed to get entry", noCave: "No entries found", invalidId: "Please enter a valid entry ID", notFound: "Entry not found" }, message: { blacklisted: "You have been blacklisted", managerOnly: "This operation is restricted to administrators", cooldown: "Group chat cooling down... Please try again in {0} seconds", caveTitle: "Echo Cave —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0} file size exceeds limit" } } } };
44
44
  }
45
45
  });
46
46
 
@@ -86,18 +86,15 @@ async function apply(ctx, config) {
86
86
  await FileHandler.ensureDirectory(resourceDir);
87
87
  await FileHandler.ensureJsonFile(caveFilePath);
88
88
  await FileHandler.ensureJsonFile(pendingFilePath);
89
+ const idManager = new IdManager(ctx.baseDir);
90
+ await idManager.initialize(caveFilePath, pendingFilePath);
89
91
  const lastUsed = /* @__PURE__ */ new Map();
90
- async function processList(caveFilePath2, session, content, options, config2) {
91
- const caveData = await CacheManager.getCaveData(caveFilePath2, session);
92
- const caveDir2 = path.dirname(caveFilePath2);
93
- const stats = {};
94
- for (const cave of caveData) {
95
- if (cave.contributor_number === "10000") continue;
96
- if (!stats[cave.contributor_number]) stats[cave.contributor_number] = [];
97
- stats[cave.contributor_number].push(cave.cave_id);
98
- }
99
- const statFilePath = path.join(caveDir2, "stat.json");
100
- fs.writeFileSync(statFilePath, JSON.stringify(stats, null, 2), "utf8");
92
+ async function processList(session, config2, userId, pageNum = 1) {
93
+ const stats = idManager.getStats();
94
+ if (userId && userId in stats) {
95
+ const ids = stats[userId];
96
+ return session.text("commands.cave.list.totalItems", [userId, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
97
+ }
101
98
  const lines = Object.entries(stats).map(([cid, ids]) => {
102
99
  return session.text("commands.cave.list.totalItems", [cid, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
103
100
  });
@@ -105,10 +102,7 @@ async function apply(ctx, config) {
105
102
  if (config2.enablePagination) {
106
103
  const itemsPerPage = config2.itemsPerPage;
107
104
  const totalPages = Math.max(1, Math.ceil(lines.length / itemsPerPage));
108
- let query = (content[0] || String(options.l) || "").trim();
109
- let pageNum = parseInt(query, 10);
110
- if (isNaN(pageNum) || pageNum < 1) pageNum = 1;
111
- if (pageNum > totalPages) pageNum = totalPages;
105
+ pageNum = Math.min(Math.max(1, pageNum), totalPages);
112
106
  const start = (pageNum - 1) * itemsPerPage;
113
107
  const paginatedLines = lines.slice(start, start + itemsPerPage);
114
108
  return session.text("commands.cave.list.header", [totalSubmissions]) + "\n" + paginatedLines.join("\n") + "\n" + session.text("commands.cave.list.pageInfo", [pageNum, totalPages]);
@@ -118,7 +112,7 @@ async function apply(ctx, config) {
118
112
  }
119
113
  __name(processList, "processList");
120
114
  async function processAudit(ctx2, pendingFilePath2, caveFilePath2, resourceDir2, session, options, content) {
121
- const pendingData = await CacheManager.getPendingData(pendingFilePath2, session);
115
+ const pendingData = await FileHandler.readJsonData(pendingFilePath2);
122
116
  const isApprove = Boolean(options.p);
123
117
  if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
124
118
  return await handleAudit(ctx2, pendingData, isApprove, caveFilePath2, resourceDir2, pendingFilePath2, session);
@@ -130,59 +124,47 @@ async function apply(ctx, config) {
130
124
  return sendMessage(session, await handleAudit(ctx2, pendingData, isApprove, caveFilePath2, resourceDir2, pendingFilePath2, session, id), [], true);
131
125
  }
132
126
  __name(processAudit, "processAudit");
133
- async function processView(caveFilePath2, resourceDir2, session, options, content) {
127
+ async function processView(caveFilePath2, resourceDir2, session, options, content, config2) {
128
+ if (!await checkCooldown(session, config2)) {
129
+ return "";
130
+ }
134
131
  const caveId = parseInt(content[0] || (typeof options.g === "string" ? options.g : ""));
135
132
  if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
136
- const data = await CacheManager.getCaveData(caveFilePath2, session);
133
+ const data = await FileHandler.readJsonData(caveFilePath2);
137
134
  const cave = data.find((item) => item.cave_id === caveId);
138
135
  if (!cave) return sendMessage(session, "commands.cave.error.notFound", [], true);
139
- const caveContent = await buildMessage(cave, resourceDir2, session);
140
- return caveContent;
136
+ return buildMessage(cave, resourceDir2, session);
141
137
  }
142
138
  __name(processView, "processView");
143
- async function processRandom(caveFilePath2, resourceDir2, session, config2, lastUsed2) {
144
- try {
145
- const data = await CacheManager.getCaveData(caveFilePath2, session);
146
- if (data.length === 0) {
147
- return sendMessage(session, "commands.cave.error.noCave", [], true);
148
- }
149
- const guildId = session.guildId;
150
- const now = Date.now();
151
- const lastCall = lastUsed2.get(guildId) || 0;
152
- const isManager = config2.manager.includes(session.userId);
153
- if (!isManager && now - lastCall < config2.number * 1e3) {
154
- const waitTime = Math.ceil((config2.number * 1e3 - (now - lastCall)) / 1e3);
155
- return sendMessage(session, "commands.cave.message.cooldown", [waitTime], true);
156
- }
157
- if (!isManager) lastUsed2.set(guildId, now);
158
- const cave = (() => {
159
- const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
160
- if (!validCaves.length) return void 0;
161
- const randomIndex = Math.floor(Math.random() * validCaves.length);
162
- return validCaves[randomIndex];
163
- })();
164
- return cave ? buildMessage(cave, resourceDir2, session) : sendMessage(session, "commands.cave.error.getCave", [], true);
165
- } catch (error) {
166
- return sendMessage(session, "commands.cave.error.commandProcess", [error.message], true);
139
+ async function processRandom(caveFilePath2, resourceDir2, session, config2) {
140
+ const data = await FileHandler.readJsonData(caveFilePath2);
141
+ if (data.length === 0) {
142
+ return sendMessage(session, "commands.cave.error.noCave", [], true);
167
143
  }
144
+ if (!await checkCooldown(session, config2)) {
145
+ return "";
146
+ }
147
+ const cave = (() => {
148
+ const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
149
+ if (!validCaves.length) return void 0;
150
+ const randomIndex = Math.floor(Math.random() * validCaves.length);
151
+ return validCaves[randomIndex];
152
+ })();
153
+ return cave ? buildMessage(cave, resourceDir2, session) : sendMessage(session, "commands.cave.error.getCave", [], true);
168
154
  }
169
155
  __name(processRandom, "processRandom");
170
156
  async function processDelete(caveFilePath2, resourceDir2, pendingFilePath2, session, config2, options, content) {
171
157
  const caveId = parseInt(content[0] || (typeof options.r === "string" ? options.r : ""));
172
158
  if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
173
- const data = await CacheManager.getCaveData(caveFilePath2, session);
174
- const pendingData = await CacheManager.getPendingData(pendingFilePath2, session);
175
- const index = data.findIndex((item) => item.cave_id === caveId);
176
- const pendingIndex = pendingData.findIndex((item) => item.cave_id === caveId);
177
- if (index === -1 && pendingIndex === -1) return sendMessage(session, "commands.cave.error.notFound", [], true);
178
- let targetCave;
179
- let isPending = false;
180
- if (index !== -1) {
181
- targetCave = data[index];
182
- } else {
183
- targetCave = pendingData[pendingIndex];
184
- isPending = true;
159
+ const data = await FileHandler.readJsonData(caveFilePath2);
160
+ const pendingData = await FileHandler.readJsonData(pendingFilePath2);
161
+ const targetInData = data.find((item) => item.cave_id === caveId);
162
+ const targetInPending = pendingData.find((item) => item.cave_id === caveId);
163
+ if (!targetInData && !targetInPending) {
164
+ return sendMessage(session, "commands.cave.error.notFound", [], true);
185
165
  }
166
+ const targetCave = targetInData || targetInPending;
167
+ const isPending = !targetInData;
186
168
  if (targetCave.contributor_number !== session.userId && !config2.manager.includes(session.userId)) {
187
169
  return sendMessage(session, "commands.cave.remove.noPermission", [], true);
188
170
  }
@@ -191,180 +173,285 @@ async function apply(ctx, config) {
191
173
  for (const element of targetCave.elements) {
192
174
  if ((element.type === "img" || element.type === "video") && element.file) {
193
175
  const fullPath = path.join(resourceDir2, element.file);
194
- if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
176
+ if (fs.existsSync(fullPath)) {
177
+ await fs.promises.unlink(fullPath);
178
+ }
195
179
  }
196
180
  }
197
181
  }
198
182
  if (isPending) {
199
- pendingData.splice(pendingIndex, 1);
200
- await FileHandler.writeJsonData(pendingFilePath2, pendingData, session);
201
- const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
202
- const deleteMessage = session.text("commands.cave.remove.deleted");
203
- return `${deleteMessage}${deleteStatus}
204
- ${caveContent}`;
183
+ const newPendingData = pendingData.filter((item) => item.cave_id !== caveId);
184
+ await FileHandler.writeJsonData(pendingFilePath2, newPendingData);
205
185
  } else {
206
- data.splice(index, 1);
207
- await FileHandler.writeJsonData(caveFilePath2, data, session);
208
- const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
209
- const deleteMessage = session.text("commands.cave.remove.deleted");
210
- return `${deleteMessage}${deleteStatus}${caveContent}`;
186
+ const newData = data.filter((item) => item.cave_id !== caveId);
187
+ await FileHandler.writeJsonData(caveFilePath2, newData);
188
+ await idManager.removeStat(targetCave.contributor_number, caveId);
211
189
  }
190
+ await idManager.markDeleted(caveId);
191
+ const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
192
+ const deleteMessage = session.text("commands.cave.remove.deleted");
193
+ return `${deleteMessage}${deleteStatus}${caveContent}`;
212
194
  }
213
195
  __name(processDelete, "processDelete");
214
196
  async function processAdd(ctx2, config2, caveFilePath2, resourceDir2, pendingFilePath2, session, content) {
215
197
  try {
216
- let inputParts = [];
217
- if (content.length > 0) {
218
- inputParts = content;
219
- }
220
- if (!inputParts.length) {
198
+ const inputContent = content.length > 0 ? content.join("\n") : await (async () => {
221
199
  await sendMessage(session, "commands.cave.add.noContent", [], true);
222
200
  const reply = await session.prompt({ timeout: 6e4 });
223
- if (!reply || reply.trim() === "") {
224
- return sendMessage(session, "commands.cave.add.operationTimeout", [], true);
225
- }
226
- inputParts = [reply];
227
- }
228
- const combinedInput = inputParts.join("\n");
229
- if (combinedInput.includes("/app/.config/QQ/")) {
201
+ if (!reply) throw new Error(session.text("commands.cave.add.operationTimeout"));
202
+ return reply;
203
+ })();
204
+ const caveId = await idManager.getNextId();
205
+ if (inputContent.includes("/app/.config/QQ/")) {
230
206
  return sendMessage(session, "commands.cave.add.localFileNotAllowed", [], true);
231
207
  }
232
- let { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(combinedInput);
208
+ const bypassAudit = config2.whitelist.includes(session.userId) || config2.whitelist.includes(session.guildId) || config2.whitelist.includes(session.channelId);
209
+ const { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(inputContent, config2, session);
233
210
  if (videoUrls.length > 0 && !config2.allowVideo) {
234
211
  return sendMessage(session, "commands.cave.add.videoDisabled", [], true);
235
212
  }
236
- const pendingData = await CacheManager.getPendingData(pendingFilePath2, session);
237
- const data = await CacheManager.getCaveData(caveFilePath2, session);
238
- const usedIds = /* @__PURE__ */ new Set([...data.map((item) => item.cave_id), ...pendingData.map((item) => item.cave_id)]);
239
- let caveId = 1;
240
- while (usedIds.has(caveId)) {
241
- caveId++;
242
- }
243
- let savedImages = [];
244
- if (imageUrls.length > 0) {
245
- try {
246
- const imageFileNames = imageElements.map((el) => el.fileName);
247
- const imageFileSizes = imageElements.map((el) => el.fileSize);
248
- savedImages = await saveMedia(
249
- imageUrls,
250
- imageFileNames,
251
- imageFileSizes,
252
- resourceDir2,
253
- caveId,
254
- config2,
255
- ctx2,
256
- "img",
257
- session
258
- );
259
- } catch (error) {
260
- return sendMessage(session, "commands.cave.error.uploadImageFailed", [], true);
261
- }
262
- }
263
- let savedVideos = [];
264
- if (videoUrls.length > 0) {
265
- try {
266
- const videoFileNames = videoElements.map((el) => el.fileName);
267
- const videoFileSizes = videoElements.map((el) => el.fileSize);
268
- savedVideos = await saveMedia(
269
- videoUrls,
270
- videoFileNames,
271
- videoFileSizes,
272
- resourceDir2,
273
- caveId,
274
- config2,
275
- ctx2,
276
- "video",
277
- session
278
- );
279
- } catch (error) {
280
- return sendMessage(session, "commands.cave.error.uploadVideoFailed", [], true);
281
- }
282
- }
283
- const elements = [];
284
- elements.push(...textParts);
285
- savedImages.forEach((file, idx) => {
286
- if (imageElements[idx]) {
287
- elements.push({ ...imageElements[idx], type: "img", file });
288
- }
289
- });
290
- savedVideos.forEach((file, idx) => {
291
- if (videoElements[idx]) {
292
- elements.push({ ...videoElements[idx], type: "video", file });
293
- }
294
- });
295
- elements.sort((a, b) => a.index - b.index);
296
- let contributorName = session.username;
297
- if (ctx2.database) {
298
- try {
299
- const userInfo = await ctx2.database.getUser(session.platform, session.userId);
300
- contributorName = userInfo?.nickname || session.username;
301
- } catch (error) {
302
- return sendMessage(
303
- session,
304
- "commands.cave.error.userInfo",
305
- [error.message],
306
- true
307
- );
308
- }
309
- }
213
+ const [savedImages, savedVideos] = await Promise.all([
214
+ imageUrls.length > 0 ? saveMedia(
215
+ imageUrls,
216
+ imageElements.map((el) => el.fileName),
217
+ resourceDir2,
218
+ caveId,
219
+ "img",
220
+ ctx2,
221
+ session
222
+ ) : [],
223
+ videoUrls.length > 0 ? saveMedia(
224
+ videoUrls,
225
+ videoElements.map((el) => el.fileName),
226
+ resourceDir2,
227
+ caveId,
228
+ "video",
229
+ ctx2,
230
+ session
231
+ ) : []
232
+ ]);
310
233
  const newCave = {
311
234
  cave_id: caveId,
312
- elements: cleanElementsForSave(elements, true),
235
+ elements: [
236
+ ...textParts,
237
+ ...imageElements.map((el, idx) => ({
238
+ ...el,
239
+ file: savedImages[idx],
240
+ // 保持原始文本和图片的相对位置
241
+ index: el.index
242
+ }))
243
+ ].sort((a, b) => a.index - b.index),
313
244
  contributor_number: session.userId,
314
- contributor_name: contributorName
245
+ contributor_name: session.username
315
246
  };
316
- const bypassAudit = config2.whitelist.includes(session.userId) || session.guildId && config2.whitelist.includes(session.guildId) || session.channelId && config2.whitelist.includes(session.channelId);
247
+ if (videoUrls.length > 0 && savedVideos.length > 0) {
248
+ newCave.elements.push({
249
+ type: "video",
250
+ file: savedVideos[0],
251
+ index: Number.MAX_SAFE_INTEGER
252
+ // 确保视频总是在最后
253
+ });
254
+ }
317
255
  if (config2.enableAudit && !bypassAudit) {
318
- pendingData.push({ ...newCave, elements: cleanElementsForSave(elements, true) });
319
- await FileHandler.writeJsonData(pendingFilePath2, pendingData, session);
320
- await sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave, resourceDir2, session), session);
256
+ const pendingData = await FileHandler.readJsonData(pendingFilePath2);
257
+ pendingData.push(newCave);
258
+ await Promise.all([
259
+ FileHandler.writeJsonData(pendingFilePath2, pendingData),
260
+ sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave, resourceDir2, session), session)
261
+ ]);
321
262
  return sendMessage(session, "commands.cave.add.submitPending", [caveId], false);
322
- } else {
323
- const caveWithoutIndex = { ...newCave, elements: cleanElementsForSave(elements, false) };
324
- data.push(caveWithoutIndex);
325
- await FileHandler.writeJsonData(caveFilePath2, data, session);
326
- return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
327
263
  }
264
+ const data = await FileHandler.readJsonData(caveFilePath2);
265
+ data.push({
266
+ ...newCave,
267
+ elements: cleanElementsForSave(newCave.elements, false)
268
+ });
269
+ await FileHandler.writeJsonData(caveFilePath2, data);
270
+ await idManager.addStat(session.userId, caveId);
271
+ return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
328
272
  } catch (error) {
329
- return sendMessage(session, "commands.cave.error.commandProcess", [error.message], true);
273
+ logger.error(`Failed to process add command: ${error.message}`);
274
+ return sendMessage(session, `commands.cave.error.${error.code || "unknown"}`, [], true);
330
275
  }
331
276
  }
332
277
  __name(processAdd, "processAdd");
333
- ctx.command("cave [message]").option("a", "添加回声洞").option("g", "查看回声洞", { type: "string" }).option("r", "删除回声洞", { type: "string" }).option("p", "通过审核", { type: "string" }).option("d", "拒绝审核", { type: "string" }).option("l", "查询投稿统计", { type: "string" }).before(async ({ session, options }) => {
334
- if (config.blacklist.includes(session.userId)) {
335
- return sendTempMessage(session, "commands.cave.message.blacklisted");
336
- }
337
- if (session.content && session.content.includes("-help")) return;
338
- if ((options.l || options.p || options.d) && !config.manager.includes(session.userId)) {
339
- return sendTempMessage(session, "commands.cave.message.managerOnly");
278
+ async function handleAudit(ctx2, pendingData, isApprove, caveFilePath2, resourceDir2, pendingFilePath2, session, targetId) {
279
+ if (pendingData.length === 0) {
280
+ return sendMessage(session, "commands.cave.audit.noPending", [], true);
340
281
  }
341
- }).action(async ({ session, options }, ...content) => {
342
- try {
343
- const dataDir2 = path.join(ctx.baseDir, "data");
344
- const caveDir2 = path.join(dataDir2, "cave");
345
- const caveFilePath2 = path.join(caveDir2, "cave.json");
346
- const resourceDir2 = path.join(caveDir2, "resources");
347
- const pendingFilePath2 = path.join(caveDir2, "pending.json");
348
- if (options.l !== void 0) {
349
- return await processList(caveFilePath2, session, content, options, config);
282
+ if (typeof targetId === "number") {
283
+ const targetCave = pendingData.find((item) => item.cave_id === targetId);
284
+ if (!targetCave) {
285
+ return sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
350
286
  }
351
- if (options.p || options.d) {
352
- return await processAudit(ctx, pendingFilePath2, caveFilePath2, resourceDir2, session, options, content);
287
+ const newPendingData = pendingData.filter((item) => item.cave_id !== targetId);
288
+ if (isApprove) {
289
+ const oldCaveData = await FileHandler.readJsonData(caveFilePath2);
290
+ const newCaveData = [...oldCaveData, {
291
+ ...targetCave,
292
+ cave_id: targetId,
293
+ // 明确指定ID
294
+ // 保存到 cave.json 时移除 index
295
+ elements: cleanElementsForSave(targetCave.elements, false)
296
+ }];
297
+ await FileHandler.withTransaction([
298
+ {
299
+ filePath: caveFilePath2,
300
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath2, newCaveData), "operation"),
301
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath2, oldCaveData), "rollback")
302
+ },
303
+ {
304
+ filePath: pendingFilePath2,
305
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath2, newPendingData), "operation"),
306
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath2, pendingData), "rollback")
307
+ }
308
+ ]);
309
+ await idManager.addStat(targetCave.contributor_number, targetId);
310
+ } else {
311
+ await FileHandler.writeJsonData(pendingFilePath2, newPendingData);
312
+ await idManager.markDeleted(targetId);
313
+ if (targetCave.elements) {
314
+ for (const element of targetCave.elements) {
315
+ if ((element.type === "img" || element.type === "video") && element.file) {
316
+ const fullPath = path.join(resourceDir2, element.file);
317
+ if (fs.existsSync(fullPath)) {
318
+ await fs.promises.unlink(fullPath);
319
+ }
320
+ }
321
+ }
322
+ }
353
323
  }
354
- if (options.g) {
355
- return await processView(caveFilePath2, resourceDir2, session, options, content);
324
+ const remainingCount = newPendingData.length;
325
+ if (remainingCount > 0) {
326
+ const remainingIds = newPendingData.map((c) => c.cave_id).join(", ");
327
+ const action = isApprove ? "auditPassed" : "auditRejected";
328
+ return sendMessage(session, "commands.cave.audit.pendingResult", [
329
+ session.text(`commands.cave.audit.${action}`),
330
+ remainingCount,
331
+ remainingIds
332
+ ], false);
356
333
  }
357
- if (options.r) {
358
- return await processDelete(caveFilePath2, resourceDir2, pendingFilePath2, session, config, options, content);
334
+ return sendMessage(
335
+ session,
336
+ isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
337
+ [],
338
+ false
339
+ );
340
+ }
341
+ const data = isApprove ? await FileHandler.readJsonData(caveFilePath2) : null;
342
+ let processedCount = 0;
343
+ if (isApprove && data) {
344
+ const oldData = [...data];
345
+ const newData = [...data];
346
+ await FileHandler.withTransaction([
347
+ {
348
+ filePath: caveFilePath2,
349
+ operation: /* @__PURE__ */ __name(async () => {
350
+ for (const cave of pendingData) {
351
+ newData.push({
352
+ ...cave,
353
+ cave_id: cave.cave_id,
354
+ // 确保ID保持不变
355
+ // 保存到 cave.json 时移除 index
356
+ elements: cleanElementsForSave(cave.elements, false)
357
+ });
358
+ processedCount++;
359
+ await idManager.addStat(cave.contributor_number, cave.cave_id);
360
+ }
361
+ return FileHandler.writeJsonData(caveFilePath2, newData);
362
+ }, "operation"),
363
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath2, oldData), "rollback")
364
+ },
365
+ {
366
+ filePath: pendingFilePath2,
367
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath2, []), "operation"),
368
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath2, pendingData), "rollback")
369
+ }
370
+ ]);
371
+ } else {
372
+ for (const cave of pendingData) {
373
+ await idManager.markDeleted(cave.cave_id);
359
374
  }
360
- if (options.a) {
361
- return await processAdd(ctx, config, caveFilePath2, resourceDir2, pendingFilePath2, session, content);
375
+ await FileHandler.writeJsonData(pendingFilePath2, []);
376
+ for (const cave of pendingData) {
377
+ if (cave.elements) {
378
+ for (const element of cave.elements) {
379
+ if ((element.type === "img" || element.type === "video") && element.file) {
380
+ const fullPath = path.join(resourceDir2, element.file);
381
+ if (fs.existsSync(fullPath)) {
382
+ await fs.promises.unlink(fullPath);
383
+ }
384
+ }
385
+ }
386
+ }
387
+ processedCount++;
388
+ }
389
+ }
390
+ return sendMessage(session, "commands.cave.audit.batchAuditResult", [
391
+ isApprove ? "通过" : "拒绝",
392
+ processedCount,
393
+ pendingData.length
394
+ ], false);
395
+ }
396
+ __name(handleAudit, "handleAudit");
397
+ async function checkCooldown(session, config2) {
398
+ const guildId = session.guildId;
399
+ const now = Date.now();
400
+ const lastTime = lastUsed.get(guildId) || 0;
401
+ const isManager = config2.manager.includes(session.userId);
402
+ if (!isManager && now - lastTime < config2.number * 1e3) {
403
+ const waitTime = Math.ceil((config2.number * 1e3 - (now - lastTime)) / 1e3);
404
+ await sendMessage(session, "commands.cave.message.cooldown", [waitTime], true);
405
+ return false;
406
+ }
407
+ lastUsed.set(guildId, now);
408
+ return true;
409
+ }
410
+ __name(checkCooldown, "checkCooldown");
411
+ ctx.command("cave [message]").option("a", "添加回声洞").option("g", "查看回声洞", { type: "string" }).option("r", "删除回声洞", { type: "string" }).option("p", "通过审核", { type: "string" }).option("d", "拒绝审核", { type: "string" }).option("l", "查询投稿统计", { type: "string" }).before(async ({ session, options }) => {
412
+ if (config.blacklist.includes(session.userId)) {
413
+ return sendMessage(session, "commands.cave.message.blacklisted", [], true);
414
+ }
415
+ if ((options.p || options.d) && !config.manager.includes(session.userId)) {
416
+ return sendMessage(session, "commands.cave.message.managerOnly", [], true);
417
+ }
418
+ }).action(async ({ session, options }, ...content) => {
419
+ const dataDir2 = path.join(ctx.baseDir, "data");
420
+ const caveDir2 = path.join(dataDir2, "cave");
421
+ const caveFilePath2 = path.join(caveDir2, "cave.json");
422
+ const resourceDir2 = path.join(caveDir2, "resources");
423
+ const pendingFilePath2 = path.join(caveDir2, "pending.json");
424
+ if (options.l !== void 0) {
425
+ const input = typeof options.l === "string" ? options.l : content[0];
426
+ const num = parseInt(input);
427
+ if (config.manager.includes(session.userId)) {
428
+ if (!isNaN(num)) {
429
+ if (num < 1e4) {
430
+ return await processList(session, config, void 0, num);
431
+ } else {
432
+ return await processList(session, config, num.toString());
433
+ }
434
+ } else if (input) {
435
+ return await processList(session, config, input);
436
+ }
437
+ return await processList(session, config);
438
+ } else {
439
+ return await processList(session, config, session.userId);
362
440
  }
363
- return await processRandom(caveFilePath2, resourceDir2, session, config, lastUsed);
364
- } catch (error) {
365
- logger.error(error);
366
- return sendMessage(session, "commands.cave.error.commandProcess", [error.message], true);
367
441
  }
442
+ if (options.p || options.d) {
443
+ return await processAudit(ctx, pendingFilePath2, caveFilePath2, resourceDir2, session, options, content);
444
+ }
445
+ if (options.g) {
446
+ return await processView(caveFilePath2, resourceDir2, session, options, content, config);
447
+ }
448
+ if (options.r) {
449
+ return await processDelete(caveFilePath2, resourceDir2, pendingFilePath2, session, config, options, content);
450
+ }
451
+ if (options.a) {
452
+ return await processAdd(ctx, config, caveFilePath2, resourceDir2, pendingFilePath2, session, content);
453
+ }
454
+ return await processRandom(caveFilePath2, resourceDir2, session, config);
368
455
  });
369
456
  }
370
457
  __name(apply, "apply");
@@ -373,339 +460,468 @@ var FileHandler = class {
373
460
  static {
374
461
  __name(this, "FileHandler");
375
462
  }
376
- static writeQueue = /* @__PURE__ */ new Map();
377
- static async readJsonData(filePath, session, validator) {
463
+ static locks = /* @__PURE__ */ new Map();
464
+ static RETRY_COUNT = 3;
465
+ static RETRY_DELAY = 1e3;
466
+ static CONCURRENCY_LIMIT = 5;
467
+ /**
468
+ * 并发控制
469
+ */
470
+ static async withConcurrencyLimit(operation, limit = this.CONCURRENCY_LIMIT) {
471
+ while (this.locks.size >= limit) {
472
+ await Promise.race(this.locks.values());
473
+ }
474
+ return operation();
475
+ }
476
+ /**
477
+ * 统一的文件操作包装器
478
+ */
479
+ static async withFileOp(filePath, operation) {
480
+ const key = filePath;
481
+ while (this.locks.has(key)) {
482
+ await this.locks.get(key);
483
+ }
484
+ const operationPromise = (async () => {
485
+ for (let i = 0; i < this.RETRY_COUNT; i++) {
486
+ try {
487
+ return await operation();
488
+ } catch (error) {
489
+ if (i === this.RETRY_COUNT - 1) throw error;
490
+ await new Promise((resolve) => setTimeout(resolve, this.RETRY_DELAY));
491
+ }
492
+ }
493
+ throw new Error("Operation failed after retries");
494
+ })();
495
+ this.locks.set(key, operationPromise);
496
+ try {
497
+ return await operationPromise;
498
+ } finally {
499
+ this.locks.delete(key);
500
+ }
501
+ }
502
+ /**
503
+ * 事务处理
504
+ */
505
+ static async withTransaction(operations) {
506
+ const results = [];
507
+ const completed = /* @__PURE__ */ new Set();
378
508
  try {
379
- const data = await fs.promises.readFile(filePath, "utf8");
380
- const parsed = JSON.parse(data || "[]");
381
- return Array.isArray(parsed) ? validator ? parsed.filter(validator) : parsed : [];
509
+ for (const { filePath, operation } of operations) {
510
+ const result = await this.withFileOp(filePath, operation);
511
+ results.push(result);
512
+ completed.add(filePath);
513
+ }
514
+ return results;
382
515
  } catch (error) {
383
- logger.error(session.text("commands.cave.error.fileRead", [error.message]));
384
- return [];
516
+ await Promise.all(
517
+ operations.filter(({ filePath }) => completed.has(filePath)).map(async ({ filePath, rollback }) => {
518
+ if (rollback) {
519
+ await this.withFileOp(filePath, rollback).catch(
520
+ (e) => logger.error(`Rollback failed for ${filePath}: ${e.message}`)
521
+ );
522
+ }
523
+ })
524
+ );
525
+ throw error;
385
526
  }
386
527
  }
387
- static async writeJsonData(filePath, data, session) {
388
- const queueKey = filePath;
389
- const writeOperation = /* @__PURE__ */ __name(async () => {
528
+ /**
529
+ * JSON文件读写
530
+ */
531
+ static async readJsonData(filePath) {
532
+ return this.withFileOp(filePath, async () => {
390
533
  try {
391
- const jsonString = JSON.stringify(data, null, 2);
392
- await fs.promises.writeFile(filePath, jsonString, "utf8");
534
+ const data = await fs.promises.readFile(filePath, "utf8");
535
+ return JSON.parse(data || "[]");
393
536
  } catch (error) {
394
- logger.error(session.text("commands.cave.error.fileWrite", [error.message]));
395
- throw new Error(session.text("commands.cave.error.saveFailed"));
396
- }
397
- }, "writeOperation");
398
- if (!this.writeQueue.has(queueKey)) {
399
- this.writeQueue.set(queueKey, Promise.resolve());
400
- }
401
- const currentPromise = this.writeQueue.get(queueKey).then(writeOperation).finally(() => {
402
- if (this.writeQueue.get(queueKey) === currentPromise) {
403
- this.writeQueue.delete(queueKey);
537
+ return [];
404
538
  }
405
539
  });
406
- this.writeQueue.set(queueKey, currentPromise);
407
- return currentPromise;
408
540
  }
541
+ static async writeJsonData(filePath, data) {
542
+ const tmpPath = `${filePath}.tmp`;
543
+ await this.withFileOp(filePath, async () => {
544
+ await fs.promises.writeFile(tmpPath, JSON.stringify(data, null, 2));
545
+ await fs.promises.rename(tmpPath, filePath);
546
+ });
547
+ }
548
+ /**
549
+ * 文件系统操作
550
+ */
409
551
  static async ensureDirectory(dir) {
410
- !fs.existsSync(dir) && await fs.promises.mkdir(dir, { recursive: true });
552
+ await this.withConcurrencyLimit(async () => {
553
+ if (!fs.existsSync(dir)) {
554
+ await fs.promises.mkdir(dir, { recursive: true });
555
+ }
556
+ });
411
557
  }
412
- static async ensureJsonFile(filePath, defaultContent = "[]") {
413
- !fs.existsSync(filePath) && await fs.promises.writeFile(filePath, defaultContent, "utf8");
558
+ static async ensureJsonFile(filePath) {
559
+ await this.withFileOp(filePath, async () => {
560
+ if (!fs.existsSync(filePath)) {
561
+ await fs.promises.writeFile(filePath, "[]", "utf8");
562
+ }
563
+ });
564
+ }
565
+ /**
566
+ * 媒体文件操作
567
+ */
568
+ static async saveMediaFile(filePath, data) {
569
+ await this.withConcurrencyLimit(async () => {
570
+ const dir = path.dirname(filePath);
571
+ await this.ensureDirectory(dir);
572
+ await this.withFileOp(
573
+ filePath,
574
+ () => fs.promises.writeFile(filePath, data)
575
+ );
576
+ });
577
+ }
578
+ static async deleteMediaFile(filePath) {
579
+ await this.withFileOp(filePath, async () => {
580
+ if (fs.existsSync(filePath)) {
581
+ await fs.promises.unlink(filePath);
582
+ }
583
+ });
414
584
  }
415
585
  };
416
- async function saveMedia(urls, fileNames, fileSizes, resourceDir, caveId, config, ctx, mediaType, session) {
417
- const defaults = mediaType === "img" ? { ext: "png", accept: "image/*", maxSize: config.imageMaxSize } : { ext: "mp4", accept: "video/*", maxSize: config.videoMaxSize };
418
- const extPattern = /\.[a-zA-Z0-9]+$/;
419
- const downloadPromises = urls.map(async (url, i) => {
586
+ var IdManager = class {
587
+ static {
588
+ __name(this, "IdManager");
589
+ }
590
+ deletedIds = /* @__PURE__ */ new Set();
591
+ maxId = 0;
592
+ initialized = false;
593
+ statusFilePath;
594
+ stats = {};
595
+ usedIds = /* @__PURE__ */ new Set();
596
+ // 新增:跟踪所有使用中的ID
597
+ constructor(baseDir) {
598
+ const caveDir = path.join(baseDir, "data", "cave");
599
+ this.statusFilePath = path.join(caveDir, "status.json");
600
+ }
601
+ async initialize(caveFilePath, pendingFilePath) {
602
+ if (this.initialized) return;
420
603
  try {
421
- const processedUrl = (() => {
422
- try {
423
- const decodedUrl = decodeURIComponent(url);
424
- return decodedUrl.includes("multimedia.nt.qq.com.cn") ? decodedUrl.replace(/&amp;/g, "&") : url;
425
- } catch {
426
- return url;
427
- }
428
- })();
429
- let ext = defaults.ext;
430
- const fileName = fileNames[i];
431
- const fileSize = fileSizes[i];
432
- if (fileSize) {
433
- const sizeInBytes = parseInt(fileSize);
434
- if (sizeInBytes > defaults.maxSize * 1024 * 1024) {
435
- logger.warn(session.text("commands.cave.message.mediaSizeExceeded", [mediaType]));
436
- return null;
604
+ this.initialized = true;
605
+ const status = fs.existsSync(this.statusFilePath) ? JSON.parse(await fs.promises.readFile(this.statusFilePath, "utf8")) : {
606
+ deletedIds: [],
607
+ maxId: 0,
608
+ stats: {},
609
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
610
+ };
611
+ const [caveData, pendingData] = await Promise.all([
612
+ FileHandler.readJsonData(caveFilePath),
613
+ FileHandler.readJsonData(pendingFilePath)
614
+ ]);
615
+ this.usedIds.clear();
616
+ const conflicts = /* @__PURE__ */ new Map();
617
+ const collectIds = /* @__PURE__ */ __name((items) => {
618
+ items.forEach((item) => {
619
+ if (this.usedIds.has(item.cave_id)) {
620
+ if (!conflicts.has(item.cave_id)) {
621
+ conflicts.set(item.cave_id, []);
622
+ }
623
+ conflicts.get(item.cave_id)?.push(item);
624
+ } else {
625
+ this.usedIds.add(item.cave_id);
626
+ }
627
+ });
628
+ }, "collectIds");
629
+ collectIds(caveData);
630
+ collectIds(pendingData);
631
+ if (conflicts.size > 0) {
632
+ logger.warn(`Found ${conflicts.size} ID conflicts, auto-fixing...`);
633
+ for (const [conflictId, items] of conflicts) {
634
+ items.forEach((item, index) => {
635
+ if (index > 0) {
636
+ let newId = this.maxId + 1;
637
+ while (this.usedIds.has(newId)) {
638
+ newId++;
639
+ }
640
+ logger.info(`Reassigning ID ${item.cave_id} -> ${newId} for item`);
641
+ item.cave_id = newId;
642
+ this.usedIds.add(newId);
643
+ this.maxId = Math.max(this.maxId, newId);
644
+ }
645
+ });
437
646
  }
647
+ await Promise.all([
648
+ FileHandler.writeJsonData(caveFilePath, caveData),
649
+ FileHandler.writeJsonData(pendingFilePath, pendingData)
650
+ ]);
438
651
  }
439
- if (fileName && extPattern.test(fileName)) {
440
- ext = fileName.match(extPattern)[0].slice(1);
441
- }
442
- const finalFileName = fileName ? `${caveId}_${path.basename(fileName)}` : `${caveId}_${i + 1}.${ext}`;
443
- const targetPath = path.join(resourceDir, finalFileName);
444
- const response = await ctx.http(processedUrl, {
445
- method: "GET",
446
- responseType: "arraybuffer",
447
- timeout: 3e4,
448
- headers: {
449
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
450
- "Accept": defaults.accept,
451
- "Referer": "https://qq.com"
652
+ this.maxId = Math.max(
653
+ this.maxId,
654
+ status.maxId || 0,
655
+ ...[...this.usedIds]
656
+ );
657
+ this.deletedIds = new Set(
658
+ status.deletedIds?.filter((id) => !this.usedIds.has(id)) || []
659
+ );
660
+ this.stats = {};
661
+ for (const cave of caveData) {
662
+ if (cave.contributor_number === "10000") continue;
663
+ if (!this.stats[cave.contributor_number]) {
664
+ this.stats[cave.contributor_number] = [];
452
665
  }
453
- });
454
- const fileBuffer = Buffer.from(response.data);
455
- await fs.promises.writeFile(targetPath, fileBuffer);
456
- return finalFileName;
666
+ this.stats[cave.contributor_number].push(cave.cave_id);
667
+ }
668
+ await this.saveStatus();
669
+ this.initialized = true;
457
670
  } catch (error) {
458
- const errorKey = mediaType === "img" ? "commands.cave.error.uploadImageFailed" : "commands.cave.error.uploadVideoFailed";
459
- await sendTempMessage(session, errorKey);
460
- return null;
671
+ this.initialized = false;
672
+ logger.error(`IdManager initialization failed: ${error.message}`);
673
+ throw error;
461
674
  }
462
- });
463
- const results = await Promise.all(downloadPromises);
464
- return results.filter((name2) => name2 !== null);
465
- }
466
- __name(saveMedia, "saveMedia");
467
- async function sendAuditMessage(ctx, config, cave, content, session) {
468
- const auditMessage = `${session.text("commands.cave.audit.title")}
469
- ${content}
470
- ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
471
- for (const managerId of config.manager) {
472
- try {
473
- await ctx.bots[0]?.sendPrivateMessage(managerId, auditMessage);
474
- } catch (error) {
475
- logger.error(session.text("commands.cave.audit.sendFailed", [managerId]));
675
+ }
676
+ getNextId() {
677
+ if (!this.initialized) {
678
+ throw new Error("IdManager not initialized");
679
+ }
680
+ let nextId;
681
+ if (this.deletedIds.size === 0) {
682
+ nextId = ++this.maxId;
683
+ } else {
684
+ nextId = Math.min(...Array.from(this.deletedIds));
685
+ this.deletedIds.delete(nextId);
476
686
  }
687
+ while (this.usedIds.has(nextId)) {
688
+ nextId = ++this.maxId;
689
+ }
690
+ this.usedIds.add(nextId);
691
+ this.saveStatus().catch(
692
+ (err) => logger.error(`Failed to save status after getNextId: ${err.message}`)
693
+ );
694
+ return nextId;
477
695
  }
478
- }
479
- __name(sendAuditMessage, "sendAuditMessage");
480
- async function handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data, session) {
481
- try {
482
- if (isApprove && data) {
483
- const caveWithoutIndex = {
484
- ...cave,
485
- elements: cleanElementsForSave(cave.elements, false)
486
- };
487
- data.push(caveWithoutIndex);
488
- } else if (!isApprove && cave.elements) {
489
- for (const element of cave.elements) {
490
- if (element.type === "img" && element.file) {
491
- const fullPath = path.join(resourceDir, element.file);
492
- if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
493
- }
696
+ async markDeleted(id) {
697
+ if (!this.initialized) {
698
+ throw new Error("IdManager not initialized");
699
+ }
700
+ this.deletedIds.add(id);
701
+ this.usedIds.delete(id);
702
+ if (id === this.maxId) {
703
+ const maxUsedId = Math.max(...Array.from(this.usedIds));
704
+ this.maxId = maxUsedId;
705
+ }
706
+ await this.saveStatus();
707
+ }
708
+ // 添加新的统计记录
709
+ async addStat(contributorNumber, caveId) {
710
+ if (contributorNumber === "10000") return;
711
+ if (!this.stats[contributorNumber]) {
712
+ this.stats[contributorNumber] = [];
713
+ }
714
+ this.stats[contributorNumber].push(caveId);
715
+ await this.saveStatus();
716
+ }
717
+ // 删除统计记录
718
+ async removeStat(contributorNumber, caveId) {
719
+ if (this.stats[contributorNumber]) {
720
+ this.stats[contributorNumber] = this.stats[contributorNumber].filter((id) => id !== caveId);
721
+ if (this.stats[contributorNumber].length === 0) {
722
+ delete this.stats[contributorNumber];
494
723
  }
724
+ await this.saveStatus();
495
725
  }
496
- return true;
497
- } catch (error) {
498
- return sendTempMessage(session, "commands.cave.error.auditProcess", [error.message]);
499
726
  }
500
- }
501
- __name(handleSingleCaveAudit, "handleSingleCaveAudit");
502
- async function handleAudit(ctx, pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
503
- if (pendingData.length === 0) return sendMessage(session, "commands.cave.audit.noPending", [], true);
504
- if (typeof targetId === "number") {
505
- const pendingIndex = pendingData.findIndex((item) => item.cave_id === targetId);
506
- if (pendingIndex === -1) return sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
507
- const cave = pendingData[pendingIndex];
508
- const data2 = isApprove ? await FileHandler.readJsonData(caveFilePath, session) : null;
509
- const auditResult = await handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data2, session);
510
- if (typeof auditResult === "string") return auditResult;
511
- if (isApprove && data2) await FileHandler.writeJsonData(caveFilePath, data2, session);
512
- pendingData.splice(pendingIndex, 1);
513
- await FileHandler.writeJsonData(pendingFilePath, pendingData, session);
514
- const remainingCount = pendingData.length;
515
- if (remainingCount > 0) {
516
- const remainingIds = pendingData.map((c) => c.cave_id).join(", ");
517
- const action = isApprove ? "auditPassed" : "auditRejected";
518
- return sendMessage(session, "commands.cave.audit.pendingResult", [
519
- session.text(`commands.cave.audit.${action}`),
520
- remainingCount,
521
- remainingIds
522
- ], false);
523
- }
524
- return sendMessage(
525
- session,
526
- isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
527
- [],
528
- false
529
- // 审核结果改为永久消息
530
- );
727
+ // 获取统计信息
728
+ getStats() {
729
+ return this.stats;
531
730
  }
532
- const data = isApprove ? await FileHandler.readJsonData(caveFilePath, session) : null;
533
- let processedCount = 0;
534
- for (const cave of pendingData) {
535
- await handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data, session) && processedCount++;
536
- }
537
- if (isApprove && data) await FileHandler.writeJsonData(caveFilePath, data, session);
538
- await FileHandler.writeJsonData(pendingFilePath, [], session);
539
- return sendMessage(session, "commands.cave.audit.batchAuditResult", [
540
- isApprove ? "通过" : "拒绝",
541
- processedCount,
542
- pendingData.length
543
- ], false);
544
- }
545
- __name(handleAudit, "handleAudit");
546
- function cleanElementsForSave(elements, keepIndex = false) {
547
- const sorted = elements.sort((a, b) => a.index - b.index);
548
- return sorted.map(({ type, content, file, index }) => ({
549
- type,
550
- ...keepIndex && { index },
551
- ...content && { content },
552
- ...file && { file }
553
- }));
554
- }
555
- __name(cleanElementsForSave, "cleanElementsForSave");
556
- async function sendTempMessage(session, key, params = [], timeout = 1e4) {
557
- const msg = await session.send(session.text(key, params));
558
- setTimeout(async () => {
731
+ async saveStatus() {
559
732
  try {
560
- await session.bot.deleteMessage(session.channelId, msg);
733
+ const status = {
734
+ deletedIds: Array.from(this.deletedIds).sort((a, b) => a - b),
735
+ maxId: this.maxId,
736
+ stats: this.stats,
737
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
738
+ };
739
+ const tmpPath = `${this.statusFilePath}.tmp`;
740
+ await fs.promises.writeFile(tmpPath, JSON.stringify(status, null, 2), "utf8");
741
+ await fs.promises.rename(tmpPath, this.statusFilePath);
561
742
  } catch (error) {
562
- logger.error("Failed to delete message:", error);
743
+ logger.error(`Failed to save status: ${error.message}`);
744
+ throw error;
563
745
  }
564
- }, timeout);
565
- return "";
566
- }
567
- __name(sendTempMessage, "sendTempMessage");
568
- var messageQueue = /* @__PURE__ */ new Map();
746
+ }
747
+ };
569
748
  async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
570
- const channelId = session.channelId;
571
- const sendOperation = /* @__PURE__ */ __name(async () => {
749
+ try {
572
750
  const msg = await session.send(session.text(key, params));
573
- if (isTemp) {
751
+ if (isTemp && msg) {
574
752
  setTimeout(async () => {
575
753
  try {
576
- await session.bot.deleteMessage(channelId, msg);
754
+ await session.bot.deleteMessage(session.channelId, msg);
577
755
  } catch (error) {
578
- logger.error("Failed to delete message:", error);
756
+ logger.debug(`Failed to delete temporary message: ${error.message}`);
579
757
  }
580
758
  }, timeout);
581
759
  }
582
- }, "sendOperation");
583
- if (!messageQueue.has(channelId)) {
584
- messageQueue.set(channelId, Promise.resolve());
760
+ } catch (error) {
761
+ logger.error(`Failed to send message: ${error.message}`);
585
762
  }
586
- const currentPromise = messageQueue.get(channelId).then(sendOperation).finally(() => {
587
- if (messageQueue.get(channelId) === currentPromise) {
588
- messageQueue.delete(channelId);
589
- }
590
- });
591
- messageQueue.set(channelId, currentPromise);
592
763
  return "";
593
764
  }
594
765
  __name(sendMessage, "sendMessage");
595
- async function extractMediaContent(originalContent) {
596
- const parsedTexts = originalContent.split(/<img[^>]+>|<video[^>]+>/g).map((t) => t.trim()).filter((t) => t);
597
- const textParts = [];
598
- parsedTexts.forEach((text, idx) => {
599
- textParts.push({ type: "text", content: text, index: idx * 3 });
600
- });
601
- const imageUrls = [];
602
- const imageElements = [];
603
- const videoUrls = [];
604
- const videoElements = [];
605
- const imgMatches = originalContent.match(/<img[^>]+src="([^"]+)"[^>]*>/g) || [];
606
- imgMatches.forEach((img, idx) => {
607
- const srcMatch = img.match(/src="([^"]+)"/);
608
- const fileName = img.match(/file="([^"]+)"/)?.[1];
609
- const fileSize = img.match(/fileSize="([^"]+)"/)?.[1];
610
- if (srcMatch?.[1]) {
611
- imageUrls.push(srcMatch[1]);
612
- imageElements.push({ type: "img", index: idx * 3 + 1, fileName, fileSize });
766
+ async function sendAuditMessage(ctx, config, cave, content, session) {
767
+ const auditMessage = `${session.text("commands.cave.audit.title")}
768
+ ${content}
769
+ ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
770
+ for (const managerId of config.manager) {
771
+ const bot = ctx.bots[0];
772
+ if (bot) {
773
+ try {
774
+ await bot.sendPrivateMessage(managerId, auditMessage);
775
+ } catch (error) {
776
+ logger.error(session.text("commands.cave.audit.sendFailed", [managerId]));
777
+ }
613
778
  }
614
- });
615
- const videoMatches = originalContent.match(/<video[^>]+src="([^"]+)"[^>]*>/g) || [];
616
- videoMatches.forEach((video, idx) => {
617
- const srcMatch = video.match(/src="([^"]+)"/);
618
- const fileName = video.match(/file="([^"]+)"/)?.[1];
619
- const fileSize = video.match(/fileSize="([^"]+)"/)?.[1];
620
- if (srcMatch?.[1]) {
621
- videoUrls.push(srcMatch[1]);
622
- videoElements.push({ type: "video", index: idx * 3 + 2, fileName, fileSize });
779
+ }
780
+ }
781
+ __name(sendAuditMessage, "sendAuditMessage");
782
+ function cleanElementsForSave(elements, keepIndex = false) {
783
+ if (!elements?.length) return [];
784
+ const cleanedElements = elements.map((element) => {
785
+ if (element.type === "text") {
786
+ const cleanedElement = {
787
+ type: "text",
788
+ content: element.content
789
+ };
790
+ if (keepIndex) cleanedElement.index = element.index;
791
+ return cleanedElement;
792
+ } else if (element.type === "img" || element.type === "video") {
793
+ const mediaElement = element;
794
+ const cleanedElement = {
795
+ type: mediaElement.type
796
+ };
797
+ if (mediaElement.file) cleanedElement.file = mediaElement.file;
798
+ if (keepIndex) cleanedElement.index = element.index;
799
+ return cleanedElement;
623
800
  }
801
+ return element;
624
802
  });
625
- return { imageUrls, imageElements, videoUrls, videoElements, textParts };
803
+ return keepIndex ? cleanedElements.sort((a, b) => (a.index || 0) - (b.index || 0)) : cleanedElements;
626
804
  }
627
- __name(extractMediaContent, "extractMediaContent");
805
+ __name(cleanElementsForSave, "cleanElementsForSave");
806
+ async function processMediaFile(filePath, type) {
807
+ const data = await fs.promises.readFile(filePath).catch(() => null);
808
+ if (!data) return null;
809
+ return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
810
+ }
811
+ __name(processMediaFile, "processMediaFile");
628
812
  async function buildMessage(cave, resourceDir, session) {
629
- let content = session.text("commands.cave.message.caveTitle", [cave.cave_id]) + "\n";
630
- const videoElements = [];
631
- for (const element of cave.elements) {
813
+ if (!cave?.elements?.length) {
814
+ return session.text("commands.cave.error.noContent");
815
+ }
816
+ const videoElement = cave.elements.find((el) => el.type === "video");
817
+ const nonVideoElements = cave.elements.filter((el) => el.type !== "video").sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
818
+ if (videoElement?.file) {
819
+ const basicInfo = [
820
+ session.text("commands.cave.message.caveTitle", [cave.cave_id]),
821
+ session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
822
+ ].join("\n");
823
+ await session?.send(basicInfo);
824
+ const filePath = path.join(resourceDir, videoElement.file);
825
+ const base64Data = await processMediaFile(filePath, "video");
826
+ if (base64Data && session) {
827
+ await session.send((0, import_koishi.h)("video", { src: base64Data }));
828
+ }
829
+ return "";
830
+ }
831
+ const lines = [session.text("commands.cave.message.caveTitle", [cave.cave_id])];
832
+ for (const element of nonVideoElements) {
632
833
  if (element.type === "text") {
633
- content += element.content + "\n";
834
+ lines.push(element.content);
634
835
  } else if (element.type === "img" && element.file) {
635
- const fullImagePath = path.join(resourceDir, element.file);
636
- if (fs.existsSync(fullImagePath)) {
637
- try {
638
- const imageBuffer = fs.readFileSync(fullImagePath);
639
- content += (0, import_koishi.h)("image", { src: `data:image/png;base64,${imageBuffer.toString("base64")}` }) + "\n";
640
- } catch (error) {
641
- content += session.text("commands.cave.error.mediaLoadFailed", ["图片"]) + "\n";
642
- }
643
- } else {
644
- content += session.text("commands.cave.message.mediaInvalid", ["图片"]) + "\n";
836
+ const filePath = path.join(resourceDir, element.file);
837
+ const base64Data = await processMediaFile(filePath, "image");
838
+ if (base64Data) {
839
+ lines.push((0, import_koishi.h)("image", { src: base64Data }));
645
840
  }
646
- } else if (element.type === "video" && element.file) {
647
- videoElements.push({ file: element.file });
648
841
  }
649
842
  }
650
- if (videoElements.length > 0 && session) {
651
- content += session.text("commands.cave.message.videoSending") + "\n";
652
- for (const video of videoElements) {
653
- const fullVideoPath = path.join(resourceDir, video.file);
654
- if (fs.existsSync(fullVideoPath)) {
655
- try {
656
- const videoBuffer = fs.readFileSync(fullVideoPath);
657
- session.send((0, import_koishi.h)("video", { src: `data:video/mp4;base64,${videoBuffer.toString("base64")}` })).catch((error) => logger.warn(session.text("commands.cave.error.videoSendFailed"), error.message));
658
- } catch (error) {
659
- content += session.text("commands.cave.error.mediaLoadFailed", ["视频"]) + "\n";
843
+ lines.push(session.text("commands.cave.message.contributorSuffix", [cave.contributor_name]));
844
+ return lines.join("\n");
845
+ }
846
+ __name(buildMessage, "buildMessage");
847
+ async function extractMediaContent(originalContent, config, session) {
848
+ const textParts = originalContent.split(/<(img|video)[^>]+>/).map((text, idx) => text.trim() && {
849
+ type: "text",
850
+ content: text.replace(/^(img|video)$/, "").trim(),
851
+ index: idx * 3
852
+ }).filter((text) => text && text.content);
853
+ const getMediaElements = /* @__PURE__ */ __name((type, maxSize) => {
854
+ const regex = new RegExp(`<${type}[^>]+src="([^"]+)"[^>]*>`, "g");
855
+ const elements = [];
856
+ const urls = [];
857
+ let match;
858
+ let idx = 0;
859
+ while ((match = regex.exec(originalContent)) !== null) {
860
+ const element = match[0];
861
+ const url = match[1];
862
+ const fileName = element.match(/file="([^"]+)"/)?.[1];
863
+ const fileSize = element.match(/fileSize="([^"]+)"/)?.[1];
864
+ if (fileSize) {
865
+ const sizeInBytes = parseInt(fileSize);
866
+ if (sizeInBytes > maxSize * 1024 * 1024) {
867
+ throw new Error(session.text("commands.cave.message.mediaSizeExceeded", [type]));
660
868
  }
661
- } else {
662
- content += session.text("commands.cave.message.mediaInvalid", ["视频"]) + "\n";
663
869
  }
870
+ urls.push(url);
871
+ elements.push({
872
+ type,
873
+ index: type === "video" ? Number.MAX_SAFE_INTEGER : idx * 3 + 1,
874
+ // 视频始终在最后
875
+ fileName,
876
+ fileSize
877
+ });
878
+ idx++;
664
879
  }
665
- }
666
- content += session.text("commands.cave.message.contributorSuffix", [cave.contributor_name]);
667
- return content;
880
+ return { urls, elements };
881
+ }, "getMediaElements");
882
+ const { urls: imageUrls, elements: imageElementsRaw } = getMediaElements("img", config.imageMaxSize);
883
+ const imageElements = imageElementsRaw;
884
+ const { urls: videoUrls, elements: videoElementsRaw } = getMediaElements("video", config.videoMaxSize);
885
+ const videoElements = videoElementsRaw;
886
+ return { imageUrls, imageElements, videoUrls, videoElements, textParts };
668
887
  }
669
- __name(buildMessage, "buildMessage");
670
- var CacheManager = class {
671
- static {
672
- __name(this, "CacheManager");
673
- }
674
- static caveDataCache = null;
675
- static pendingDataCache = null;
676
- static cacheTimeout = 5 * 60 * 1e3;
677
- // 5分钟缓存
678
- static lastCaveUpdate = 0;
679
- static lastPendingUpdate = 0;
680
- static clearCache() {
681
- this.caveDataCache = null;
682
- this.pendingDataCache = null;
683
- }
684
- static async getCaveData(filePath, session) {
685
- const now = Date.now();
686
- if (!this.caveDataCache || now - this.lastCaveUpdate > this.cacheTimeout) {
687
- this.caveDataCache = await FileHandler.readJsonData(filePath, session);
688
- this.lastCaveUpdate = now;
689
- }
690
- return this.caveDataCache;
691
- }
692
- static async getPendingData(filePath, session) {
693
- const now = Date.now();
694
- if (!this.pendingDataCache || now - this.lastPendingUpdate > this.cacheTimeout) {
695
- this.pendingDataCache = await FileHandler.readJsonData(filePath, session);
696
- this.lastPendingUpdate = now;
888
+ __name(extractMediaContent, "extractMediaContent");
889
+ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, ctx, session) {
890
+ const { ext, accept } = mediaType === "img" ? { ext: "png", accept: "image/*" } : { ext: "mp4", accept: "video/*" };
891
+ const downloadTasks = urls.map(async (url, i) => {
892
+ const fileName = fileNames[i];
893
+ const fileExt = fileName?.match(/\.[a-zA-Z0-9]+$/)?.[0]?.slice(1) || ext;
894
+ const baseName = fileName;
895
+ path.basename(fileName, path.extname(fileName)).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
896
+ const finalFileName = `${caveId}_${baseName}.${fileExt}`;
897
+ const filePath = path.join(resourceDir, finalFileName);
898
+ try {
899
+ const response = await ctx.http(decodeURIComponent(url).replace(/&amp;/g, "&"), {
900
+ method: "GET",
901
+ responseType: "arraybuffer",
902
+ timeout: 3e4,
903
+ headers: {
904
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
905
+ "Accept": accept,
906
+ "Referer": "https://qq.com"
907
+ }
908
+ });
909
+ if (!response.data) throw new Error("empty_response");
910
+ await FileHandler.saveMediaFile(filePath, Buffer.from(response.data));
911
+ return finalFileName;
912
+ } catch (error) {
913
+ logger.error(`Failed to download media: ${error.message}`);
914
+ throw error;
697
915
  }
698
- return this.pendingDataCache;
699
- }
700
- static updateCaveData(data) {
701
- this.caveDataCache = data;
702
- this.lastCaveUpdate = Date.now();
703
- }
704
- static updatePendingData(data) {
705
- this.pendingDataCache = data;
706
- this.lastPendingUpdate = Date.now();
916
+ });
917
+ const results = await Promise.allSettled(downloadTasks);
918
+ const successfulResults = results.filter((result) => result.status === "fulfilled").map((result) => result.value);
919
+ if (!successfulResults.length) {
920
+ throw new Error(session.text(`commands.cave.error.upload${mediaType === "img" ? "Image" : "Video"}Failed`));
707
921
  }
708
- };
922
+ return successfulResults;
923
+ }
924
+ __name(saveMedia, "saveMedia");
709
925
  // Annotate the CommonJS export names for ESM import in node:
710
926
  0 && (module.exports = {
711
927
  Config,