UNPKG

koishi-plugin-message-counter

Version:

Koishi 的消息数量统计插件。生成各种发言排行榜。

1,303 lines (1,270 loc) 103 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var import_path = __toESM(require("path")); var fs = __toESM(require("fs/promises")); var import_fs = require("fs"); var crypto = __toESM(require("crypto")); var assetsDir = import_path.default.resolve(__dirname, "..", "assets"); var fallbackBase64 = [""]; var name = "message-counter"; var inject = { required: ["database", "cron"], optional: ["markdownToImage", "puppeteer", "canvas"] }; var usage = `## 📝 注意事项 - 只统计群聊消息 - 初始化需权限等级 3 - 依赖 database 与 cron 服务 - 生成图片时,需 puppeteer 提供 canvas 支持 ## 🔍 关键指令 ### \`messageCounter.查询 [指定用户]\` 查询指定用户的发言次数信息(次数[排名])。若不带任何选项,则显示所有时段的数据。 **选项:** | 参数 | 说明 | |------|------| | \`-d, --day\` | 今日发言次数[排名] | | \`--yd, --yesterday\` | 昨日发言次数[排名] | | \`-w, --week\` | 本周发言次数[排名] | | \`-m, --month\` | 本月发言次数[排名] | | \`-y, --year\` | 今年发言次数[排名] | | \`-t, --total\` | 总发言次数[排名] | | \`--dag\` | 跨群今日发言次数[排名] | | \`--ydag\` | 跨群昨日发言次数[排名] | | \`--wag\` | 跨群本周发言次数[排名] | | \`--mag\` | 跨群本月发言次数[排名] | | \`--yag\` | 跨群本年发言次数[排名] | | \`-a, --across\` | 跨群总发言次数[排名] | ### \`messageCounter.排行榜 [显示的人数]\` 发言排行榜。默认为今日发言榜。 **选项:** | 参数 | 说明 | |------|------| | \`--yd, --yesterday\` | 昨日发言排行榜 | | \`-w\` | 本周发言排行榜 | | \`-m\` | 本月发言排行榜 | | \`-y\` | 今年发言排行榜 | | \`-t\` | 总发言排行榜 | | \`--dag\` | 跨群今日发言排行榜 | | \`--ydag\` | 跨群昨日发言排行榜 | | \`--wag\` | 跨群本周发言排行榜 | | \`--mag\` | 跨群本月发言排行榜 | | \`--yag\` | 跨群今年发言排行榜 | | \`--dragon\` | 跨群总发言排行榜(圣龙王榜) | | \`--whites\` | 白名单,只显示白名单用户 | | \`--blacks\` | 黑名单,不显示黑名单用户 | ### \`messageCounter.群排行榜 [number:number]\` 各群聊的发言排行榜。默认为今日发言榜。 **选项:** | 参数 | 说明 | |------|------| | \`--yd, --yesterday\` | 昨日发言排行榜 | | \`-w, -m, -y, -t\` | 本周/本月/今年/总发言排行榜 | | \`-s\` | 指定用户的群发言排行榜 | | \`--whites\` | 白名单,只显示白名单群 | | \`--blacks\` | 黑名单,不显示黑名单群 | ### \`messageCounter.上传柱状条背景\` - 为自己上传一张自定义的水平柱状条背景图片 - 新图片会覆盖旧的图片。若上传失败,旧图片也会被删除 - 使用此指令时需附带图片 ### \`messageCounter.重载资源\` - 实时重载用户图标、柱状条背景和字体文件,使其更改即时生效(需要权限等级 2) ### \`messageCounter.清理缓存\` - 清理过期的头像缓存文件,以释放磁盘空间(需要权限等级 3) - 用户更换头像后,旧的头像缓存会变成“孤儿缓存”。此指令可以安全地移除它们。 ## 🎨 自定义水平柱状图样式 - 重载插件或使用 \`messageCounter.重载资源\` 指令可使新增的文件立即生效。 ### 1. 用户图标 - 在 \`data/messageCounter/icons\` 文件夹下添加用户图标 - 文件名格式为 \`用户ID.png\`(例:\`1234567890.png\`) - 支持多图标,文件名格式为 \`用户ID-1.png\`, \`用户ID-2.png\` ### 2. 柱状条背景 - **推荐方式**:使用 \`messageCounter.上传柱状条背景\` 指令 - **手动方式**:在 \`data/messageCounter/barBgImgs\` 文件夹下添加背景图片 - 支持多背景(随机选用),文件名格式为 \`用户ID-1.png\` 等 - 建议尺寸 850x50 像素,文件名 \`用户ID.png\` ### 3. 自定义字体 - 插件启动时,会自动将内置字体 \`HarmonyOS_Sans_Medium.ttf\` 拷贝到 \`data/messageCounter/fonts/\` 目录下。 - 您可以将自己喜爱的字体文件放入此文件夹,并在配置项的“字体设置”中填入该字体的文件名称(不带后缀)。 --- ## 💬 QQ 群 - 956758505`; var logger = new import_koishi.Logger("messageCounter"); var FONT_OPTIONS = { TITLE: "HarmonyOS_Sans_Medium", NICKNAME: "HarmonyOS_Sans_Medium" }; var Config = import_koishi.Schema.intersect([ // --- 核心功能 --- import_koishi.Schema.object({ isBotMessageTrackingEnabled: import_koishi.Schema.boolean().default(false).description("是否统计 Bot 自己发送的消息。") }).description("核心功能"), // --- 排行榜设置 --- import_koishi.Schema.object({ defaultMaxDisplayCount: import_koishi.Schema.number().min(0).default(20).description("排行榜默认显示的人数。设置为 0 则显示所有。"), isTimeInfoSupplementEnabled: import_koishi.Schema.boolean().default(true).description("是否在排行榜标题中显示生成时间。"), isUserMessagePercentageVisible: import_koishi.Schema.boolean().default(true).description("是否在排行榜中显示用户的消息数占比。"), hiddenUserIdsInLeaderboard: import_koishi.Schema.array(String).role("table").description("全局隐藏的用户 ID 列表,在所有用户排行榜中生效。"), hiddenChannelIdsInLeaderboard: import_koishi.Schema.array(String).role("table").description("全局隐藏的频道 ID 列表,在群排行榜中生效。") }).description("排行榜设置"), // --- 图片生成 --- import_koishi.Schema.intersect([ import_koishi.Schema.object({ isTextToImageConversionEnabled: import_koishi.Schema.boolean().default(false).description( "是否将文本排行榜转为 Markdown 图片(依赖 `markdownToImage` 服务)。" ), isLeaderboardToHorizontalBarChartConversionEnabled: import_koishi.Schema.boolean().default(false).description("是否将排行榜渲染为水平柱状图(依赖 `puppeteer` 服务)。") }).description("图片生成"), // 仅在开启柱状图功能时显示以下详细选项 import_koishi.Schema.union([ import_koishi.Schema.intersect([ import_koishi.Schema.object({ isLeaderboardToHorizontalBarChartConversionEnabled: import_koishi.Schema.const(true).required(), imageType: import_koishi.Schema.union(["png", "jpeg", "webp"]).default("png").description(`生成的柱状图图片格式。`) }).description("柱状图基础设置"), import_koishi.Schema.object({ chartViewportWidth: import_koishi.Schema.number().min(1).default(1080).description( "渲染页面的视口宽度(像素)。此值会影响图片清晰度及背景图的展示。" ), deviceScaleFactor: import_koishi.Schema.number().min(0.1).max(4).step(0.1).default(1).description( "设备像素比 (DPR)。更高的值可生成更清晰的图片(如 2 倍图),但会增加文件体积。" ), waitUntil: import_koishi.Schema.union([ "load", "domcontentloaded", "networkidle0", "networkidle2" ]).default("networkidle0").description( "页面加载等待策略,影响图片生成速度和稳定性。`networkidle0` 最稳定。" ) }).description("渲染与性能"), import_koishi.Schema.object({ avatarCacheTTL: import_koishi.Schema.number().default(86400).description( "头像缓存有效期(秒)。设置为 0 则永不刷新。过短的有效期会增加网络请求。" ), avatarFailureCacheTTL: import_koishi.Schema.number().default(300).description( "头像获取失败后的重试间隔(秒)。期间将使用默认头像,避免频繁请求无效链接。" ) }).description("缓存设置"), import_koishi.Schema.object({ shouldMoveIconToBarEndLeft: import_koishi.Schema.boolean().default(true).description( "是否将自定义图标显示在进度条的末端。关闭则显示在用户名旁。" ), showStarInChart: import_koishi.Schema.boolean().default(true).description( "是否在图表中对触发指令的用户/群聊名称前添加 ★ 以高亮显示。" ), horizontalBarBackgroundOpacity: import_koishi.Schema.number().min(0).max(1).step(0.05).default(0.6).description("自定义背景图在进度条区域的不透明度。"), horizontalBarBackgroundFullOpacity: import_koishi.Schema.number().min(0).max(1).step(0.05).default(0).description("自定义背景图在整行背景区域的不透明度。") }).description("样式定制"), import_koishi.Schema.object({ maxBarBgWidth: import_koishi.Schema.number().min(0).default(850).description("允许上传的背景图最大宽度(像素)。0为不限制。"), maxBarBgHeight: import_koishi.Schema.number().min(0).default(50).description("允许上传的背景图最大高度(像素)。0为不限制。"), maxBarBgSize: import_koishi.Schema.number().min(0).default(5).description("允许上传的背景图最大体积(MB)。0为不限制。") }).description("上传限制"), // 背景设置(条件化) import_koishi.Schema.intersect([ import_koishi.Schema.object({ backgroundType: import_koishi.Schema.union([ import_koishi.Schema.const("none").description("默认渐变"), import_koishi.Schema.const("api").description("API 获取"), import_koishi.Schema.const("css").description("自定义 CSS") ]).default("none").description("图片整体背景类型。") }), import_koishi.Schema.union([ import_koishi.Schema.object({ backgroundType: import_koishi.Schema.const("api").required(), apiBackgroundConfig: import_koishi.Schema.object({ apiUrl: import_koishi.Schema.string().description("获取背景图的 API 地址。").required(), apiKey: import_koishi.Schema.string().role("secret").description("API 的访问凭证(可选)。"), responseType: import_koishi.Schema.union(["binary", "url", "base64"]).default("binary").description("API 返回的数据类型。") }).description("API 背景配置").collapse() }), import_koishi.Schema.object({ backgroundType: import_koishi.Schema.const("css").required(), backgroundValue: import_koishi.Schema.string().role("textarea", { rows: [2, 4] }).default( `html { background: linear-gradient(135deg, #f6f8f9 0%, #e5ebee 100%); }` ).description( "自定义背景的 CSS 代码。建议使用 `html` 选择器来设置背景。" ) }), import_koishi.Schema.object({}) ]) ]).description("背景设置"), import_koishi.Schema.object({ chartTitleFont: import_koishi.Schema.string().default(FONT_OPTIONS.TITLE).description( `标题字体。填写 'data/messageCounter/fonts' 目录中的字体文件名(不含后缀)。` ), chartNicknameFont: import_koishi.Schema.string().default(FONT_OPTIONS.NICKNAME).description( `昵称与计数字体。填写 'data/messageCounter/fonts' 目录中的字体文件名(不含后缀),或使用通用字体名称。` ) }).description("字体设置") ]), import_koishi.Schema.object({}) ]) ]), // --- 自动推送 --- import_koishi.Schema.intersect([ import_koishi.Schema.object({ autoPush: import_koishi.Schema.boolean().default(false).description("是否启用定时自动推送排行榜功能。") }).description("自动推送"), import_koishi.Schema.union([ import_koishi.Schema.intersect([ import_koishi.Schema.object({ autoPush: import_koishi.Schema.const(true).required(), shouldSendDailyLeaderboardAtMidnight: import_koishi.Schema.boolean().default(true).description("每日 0 点自动发送昨日排行榜。"), shouldSendWeeklyLeaderboard: import_koishi.Schema.boolean().default(false).description("每周一 0 点自动发送上周排行榜。"), shouldSendMonthlyLeaderboard: import_koishi.Schema.boolean().default(false).description("每月一日 0 点自动发送上月排行榜。"), shouldSendYearlyLeaderboard: import_koishi.Schema.boolean().default(false).description("每年一日 0 点自动发送去年排行榜。"), dailyScheduledTimers: import_koishi.Schema.array(String).role("table").description( "其他定时发送今日排行榜的时间点(24小时制,如 `08:00`)。" ) }).description("推送时机"), import_koishi.Schema.object({ pushChannelIds: import_koishi.Schema.array(String).role("table").description("需要接收自动推送的频道 ID 列表。"), shouldSendLeaderboardNotificationsToAllChannels: import_koishi.Schema.boolean().default(false).description( "是否向机器人所在的所有群聊推送(可能造成打扰,请谨慎开启)。" ), excludedLeaderboardChannels: import_koishi.Schema.array(String).role("table").description( "当“向所有群聊推送”开启时,此处指定的频道将不会收到推送。" ) }).description("推送目标"), import_koishi.Schema.object({ isGeneratingRankingListPromptVisible: import_koishi.Schema.boolean().default(true).description("发送排行榜前,是否先发送一条“正在生成”的提示消息。"), leaderboardGenerationWaitTime: import_koishi.Schema.number().min(0).default(3).description("发送提示消息后,等待多少秒再发送排行榜图片。"), delayBetweenGroupPushesInSeconds: import_koishi.Schema.number().min(0).default(5).description( "批量推送时,每个群之间的基础发送延迟(秒),以防风控。" ), groupPushDelayRandomizationSeconds: import_koishi.Schema.number().min(0).default(10).description( "在基础延迟之上,增加一个随机波动范围(秒),以模拟人工操作。" ) }).description("推送行为") ]), import_koishi.Schema.object({}) ]) ]), // --- 龙王禁言 --- import_koishi.Schema.intersect([ import_koishi.Schema.object({ enableMostActiveUserMuting: import_koishi.Schema.boolean().default(false).description("是否在每日 0 点自动禁言昨日发言最多的人(“抓龙王”)。") }).description("龙王禁言"), import_koishi.Schema.union([ import_koishi.Schema.object({ enableMostActiveUserMuting: import_koishi.Schema.const(true).required(), dragonKingDetainmentTime: import_koishi.Schema.number().min(0).default(5).description("0 点后,等待多少秒再执行禁言操作。"), detentionDuration: import_koishi.Schema.number().min(1).default(1).description("禁言时长(天)。"), muteChannelIds: import_koishi.Schema.array(String).role("table").description("在哪些频道中执行“抓龙王”操作。") }), import_koishi.Schema.object({}) ]) ]) ]); var periodMapping = { today: { field: "todayPostCount", name: "今日" }, yesterday: { field: "yesterdayPostCount", name: "昨日" }, week: { field: "thisWeekPostCount", name: "本周" }, month: { field: "thisMonthPostCount", name: "本月" }, year: { field: "thisYearPostCount", name: "今年" }, total: { field: "totalPostCount", name: "总" } }; async function apply(ctx, config) { const PROCESSED = Symbol("message-counter.processed"); const dataRoot = import_path.default.join(ctx.baseDir, "data"); const messageCounterRoot = import_path.default.join(dataRoot, "messageCounter"); const iconsPath = import_path.default.join(messageCounterRoot, "icons"); const barBgImgsPath = import_path.default.join(messageCounterRoot, "barBgImgs"); const fontsPath = import_path.default.join(messageCounterRoot, "fonts"); const avatarsPath = import_path.default.join(messageCounterRoot, "avatars"); const emptyHtmlPath = import_path.default.join(messageCounterRoot, "emptyHtml.html").replace(/\\/g, "/"); const oldIconsPath = import_path.default.join(dataRoot, "messageCounterIcons"); const oldBarBgImgsPath = import_path.default.join(dataRoot, "messageCounterBarBgImgs"); await fs.mkdir(fontsPath, { recursive: true }); await fs.mkdir(iconsPath, { recursive: true }); await fs.mkdir(barBgImgsPath, { recursive: true }); await fs.mkdir(avatarsPath, { recursive: true }); await migrateFolder(oldIconsPath, iconsPath); await migrateFolder(oldBarBgImgsPath, barBgImgsPath); try { await fs.access(emptyHtmlPath, import_fs.constants.F_OK); } catch { await fs.writeFile(emptyHtmlPath, ""); logger.info(`已创建空的渲染模板文件: emptyHtml.html`); } try { const fallbackJson = await fs.readFile( import_path.default.join(assetsDir, "fallbackBase64.json"), "utf-8" ); fallbackBase64 = JSON.parse(fallbackJson); } catch (error) { logger.warn("无法加载 fallbackBase64.json,将使用空的回退头像。", error); } const fontFiles = ["HarmonyOS_Sans_Medium.ttf"]; for (const fontFile of fontFiles) { await copyAssetIfNotExists( import_path.default.join(assetsDir, "fonts"), fontsPath, fontFile ); } const avatarCache = /* @__PURE__ */ new Map(); let iconCache = []; let barBgImgCache = []; let fontFilesCache = []; ctx.model.extend( "message_counter_records", { // id: "unsigned", channelId: "string", channelName: "string", userId: "string", username: "string", userAvatar: "string", todayPostCount: "unsigned", thisWeekPostCount: "unsigned", thisMonthPostCount: "unsigned", thisYearPostCount: "unsigned", totalPostCount: "unsigned", yesterdayPostCount: "unsigned" }, { primary: ["channelId", "userId"] } ); ctx.model.extend( "message_counter_state", { key: "string", value: "timestamp" }, { primary: "key" } ); const channelCtx = ctx.channel(); ctx.on("ready", async () => { await reloadIconCache(); await reloadBarBgImgCache(); await reloadFontCache(); await initializeResetStates(); await checkForMissedResets(); if (config.autoPush) { if (config.shouldSendDailyLeaderboardAtMidnight) { const task = ctx.cron( "1 0 * * *", () => generateAndPushLeaderboard("yesterday") ); scheduledTasks.push(task); logger.info("[自动推送] 已设置每日 00:01 推送昨日排行榜的任务。"); } (config.dailyScheduledTimers || []).forEach((time) => { const match = /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9])$/.exec(time); if (match) { const [_, hour, minute] = match; const cron = `${minute} ${hour} * * *`; const task = ctx.cron( cron, () => generateAndPushLeaderboard("today") ); scheduledTasks.push(task); logger.info(`[自动推送] 已设置每日 ${time} 推送今日排行榜的任务。`); } else { logger.warn( `[自动推送] 无效的时间格式: "${time}",已跳过。请使用 "HH:mm" 格式。` ); } }); } if (config.enableMostActiveUserMuting) { const task = ctx.cron("1 0 * * *", () => performDragonKingMuting()); scheduledTasks.push(task); logger.info("[抓龙王] 已设置每日 00:01 执行的禁言任务。"); } const resetTask = ctx.cron("0 0 * * *", async () => { const now = /* @__PURE__ */ new Date(); const dayOfMonth = now.getDate(); const month = now.getMonth(); const dayOfWeek = now.getDay(); if (config.autoPush && config.shouldSendYearlyLeaderboard && dayOfMonth === 1 && month === 0) { await generateAndPushLeaderboard("year"); } if (config.autoPush && config.shouldSendMonthlyLeaderboard && dayOfMonth === 1) { await generateAndPushLeaderboard("month"); } if (config.autoPush && config.shouldSendWeeklyLeaderboard && dayOfWeek === 1) { await generateAndPushLeaderboard("week"); } await resetCounter("todayPostCount", "今日发言榜已成功置空!", "daily"); if (dayOfWeek === 1) { await resetCounter( "thisWeekPostCount", "本周发言榜已成功置空!", "weekly" ); } if (dayOfMonth === 1) { await resetCounter( "thisMonthPostCount", "本月发言榜已成功置空!", "monthly" ); } if (dayOfMonth === 1 && month === 0) { await resetCounter( "thisYearPostCount", "今年发言榜已成功置空!", "yearly" ); } }); scheduledTasks.push(resetTask); logger.info("已设置统一的推送与数据重置任务(每日、周、月、年)。"); }); ctx.on("dispose", () => { scheduledTasks.forEach((task) => task()); avatarCache.clear(); iconCache = []; barBgImgCache = []; fontFilesCache = []; logger.info("所有已安排的任务和缓存都已清除。"); }); channelCtx.middleware(async (session, next) => { if (session[PROCESSED]) return next(); if (!session.userId || !session.channelId || session.author?.isBot && !config.isBotMessageTrackingEnabled) { return next(); } session[PROCESSED] = true; const { userId, channelId, author } = session; let sessionChannelName = session.event.channel.name; const username = author?.nick || author?.name || userId; const userAvatar = author?.avatar; try { const channelName = sessionChannelName || (channelId ? await getChannelName(session.bot, channelId) : channelId); await ctx.database.upsert( "message_counter_records", (row) => [ { channelId, userId, username, userAvatar: userAvatar || row.userAvatar, channelName: channelName || row.channelName, todayPostCount: import_koishi.$.add(row.todayPostCount, 1), thisWeekPostCount: import_koishi.$.add(row.thisWeekPostCount, 1), thisMonthPostCount: import_koishi.$.add(row.thisMonthPostCount, 1), thisYearPostCount: import_koishi.$.add(row.thisYearPostCount, 1), totalPostCount: import_koishi.$.add(row.totalPostCount, 1) } ], ["channelId", "userId"] ); } catch (error) { logger.error( "Failed to update message count for user %s in channel %s:", userId, channelId, error ); } return next(); }, true); if (config.isBotMessageTrackingEnabled) { ctx.before("send", async (session) => { if (!session.channelId) return; const { channelId, bot } = session; let sessionChannelName = session.event.channel.name; const botUser = bot.user; if (!botUser) { logger.warn("Bot user is undefined, skipping bot message tracking."); return; } try { const channelName = sessionChannelName || await getChannelName(bot, channelId) || channelId; await ctx.database.upsert( "message_counter_records", (row) => [ { channelId, userId: botUser.id, username: botUser.name, userAvatar: botUser.avatar, channelName: channelName || row.channelName, todayPostCount: import_koishi.$.add(row.todayPostCount, 1), thisWeekPostCount: import_koishi.$.add(row.thisWeekPostCount, 1), thisMonthPostCount: import_koishi.$.add(row.thisMonthPostCount, 1), thisYearPostCount: import_koishi.$.add(row.thisYearPostCount, 1), totalPostCount: import_koishi.$.add(row.totalPostCount, 1) } ], ["channelId", "userId"] ); } catch (error) { logger.error( "Failed to update bot message count in channel %s:", channelId, error ); } }); } ctx.command("messageCounter", "查看messageCounter帮助").action(({ session }) => session?.execute(`help messageCounter`)); ctx.command("messageCounter.初始化", "初始化", { authority: 3 }).action(async ({ session }) => { if (!session) return; await session.send("正在清空所有发言记录,请稍候..."); await ctx.database.remove("message_counter_records", {}); await session.send("所有发言记录已清空!"); }); ctx.command( "messageCounter.查询 [targetUser:text]", "查询指定用户的发言次数信息" ).userFields(["id", "name"]).option("yesterday", "--yd 昨日发言").option("day", "-d 今日发言").option("week", "-w 本周发言").option("month", "-m 本月发言").option("year", "-y 今年发言").option("total", "-t 总发言").option("ydag", "跨群昨日发言").option("dag", "跨群今日发言").option("wag", "跨群本周发言").option("mag", "跨群本月发言").option("yag", "跨群本年发言").option("across", "-a 跨群总发言").action(async ({ session, options }, targetUser) => { const optionKeys = [ "day", "week", "month", "year", "total", "yesterday", "dag", "wag", "mag", "yag", "ydag", "across" ]; const selectedOptions = {}; let noOptionSelected = true; for (const key of optionKeys) { if (options[key]) { selectedOptions[key] = true; noOptionSelected = false; } } if (noOptionSelected) { for (const key of optionKeys) { selectedOptions[key] = true; } } let channelId = session?.channelId; let userId = session?.userId; let targetUserRecord = []; if (targetUser) { if (session) targetUser = await replaceAtTags(session, targetUser); const match = targetUser.match(/<at id="([^"]+)"/); if (match) userId = match[1]; } targetUserRecord = await ctx.database.get("message_counter_records", { channelId, userId }); if (targetUserRecord.length === 0) return `被查询对象无任何发言记录。`; const channelUsers = await ctx.database.get("message_counter_records", { channelId }); const allUsers = await ctx.database.get("message_counter_records", {}); const channelStats = []; const acrossStats = []; const accumulate = /* @__PURE__ */ __name((records) => records.reduce( (sums, user) => { for (const key in periodMapping) { sums[periodMapping[key].field] = (sums[periodMapping[key].field] || 0) + user[periodMapping[key].field]; } return sums; }, {} ), "accumulate"); const channelTotals = accumulate(channelUsers); const acrossTotals = accumulate(allUsers); const getRank = /* @__PURE__ */ __name((records, field, uid) => { const sorted = [...records].sort((a, b) => b[field] - a[field]); const index = sorted.findIndex((u) => u.userId === uid); return index !== -1 ? index + 1 : null; }, "getRank"); const getAcrossRank = /* @__PURE__ */ __name((records, field, uid) => { const userTotals = records.reduce((acc, cur) => { acc[cur.userId] = (acc[cur.userId] || 0) + cur[field]; return acc; }, {}); const sorted = Object.entries(userTotals).sort(([, a], [, b]) => b - a); const index = sorted.findIndex(([id]) => id === uid); return index !== -1 ? index + 1 : null; }, "getAcrossRank"); const getAcrossCount = /* @__PURE__ */ __name((records, field, uid) => { return records.filter((r) => r.userId === uid).reduce((sum, r) => sum + r[field], 0); }, "getAcrossCount"); channelStats.push({ label: "昨日", count: targetUserRecord[0].yesterdayPostCount, total: channelTotals.yesterdayPostCount, rank: getRank(channelUsers, "yesterdayPostCount", userId), enabled: selectedOptions.yesterday }); channelStats.push({ label: "今日", count: targetUserRecord[0].todayPostCount, total: channelTotals.todayPostCount, rank: getRank(channelUsers, "todayPostCount", userId), enabled: selectedOptions.day }); channelStats.push({ label: "本周", count: targetUserRecord[0].thisWeekPostCount, total: channelTotals.thisWeekPostCount, rank: getRank(channelUsers, "thisWeekPostCount", userId), enabled: selectedOptions.week }); channelStats.push({ label: "本月", count: targetUserRecord[0].thisMonthPostCount, total: channelTotals.thisMonthPostCount, rank: getRank(channelUsers, "thisMonthPostCount", userId), enabled: selectedOptions.month }); channelStats.push({ label: "全年", count: targetUserRecord[0].thisYearPostCount, total: channelTotals.thisYearPostCount, rank: getRank(channelUsers, "thisYearPostCount", userId), enabled: selectedOptions.year }); channelStats.push({ label: "总计", count: targetUserRecord[0].totalPostCount, total: channelTotals.totalPostCount, rank: getRank(channelUsers, "totalPostCount", userId), enabled: selectedOptions.total }); acrossStats.push({ label: "昨日", count: getAcrossCount(allUsers, "yesterdayPostCount", userId), total: acrossTotals.yesterdayPostCount, rank: getAcrossRank(allUsers, "yesterdayPostCount", userId), enabled: selectedOptions.ydag }); acrossStats.push({ label: "今日", count: getAcrossCount(allUsers, "todayPostCount", userId), total: acrossTotals.todayPostCount, rank: getAcrossRank(allUsers, "todayPostCount", userId), enabled: selectedOptions.dag }); acrossStats.push({ label: "本周", count: getAcrossCount(allUsers, "thisWeekPostCount", userId), total: acrossTotals.thisWeekPostCount, rank: getAcrossRank(allUsers, "thisWeekPostCount", userId), enabled: selectedOptions.wag }); acrossStats.push({ label: "本月", count: getAcrossCount(allUsers, "thisMonthPostCount", userId), total: acrossTotals.thisMonthPostCount, rank: getAcrossRank(allUsers, "thisMonthPostCount", userId), enabled: selectedOptions.mag }); acrossStats.push({ label: "全年", count: getAcrossCount(allUsers, "thisYearPostCount", userId), total: acrossTotals.thisYearPostCount, rank: getAcrossRank(allUsers, "thisYearPostCount", userId), enabled: selectedOptions.yag }); acrossStats.push({ label: "总计", count: getAcrossCount(allUsers, "totalPostCount", userId), total: acrossTotals.totalPostCount, rank: getAcrossRank(allUsers, "totalPostCount", userId), enabled: selectedOptions.across }); const formatPercentage = /* @__PURE__ */ __name((count, total) => { if (total === 0) return "(0%)"; const percentage = count / total * 100; const numStr = percentage % 1 === 0 ? String(percentage) : percentage.toFixed(2); return `(${numStr}%)`; }, "formatPercentage"); const formatStatsTable = /* @__PURE__ */ __name((title, stats) => { const activeStats = stats.filter((s) => s.enabled && s.count > 0); if (activeStats.length === 0) return ""; const counts = activeStats.map((s) => String(s.count)); const percents = activeStats.map( (s) => formatPercentage(s.count, s.total) ); const maxCountWidth = Math.max(0, ...counts.map((s) => s.length)); const maxPercentWidth = Math.max(0, ...percents.map((s) => s.length)); let table = `${title} `; for (const row of activeStats) { const label = row.label.padEnd(2, " "); const countStr = String(row.count).padStart(maxCountWidth, " "); const percentStr = formatPercentage(row.count, row.total).padEnd( maxPercentWidth, " " ); const rankStr = row.rank ? `#${row.rank}` : "#-"; table += `${label} ${countStr} ${percentStr} ${rankStr} `; } return table; }, "formatStatsTable"); const channelTable = formatStatsTable("群发言", channelStats); const acrossTable = formatStatsTable("跨群发言", acrossStats); const body = [channelTable, acrossTable].filter(Boolean).join("\n"); if (!body) return `被查询对象在指定时段内无发言记录。`; const timestamp = (/* @__PURE__ */ new Date()).toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }); const header = `${timestamp} ${targetUserRecord[0].username} `; const message = header + body; if (config.isTextToImageConversionEnabled && ctx.markdownToImage) { try { const imageBuffer = await ctx.markdownToImage.convertToImage(message); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } catch (error) { logger.warn("生成图片失败,将回退到文本输出:", error); } } return message; }); ctx.command("messageCounter.排行榜 [limit:number]", "用户发言排行榜").userFields(["id", "name"]).option("whites", "<users:text> 白名单,用空格、逗号等分隔").option("blacks", "<users:text> 黑名单,用空格、逗号等分隔").option("yesterday", "--yd").option("day", "-d").option("week", "-w").option("month", "-m").option("year", "-y").option("total", "-t").option("ydag", "跨群昨日").option("dag", "跨群今日").option("wag", "跨群本周").option("mag", "跨群本月").option("yag", "跨群本年").option("dragon", "圣龙王榜 (跨群总榜)").action(async ({ session, options }, limit) => { if (!session) return; const number = limit ?? config.defaultMaxDisplayCount; if (typeof number !== "number" || isNaN(number) || number < 0) { return "请输入大于等于 0 的数字作为排行榜显示人数。"; } const whites = parseList(options?.whites); const blacks = [ ...parseList(options?.blacks), ...config.hiddenUserIdsInLeaderboard ]; const period = getPeriodFromOptions(options, "today"); const isAcross = isAcrossChannel(options); const { field, name: periodName } = periodMapping[period]; const scopeName = isAcross ? "跨群" : "本群"; const rankTitle = `${scopeName}${periodName}发言排行榜`; const rankTimeTitle = getCurrentBeijingTime(); let records; if (isAcross) { records = await ctx.database.get("message_counter_records", {}); } else { records = await ctx.database.get("message_counter_records", { channelId: session.channelId }); } const filteredRecords = filterRecordsByWhitesAndBlacks( records, "userId", whites, blacks ); if (filteredRecords.length === 0) { return "当前范围内暂无发言记录。"; } const userPostCounts = {}; const userInfo = {}; let totalCount = 0; for (const record of filteredRecords) { const count = record[field]; userPostCounts[record.userId] = (userPostCounts[record.userId] || 0) + count; if (!userInfo[record.userId]) { userInfo[record.userId] = { username: record.username, avatar: record.userAvatar || `https://q1.qlogo.cn/g?b=qq&nk=${record.userId}&s=640` }; } totalCount += count; } const sortedUsers = Object.entries(userPostCounts).sort( ([, a], [, b]) => b - a ); const rankingData = prepareRankingData( sortedUsers, userInfo, totalCount, number, session.userId ); return renderLeaderboard({ rankTimeTitle, rankTitle, rankingData }); }); ctx.command("messageCounter.群排行榜 [limit:number]", "群发言排行榜").option("specificUser", "-s <user:text> 特定用户的群发言榜").option("whites", "<channels:text> 白名单群号").option("blacks", "<channels:text> 黑名单群号").option("yesterday", "--yd").option("day", "-d").option("week", "-w").option("month", "-m").option("year", "-y").option("total", "-t").action(async ({ session, options }, limit) => { if (!session) return; const number = limit ?? config.defaultMaxDisplayCount; if (typeof number !== "number" || isNaN(number) || number < 0) { return "请输入大于等于 0 的数字作为排行榜显示人数。"; } const whites = parseList(options?.whites); const blacks = [ ...parseList(options?.blacks), ...config.hiddenChannelIdsInLeaderboard ]; const period = getPeriodFromOptions(options, "today"); const { field, name: periodName } = periodMapping[period]; let records; let rankTitle; const rankTimeTitle = getCurrentBeijingTime(); if (options?.specificUser) { const at = import_koishi.h.select(options.specificUser, "at"); const userId = at.length ? at[0].attrs.id : options.specificUser; const userRecords = await ctx.database.get("message_counter_records", { userId, channelId: session.channelId }); const username = userRecords.length > 0 ? userRecords[0].username || `用户${userId}` : `用户${userId}`; rankTitle = `${username}的${periodName}群发言排行榜`; records = await ctx.database.get("message_counter_records", { userId }); } else { rankTitle = `全群${periodName}发言排行榜`; records = await ctx.database.get("message_counter_records", {}); } const filteredRecords = filterRecordsByWhitesAndBlacks( records, "channelId", whites, blacks ); if (filteredRecords.length === 0) { return `在当前条件下找不到任何群聊发言记录。`; } const { channelPostCounts, channelInfo, totalCount } = aggregateChannelData(filteredRecords, field); const sortedChannels = Object.entries(channelPostCounts).sort( ([, a], [, b]) => b - a ); const rankingData = prepareChannelRankingData( sortedChannels, channelInfo, totalCount, number, session.channelId ); return renderLeaderboard({ rankTimeTitle, rankTitle, rankingData }); }); ctx.command( "messageCounter.上传柱状条背景", "上传/更新自定义的水平柱状条背景图" ).action(async ({ session }) => { if (!session || !session.userId) { return "无法获取用户信息,请稍后再试。"; } if (!session.content) { return "请在发送指令的同时附带一张图片。新图片将会覆盖旧的背景。"; } const imageElements = import_koishi.h.select(session.content, "img"); if (imageElements.length === 0) { return "请在发送指令的同时附带一张图片。新图片将会覆盖旧的背景。"; } const { userId } = session; const cleanupOldBackground = /* @__PURE__ */ __name(async () => { try { const allFiles = await fs.readdir(barBgImgsPath); const userFiles = allFiles.filter( (file) => file.startsWith(`${userId}.`) ); if (userFiles.length > 0) { await Promise.all( userFiles.map( (file) => fs.unlink(import_path.default.join(barBgImgsPath, file)) ) ); } } catch (error) { if (error.code !== "ENOENT") { logger.warn(`清理用户 ${userId} 的旧背景图时出错:`, error); } } }, "cleanupOldBackground"); try { const imageUrl = imageElements[0].attrs.src; if (!imageUrl) { throw new Error("未能从消息中提取图片 URL。"); } const buffer = Buffer.from( await ctx.http.get(imageUrl, { responseType: "arraybuffer" }) ); const imageSizeInMB = buffer.byteLength / 1024 / 1024; if (config.maxBarBgSize > 0 && imageSizeInMB > config.maxBarBgSize) { throw new Error( `图片文件过大(${imageSizeInMB.toFixed(2)}MB),请上传小于 ${config.maxBarBgSize}MB 的图片。` ); } if (ctx.canvas) { try { const image = await ctx.canvas.loadImage(buffer); if (config.maxBarBgWidth > 0 && image.naturalWidth > config.maxBarBgWidth || config.maxBarBgHeight > 0 && image.naturalHeight > config.maxBarBgHeight) { throw new Error( `图片尺寸(${image.naturalWidth}x${image.naturalHeight})超出限制(最大 ${config.maxBarBgWidth}x${config.maxBarBgHeight})。 建议尺寸为 850x50 像素。` ); } } catch (error) { logger.error("解析图片尺寸失败:", error); throw new Error( "无法解析图片尺寸,请尝试使用其他标准图片格式(如 PNG, JPEG)。" ); } } else { logger.warn("Canvas 服务未启用,跳过背景图尺寸检查。"); } await cleanupOldBackground(); const newFileName = `${userId}.png`; const newFilePath = import_path.default.join(barBgImgsPath, newFileName); await fs.writeFile(newFilePath, buffer); await reloadBarBgImgCache(); return "您的自定义柱状条背景已成功更新!"; } catch (error) { logger.error(`为用户 ${userId} 上传背景图失败:`, error); await cleanupOldBackground(); await reloadBarBgImgCache(); const userMessage = error instanceof Error ? error.message : "图片保存时发生未知错误,请联系管理员。"; return `图片上传失败: ${userMessage} 您之前的自定义背景(如有)已被移除。`; } }); ctx.command("messageCounter.重载资源", "重载图标、背景和字体资源", { authority: 2 }).action(async ({ session }) => { if (!session) return; await session.send("正在重新加载用户图标、背景图片和字体文件缓存..."); await reloadIconCache(); await reloadBarBgImgCache(); await reloadFontCache(); return `资源重载完毕! - 已加载 ${iconCache.length} 个用户图标。 - 已加载 ${barBgImgCache.length} 个柱状条背景图片。 - 已加载 ${fontFilesCache.length} 个字体文件。`; }); ctx.command("messageCounter.清理缓存", "清理过期的头像缓存文件", { authority: 3 }).option( "days", "-d <days:number> 清理超过指定天数未使用的缓存文件 (默认: 30)" ).action(async ({ session, options }) => { if (!session) return; const days = options.days ?? 30; if (typeof days !== "number" || days < 0) { return "请输入一个有效的天数(大于等于0)。"; } await session.send(`正在开始清理 ${days} 天前的头像缓存,请稍候...`); const cacheDir = avatarsPath; let deletedCount = 0; let totalFreedSize = 0; const now = Date.now(); const expirationTime = now - days * 24 * 60 * 60 * 1e3; try { const files = await fs.readdir(cacheDir); for (const file of files) { if (!file.endsWith(".json")) continue; const filePath = import_path.default.join(cacheDir, file); try { const stats = await fs.stat(filePath); const content = await fs.readFile(filePath, "utf-8"); const entry = JSON.parse(content); if (entry.timestamp < expirationTime) { await fs.unlink(filePath); deletedCount++; totalFreedSize += stats.size; } } catch (error) { logger.warn(`处理缓存文件 ${file} 时出错,已跳过:`, error); } } const freedSizeFormatted = formatBytes(totalFreedSize); return `缓存清理完成! - 共删除 ${deletedCount} 个过期缓存文件。 - 释放磁盘空间约 ${freedSizeFormatted}。`; } catch (error) { if (error.code === "ENOENT") { return "头像缓存目录不存在,无需清理。"; } logger.error("清理头像缓存时发生未知错误:", error); return "清理过程中发生错误,请查看控制台日志。"; } }); async function patchAndGetUsableFontPath(filePath) { let buffer; try { buffer = await fs.readFile(filePath); } catch (readError) { logger.warn( `读取字体文件 "${import_path.default.basename(filePath)}" 失败,已跳过。错误: ${readError.message}` ); return filePath; } if (buffer.length < 12) return filePath; const numTables = buffer.readUInt16BE(4); for (let i = 0; i < numTables; i++) { const recordOffset = 12 + i * 16; if (recordOffset + 16 > buffer.length) break; if (buffer.toString("ascii", recordOffset, recordOffset + 4) === "vhea") { const tableOffset = buffer.readUInt32BE(recordOffset + 8); if (tableOffset + 4 > buffer.length) break; const version = buffer.readUInt32BE(tableOffset); const INCORRECT_VERSION = 65537; if (version === INCORRECT_VERSION) { const parsedPath = import_path.default.parse(filePath); const patchedFilename = `${parsedPath.name}-patched${parsedPath.ext}`; const patchedFilePath = import_path.default.join(parsedPath.dir, patchedFilename); try { await fs.access(patchedFilePath); return patchedFilePath; } catch (e) { logger.info( `检测到字体 "${import_path.default.basename( filePath )}" 不规范,正在创建修复版本 "${patchedFilename}"...` ); const CORRECT_VERSION = 65536; buffer.writeUInt32BE(CORRECT_VERSION, tableOffset); try { await fs.writeFile(patchedFilePath, buffer); logger.success( `已成功创建修复后的字体文件 "${patchedFilename}"。` ); return patchedFilePath; } catch (writeError) { logger.warn(`创建修复字体副本失败: ${writeError.message}`); return filePath; } } } break; } } return filePath; } __name(patchAndGetUsableFontPath, "patchAndGetUsableFontPath"); function formatBytes(bytes, decimals = 2) { if (bytes === 0) return "0 Bytes"; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; } __name(formatBytes, "formatBytes"); async function generateAndPushLeaderboard(period) { const pushPeriodConfig = { today: { field: "todayPostCount", name: "今日" }, yesterday: { field: "yesterdayPostCount", name: "昨日" }, week: { field: "thisWeekPostCount", name: "上周" }, month: { field: "thisMonthPostCount", name: "上月" }, year: { field: "thisYearPostCount", name: "去年" } }; const { field, name: periodName } = pushPeriodConfig[period]; logger.info(`[自动推送] 开始执行 ${periodName} 发言排行榜推送任务。`); const scopeName = "本群"; const rankTimeTitle = getCurrentBeijingTime(); const guildChannelIdMap = /* @__PURE__ */ new Map(); const lookupChannelIdMap = /* @__PURE__ */ new Map(); const isDirectChannelId = /* @__PURE__ */ __name((id) => { const sepIdx = id.indexOf(":"); const unprefixed = sepIdx !== -1 ? id.substring(sepIdx + 1) : id; return id.startsWith("private:") || unprefixed.startsWith("private:") || unprefixed.startsWith("private_"); }, "isDirectChannelId"); try { const channelListPromises = ctx.bots.map(async (bot) => { if (!bot.online || !bot.getGuildList) return []; try { let next; do { const result = await bot.getGuildList(next); if (!result || !result.data) { break; } if (Array.isArray(result.data)) { result.data.forEach((channel) => { if (isDirectChannelId(channel.id)) return; if (!guildChannelIdMap.has(channel.id)) { const prefixed = `${bot.platform}:${channel.id}`; guildChannelIdMap.set(channel.id, prefixed); lookupChannelIdMap.set(channel.id, prefixed); } }); } next = result.next; } while (next); } catch (error) { logger.warn( `[自动推送] 机器人 ${bot.platform} 获取群聊列表失败,将尝试使用数据库记录回退: ${error.message}` ); } }); await Promise.all(channelListPromises); } catch (error) { logger.error("[自动推送] 获取群聊列表的主流程发生错误:", error); } try { const allRecords = await ctx.database.get("message_counter_records", {}, [ "channelId" ]); const dbChannelIds = new Set(allRecords.map((r) => r.channelId)); const activeBot = ctx.bots.find((b) => b.status === 1); for (const dbCid of dbChannelIds) { if (isDirectChannelId(dbCid)) continue; if (!lookupChannelIdMap.has(dbCid) && activeBot) { lookupChannelIdMap.set(dbCid, `${activeBot.platform}:${dbCid}`); } } } catch (dbError) { logger.warn("[自动推送] 读取数据库记录进行回退时出错:", dbError); } const targetChannels = /* @__PURE__ */ new Set(); for (const channelId of config.pushChannelIds || []) { if (channelId.includes(":")) { targetChannels.add(channelId); } else if (lookupChannelIdMap.has(channelId)) { targetChannels.add(lookupChannelIdMap.get(channelId)); } else { const activeBot = ctx.bots.find((b) => b.status === 1); if (activeBot) { targetChannels.add(`${activeBot.platform}:${channelId}`); } else { logger.warn( `[自动推送] 无法处理配置的频道 ID: ${channelId},未找到在线 Bot。` ); } } } if (config.shouldSendLeaderboardNotificationsToAllChannels) { guildChannelIdMap.forEach((prefixedId) => targetChannels.add(prefixedId)); } const excluded = new Set(config.excludedLeaderboardChannels || []); if (excluded.size > 0) { for (const id of Array.from(targetChannels)) { const separatorIdx = id.indexOf(":"); const unprefixedId = separatorIdx !== -1 ? id.substring(separatorIdx + 1) : id; if (excluded.has(id) || excluded.has(unprefixedId)) { targetChannels.delete(id); } } } if (targetChannels.size === 0) { logger.info("[自动推送] 没有配置任何需要推送的频道,任务结束。"); return; } logger.info(`[自动推送] 将向 ${targetChannels.size} 个频道进行推送。`); for (const prefixedChannelId of targetChannels) { try { const platformSeparatorIndex = prefixedChannelId.indexOf(":"); const channelId = platformSeparatorIndex === -1 ? prefixedChannelId : prefixedChannelId.substring(platformSeparatorIndex + 1); const records = await ctx.database.get("message_counter_records", { channelId }); if (records.length === 0) { logger.info( `[自动推送] 频道 ${prefixedChannelId} 无发言记录,跳过。` ); continue; } const userPostCounts = {}; const userInfo = {}; let totalCount = 0; for (const record of records) { const count = record[field] || 0; userPostCounts[record.userId] = (userPostCounts[record.userId] || 0) + count; if (!userInfo[record.userId]) { userInfo[record.userId] = { username: record.username, avatar: record.userAvatar || `https://q1.qlogo.cn/g?b=qq&nk=${record.userId}&s=640` }; } totalCount += count; } const sortedUsers = Object.entries(userPostCounts).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a); if (sortedUsers.length === 0) { logger.info( `[自动推送] 频道 ${prefixedChannelId} 在 ${periodName} 榜单上无有效数据,跳过。` ); continue; } const rankingData = prepareRankingData( sortedUsers, userInfo, totalCount, config.defaultMaxDisplayCount ); if (config.isGeneratingRankingListPromptVisible) { try { await ctx.broadcast( [prefixedChannelId], `正在为本群生成${periodName}发言排行榜...` ); } catch (e) { } await (0, import_koishi.sleep)(config.leaderboardGenerationWaitTime * 1e3); } const rankTitle = `${scopeName}${periodName}发言排行榜`; const renderedMessage = await renderLeaderboard({ rankTimeTitle, rankTitle, rankingData }); await ctx.broadcast([prefixedChannelId], renderedMessage); logger.success( `[自动推送] 已成功向频道 ${prefixedChannelId} 推送${periodName}排行榜。` ); const randomDelay = Math.random() * config.groupPushDelayRandomizationSeconds; const delay = (config.delayBetweenGroupPushesInSeconds + randomDelay) * 1e3; if (delay > 0) { await (0, import_koishi.sleep)(delay); } } catch (error) { logger.error( `[自动推送] 向频道 ${prefixedChannelId} 推送时发生错误:`, error ); } } logger.info(`[自动推送] 所有推送任务执行完毕。`); } __name(generateAndPushLeaderboard, "generateAndPushLeaderboard"); async function performDragonKingMuting() { if (!config.enableMostActiveUserMuting || !config.muteChannelIds || config.muteChannelIds.length === 0) { return; } logger.info("[抓龙王] 开始执行禁言任务。"); await (0, import_koishi.sleep)(config.dragonKingDetainmentTime * 1e3); for (const channel