UNPKG

koishi-plugin-checkin-custom

Version:

一个高度可配置的、支持多类型打卡和独立排行榜的 Koishi 插件。

268 lines (265 loc) 12.9 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 }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var import_node_fs = require("node:fs"); var import_node_path = __toESM(require("node:path")); var name = "checkin-custom"; var inject = { required: ["database", "puppeteer"] }; var checkinCommandSchema = import_koishi.Schema.object({ commandName: import_koishi.Schema.string().description("打卡指令的名称(这也是类型的唯一标识符)。").required(), successMessage: import_koishi.Schema.string().default("打卡成功!这是你本月的打卡日历:").description("成功打卡后发送的提示文案。"), alreadyCheckedMessage: import_koishi.Schema.string().default("你今天已经打过卡啦!").description("重复打卡时发送的提示文案。"), checkinTitle: import_koishi.Schema.string().default("每日打卡").description("打卡日历上显示的标题。"), backgroundImage: import_koishi.Schema.string().role("path").description("【可选】自定义日历的背景图片路径。"), backgroundSize: import_koishi.Schema.union(["cover", "contain"]).default("cover").description("背景图片缩放模式。"), backgroundBlur: import_koishi.Schema.number().min(0).max(50).default(5).description("背景模糊程度(0-50像素)。"), themeColor: import_koishi.Schema.string().role("color").default("#fde2e2").description("【可选】自定义日历中“今天”高亮显示的颜色。") }).description("打卡指令配置"); var rankCommandSchema = import_koishi.Schema.object({ commandName: import_koishi.Schema.string().description("排行榜指令的名称。").required(), title: import_koishi.Schema.string().default("本月打卡排行榜").description("排行榜上显示的标题。"), includedTypes: import_koishi.Schema.array(import_koishi.Schema.string()).role("table").description("要统计的打卡类型(填写打卡指令的名称)。如果留空,将统计所有类型的打卡总和。") }).description("排行榜配置"); var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ checkinCommands: import_koishi.Schema.array(checkinCommandSchema).description("在这里定义一个或多个打卡指令,每个指令都是一个独立的“打卡类型”。") }).description("打卡指令设置"), import_koishi.Schema.object({ rankCommands: import_koishi.Schema.array(rankCommandSchema).description("在这里定义一个或多个排行榜,每个排行榜可以独立统计不同的打卡类型。") }).description("排行榜设置"), import_koishi.Schema.object({ debug: import_koishi.Schema.boolean().default(false).description("是否开启调试模式。") }).description("高级设置") ]); function apply(ctx, config) { ctx.model.extend("checkin-custom-record", { id: "unsigned", userId: "string", platform: "string", checkinType: "string", date: "date" }, { primary: "id", autoInc: true }); for (const cmdConfig of config.checkinCommands) { ctx.command(cmdConfig.commandName, "进行每日打卡并查看日历").action(async ({ session }) => { const { userId, platform } = session; const today = /* @__PURE__ */ new Date(); today.setHours(0, 0, 0, 0); const checkinType = cmdConfig.commandName; try { const existingRecord = await ctx.database.get("checkin-custom-record", { userId, platform, date: today, checkinType }); let message; if (existingRecord.length > 0) { message = (0, import_koishi.h)("p", cmdConfig.alreadyCheckedMessage); } else { await ctx.database.create("checkin-custom-record", { userId, platform, date: today, checkinType }); message = (0, import_koishi.h)("p", cmdConfig.successMessage); } if (config.debug) ctx.logger.info(`Generating calendar for user ${userId}, type: ${checkinType}`); const calendarImage = await generateCalendar(ctx, userId, platform, cmdConfig, config.debug); return [message, calendarImage]; } catch (error) { ctx.logger.error(`[${cmdConfig.commandName}] command error:`, error); return "打卡时发生内部错误,请联系管理员。"; } }); } for (const rankConfig of config.rankCommands) { ctx.command(rankConfig.commandName, `查看 ${rankConfig.title}`).action(async ({ session }) => { const now = /* @__PURE__ */ new Date(); const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); try { const whereClause = { date: { $gte: firstDayOfMonth, $lte: lastDayOfMonth } }; if (rankConfig.includedTypes && rankConfig.includedTypes.length > 0) { whereClause.checkinType = { $in: rankConfig.includedTypes }; } const rankData = await ctx.database.select("checkin-custom-record").where(whereClause).groupBy("userId", { count: /* @__PURE__ */ __name((row) => import_koishi.$.count(row.id), "count") }).orderBy("count", "desc").limit(10).execute(); if (rankData.length === 0) { return `本月还没有相关的打卡记录哦,快来争做第一名吧!`; } let rankMessage = `📅 ${rankConfig.title} Top ${rankData.length} 📅 `; let rankIndex = 1; for (const item of rankData) { let userName; try { const user = await session.bot.getUser(item.userId); userName = user.name || user.nick || item.userId; } catch { userName = item.userId; } const medal = rankIndex === 1 ? "🥇" : rankIndex === 2 ? "🥈" : rankIndex === 3 ? "🥉" : `${rankIndex}.`; rankMessage += `${medal} ${userName} - ${item.count} 天 `; rankIndex++; } return rankMessage; } catch (error) { ctx.logger.error(`[${rankConfig.commandName}] rank command error:`, error); return "查询排行榜时发生错误。"; } }); } } __name(apply, "apply"); async function generateCalendar(ctx, userId, platform, cmdConfig, debug) { const now = /* @__PURE__ */ new Date(); const year = now.getFullYear(); const month = now.getMonth(); const firstDayOfMonth = new Date(year, month, 1); const lastDayOfMonth = new Date(year, month + 1, 0); const checkinType = cmdConfig.commandName; const records = await ctx.database.get("checkin-custom-record", { userId, platform, checkinType, date: { $gte: firstDayOfMonth, $lte: lastDayOfMonth } }); const checkedInDays = new Set(records.map((r) => new Date(r.date).getDate())); if (debug) ctx.logger.info(`User ${userId} checked in on days: ${[...checkedInDays].join(", ")} for type: ${checkinType}`); let customStyle = ""; if (cmdConfig.backgroundImage) { try { const imagePath = import_node_path.default.resolve(ctx.baseDir, cmdConfig.backgroundImage); if (debug) ctx.logger.info(`Attempting to load background image from: ${imagePath}`); const buffer = await import_node_fs.promises.readFile(imagePath); const base64 = buffer.toString("base64"); const mimeType = import_node_path.default.extname(imagePath).substring(1) || "png"; customStyle += `body { background-image: url(data:image/${mimeType};base64,${base64}); background-size: ${cmdConfig.backgroundSize}; background-position: center; background-repeat: no-repeat; }`; } catch (error) { ctx.logger.warn(`加载背景图片失败: ${cmdConfig.backgroundImage}. 请检查路径和文件权限。`); ctx.logger.warn(error); } } const calendarBackground = cmdConfig.backgroundBlur > 0 ? `background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(${cmdConfig.backgroundBlur}px);` : `background: rgba(255, 255, 255, 1);`; const html = ` <html> <head> <style> body { font-family: sans-serif; background-color: #f0f2f5; display: flex; justify-content: center; align-items: center; height: 100%; width: 100%; margin: 0; } .calendar { width: 350px; ${calendarBackground} border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); padding: 20px; } .header { text-align: center; margin-bottom: 20px; } .header h1 { margin: 0; font-size: 1.5em; color: #333; } .header p { margin: 5px 0 0; color: #888; } table { width: 100%; border-collapse: collapse; } th, td { text-align: center; padding: 10px; } th { color: #555; } td { color: #444; font-weight: bold; } .day { position: relative; } .day.checked::after { content: '✔️'; font-size: 0.7em; color: #4CAF50; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); } .day.today { background-color: ${cmdConfig.themeColor}; border-radius: 50%; } .empty { background-color: transparent; } ${customStyle} </style> </head> <body> <div class="calendar"> <div class="header"> <h1>${cmdConfig.checkinTitle}</h1> <p>${year}${month + 1}月</p> </div> <table> <thead> <tr><th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th></tr> </thead> <tbody> ${generateCalendarGrid(year, month, checkedInDays)} </tbody> </table> </div> </body> </html> `; const page = await ctx.puppeteer.page(); try { await page.setViewport({ width: 450, height: 600 }); await page.setContent(html); const imageBuffer = await page.screenshot({ type: "png" }); return import_koishi.h.image(imageBuffer, "image/png"); } finally { await page.close(); } } __name(generateCalendar, "generateCalendar"); function generateCalendarGrid(year, month, checkedInDays) { let html = "<tr>"; const firstDayOfWeek = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const todayDate = (/* @__PURE__ */ new Date()).getDate(); const isCurrentMonth = (/* @__PURE__ */ new Date()).getMonth() === month && (/* @__PURE__ */ new Date()).getFullYear() === year; for (let i = 0; i < firstDayOfWeek; i++) { html += '<td class="empty"></td>'; } for (let day = 1; day <= daysInMonth; day++) { const dayOfWeek = (firstDayOfWeek + day - 1) % 7; if (dayOfWeek === 0 && day > 1) { html += "</tr><tr>"; } const classes = ["day"]; if (checkedInDays.has(day)) { classes.push("checked"); } if (isCurrentMonth && day === todayDate) { classes.push("today"); } html += `<td class="${classes.join(" ")}">${day}</td>`; } const lastDayOfWeek = (firstDayOfWeek + daysInMonth - 1) % 7; for (let i = lastDayOfWeek + 1; i < 7; i++) { html += '<td class="empty"></td>'; } html += "</tr>"; return html; } __name(generateCalendarGrid, "generateCalendarGrid"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name });