karin-plugin-timechange
Version:
Karin智能头像切换插件 - 根据时间自动在白天和晚上切换不同的头像
376 lines (372 loc) • 14.7 kB
JavaScript
import {
AvatarConfig
} from "../chunk-G5C6VDKH.js";
// src/apps/avatar-api.ts
import { karin, logger as logger2 } from "node-karin";
// src/utils/file-upload.ts
import fs from "fs";
import path from "path";
import { logger } from "node-karin";
var FileUploadHandler = class {
avatarConfig;
constructor() {
this.avatarConfig = new AvatarConfig();
}
/**
* 处理base64图片上传
* @param base64Data base64编码的图片数据
* @param type 头像类型 'day' | 'night'
* @returns 保存的文件路径
*/
async handleBase64Upload(base64Data, type) {
try {
if (!base64Data || !base64Data.startsWith("data:image/")) {
throw new Error("\u65E0\u6548\u7684\u56FE\u7247\u6570\u636E\u683C\u5F0F");
}
const matches = base64Data.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/);
if (!matches) {
throw new Error("\u65E0\u6CD5\u89E3\u6790\u56FE\u7247\u6570\u636E");
}
const imageType = matches[1].toLowerCase();
const imageBuffer = Buffer.from(matches[2], "base64");
const allowedTypes = ["jpg", "jpeg", "png", "gif", "bmp", "webp"];
if (!allowedTypes.includes(imageType)) {
throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u683C\u5F0F: ${imageType}`);
}
const maxSize = 5 * 1024 * 1024;
if (imageBuffer.length > maxSize) {
throw new Error("\u56FE\u7247\u6587\u4EF6\u8FC7\u5927\uFF0C\u8BF7\u9009\u62E9\u5C0F\u4E8E5MB\u7684\u56FE\u7247");
}
const resourcePath = this.avatarConfig.getResourcePath();
if (!fs.existsSync(resourcePath)) {
fs.mkdirSync(resourcePath, { recursive: true });
}
const fileName = `${type}-avatar.${imageType === "jpeg" ? "jpg" : imageType}`;
const filePath = path.join(resourcePath, fileName);
fs.writeFileSync(filePath, imageBuffer);
if (type === "day") {
this.avatarConfig.setDayAvatarPath(filePath);
} else {
this.avatarConfig.setNightAvatarPath(filePath);
}
logger.info(`[\u6587\u4EF6\u4E0A\u4F20] ${type === "day" ? "\u767D\u5929" : "\u665A\u4E0A"}\u5934\u50CF\u4E0A\u4F20\u6210\u529F: ${filePath}`);
return filePath;
} catch (error) {
logger.error(`[\u6587\u4EF6\u4E0A\u4F20] \u4E0A\u4F20${type === "day" ? "\u767D\u5929" : "\u665A\u4E0A"}\u5934\u50CF\u5931\u8D25:`, error);
throw error;
}
}
/**
* 处理文件路径设置
* @param filePath 文件路径
* @param type 头像类型 'day' | 'night'
* @returns 是否设置成功
*/
async handleFilePathSetting(filePath, type) {
try {
if (!filePath || !fs.existsSync(filePath)) {
throw new Error("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u5B58\u5728");
}
const ext = path.extname(filePath).toLowerCase();
const allowedExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"];
if (!allowedExts.includes(ext)) {
throw new Error("\u4E0D\u652F\u6301\u7684\u56FE\u7247\u683C\u5F0F");
}
const stats = fs.statSync(filePath);
const maxSize = 5 * 1024 * 1024;
if (stats.size > maxSize) {
throw new Error("\u56FE\u7247\u6587\u4EF6\u8FC7\u5927\uFF0C\u8BF7\u9009\u62E9\u5C0F\u4E8E5MB\u7684\u56FE\u7247");
}
if (type === "day") {
this.avatarConfig.setDayAvatarPath(filePath);
} else {
this.avatarConfig.setNightAvatarPath(filePath);
}
logger.info(`[\u6587\u4EF6\u8BBE\u7F6E] ${type === "day" ? "\u767D\u5929" : "\u665A\u4E0A"}\u5934\u50CF\u8DEF\u5F84\u8BBE\u7F6E\u6210\u529F: ${filePath}`);
return true;
} catch (error) {
logger.error(`[\u6587\u4EF6\u8BBE\u7F6E] \u8BBE\u7F6E${type === "day" ? "\u767D\u5929" : "\u665A\u4E0A"}\u5934\u50CF\u8DEF\u5F84\u5931\u8D25:`, error);
throw error;
}
}
/**
* 获取头像文件的base64数据(用于预览)
* @param type 头像类型 'day' | 'night'
* @returns base64数据或null
*/
getAvatarBase64(type) {
try {
const config = this.avatarConfig.getConfig();
const filePath = type === "day" ? config.dayAvatarPath : config.nightAvatarPath;
if (!filePath || !fs.existsSync(filePath)) {
return null;
}
const fileBuffer = fs.readFileSync(filePath);
const ext = path.extname(filePath).toLowerCase().substring(1);
const mimeType = this.getMimeType(ext);
return `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
} catch (error) {
logger.error(`[\u5934\u50CF\u9884\u89C8] \u83B7\u53D6${type === "day" ? "\u767D\u5929" : "\u665A\u4E0A"}\u5934\u50CF\u9884\u89C8\u5931\u8D25:`, error);
return null;
}
}
/**
* 根据文件扩展名获取MIME类型
* @param ext 文件扩展名
* @returns MIME类型
*/
getMimeType(ext) {
const mimeTypes = {
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"bmp": "image/bmp",
"webp": "image/webp"
};
return mimeTypes[ext] || "image/jpeg";
}
/**
* 删除头像文件
* @param type 头像类型 'day' | 'night'
* @returns 是否删除成功
*/
deleteAvatar(type) {
try {
const config = this.avatarConfig.getConfig();
const filePath = type === "day" ? config.dayAvatarPath : config.nightAvatarPath;
if (!filePath) {
return true;
}
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
logger.info(`[\u6587\u4EF6\u5220\u9664] ${type === "day" ? "\u767D\u5929" : "\u665A\u4E0A"}\u5934\u50CF\u6587\u4EF6\u5DF2\u5220\u9664: ${filePath}`);
}
if (type === "day") {
this.avatarConfig.setDayAvatarPath("");
} else {
this.avatarConfig.setNightAvatarPath("");
}
return true;
} catch (error) {
logger.error(`[\u6587\u4EF6\u5220\u9664] \u5220\u9664${type === "day" ? "\u767D\u5929" : "\u665A\u4E0A"}\u5934\u50CF\u5931\u8D25:`, error);
return false;
}
}
/**
* 获取文件信息
* @param filePath 文件路径
* @returns 文件信息
*/
getFileInfo(filePath) {
try {
if (!filePath || !fs.existsSync(filePath)) {
return { exists: false };
}
const stats = fs.statSync(filePath);
const sizeInMB = (stats.size / (1024 * 1024)).toFixed(2);
return {
exists: true,
size: stats.size,
sizeText: `${sizeInMB}MB`
};
} catch (error) {
return { exists: false };
}
}
/**
* 验证图片文件
* @param filePath 文件路径
* @returns 验证结果
*/
validateImageFile(filePath) {
try {
if (!filePath) {
return { valid: false, error: "\u6587\u4EF6\u8DEF\u5F84\u4E3A\u7A7A" };
}
if (!fs.existsSync(filePath)) {
return { valid: false, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" };
}
const ext = path.extname(filePath).toLowerCase();
const allowedExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"];
if (!allowedExts.includes(ext)) {
return { valid: false, error: "\u4E0D\u652F\u6301\u7684\u56FE\u7247\u683C\u5F0F" };
}
const stats = fs.statSync(filePath);
const maxSize = 5 * 1024 * 1024;
if (stats.size > maxSize) {
return { valid: false, error: "\u6587\u4EF6\u8FC7\u5927\uFF0C\u8BF7\u9009\u62E9\u5C0F\u4E8E5MB\u7684\u56FE\u7247" };
}
return { valid: true };
} catch (error) {
return { valid: false, error: `\u9A8C\u8BC1\u5931\u8D25: ${error}` };
}
}
};
// src/apps/avatar-api.ts
var fileUploadHandler = new FileUploadHandler();
var avatarConfig = new AvatarConfig();
var avatarUploadApi = karin.command("^(#)?\u4E0A\u4F20\u5934\u50CF\\s+(\u767D\u5929|\u665A\u4E0A)$", async (e) => {
const timeType = e.msg.includes("\u767D\u5929") ? "day" : "night";
const timeText = timeType === "day" ? "\u767D\u5929" : "\u665A\u4E0A";
await e.reply(`\u8BF7\u53D1\u9001\u8981\u8BBE\u7F6E\u4E3A${timeText}\u5934\u50CF\u7684\u56FE\u7247`);
const response = await e.waitInput(6e4);
if (!response) {
await e.reply("\u274C \u8D85\u65F6\u672A\u6536\u5230\u56DE\u590D\uFF0C\u4E0A\u4F20\u53D6\u6D88");
return true;
}
if (response.elements && response.elements.length > 0) {
for (const element of response.elements) {
if (element.type === "image") {
try {
if (element.url) {
const imageResponse = await fetch(element.url);
if (!imageResponse.ok) {
throw new Error(`\u4E0B\u8F7D\u56FE\u7247\u5931\u8D25: ${imageResponse.status}`);
}
const arrayBuffer = await imageResponse.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const imageType = detectImageType(buffer);
if (!imageType) {
await e.reply("\u274C \u4E0D\u652F\u6301\u7684\u56FE\u7247\u683C\u5F0F\uFF0C\u8BF7\u53D1\u9001 jpg\u3001png\u3001gif\u3001bmp \u6216 webp \u683C\u5F0F\u7684\u56FE\u7247");
return true;
}
const base64Data = `data:image/${imageType};base64,${buffer.toString("base64")}`;
const filePath = await fileUploadHandler.handleBase64Upload(base64Data, timeType);
await e.reply(`\u2705 ${timeText}\u5934\u50CF\u4E0A\u4F20\u6210\u529F\uFF01
\u{1F4C1} \u4FDD\u5B58\u8DEF\u5F84: ${filePath}`);
const config = avatarConfig.getConfig();
if (config.autoSwitch) {
await e.reply("\u{1F4A1} \u81EA\u52A8\u5207\u6362\u5DF2\u5F00\u542F\uFF0C\u63D2\u4EF6\u5C06\u5728\u9002\u5F53\u65F6\u95F4\u81EA\u52A8\u4F7F\u7528\u65B0\u5934\u50CF");
}
} else {
await e.reply("\u274C \u65E0\u6CD5\u83B7\u53D6\u56FE\u7247\u6570\u636E\uFF0C\u8BF7\u91CD\u8BD5");
}
} catch (error) {
logger2.error(`[\u5934\u50CF\u4E0A\u4F20] \u4E0A\u4F20${timeText}\u5934\u50CF\u5931\u8D25:`, error);
await e.reply(`\u274C ${timeText}\u5934\u50CF\u4E0A\u4F20\u5931\u8D25: ${error}`);
}
return true;
}
}
}
await e.reply("\u274C \u8BF7\u53D1\u9001\u56FE\u7247\u6587\u4EF6");
return true;
}, {
name: "\u4E0A\u4F20\u5934\u50CF",
perm: "master",
authFailMsg: "\u53EA\u6709\u4E3B\u4EBA\u624D\u80FD\u4E0A\u4F20\u5934\u50CF\u54E6~"
});
var avatarPreview = karin.command("^(#)?\u5934\u50CF\u9884\u89C8\\s+(\u767D\u5929|\u665A\u4E0A)$", async (e) => {
const timeType = e.msg.includes("\u767D\u5929") ? "day" : "night";
const timeText = timeType === "day" ? "\u767D\u5929" : "\u665A\u4E0A";
try {
const base64Data = fileUploadHandler.getAvatarBase64(timeType);
if (!base64Data) {
await e.reply(`\u274C \u672A\u627E\u5230${timeText}\u5934\u50CF\uFF0C\u8BF7\u5148\u4E0A\u4F20\u5934\u50CF`);
return true;
}
await e.reply([
`\u{1F5BC}\uFE0F ${timeText}\u5934\u50CF\u9884\u89C8\uFF1A`,
{ type: "image", url: base64Data }
]);
} catch (error) {
logger2.error(`[\u5934\u50CF\u9884\u89C8] \u83B7\u53D6${timeText}\u5934\u50CF\u9884\u89C8\u5931\u8D25:`, error);
await e.reply(`\u274C \u83B7\u53D6${timeText}\u5934\u50CF\u9884\u89C8\u5931\u8D25: ${error}`);
}
return true;
}, {
name: "\u5934\u50CF\u9884\u89C8",
perm: "master",
authFailMsg: "\u53EA\u6709\u4E3B\u4EBA\u624D\u80FD\u67E5\u770B\u5934\u50CF\u9884\u89C8\u54E6~"
});
var deleteAvatar = karin.command("^(#)?\u5220\u9664\u5934\u50CF\\s+(\u767D\u5929|\u665A\u4E0A)$", async (e) => {
const timeType = e.msg.includes("\u767D\u5929") ? "day" : "night";
const timeText = timeType === "day" ? "\u767D\u5929" : "\u665A\u4E0A";
try {
const success = fileUploadHandler.deleteAvatar(timeType);
if (success) {
await e.reply(`\u2705 ${timeText}\u5934\u50CF\u5DF2\u5220\u9664`);
} else {
await e.reply(`\u274C \u5220\u9664${timeText}\u5934\u50CF\u5931\u8D25`);
}
} catch (error) {
logger2.error(`[\u5934\u50CF\u5220\u9664] \u5220\u9664${timeText}\u5934\u50CF\u5931\u8D25:`, error);
await e.reply(`\u274C \u5220\u9664${timeText}\u5934\u50CF\u5931\u8D25: ${error}`);
}
return true;
}, {
name: "\u5220\u9664\u5934\u50CF",
perm: "master",
authFailMsg: "\u53EA\u6709\u4E3B\u4EBA\u624D\u80FD\u5220\u9664\u5934\u50CF\u54E6~"
});
var avatarInfo = karin.command("^(#)?\u5934\u50CF\u4FE1\u606F$", async (e) => {
try {
const config = avatarConfig.getConfig();
let infoMsg = "\u{1F4CA} \u5934\u50CF\u6587\u4EF6\u4FE1\u606F\n";
infoMsg += "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n";
const dayInfo = fileUploadHandler.getFileInfo(config.dayAvatarPath);
infoMsg += `\u{1F305} \u767D\u5929\u5934\u50CF:
`;
if (dayInfo.exists) {
infoMsg += ` \u2705 \u6587\u4EF6\u5B58\u5728 (${dayInfo.sizeText})
`;
infoMsg += ` \u{1F4C1} ${config.dayAvatarPath}
`;
} else {
infoMsg += ` \u274C \u672A\u914D\u7F6E\u6216\u6587\u4EF6\u4E0D\u5B58\u5728
`;
}
infoMsg += "\n";
const nightInfo = fileUploadHandler.getFileInfo(config.nightAvatarPath);
infoMsg += `\u{1F319} \u665A\u4E0A\u5934\u50CF:
`;
if (nightInfo.exists) {
infoMsg += ` \u2705 \u6587\u4EF6\u5B58\u5728 (${nightInfo.sizeText})
`;
infoMsg += ` \u{1F4C1} ${config.nightAvatarPath}
`;
} else {
infoMsg += ` \u274C \u672A\u914D\u7F6E\u6216\u6587\u4EF6\u4E0D\u5B58\u5728
`;
}
infoMsg += "\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n";
infoMsg += "\u{1F4A1} \u4F7F\u7528 #\u4E0A\u4F20\u5934\u50CF \u767D\u5929/\u665A\u4E0A \u6765\u4E0A\u4F20\u65B0\u5934\u50CF";
await e.reply(infoMsg);
} catch (error) {
logger2.error("[\u5934\u50CF\u4FE1\u606F] \u83B7\u53D6\u5934\u50CF\u4FE1\u606F\u5931\u8D25:", error);
await e.reply(`\u274C \u83B7\u53D6\u5934\u50CF\u4FE1\u606F\u5931\u8D25: ${error}`);
}
return true;
}, {
name: "\u5934\u50CF\u4FE1\u606F",
perm: "master",
authFailMsg: "\u53EA\u6709\u4E3B\u4EBA\u624D\u80FD\u67E5\u770B\u5934\u50CF\u4FE1\u606F\u54E6~"
});
function detectImageType(buffer) {
if (buffer.length < 4) return null;
if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
return "jpeg";
}
if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
return "png";
}
if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) {
return "gif";
}
if (buffer[0] === 66 && buffer[1] === 77) {
return "bmp";
}
if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70) {
if (buffer.length >= 12 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80) {
return "webp";
}
}
return null;
}
export {
avatarInfo,
avatarPreview,
avatarUploadApi,
deleteAvatar
};