koishi-plugin-best-cave 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Context, Schema } from 'koishi';
2
2
  export declare const name = "cave";
3
3
  export declare const inject: string[];
4
+ export declare const Config: Schema<Config>;
5
+ export declare function apply(ctx: Context, config: Config): Promise<void>;
4
6
  export interface User {
5
7
  userId: string;
6
8
  username: string;
@@ -14,6 +16,19 @@ export interface Config {
14
16
  manager: string[];
15
17
  number: number;
16
18
  enableAudit: boolean;
19
+ allowVideo: boolean;
20
+ videoMaxSize: number;
21
+ imageMaxSize: number;
22
+ blacklist: string[];
23
+ whitelist: string[];
24
+ enablePagination: boolean;
25
+ itemsPerPage: number;
17
26
  }
18
- export declare const Config: Schema<Config>;
19
- export declare function apply(ctx: Context, config: Config): Promise<void>;
27
+ export declare function initCavePaths(ctx: Context): Promise<{
28
+ dataDir: string;
29
+ caveDir: string;
30
+ caveFilePath: string;
31
+ resourceDir: string;
32
+ pendingFilePath: string;
33
+ }>;
34
+ export declare function handleCaveAction(ctx: Context, config: Config, session: any, options: any, content: string[], lastUsed: Map<string, number>): Promise<string | void>;
package/lib/index.js CHANGED
@@ -32,6 +32,8 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply,
35
+ handleCaveAction: () => handleCaveAction,
36
+ initCavePaths: () => initCavePaths,
35
37
  inject: () => inject,
36
38
  name: () => name
37
39
  });
@@ -39,96 +41,134 @@ module.exports = __toCommonJS(src_exports);
39
41
  var import_koishi = require("koishi");
40
42
  var fs = __toESM(require("fs"));
41
43
  var path = __toESM(require("path"));
42
- var logger = new import_koishi.Logger("cave");
43
44
  var name = "cave";
44
45
  var inject = ["database"];
45
46
  var Config = import_koishi.Schema.object({
46
- manager: import_koishi.Schema.array(import_koishi.Schema.string()).required().description("管理员账号"),
47
- number: import_koishi.Schema.number().default(60).description("群内调用冷却时间(秒)"),
48
- enableAudit: import_koishi.Schema.boolean().default(false).description("是否开启审核功能")
47
+ manager: import_koishi.Schema.array(import_koishi.Schema.string()).required().description("管理员"),
48
+ blacklist: import_koishi.Schema.array(import_koishi.Schema.string()).default([]).description("黑名单"),
49
+ number: import_koishi.Schema.number().default(60).description("调用冷却时间(秒)"),
50
+ enableAudit: import_koishi.Schema.boolean().default(false).description("审核功能"),
51
+ allowVideo: import_koishi.Schema.boolean().default(true).description("允许添加视频"),
52
+ videoMaxSize: import_koishi.Schema.number().default(10).description("视频最大大小(MB)"),
53
+ imageMaxSize: import_koishi.Schema.number().default(5).description("图片最大大小(MB)")
49
54
  });
50
- function readJsonData(filePath, validator) {
51
- try {
52
- const data = fs.readFileSync(filePath, "utf8");
53
- const parsed = JSON.parse(data || "[]");
54
- if (!Array.isArray(parsed)) return [];
55
- return validator ? parsed.filter(validator) : parsed;
56
- } catch (error) {
57
- logger.error(`读取文件失败 ${filePath}: ${error.message}`);
58
- return [];
59
- }
55
+ async function apply(ctx, config) {
56
+ const { caveFilePath, resourceDir, pendingFilePath } = await initCavePaths(ctx);
57
+ const lastUsed = /* @__PURE__ */ new Map();
58
+ ctx.command("cave [message:text]", "回声洞").usage("支持添加、抽取、查看、查询回声洞").example("cave 随机抽取回声洞").example("cave -a 内容 添加新回声洞").example("cave -g/r x 查看/删除指定回声洞").example("cave -p/d x/all 通过/拒绝待审回声洞").example("cave -l x 查询投稿者投稿列表").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 }) => {
59
+ if (config.blacklist.includes(session.userId)) return "你已被列入黑名单";
60
+ if (session.content && session.content.includes("-help")) return;
61
+ if ((options.l || options.p || options.d) && !config.manager.includes(session.userId)) {
62
+ return "此操作仅管理员可用";
63
+ }
64
+ }).action(async ({ session, options }, ...content) => {
65
+ return await handleCaveAction(ctx, config, session, options, content, lastUsed);
66
+ });
60
67
  }
61
- __name(readJsonData, "readJsonData");
62
- function writeJsonData(filePath, data) {
63
- try {
64
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
65
- } catch (error) {
66
- logger.error(`写入文件失败: ${error.message}`);
67
- throw error;
68
+ __name(apply, "apply");
69
+ var logger = new import_koishi.Logger("cave");
70
+ var FileHandler = class {
71
+ static {
72
+ __name(this, "FileHandler");
68
73
  }
69
- }
70
- __name(writeJsonData, "writeJsonData");
71
- async function ensureDirectory(dir) {
72
- try {
73
- if (!fs.existsSync(dir)) {
74
- await fs.promises.mkdir(dir, { recursive: true });
74
+ static readJsonData(filePath, validator) {
75
+ try {
76
+ const data = fs.readFileSync(filePath, "utf8");
77
+ const parsed = JSON.parse(data || "[]");
78
+ if (!Array.isArray(parsed)) return [];
79
+ return validator ? parsed.filter(validator) : parsed;
80
+ } catch (error) {
81
+ throw new Error(`操作失败: ${error.message}`);
75
82
  }
76
- } catch (error) {
77
- logger.error(`创建目录失败 ${dir}: ${error.message}`);
78
- throw error;
79
83
  }
80
- }
81
- __name(ensureDirectory, "ensureDirectory");
82
- async function ensureJsonFile(filePath, defaultContent = "[]") {
83
- try {
84
- if (!fs.existsSync(filePath)) {
85
- await fs.promises.writeFile(filePath, defaultContent, "utf8");
84
+ static writeJsonData(filePath, data) {
85
+ try {
86
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
87
+ } catch (error) {
88
+ throw new Error(`操作失败: ${error.message}`);
86
89
  }
87
- } catch (error) {
88
- logger.error(`创建文件失败 ${filePath}: ${error.message}`);
89
- throw error;
90
90
  }
91
- }
92
- __name(ensureJsonFile, "ensureJsonFile");
93
- async function saveImages(urls, imageDir, caveId, config, ctx) {
91
+ static async ensureDirectory(dir) {
92
+ try {
93
+ if (!fs.existsSync(dir)) {
94
+ await fs.promises.mkdir(dir, { recursive: true });
95
+ }
96
+ } catch (error) {
97
+ throw new Error(`操作失败: ${error.message}`);
98
+ }
99
+ }
100
+ static async ensureJsonFile(filePath, defaultContent = "[]") {
101
+ try {
102
+ if (!fs.existsSync(filePath)) {
103
+ await fs.promises.writeFile(filePath, defaultContent, "utf8");
104
+ }
105
+ } catch (error) {
106
+ throw new Error(`操作失败: ${error.message}`);
107
+ }
108
+ }
109
+ };
110
+ async function saveMedia(urls, fileSuggestions, resourceDir, caveId, config, ctx, mediaType) {
94
111
  const savedFiles = [];
112
+ const defaults = mediaType === "img" ? { ext: "png", accept: "image/*", maxSize: config.imageMaxSize } : { ext: "mp4", accept: "video/*", maxSize: config.videoMaxSize };
113
+ const extPattern = /\.[a-zA-Z0-9]+$/;
95
114
  for (let i = 0; i < urls.length; i++) {
96
115
  try {
97
116
  const url = urls[i];
98
117
  const processedUrl = (() => {
99
118
  try {
100
119
  const decodedUrl = decodeURIComponent(url);
101
- if (decodedUrl.includes("multimedia.nt.qq.com.cn")) {
102
- return decodedUrl.replace(/&amp;/g, "&");
103
- }
104
- return url;
120
+ return decodedUrl.includes("multimedia.nt.qq.com.cn") ? decodedUrl.replace(/&amp;/g, "&") : url;
105
121
  } catch {
106
122
  return url;
107
123
  }
108
124
  })();
109
- const ext = url.match(/\.([^./?]+)(?:[?#]|$)/)?.[1] || "png";
110
- const filename = `${caveId}_${i + 1}.${ext}`;
111
- const targetPath = path.join(imageDir, filename);
112
- const buffer = await ctx.http.get(processedUrl, {
125
+ let ext = defaults.ext;
126
+ const suggestion = fileSuggestions[i];
127
+ if (suggestion && suggestion.includes(";")) {
128
+ const parts = suggestion.split(";");
129
+ const filenameCandidate = parts[0];
130
+ const sizeCandidate = parseInt(parts[1]);
131
+ if (extPattern.test(filenameCandidate)) {
132
+ ext = filenameCandidate.match(extPattern)[0].slice(1);
133
+ }
134
+ if (sizeCandidate > defaults.maxSize * 1024 * 1024) {
135
+ throw new Error(`${mediaType === "img" ? "图片" : "视频"}超出大小限制 (${defaults.maxSize}MB),实际大小为 ${(sizeCandidate / (1024 * 1024)).toFixed(2)}MB`);
136
+ }
137
+ } else if (suggestion && extPattern.test(suggestion)) {
138
+ ext = suggestion.match(extPattern)[0].slice(1);
139
+ }
140
+ let filename;
141
+ if (suggestion && suggestion.includes(";")) {
142
+ const parts = suggestion.split(";");
143
+ const srcFilename = path.basename(parts[0]);
144
+ filename = `${caveId}_${srcFilename}`;
145
+ } else if (suggestion && extPattern.test(suggestion)) {
146
+ const srcFilename = path.basename(suggestion);
147
+ filename = `${caveId}_${srcFilename}`;
148
+ } else {
149
+ filename = `${caveId}_${i + 1}.${ext}`;
150
+ }
151
+ const targetPath = path.join(resourceDir, filename);
152
+ const response = await ctx.http(processedUrl, {
153
+ method: "GET",
113
154
  responseType: "arraybuffer",
114
155
  timeout: 3e4,
115
156
  headers: {
116
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
117
- "Accept": "image/*",
157
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
158
+ "Accept": defaults.accept,
118
159
  "Referer": "https://qq.com"
119
160
  }
120
161
  });
121
- if (buffer && buffer.byteLength > 0) {
122
- await fs.promises.writeFile(targetPath, Buffer.from(buffer));
123
- savedFiles.push(filename);
124
- }
162
+ const fileBuffer = Buffer.from(response.data);
163
+ await fs.promises.writeFile(targetPath, fileBuffer);
164
+ savedFiles.push(filename);
125
165
  } catch (error) {
126
- logger.error(`保存图片失败: ${error.message}`);
166
+ throw new Error(`操作失败: ${error.message}`);
127
167
  }
128
168
  }
129
169
  return savedFiles;
130
170
  }
131
- __name(saveImages, "saveImages");
171
+ __name(saveMedia, "saveMedia");
132
172
  async function sendAuditMessage(ctx, config, cave, content) {
133
173
  const auditMessage = `待审核回声洞:
134
174
  ${content}
@@ -137,12 +177,12 @@ ${content}
137
177
  try {
138
178
  await ctx.bots[0]?.sendPrivateMessage(managerId, auditMessage);
139
179
  } catch (error) {
140
- logger.error(`发送审核消息 ${managerId} 失败: ${error.message}`);
180
+ logger.error(`操作失败: ${error.message}`);
141
181
  }
142
182
  }
143
183
  }
144
184
  __name(sendAuditMessage, "sendAuditMessage");
145
- async function handleSingleCaveAudit(ctx, cave, isApprove, imageDir, data) {
185
+ async function handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data) {
146
186
  try {
147
187
  if (isApprove && data) {
148
188
  const caveWithoutIndex = {
@@ -150,108 +190,159 @@ async function handleSingleCaveAudit(ctx, cave, isApprove, imageDir, data) {
150
190
  elements: cleanElementsForSave(cave.elements, false)
151
191
  };
152
192
  data.push(caveWithoutIndex);
153
- logger.info(`审核通过回声洞(${cave.cave_id})`);
154
193
  } else if (!isApprove && cave.elements) {
155
194
  for (const element of cave.elements) {
156
195
  if (element.type === "img" && element.file) {
157
- const fullPath = path.join(imageDir, element.file);
196
+ const fullPath = path.join(resourceDir, element.file);
158
197
  if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
159
198
  }
160
199
  }
161
- logger.info(`审核失败回声洞(${cave.cave_id})`);
162
200
  }
163
201
  return true;
164
202
  } catch (error) {
165
- logger.error(`处理回声洞(${cave.cave_id})失败: ${error.message}`);
166
- return false;
203
+ throw new Error(`操作失败: ${error.message}`);
167
204
  }
168
205
  }
169
206
  __name(handleSingleCaveAudit, "handleSingleCaveAudit");
170
- async function handleAudit(ctx, pendingData, isApprove, caveFilePath, imageDir, pendingFilePath, targetId) {
171
- if (pendingData.length === 0) return "没有待审核回声洞";
172
- if (typeof targetId === "number") {
173
- const pendingIndex = pendingData.findIndex((item) => item.cave_id === targetId);
174
- if (pendingIndex === -1) return "未找到该待审核回声洞";
175
- const cave = pendingData[pendingIndex];
176
- const data2 = isApprove ? readJsonData(caveFilePath) : null;
177
- const success = await handleSingleCaveAudit(ctx, cave, isApprove, imageDir, data2);
178
- if (!success) return "处理失败,请稍后重试";
179
- if (isApprove && data2) writeJsonData(caveFilePath, data2);
180
- pendingData.splice(pendingIndex, 1);
181
- writeJsonData(pendingFilePath, pendingData);
182
- const remainingCount = pendingData.length;
183
- if (remainingCount > 0) {
184
- const remainingIds = pendingData.map((c) => c.cave_id).join(", ");
185
- return `${isApprove ? "审核通过" : "拒绝"}成功,还有 ${remainingCount} 条待审核:[${remainingIds}]`;
207
+ async function handleAudit(ctx, pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, targetId) {
208
+ try {
209
+ if (pendingData.length === 0) return "没有待审核回声洞";
210
+ if (typeof targetId === "number") {
211
+ const pendingIndex = pendingData.findIndex((item) => item.cave_id === targetId);
212
+ if (pendingIndex === -1) return "未找到该待审核回声洞";
213
+ const cave = pendingData[pendingIndex];
214
+ const data2 = isApprove ? FileHandler.readJsonData(caveFilePath) : null;
215
+ const success = await handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data2);
216
+ if (!success) return "处理失败,请稍后重试";
217
+ if (isApprove && data2) FileHandler.writeJsonData(caveFilePath, data2);
218
+ pendingData.splice(pendingIndex, 1);
219
+ FileHandler.writeJsonData(pendingFilePath, pendingData);
220
+ const remainingCount = pendingData.length;
221
+ if (remainingCount > 0) {
222
+ const remainingIds = pendingData.map((c) => c.cave_id).join(", ");
223
+ return `${isApprove ? "审核通过" : "拒绝"}成功,还有 ${remainingCount} 条待审核:[${remainingIds}]`;
224
+ }
225
+ return isApprove ? "已通过该回声洞" : "已拒绝该回声洞";
186
226
  }
187
- return isApprove ? "已通过该回声洞" : "已拒绝该回声洞";
188
- }
189
- const data = isApprove ? readJsonData(caveFilePath) : null;
190
- let processedCount = 0;
191
- for (const cave of pendingData) {
192
- const success = await handleSingleCaveAudit(ctx, cave, isApprove, imageDir, data);
193
- if (success) processedCount++;
227
+ const data = isApprove ? FileHandler.readJsonData(caveFilePath) : null;
228
+ let processedCount = 0;
229
+ for (const cave of pendingData) {
230
+ const success = await handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data);
231
+ if (success) processedCount++;
232
+ }
233
+ if (isApprove && data) FileHandler.writeJsonData(caveFilePath, data);
234
+ FileHandler.writeJsonData(pendingFilePath, []);
235
+ return isApprove ? `已通过 ${processedCount}/${pendingData.length} 条回声洞` : `已拒绝 ${processedCount}/${pendingData.length} 条回声洞`;
236
+ } catch (error) {
237
+ throw new Error(`操作失败: ${error.message}`);
194
238
  }
195
- if (isApprove && data) writeJsonData(caveFilePath, data);
196
- writeJsonData(pendingFilePath, []);
197
- return isApprove ? `✅ 已通过 ${processedCount}/${pendingData.length} 条回声洞` : `❌ 已拒绝 ${processedCount}/${pendingData.length} 条回声洞`;
198
239
  }
199
240
  __name(handleAudit, "handleAudit");
200
- function buildMessage(cave, imageDir) {
241
+ function cleanElementsForSave(elements, keepIndex = false) {
242
+ const sorted = elements.sort((a, b) => a.index - b.index);
243
+ return sorted.map(({ type, content, file, index }) => ({
244
+ type,
245
+ ...keepIndex && { index },
246
+ ...content && { content },
247
+ ...file && { file }
248
+ }));
249
+ }
250
+ __name(cleanElementsForSave, "cleanElementsForSave");
251
+ async function extractMediaContent(originalContent) {
252
+ const parsedTexts = originalContent.split(/<img[^>]+>|<video[^>]+>/g).map((t) => t.trim()).filter((t) => t);
253
+ const textParts = [];
254
+ parsedTexts.forEach((text, idx) => {
255
+ textParts.push({ type: "text", content: text, index: idx * 3 });
256
+ });
257
+ const imageUrls = [];
258
+ const imageElements = [];
259
+ const videoUrls = [];
260
+ const videoElements = [];
261
+ const imgMatches = originalContent.match(/<img[^>]+src="([^"]+)"[^>]*>/g) || [];
262
+ imgMatches.forEach((img, idx) => {
263
+ const srcMatch = img.match(/src="([^"]+)"/);
264
+ const fileMatch = img.match(/file="([^"]+)"(?:\s+fileSize="([^"]+)")?/);
265
+ if (srcMatch?.[1]) {
266
+ imageUrls.push(srcMatch[1]);
267
+ const suggestion = fileMatch ? fileMatch[2] ? `${fileMatch[1]};${fileMatch[2]}` : fileMatch[1] : void 0;
268
+ imageElements.push({ type: "img", index: idx * 3 + 1, fileAttr: suggestion });
269
+ }
270
+ });
271
+ const videoMatches = originalContent.match(/<video[^>]+src="([^"]+)"[^>]*>/g) || [];
272
+ videoMatches.forEach((video, idx) => {
273
+ const srcMatch = video.match(/src="([^"]+)"/);
274
+ const fileMatch = video.match(/file="([^"]+)"(?:\s+fileSize="([^"]+)")?/);
275
+ if (srcMatch?.[1]) {
276
+ videoUrls.push(srcMatch[1]);
277
+ const suggestion = fileMatch ? fileMatch[2] ? `${fileMatch[1]};${fileMatch[2]}` : fileMatch[1] : void 0;
278
+ videoElements.push({ type: "video", index: idx * 3 + 2, fileAttr: suggestion });
279
+ }
280
+ });
281
+ return { imageUrls, imageElements, videoUrls, videoElements, textParts };
282
+ }
283
+ __name(extractMediaContent, "extractMediaContent");
284
+ async function buildMessage(cave, resourceDir, session) {
201
285
  let content = `回声洞 ——(${cave.cave_id})
202
286
  `;
287
+ const videoElements = [];
203
288
  for (const element of cave.elements) {
204
289
  if (element.type === "text") {
205
290
  content += element.content + "\n";
206
291
  } else if (element.type === "img" && element.file) {
207
292
  try {
208
- const fullImagePath = path.join(imageDir, element.file);
293
+ const fullImagePath = path.join(resourceDir, element.file);
209
294
  if (fs.existsSync(fullImagePath)) {
210
295
  const imageBuffer = fs.readFileSync(fullImagePath);
211
296
  const base64Image = imageBuffer.toString("base64");
212
297
  content += (0, import_koishi.h)("image", { src: `data:image/png;base64,${base64Image}` }) + "\n";
213
298
  }
214
299
  } catch (error) {
215
- logger.error(`读取图片失败: ${error.message}`);
300
+ throw new Error(`操作失败: ${error.message}`);
216
301
  }
302
+ } else if (element.type === "video" && element.file) {
303
+ videoElements.push({ file: element.file });
217
304
  }
218
305
  }
219
- return content + `—— ${cave.contributor_name}`;
306
+ if (videoElements.length > 0 && session) {
307
+ content += `[视频已发送]
308
+ `;
309
+ for (const video of videoElements) {
310
+ try {
311
+ const fullVideoPath = path.join(resourceDir, video.file);
312
+ if (fs.existsSync(fullVideoPath)) {
313
+ const videoBuffer = fs.readFileSync(fullVideoPath);
314
+ const base64Video = videoBuffer.toString("base64");
315
+ session.send((0, import_koishi.h)("video", { src: `data:video/mp4;base64,${base64Video}` })).catch((error) => {
316
+ throw new Error(`操作失败: ${error.message}`);
317
+ });
318
+ }
319
+ } catch (error) {
320
+ throw new Error(`操作失败: ${error.message}`);
321
+ }
322
+ }
323
+ }
324
+ content += `—— ${cave.contributor_name}`;
325
+ return content;
220
326
  }
221
327
  __name(buildMessage, "buildMessage");
222
- function cleanElementsForSave(elements, keepIndex = false) {
223
- const sorted = elements.sort((a, b) => a.index - b.index);
224
- return sorted.map(({ type, content, file, index }) => ({
225
- type,
226
- ...keepIndex && { index },
227
- ...content && { content },
228
- ...file && { file }
229
- }));
230
- }
231
- __name(cleanElementsForSave, "cleanElementsForSave");
232
- async function apply(ctx, config) {
328
+ async function initCavePaths(ctx) {
233
329
  const dataDir = path.join(ctx.baseDir, "data");
234
330
  const caveDir = path.join(dataDir, "cave");
235
331
  const caveFilePath = path.join(caveDir, "cave.json");
236
- const imageDir = path.join(caveDir, "images");
332
+ const resourceDir = path.join(caveDir, "resources");
237
333
  const pendingFilePath = path.join(caveDir, "pending.json");
238
- try {
239
- await ensureDirectory(dataDir);
240
- await ensureDirectory(caveDir);
241
- await ensureDirectory(imageDir);
242
- await ensureJsonFile(caveFilePath);
243
- await ensureJsonFile(pendingFilePath);
244
- } catch (error) {
245
- logger.error("初始化目录结构失败:", error);
246
- throw error;
247
- }
248
- const lastUsed = /* @__PURE__ */ new Map();
249
- ctx.command("cave", "回声洞").usage("支持添加、抽取、查看、查询回声洞").example("cave 随机抽取回声洞").example("cave -a 内容 添加新回声洞").example("cave -g/r x 查看/删除指定回声洞").example("cave -p/d x/all 通过/拒绝待审回声洞").example("cave -l x 查询投稿者投稿列表").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 }) => {
250
- if ((options.l || options.p || options.d) && !config.manager.includes(session.userId)) {
251
- return "只有管理员才能执行此操作";
252
- }
253
- }).action(async ({ session, options }, ...content) => {
254
- if (options.l !== void 0) {
334
+ await FileHandler.ensureDirectory(dataDir);
335
+ await FileHandler.ensureDirectory(caveDir);
336
+ await FileHandler.ensureDirectory(resourceDir);
337
+ await FileHandler.ensureJsonFile(caveFilePath);
338
+ await FileHandler.ensureJsonFile(pendingFilePath);
339
+ return { dataDir, caveDir, caveFilePath, resourceDir, pendingFilePath };
340
+ }
341
+ __name(initCavePaths, "initCavePaths");
342
+ async function handleCaveAction(ctx, config, session, options, content, lastUsed) {
343
+ const { caveFilePath, resourceDir, pendingFilePath } = await initCavePaths(ctx);
344
+ async function processList() {
345
+ try {
255
346
  let formatIds = function(ids) {
256
347
  const lines = [];
257
348
  for (let i = 0; i < ids.length; i += 10) {
@@ -260,30 +351,27 @@ async function apply(ctx, config) {
260
351
  return lines.join("\n");
261
352
  };
262
353
  __name(formatIds, "formatIds");
263
- const caveFilePath2 = path.join(ctx.baseDir, "data", "cave", "cave.json");
264
- const caveDir2 = path.join(ctx.baseDir, "data", "cave");
265
- const caveData = readJsonData(caveFilePath2);
354
+ const caveData = FileHandler.readJsonData(caveFilePath);
355
+ const caveDir = path.dirname(caveFilePath);
266
356
  const stats = {};
267
357
  for (const cave of caveData) {
268
358
  if (cave.contributor_number === "10000") continue;
269
359
  if (!stats[cave.contributor_number]) stats[cave.contributor_number] = [];
270
360
  stats[cave.contributor_number].push(cave.cave_id);
271
361
  }
272
- const statFilePath = path.join(caveDir2, "stat.json");
362
+ const statFilePath = path.join(caveDir, "stat.json");
273
363
  try {
274
364
  fs.writeFileSync(statFilePath, JSON.stringify(stats, null, 2), "utf8");
275
365
  } catch (error) {
276
- logger.error(`写入投稿统计失败: ${error.message}`);
366
+ throw new Error(`操作失败: ${error.message}`);
277
367
  }
278
368
  let queryId = null;
279
369
  if (typeof options.l === "string") {
280
370
  const match = String(options.l).match(/\d+/);
281
371
  if (match) queryId = match[0];
282
- } else if (!queryId && content.length > 0) {
372
+ } else if (content.length > 0) {
283
373
  const numberMatch = content.join(" ").match(/\d+/);
284
- if (numberMatch) {
285
- queryId = numberMatch[0];
286
- }
374
+ if (numberMatch) queryId = numberMatch[0];
287
375
  }
288
376
  if (queryId) {
289
377
  if (stats[queryId]) {
@@ -300,203 +388,236 @@ async function apply(ctx, config) {
300
388
  return `${cid} 共计投稿 ${ids.length} 项回声洞:
301
389
  ` + formatIds(ids);
302
390
  });
303
- return `共计投稿 ${total} 项回声洞:
391
+ return `==回声洞共计投稿 ${total} 项==
304
392
  ` + lines.join("\n");
305
393
  }
394
+ } catch (error) {
395
+ return `操作失败: ${error.message}`;
306
396
  }
397
+ }
398
+ __name(processList, "processList");
399
+ async function processAudit() {
307
400
  try {
308
- if (options.p || options.d) {
309
- const pendingData = readJsonData(pendingFilePath);
310
- const isApprove = Boolean(options.p);
311
- if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
312
- return await handleAudit(ctx, pendingData, isApprove, caveFilePath, imageDir, pendingFilePath);
313
- }
314
- const id = parseInt(content[0] || (typeof options.p === "string" ? options.p : "") || (typeof options.d === "string" ? options.d : ""));
315
- if (isNaN(id)) return "请输入正确的回声洞序号";
316
- return await handleAudit(ctx, pendingData, isApprove, caveFilePath, imageDir, pendingFilePath, id);
401
+ const pendingData = FileHandler.readJsonData(pendingFilePath);
402
+ const isApprove = Boolean(options.p);
403
+ if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
404
+ return await handleAudit(ctx, pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath);
317
405
  }
318
- const data = readJsonData(
406
+ const id = parseInt(content[0] || (typeof options.p === "string" ? options.p : "") || (typeof options.d === "string" ? options.d : ""));
407
+ if (isNaN(id)) return "请输入正确的回声洞序号";
408
+ return await handleAudit(ctx, pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, id);
409
+ } catch (error) {
410
+ return `操作失败: ${error.message}`;
411
+ }
412
+ }
413
+ __name(processAudit, "processAudit");
414
+ async function processView() {
415
+ try {
416
+ const caveId = parseInt(content[0] || (typeof options.g === "string" ? options.g : ""));
417
+ if (isNaN(caveId)) return "请输入正确的回声洞序号";
418
+ const data = FileHandler.readJsonData(
319
419
  caveFilePath,
320
420
  (item) => item && typeof item.cave_id === "number" && Array.isArray(item.elements) && item.elements.every(
321
- (el) => el.type === "text" && typeof el.content === "string" || el.type === "img" && typeof el.file === "string"
421
+ (el) => el.type === "text" && typeof el.content === "string" || el.type === "img" && typeof el.file === "string" || el.type === "video" && typeof el.file === "string"
322
422
  ) && typeof item.contributor_number === "string" && typeof item.contributor_name === "string"
323
423
  );
324
- if (options.a) {
325
- const originalContent = session.quote?.content || session.content;
326
- const elements = [];
327
- const imageUrls = [];
328
- const prefixes = Array.isArray(session.app.config.prefix) ? session.app.config.prefix : [session.app.config.prefix];
329
- const nicknames = Array.isArray(session.app.config.nickname) ? session.app.config.nickname : session.app.config.nickname ? [session.app.config.nickname] : [];
330
- const allTriggers = [...prefixes, ...nicknames];
331
- const triggerPattern = allTriggers.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
332
- const commandPattern = new RegExp(`^(?:${triggerPattern})?\\s*cave -a\\s*`);
333
- const textParts = originalContent.replace(commandPattern, "").split(/<img[^>]+>/g).map((text) => text.trim()).filter((text) => text).map((text, idx) => ({
334
- type: "text",
335
- content: text,
336
- index: idx * 2
337
- // 文本使用偶数索引
338
- }));
339
- const imgMatches = originalContent.match(/<img[^>]+src="([^"]+)"[^>]*>/g) || [];
340
- const imageElements = imgMatches.map((img, idx) => {
341
- const match = img.match(/src="([^"]+)"/);
342
- if (match?.[1]) {
343
- imageUrls.push(match[1]);
344
- return {
345
- type: "img",
346
- index: idx * 2 + 1
347
- // 图片使用奇数索引
348
- };
349
- }
350
- return null;
351
- }).filter((el) => el !== null);
352
- const pendingData = readJsonData(pendingFilePath);
353
- const maxDataId = data.length > 0 ? Math.max(...data.map((item) => item.cave_id)) : 0;
354
- const maxPendingId = pendingData.length > 0 ? Math.max(...pendingData.map((item) => item.cave_id)) : 0;
355
- const caveId = Math.max(maxDataId, maxPendingId) + 1;
356
- let savedImages = [];
357
- if (imageUrls.length > 0) {
358
- try {
359
- savedImages = await saveImages(imageUrls, imageDir, caveId, config, ctx);
360
- } catch (error) {
361
- logger.error(`保存图片失败: ${error.message}`);
362
- }
363
- }
364
- elements.push(...textParts);
365
- savedImages.forEach((file, idx) => {
366
- if (imageElements[idx]) {
367
- elements.push({
368
- ...imageElements[idx],
369
- type: "img",
370
- file
371
- });
372
- }
373
- });
374
- elements.sort((a, b) => a.index - b.index);
375
- if (elements.length === 0) {
376
- return "添加失败:无内容,请尝试重新发送";
377
- }
378
- let contributorName = session.username;
379
- if (ctx.database) {
380
- try {
381
- const userInfo = await ctx.database.getUser(session.platform, session.userId);
382
- contributorName = userInfo?.nickname || session.username;
383
- } catch (error) {
384
- logger.error(`获取用户昵称失败: ${error.message}`);
424
+ const cave = data.find((item) => item.cave_id === caveId);
425
+ if (!cave) return "未找到该序号的回声洞";
426
+ const caveContent = await buildMessage(cave, resourceDir, session);
427
+ return caveContent;
428
+ } catch (error) {
429
+ return `操作失败: ${error.message}`;
430
+ }
431
+ }
432
+ __name(processView, "processView");
433
+ async function processRandom() {
434
+ const data = FileHandler.readJsonData(
435
+ caveFilePath,
436
+ (item) => item && typeof item.cave_id === "number" && Array.isArray(item.elements) && item.elements.every(
437
+ (el) => el.type === "text" && typeof el.content === "string" || el.type === "img" && typeof el.file === "string"
438
+ ) && typeof item.contributor_number === "string" && typeof item.contributor_name === "string"
439
+ );
440
+ if (data.length === 0) return "暂无回声洞可用";
441
+ const guildId = session.guildId;
442
+ const now = Date.now();
443
+ const lastCall = lastUsed.get(guildId) || 0;
444
+ const isManager = config.manager.includes(session.userId);
445
+ if (!isManager && now - lastCall < config.number * 1e3) {
446
+ const waitTime = Math.ceil((config.number * 1e3 - (now - lastCall)) / 1e3);
447
+ return `群聊冷却中...请${waitTime}秒后再试`;
448
+ }
449
+ if (!isManager) lastUsed.set(guildId, now);
450
+ const cave = (() => {
451
+ const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
452
+ if (!validCaves.length) return void 0;
453
+ const randomIndex = Math.floor(Math.random() * validCaves.length);
454
+ return validCaves[randomIndex];
455
+ })();
456
+ return cave ? buildMessage(cave, resourceDir, session) : "获取回声洞失败";
457
+ }
458
+ __name(processRandom, "processRandom");
459
+ async function processDelete() {
460
+ try {
461
+ const caveId = parseInt(content[0] || (typeof options.r === "string" ? options.r : ""));
462
+ if (isNaN(caveId)) return "请输入正确的回声洞序号";
463
+ const data = FileHandler.readJsonData(
464
+ caveFilePath,
465
+ (item) => item && typeof item.cave_id === "number"
466
+ );
467
+ const pendingData = FileHandler.readJsonData(pendingFilePath);
468
+ const index = data.findIndex((item) => item.cave_id === caveId);
469
+ const pendingIndex = pendingData.findIndex((item) => item.cave_id === caveId);
470
+ if (index === -1 && pendingIndex === -1) return "未找到该序号的回声洞";
471
+ let targetCave;
472
+ let isPending = false;
473
+ if (index !== -1) {
474
+ targetCave = data[index];
475
+ } else {
476
+ targetCave = pendingData[pendingIndex];
477
+ isPending = true;
478
+ }
479
+ if (targetCave.contributor_number !== session.userId && !config.manager.includes(session.userId)) {
480
+ return "不可删除他人添加的回声洞!";
481
+ }
482
+ const caveContent = await buildMessage(targetCave, resourceDir, session);
483
+ if (targetCave.elements) {
484
+ try {
485
+ for (const element of targetCave.elements) {
486
+ if ((element.type === "img" || element.type === "video") && element.file) {
487
+ const fullPath = path.join(resourceDir, element.file);
488
+ if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
489
+ }
385
490
  }
491
+ } catch (error) {
492
+ return `操作失败: ${error.message}`;
386
493
  }
387
- const newCave = {
388
- cave_id: caveId,
389
- elements: cleanElementsForSave(elements, true),
390
- contributor_number: session.userId,
391
- contributor_name: contributorName
392
- };
393
- if (config.enableAudit) {
394
- pendingData.push({
395
- ...newCave,
396
- elements: cleanElementsForSave(elements, true)
397
- });
398
- writeJsonData(pendingFilePath, pendingData);
399
- await sendAuditMessage(ctx, config, newCave, buildMessage(newCave, imageDir));
400
- return `✨ 已提交审核,序号为 (${caveId})`;
401
- }
402
- const caveWithoutIndex = {
403
- ...newCave,
404
- elements: cleanElementsForSave(elements, false)
405
- };
406
- data.push(caveWithoutIndex);
407
- writeJsonData(caveFilePath, data);
408
- return `✨ 添加成功!序号为 (${caveId})`;
409
494
  }
410
- if (options.g) {
411
- const caveId = parseInt(content[0] || (typeof options.g === "string" ? options.g : ""));
412
- if (isNaN(caveId)) {
413
- return "请输入正确的回声洞序号";
414
- }
415
- const cave = data.find((item) => item.cave_id === caveId);
416
- if (!cave) {
417
- return "未找到该序号的回声洞";
418
- }
419
- return buildMessage(cave, imageDir);
495
+ if (isPending) {
496
+ pendingData.splice(pendingIndex, 1);
497
+ FileHandler.writeJsonData(pendingFilePath, pendingData);
498
+ return `已删除(待审核)
499
+ ${caveContent}`;
500
+ } else {
501
+ data.splice(index, 1);
502
+ FileHandler.writeJsonData(caveFilePath, data);
503
+ return `已删除
504
+ ${caveContent}`;
420
505
  }
421
- if (!options.a && !options.g && !options.r) {
422
- if (data.length === 0) return "暂无回声洞可用";
423
- const guildId = session.guildId;
424
- const now = Date.now();
425
- const lastCall = lastUsed.get(guildId) || 0;
426
- const isManager = config.manager.includes(session.userId);
427
- if (!isManager && now - lastCall < config.number * 1e3) {
428
- const waitTime = Math.ceil((config.number * 1e3 - (now - lastCall)) / 1e3);
429
- return `群聊冷却中...请${waitTime}秒后再试`;
430
- }
431
- if (!isManager) {
432
- lastUsed.set(guildId, now);
506
+ } catch (error) {
507
+ return `操作失败: ${error.message}`;
508
+ }
509
+ }
510
+ __name(processDelete, "processDelete");
511
+ async function processAdd() {
512
+ try {
513
+ let originalContent = session.quote?.content || session.content;
514
+ const prefixes = Array.isArray(session.app.config.prefix) ? session.app.config.prefix : [session.app.config.prefix];
515
+ const nicknames = Array.isArray(session.app.config.nickname) ? session.app.config.nickname : session.app.config.nickname ? [session.app.config.nickname] : [];
516
+ const allTriggers = [...prefixes, ...nicknames];
517
+ const triggerPattern = allTriggers.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
518
+ const commandPattern = new RegExp(`^(?:${triggerPattern}).*?-a\\s*`);
519
+ originalContent = originalContent.replace(commandPattern, "");
520
+ let { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(originalContent);
521
+ if (textParts.length === 0 && imageUrls.length === 0 && videoUrls.length === 0) {
522
+ await session.send("请在一分钟内发送你要添加的内容");
523
+ const reply = await session.prompt({ timeout: 6e4 });
524
+ if (!reply || reply.trim() === "") {
525
+ return "操作超时,放弃本次添加";
433
526
  }
434
- const cave = (() => {
435
- const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
436
- if (!validCaves.length) return void 0;
437
- const randomIndex = Math.floor(Math.random() * validCaves.length);
438
- return validCaves[randomIndex];
439
- })();
440
- if (!cave) return "获取回声洞失败";
441
- return buildMessage(cave, imageDir);
527
+ const replyResult = await extractMediaContent(reply);
528
+ imageUrls = replyResult.imageUrls;
529
+ imageElements = replyResult.imageElements;
530
+ videoUrls = replyResult.videoUrls;
531
+ videoElements = replyResult.videoElements;
532
+ textParts = replyResult.textParts;
442
533
  }
443
- if (options.r) {
444
- const caveId = parseInt(content[0] || (typeof options.r === "string" ? options.r : ""));
445
- if (isNaN(caveId)) {
446
- return "请输入正确的回声洞序号";
447
- }
448
- const index = data.findIndex((item) => item.cave_id === caveId);
449
- const pendingData = readJsonData(pendingFilePath);
450
- const pendingIndex = pendingData.findIndex((item) => item.cave_id === caveId);
451
- if (index === -1 && pendingIndex === -1) {
452
- return "未找到该序号的回声洞";
534
+ if (videoUrls.length > 0 && !config.allowVideo) {
535
+ return "已关闭上传视频功能";
536
+ }
537
+ const pendingData = FileHandler.readJsonData(pendingFilePath);
538
+ const data = FileHandler.readJsonData(caveFilePath, (item) => item && typeof item.cave_id === "number");
539
+ const maxDataId = data.length > 0 ? Math.max(...data.map((item) => item.cave_id)) : 0;
540
+ const maxPendingId = pendingData.length > 0 ? Math.max(...pendingData.map((item) => item.cave_id)) : 0;
541
+ const caveId = Math.max(maxDataId, maxPendingId) + 1;
542
+ let savedImages = [];
543
+ if (imageUrls.length > 0) {
544
+ try {
545
+ const fileSuggestions = imageElements.map((el) => el.fileAttr);
546
+ savedImages = await saveMedia(imageUrls, fileSuggestions, resourceDir, caveId, config, ctx, "img");
547
+ } catch (error) {
548
+ return `操作失败: ${error.message}`;
453
549
  }
454
- let targetCave;
455
- let isPending = false;
456
- if (index !== -1) {
457
- targetCave = data[index];
458
- } else {
459
- targetCave = pendingData[pendingIndex];
460
- isPending = true;
550
+ }
551
+ let savedVideos = [];
552
+ if (videoUrls.length > 0) {
553
+ try {
554
+ const fileSuggestions = videoElements.map((el) => el.fileAttr);
555
+ savedVideos = await saveMedia(videoUrls, fileSuggestions, resourceDir, caveId, config, ctx, "video");
556
+ } catch (error) {
557
+ return `操作失败: ${error.message}`;
461
558
  }
462
- if (targetCave.contributor_number !== session.userId && !config.manager.includes(session.userId)) {
463
- return "你不是这条回声洞的添加者!";
559
+ }
560
+ const elements = [];
561
+ elements.push(...textParts);
562
+ savedImages.forEach((file, idx) => {
563
+ if (imageElements[idx]) {
564
+ elements.push({ ...imageElements[idx], type: "img", file });
464
565
  }
465
- if (targetCave.elements) {
466
- try {
467
- for (const element of targetCave.elements) {
468
- if (element.type === "img" && element.file) {
469
- const fullPath = path.join(imageDir, element.file);
470
- if (fs.existsSync(fullPath)) {
471
- fs.unlinkSync(fullPath);
472
- }
473
- }
474
- }
475
- } catch (error) {
476
- logger.error(`删除图片失败: ${error.message}`);
477
- }
566
+ });
567
+ savedVideos.forEach((file, idx) => {
568
+ if (videoElements[idx]) {
569
+ elements.push({ ...videoElements[idx], type: "video", file });
478
570
  }
479
- if (isPending) {
480
- pendingData.splice(pendingIndex, 1);
481
- writeJsonData(pendingFilePath, pendingData);
482
- return `✅ 已删除待审核回声洞 (${caveId})`;
483
- } else {
484
- data.splice(index, 1);
485
- writeJsonData(caveFilePath, data);
486
- return `✅ 已删除回声洞 (${caveId})`;
571
+ });
572
+ elements.sort((a, b) => a.index - b.index);
573
+ let contributorName = session.username;
574
+ if (ctx.database) {
575
+ try {
576
+ const userInfo = await ctx.database.getUser(session.platform, session.userId);
577
+ contributorName = userInfo?.nickname || session.username;
578
+ } catch (error) {
579
+ throw new Error(`操作失败: ${error.message}`);
487
580
  }
488
581
  }
582
+ const newCave = {
583
+ cave_id: caveId,
584
+ elements: cleanElementsForSave(elements, true),
585
+ contributor_number: session.userId,
586
+ contributor_name: contributorName
587
+ };
588
+ if (config.enableAudit) {
589
+ pendingData.push({ ...newCave, elements: cleanElementsForSave(elements, true) });
590
+ FileHandler.writeJsonData(pendingFilePath, pendingData);
591
+ await sendAuditMessage(ctx, config, newCave, await buildMessage(newCave, resourceDir, session));
592
+ return `已提交审核,序号为 (${caveId})`;
593
+ }
594
+ const caveWithoutIndex = { ...newCave, elements: cleanElementsForSave(elements, false) };
595
+ data.push(caveWithoutIndex);
596
+ FileHandler.writeJsonData(caveFilePath, data);
597
+ return `添加成功!序号为 (${caveId})`;
489
598
  } catch (error) {
490
- logger.error(`操作失败: ${error.message}`);
491
- return "操作失败,请稍后重试";
599
+ return `操作失败: ${error.message}`;
492
600
  }
493
- });
601
+ }
602
+ __name(processAdd, "processAdd");
603
+ try {
604
+ if (options.l !== void 0) return await processList();
605
+ if (options.p || options.d) return await processAudit();
606
+ if (options.g) return await processView();
607
+ if (options.r) return await processDelete();
608
+ if (options.a) return await processAdd();
609
+ return await processRandom();
610
+ } catch (error) {
611
+ return `操作失败: ${error.message.replace(/^操作失败: /, "")}`;
612
+ }
494
613
  }
495
- __name(apply, "apply");
614
+ __name(handleCaveAction, "handleCaveAction");
496
615
  // Annotate the CommonJS export names for ESM import in node:
497
616
  0 && (module.exports = {
498
617
  Config,
499
618
  apply,
619
+ handleCaveAction,
620
+ initCavePaths,
500
621
  inject,
501
622
  name
502
623
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "最好的 cave 插件,可开关的审核系统,可引用添加,支持图文混合内容,可查阅投稿列表,完美复刻你的 .cave 体验!",
4
- "version": "0.0.1",
4
+ "version": "1.0.1",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
package/readme.md CHANGED
@@ -6,24 +6,25 @@
6
6
 
7
7
  最好的 cave 插件,可开关的审核系统,可引用添加,支持图文混合内容,可查阅投稿列表,完美复刻你的 .cave 体验!
8
8
 
9
- ## 核心功能
9
+ ### 核心功能
10
10
 
11
11
  - 支持文字与图片混合保存
12
12
  - 智能处理各类图片链接
13
+ - 可选的视频内容支持
13
14
  - 完整的权限管理系统
14
15
  - 可选的内容审核流程
15
16
  - 群组调用冷却机制
16
17
 
17
- ## 基础指令
18
+ ### 指令
18
19
 
19
20
  | 指令 | 说明 | 权限 |
20
21
  |------|------|------|
21
22
  | `cave` | 随机展示一条回声洞 | 所有人 |
22
- | `cave -a <内容>` | 添加新回声洞 | 所有人 |
23
+ | `cave -a <内容>` | 添加新回声洞(支持文字、图片与视频) | 所有人 |
23
24
  | `cave -g <编号>` | 查看指定回声洞 | 所有人 |
24
25
  | `cave -r <编号>` | 删除指定回声洞 | 内容贡献者/管理员 |
25
26
 
26
- ## 管理指令
27
+ #### 管理指令
27
28
 
28
29
  | 指令 | 说明 | 权限 |
29
30
  |------|------|------|
@@ -31,9 +32,10 @@
31
32
  | `cave -p <编号/all>` | 通过待审核内容 | 管理员 |
32
33
  | `cave -d <编号/all>` | 拒绝待审核内容 | 管理员 |
33
34
 
34
- ## 注意事项
35
+ ### 注意事项
35
36
 
36
- 1. 图片会自动保存到本地,请确保存储空间充足
37
- 2. 管理员不受群组冷却时间限制
38
- 3. 开启审核模式后,新内容需要审核才能生效
39
- 4. 引用消息添加支持保留原消息格式
37
+ 1. 图片以及视频会自动保存到本地,请确保存储空间充足。
38
+ 2. 管理员不受群组冷却时间限制。
39
+ 3. 开启审核模式后,新内容需要审核才能生效。
40
+ 4. 引用消息添加支持保留原消息格式。
41
+ 5. 配置文件中可分别调整是否允许添加视频,以及各自的文件大小限制。