koishi-plugin-memes
Version:
生成 Meme 表情包,支持 MemeGenerator API (v1)、内置模板和自定义 API 接口
1,124 lines (1,116 loc) • 53.3 kB
JavaScript
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,
logger: () => logger,
name: () => name,
usage: () => usage
});
module.exports = __toCommonJS(src_exports);
var import_koishi5 = require("koishi");
// src/api.ts
var import_koishi2 = require("koishi");
// src/utils.ts
var import_koishi = require("koishi");
var import_fs = __toESM(require("fs"));
function parseTarget(arg) {
if (!arg) return "";
try {
const atElement = import_koishi.h.select(import_koishi.h.parse(arg), "at")[0];
if (atElement?.attrs?.id) return atElement.attrs.id;
} catch {
}
const match = arg.match(/@(\d+)/);
if (match) return match[1];
const userId = arg.trim();
if (/^\d{5,10}$/.test(userId)) return userId;
return arg;
}
__name(parseTarget, "parseTarget");
async function getUserAvatar(session, userId) {
const targetId = userId || session.userId;
return targetId === session.userId && session.user?.avatar ? session.user.avatar : `https://q1.qlogo.cn/g?b=qq&nk=${targetId}&s=640`;
}
__name(getUserAvatar, "getUserAvatar");
async function autoRecall(session, message, delay = 1e4) {
if (!message) return null;
try {
const msg = typeof message === "string" ? await session.send(message) : message;
setTimeout(() => session.bot?.deleteMessage(session.channelId, msg.toString()).catch(() => {
}), delay);
return null;
} catch {
return null;
}
}
__name(autoRecall, "autoRecall");
function readJsonFile(filePath, logger2) {
try {
if (!import_fs.default.existsSync(filePath)) return null;
const data = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
return data;
} catch (err) {
logger2.error(`读取文件失败:${filePath} - ${err.message}`);
return null;
}
}
__name(readJsonFile, "readJsonFile");
function writeJsonFile(filePath, data, logger2) {
try {
import_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
return true;
} catch (err) {
logger2.error(`写入文件失败:${filePath} - ${err.message}`);
return false;
}
}
__name(writeJsonFile, "writeJsonFile");
function loadOrCreateConfig(filePath, defaultConfig, logger2) {
const data = readJsonFile(filePath, logger2);
return data !== null ? data : writeJsonFile(filePath, defaultConfig, logger2) ? defaultConfig : defaultConfig;
}
__name(loadOrCreateConfig, "loadOrCreateConfig");
async function apiRequest(url, options = {}, logger2) {
const { method = "get", data, formData, responseType = "json", timeout = 8e3 } = options;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort("请求超时"), timeout);
const fetchOptions = {
method: method.toUpperCase(),
signal: controller.signal,
headers: formData ? { "Accept": "image/*,application/json" } : data ? { "Content-Type": "application/json" } : {},
body: formData || (data ? JSON.stringify(data) : void 0)
};
const response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
if (!response.ok) {
let errorMessage = `HTTP状态码 ${response.status}`;
try {
if (responseType === "arraybuffer") {
const errText = Buffer.from(await response.arrayBuffer()).toString("utf-8");
try {
errorMessage = JSON.parse(errText)?.error || errorMessage;
} catch {
}
} else {
const errJson = await response.json().catch(() => null);
errorMessage = errJson?.error || errJson?.message || errorMessage;
}
} catch {
}
logger2.warn(`API请求失败: ${url} - ${errorMessage}`);
return null;
}
return responseType === "arraybuffer" ? Buffer.from(await response.arrayBuffer()) : await response.json();
} catch (e) {
logger2.error(`API请求异常: ${url} - ${e.message}`);
return null;
}
}
__name(apiRequest, "apiRequest");
async function renderTemplateListAsImage(ctx, title, templates) {
const page = await ctx.puppeteer.page();
try {
const columnCount = Math.min(Math.ceil(templates.length / 50), 6);
const itemsPerColumn = Math.ceil(templates.length / columnCount);
const columnWidth = 190;
const containerWidth = columnWidth * columnCount + (columnCount - 1) * 8 + 24;
const columns = Array.from(
{ length: columnCount },
(_, i) => templates.slice(i * itemsPerColumn, Math.min((i + 1) * itemsPerColumn, templates.length))
);
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { margin: 0; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; color: #2b333e; font-size: 14px; }
.container { margin: 12px; background: #fff; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);
padding: 14px; width: ${containerWidth}px; }
header { text-align: center; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid rgba(0,0,0,0.08); }
h1 { font-size: 20px; font-weight: 600; margin: 0 0 4px 0; }
.sub-title { font-size: 13px; color: #5c7080; }
.columns-wrap { display: flex; gap: 8px; }
.column { flex: 1; background: #f8f9fa; border-radius: 8px; border: 1px solid rgba(0,0,0,0.04);
padding: 6px 4px; width: ${columnWidth}px; }
.item { display: flex; align-items: center; padding: 3px 6px; border-radius: 4px; margin-bottom: 1px; }
.item:hover { background: #edf4ff; }
.icons { position: relative; width: 24px; height: 16px; margin-right: 8px; flex-shrink: 0; }
.keywords { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; font-size: 14.5px; }
.kw { color: #2b333e; }
.kw:not(:last-child):after { content: ","; color: #aaa; margin-right: 2px; }
.icon { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center;
position: absolute; top: 0; border-radius: 3px; box-shadow: 0 1px 2px rgba(0,0,0,0.15); }
.text-icon { background: #3e90ff; left: 0; z-index: 1; }
.image-icon { background: #38b48b; left: 10px; z-index: 2; }
.text-icon:only-child, .image-icon:only-child { left: 4px; }
svg { width: 12px; height: 12px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>${title}</h1>
<div class="sub-title">共 ${templates.length} 个模板</div>
</header>
<div class="columns-wrap">
${columns.map((items) => `
<div class="column">
${items.map((template) => {
const imgCount = (template.imgReq?.match(/图片(\d+)/)?.[1] || 0) * 1;
const textCount = (template.textReq?.match(/文本(\d+)/)?.[1] || 0) * 1;
return `<div class="item">
<div class="icons">
${textCount > 0 ? '<span class="icon text-icon"><svg viewBox="0 0 16 16" fill="none" stroke="white" stroke-width="2"><path d="M2 4h12M4 8h8M2 12h12"/></svg></span>' : ""}
${imgCount > 0 ? '<span class="icon image-icon"><svg viewBox="0 0 16 16" fill="none" stroke="white" stroke-width="2"><rect x="2" y="2" width="12" height="12" rx="1"/><circle cx="5.5" cy="5.5" r="1"/><path d="M13 10l-3-3-6 6"/></svg></span>' : ""}
</div>
<div class="keywords">
${template.keywords.map((k) => `<span class="kw">${k}</span>`).join("")}
</div>
</div>`;
}).join("")}
</div>
`).join("")}
</div>
</div>
</body>
</html>`;
await page.setViewport({ width: containerWidth + 24, height: 600, deviceScaleFactor: 1.5 });
await page.setContent(html);
await page.waitForFunction(() => document.fonts.ready).catch(() => {
});
const { width, height } = await page.evaluate(() => {
const container = document.querySelector(".container");
return { width: container.offsetWidth + 24, height: container.offsetHeight + 24 };
});
await page.setViewport({ width, height, deviceScaleFactor: 1.5 });
return await page.screenshot({ type: "png", omitBackground: true });
} finally {
await page.close();
}
}
__name(renderTemplateListAsImage, "renderTemplateListAsImage");
async function renderTemplateInfoAsImage(ctx, template, previewImgUrl) {
const page = await ctx.puppeteer.page();
try {
const keywords = Array.isArray(template.keywords) ? template.keywords : [template.keywords].filter(Boolean);
const title = `${keywords.join(", ")} (${template.id})`;
const tags = Array.isArray(template.tags) ? template.tags : [];
const pt = template.params_type || {};
const sections = [
// 参数需求
{
title: "参数需求",
className: "requirements",
itemClass: "req-item",
items: [
`图片: ${pt.min_images || 0}${pt.min_images !== pt.max_images ? `-${pt.max_images || "∞"}` : ""}张`,
`文本: ${pt.min_texts || 0}${pt.min_texts !== pt.max_texts ? `-${pt.max_texts || "∞"}` : ""}条`,
...pt.default_texts?.length ? [`默认文本: ${pt.default_texts.join(", ")}`] : []
]
},
// 其他参数
{
title: "其他参数",
items: Object.entries(pt.args_type?.args_model?.properties || {}).filter(([key]) => key !== "user_infos").map(([key, prop]) => {
const prop_obj = prop;
let desc = key;
if (prop_obj.type) {
let typeStr = prop_obj.type;
if (prop_obj.type === "array" && prop_obj.items?.$ref) {
typeStr = `${prop_obj.type}<${prop_obj.items.$ref.replace("#/$defs/", "").split("/")[0]}>`;
}
desc += ` (${typeStr})`;
}
if (prop_obj.default !== void 0) desc += ` 默认值: ${JSON.stringify(prop_obj.default)}`;
if (prop_obj.description) desc += ` - ${prop_obj.description}`;
if (prop_obj.enum?.length) desc += ` [可选值: ${prop_obj.enum.join(", ")}]`;
return desc;
})
},
// 命令行参数
{
title: "命令行参数",
items: (pt.args_type?.parser_options || []).map((opt) => {
let desc = `${opt.names.join(", ")}`;
if (opt.args?.length) {
desc += ` ${opt.args.map((arg) => {
let argDesc = arg.name;
if (arg.value) argDesc += `:${arg.value}`;
if (arg.default != null) argDesc += `=${arg.default}`;
return argDesc;
}).join(" ")}`;
}
return opt.help_text ? `${desc} - ${opt.help_text}` : desc;
})
},
// 快捷指令
{
title: "快捷指令",
items: (template.shortcuts || []).map((s) => `${s.humanized || s.key}${s.args?.length ? ` (参数: ${s.args.join(" ")})` : ""}`)
}
];
const timeInfo = [
template.date_created && `创建时间: ${template.date_created}`,
template.date_modified && `修改时间: ${template.date_modified}`
].filter(Boolean);
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { margin: 0; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; color: #2b333e; font-size: 14px; }
.container { margin: 12px; background: #fff; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);
padding: 16px; max-width: 800px; }
header { margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid rgba(0,0,0,0.08); }
h1 { font-size: 22px; font-weight: 600; margin: 0 0 6px 0; }
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.tag { background: #f0f5ff; color: #3e90ff; border-radius: 4px; padding: 2px 8px; font-size: 13px; }
.preview { margin: 16px 0; text-align: center; }
.preview img { max-width: 100%; max-height: 300px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.1); }
.section { margin-bottom: 16px; }
.section h2 { font-size: 16px; font-weight: 600; margin: 0 0 8px 0; color: #3e90ff; }
.item { margin-bottom: 8px; background: #f8f9fa; border-radius: 6px; padding: 8px 12px; }
.requirements { display: flex; flex-wrap: wrap; gap: 12px; }
.req-item { background: #f0f7ff; border-left: 3px solid #3e90ff; padding: 6px 10px; border-radius: 0 4px 4px 0; }
.info-row { color: #666; font-size: 13px; margin-top: 16px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>${title}</h1>
${tags.length ? `<div class="tags">${tags.map((tag) => `<span class="tag">${tag}</span>`).join("")}</div>` : ""}
</header>
${previewImgUrl ? `<div class="preview"><img src="${previewImgUrl}" alt="预览图"></div>` : ""}
${sections.map((section) => section.items.length ? `
<div class="section">
<h2>${section.title}</h2>
<div class="${section.className || "normal"}">
${section.items.map((item) => `<div class="${section.itemClass || "item"}">${item}</div>`).join("")}
</div>
</div>
` : "").join("")}
${timeInfo.length ? `<div class="info-row">${timeInfo.join(" · ")}</div>` : ""}
</div>
</body>
</html>`;
await page.setViewport({ width: 800, height: 600, deviceScaleFactor: 1.5 });
await page.setContent(html);
await page.waitForFunction(() => document.fonts.ready).catch(() => {
});
const { width, height } = await page.evaluate(() => {
const container = document.querySelector(".container");
return { width: container.offsetWidth + 24, height: container.offsetHeight + 24 };
});
await page.setViewport({ width, height, deviceScaleFactor: 1.5 });
return await page.screenshot({ type: "png", omitBackground: true });
} finally {
await page.close();
}
}
__name(renderTemplateInfoAsImage, "renderTemplateInfoAsImage");
// src/api.ts
var import_path = __toESM(require("path"));
var MemeAPI = class {
/**
* 创建一个 MemeAPI 实例
* @param ctx Koishi 上下文对象
* @param logger 日志记录器
*/
constructor(ctx, logger2) {
this.ctx = ctx;
this.logger = logger2;
this.configPath = import_path.default.resolve(this.ctx.baseDir, "data", "memes-api.json");
const defaultConfig = [{
description: "示例配置",
apiEndpoint: "https://example.com/api?qq=${arg1}&target=${arg2}"
}];
loadOrCreateConfig(this.configPath, defaultConfig, this.logger);
}
static {
__name(this, "MemeAPI");
}
configPath;
/**
* 注册所有表情相关的子命令
* @param meme 父命令对象
*/
registerCommands(meme) {
const api = meme.subcommand("meme [page:string]", "自定义表情生成").usage("使用自定义 API 生成表情\n查看自定义 API 表情模板列表").example("meme all - 查看表情模板列表").action(async ({}, page) => {
if (typeof page === "string" && page.trim().toLowerCase() === "make") return "请使用 meme.make 来生成自定义表情";
const apiConfigs = readJsonFile(this.configPath, this.logger) || [];
const typeDescriptions = apiConfigs.map((config) => config.description);
const lines = [];
let currentLine = "";
let currentWidth = 0;
const MAX_WIDTH = 36;
for (const desc of typeDescriptions) {
let descWidth = 0;
for (const char of desc) {
descWidth += /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) ? 2 : 1;
}
if (currentWidth + descWidth + 1 > MAX_WIDTH && currentWidth > 0) {
lines.push(currentLine);
currentLine = desc;
currentWidth = descWidth;
} else if (currentLine.length === 0) {
currentLine = desc;
currentWidth = descWidth;
} else {
currentLine += " " + desc;
currentWidth += 1 + descWidth;
}
}
if (currentLine) lines.push(currentLine);
const ITEMS_PER_PAGE = 10;
const showAll = page === "all";
const pageNum = parseInt(page) || 1;
const totalPages = Math.ceil(lines.length / ITEMS_PER_PAGE);
const validPage = Math.max(1, Math.min(pageNum, showAll ? 1 : totalPages));
const displayLines = showAll ? lines : lines.slice((validPage - 1) * ITEMS_PER_PAGE, validPage * ITEMS_PER_PAGE);
const header = showAll || totalPages <= 1 ? `表情模板列表(共${apiConfigs.length}项)
` : `表情模板列表(${validPage}/${totalPages}页)
`;
return header + displayLines.join("\n");
});
api.subcommand(".make [type:string] [arg1:string] [arg2:string]", "生成自定义表情").usage("使用自定义 API 生成表情\n将替换${arg1}和${arg2}参数\n支持@用户和QQ号").action(async ({ session }, type, arg1, arg2) => {
try {
const apiConfigs = readJsonFile(this.configPath, this.logger) || [];
const index = !type ? Math.floor(Math.random() * apiConfigs.length) : apiConfigs.findIndex(
(config) => config.description.split("|")[0].trim() === type.trim()
);
if (index === -1) return autoRecall(session, `未找到表情"${type}"`);
const apiUrl = apiConfigs[index].apiEndpoint.replace(/\${arg1}/g, parseTarget(arg1 || "")).replace(/\${arg2}/g, parseTarget(arg2 || ""));
const response = await fetch(apiUrl, { signal: AbortSignal.timeout(8e3) });
let imageUrl = apiUrl;
if (response.headers.get("content-type")?.includes("application/json")) {
const data = await response.json();
if (data?.code === 200) imageUrl = data.data;
}
return (0, import_koishi2.h)("image", { url: imageUrl });
} catch (err) {
return autoRecall(session, "生成出错:" + err.message);
}
});
}
};
// src/make.ts
var import_koishi3 = require("koishi");
var import_path2 = __toESM(require("path"));
var import_fs2 = __toESM(require("fs"));
var MemeMaker = class {
static {
__name(this, "MemeMaker");
}
ctx;
/**
* 图片配置参数
* @private
* @readonly
* @property {Object} sizes - 不同尺寸配置
* @property {Object} styles - 不同样式配置
*/
IMAGE_CONFIG = {
sizes: {
standard: { width: 1280, height: 720 },
square: { width: 800, height: 800 },
small: { width: 640, height: 360 }
},
styles: {
jiazi: { background: "PCL-Jiazi.jpg", avatarSize: 400, avatarTop: 60, borderRadius: 8 },
tntboom: { background: "HMCL-Boom.jpg", avatarSize: 320, avatarTop: 20, borderRadius: 8 },
zhuo: { background: "PCLCE-Zhuo.jpg", avatarSize: 400, avatarTop: 60, borderRadius: 8 }
}
};
/**
* 创建表情生成器实例
* @constructor
* @param {Context} ctx - Koishi 上下文实例
*/
constructor(ctx) {
this.ctx = ctx;
Object.keys(this.IMAGE_CONFIG.styles).forEach((key) => {
this.IMAGE_CONFIG.styles[key].background = import_path2.default.resolve(__dirname, "./assets", this.IMAGE_CONFIG.styles[key].background);
});
}
/**
* 将HTML内容渲染为图片
* @async
* @param {string} html - 要渲染的HTML内容
* @param {Object} options - 渲染选项
* @param {number} [options.width] - 图片宽度
* @param {number} [options.height] - 图片高度
* @returns {Promise<Buffer>} 生成的图片Buffer
* @throws {Error} 渲染过程中的错误
*/
async htmlToImage(html, { width, height } = {}) {
const page = await this.ctx.puppeteer.page();
try {
await page.setViewport({ width, height, deviceScaleFactor: 2 });
await page.setContent(
`<!DOCTYPE html><html><head><meta charset="UTF-8"><style>body{margin:0;padding:0;overflow:hidden;}</style></head><body>${html}</body></html>`,
{ waitUntil: "networkidle0" }
);
await page.evaluate(() => Promise.all(
Array.from(document.querySelectorAll("img")).map(
(img) => img.complete ? Promise.resolve() : new Promise((resolve) => {
img.addEventListener("load", resolve);
img.addEventListener("error", resolve);
})
)
));
return await page.screenshot({ type: "png", fullPage: false });
} finally {
await page.close();
}
}
/**
* 生成头像效果合成图
* @async
* @param {string} avatarUrl - 头像URL或本地文件路径
* @param {string} style - 使用的样式名称
* @returns {Promise<Buffer>} 生成的图片Buffer
* @throws {Error} 生成过程中的错误
*/
async generateAvatarEffect(avatarUrl, style) {
const styleConfig = this.IMAGE_CONFIG.styles[style] || this.IMAGE_CONFIG.styles.jiazi;
const sizeConfig = this.IMAGE_CONFIG.sizes.standard;
const getImageSrc = /* @__PURE__ */ __name((url) => {
if (url?.startsWith("http")) return url;
const filePath = url?.replace("file://", "");
return filePath && import_fs2.default.existsSync(filePath) ? `data:image/jpeg;base64,${import_fs2.default.readFileSync(filePath).toString("base64")}` : null;
}, "getImageSrc");
const avatarImageSrc = getImageSrc(avatarUrl);
const backgroundImage = getImageSrc(`file://${styleConfig.background}`);
const html = `
<div style="width:${sizeConfig.width}px;height:${sizeConfig.height}px;position:relative;margin:0;padding:0;overflow:hidden;">
<img style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;" src="${backgroundImage}" />
${avatarImageSrc ? `<img style="position:absolute;top:${styleConfig.avatarTop}px;left:50%;transform:translateX(-50%);
width:${styleConfig.avatarSize}px;height:${styleConfig.avatarSize}px;object-fit:cover;
z-index:2;border-radius:${styleConfig.borderRadius}px;box-shadow:0 5px 15px rgba(0,0,0,0.3);"
src="${avatarImageSrc}" />` : ""}
</div>`;
return this.htmlToImage(html, sizeConfig);
}
/**
* 注册表情生成相关命令
* @param {Command} parentCommand - 父命令实例
* @returns {Command} 创建的子命令
*/
registerCommands(parentCommand) {
const make = parentCommand.subcommand("make", "内置图片表情生成").usage("使用内置模板生成表情图片");
const descriptions = {
jiazi: '生成"你要被夹"图片',
tntboom: '生成"你要被炸"图片',
zhuo: '生成"你要被捉"图片'
};
Object.keys(this.IMAGE_CONFIG.styles).forEach((style) => {
make.subcommand(`.${style} [target:text]`, descriptions[style]).usage(`根据用户头像生成${descriptions[style] || style}
不指定用户时使用自己的头像`).example(`make.${style} @用户 - 使用@用户的头像生成图片`).example(`make.${style} 123456789 - 使用指定QQ号生成图片`).action(async ({ session }, target) => {
try {
const userId = target ? parseTarget(target) || session.userId : session.userId;
const avatar = await getUserAvatar(session, userId);
const result = await this.generateAvatarEffect(avatar, style);
return import_koishi3.h.image(result, "image/png");
} catch (error) {
return autoRecall(session, "生成出错:" + error.message);
}
});
});
return make;
}
};
// src/generator.ts
var import_koishi4 = require("koishi");
var import_path3 = __toESM(require("path"));
var MemeGenerator = class {
/**
* 创建表情包生成器实例
* @param {Context} ctx - Koishi 上下文
* @param {Logger} logger - 日志记录器
* @param {string} apiUrl - API服务器地址
*/
constructor(ctx, logger2, apiUrl = "") {
this.ctx = ctx;
this.logger = logger2;
this.apiUrl = apiUrl;
this.apiUrl = apiUrl?.trim().replace(/\/+$/, "");
this.cachePath = import_path3.default.resolve(this.ctx.baseDir, "data", "memes.json");
const cacheData = readJsonFile(this.cachePath, this.logger);
this.memeCache = cacheData?.data || [];
this.memeCache.length ? this.logger.info(`已加载缓存文件(${this.memeCache.length}项)`) : this.refreshCache();
}
static {
__name(this, "MemeGenerator");
}
memeCache = [];
cachePath;
/**
* 刷新模板缓存
* 从API获取最新的模板列表和信息,并更新本地缓存
* @returns {Promise<MemeInfo[]>} 更新后的模板列表
*/
async refreshCache() {
try {
const keys = await apiRequest(`${this.apiUrl}/memes/keys`, {}, this.logger);
if (!keys?.length) {
this.logger.warn("获取模板列表失败或为空");
return [];
}
this.logger.info(`已获取模板ID: ${keys.length}个`);
const templates = await Promise.all(keys.map(async (key) => {
try {
const info = await apiRequest(`${this.apiUrl}/memes/${key}/info`, {}, this.logger);
return {
id: key,
keywords: info?.keywords ? (Array.isArray(info.keywords) ? info.keywords : [info.keywords]).filter(Boolean) : [],
tags: info?.tags && Array.isArray(info.tags) ? info.tags : [],
params_type: info?.params_type || {},
...info || {}
};
} catch (e) {
this.logger.warn(`获取模板[${key}]信息失败:${e.message}`);
return { id: key, keywords: [], tags: [], params_type: {} };
}
}));
writeJsonFile(this.cachePath, { time: Date.now(), data: templates }, this.logger);
this.memeCache = templates;
return templates;
} catch (e) {
this.logger.error(`刷新缓存失败: ${e.message}`);
return [];
}
}
/**
* 匹配模板关键词
* 根据提供的关键词查找匹配的模板,并按优先级排序
* @param {string} key - 要匹配的关键词
* @returns {MemeInfo[]} 匹配到的模板列表,按匹配优先级排序
*/
matchTemplates(key) {
if (!key || !this.memeCache.length) return [];
return this.memeCache.map((template) => {
let priority = 99;
if (template.id === key || template.keywords?.some((k) => k === key)) priority = 1;
else if (template.keywords?.some((k) => k.includes(key))) priority = 2;
else if (template.keywords?.some((k) => key.includes(k))) priority = 3;
else if (template.id.includes(key)) priority = 4;
else if (template.tags?.some((tag) => tag === key || tag.includes(key))) priority = 5;
return { template, priority };
}).filter((item) => item.priority < 99).sort((a, b) => a.priority - b.priority).map((item) => item.template);
}
/**
* 查找表情包模板
* 先从缓存中查找,如果找不到则尝试从API获取
* @param {string} key - 要查找的模板ID或关键词
* @param {boolean} fuzzy - 是否启用模糊匹配,默认为true
* @returns {Promise<MemeInfo | null>} 找到的模板信息,如果未找到则返回null
*/
async findTemplate(key, fuzzy = true) {
const matchedTemplates = fuzzy ? this.matchTemplates(key) : this.memeCache.filter((t) => t.id === key || t.keywords?.some((k) => k === key));
if (matchedTemplates.length > 0) return matchedTemplates[0];
if (this.apiUrl) {
try {
const info = await apiRequest(`${this.apiUrl}/memes/${key}/info`, {}, this.logger);
if (info) {
return {
id: key,
keywords: info.keywords ? (Array.isArray(info.keywords) ? info.keywords : [info.keywords]).filter(Boolean) : [],
tags: info.tags && Array.isArray(info.tags) ? info.tags : [],
params_type: info.params_type || {},
...info || {}
};
}
} catch (e) {
this.logger.warn(`从API获取模板[${key}]信息失败:${e.message}`);
}
}
return null;
}
/**
* 获取所有关键词到模板ID的映射
* 用于快速查找和自动补全功能
* @returns {Map<string, string>} 关键词到模板ID的映射表
*/
getAllKeywordMappings() {
const keywordMap = /* @__PURE__ */ new Map();
this.memeCache.forEach((template) => {
keywordMap.set(template.id, template.id);
if (Array.isArray(template.keywords)) {
template.keywords.forEach((keyword) => keyword && keywordMap.set(keyword, template.id));
}
});
return keywordMap;
}
/**
* 生成表情包
* 解析指令参数,获取所需的图片和文本,然后调用API生成表情包
* @param {any} session - 消息会话对象
* @param {string} key - 模板ID或关键词
* @param {h[]} args - 参数元素列表
* @returns {Promise<h | string>} 生成的表情包图片元素或错误消息
*/
async generateMeme(session, key, args) {
try {
const templateInfo = await this.findTemplate(key);
if (!templateInfo) return autoRecall(session, `获取模板信息失败: ${key}`);
const tempId = templateInfo.id || key;
const {
min_images = 0,
max_images = 0,
min_texts = 0,
max_texts = 0,
default_texts = []
} = templateInfo.params_type || {};
const imageInfos = [];
const texts = [];
const options = {};
let allText = "";
if (session.quote?.elements) {
const processElement2 = /* @__PURE__ */ __name((e) => {
if (e.type === "img" && e.attrs.src) imageInfos.push({ src: e.attrs.src });
if (e.children?.length) e.children.forEach(processElement2);
}, "processElement");
session.quote.elements.forEach(processElement2);
}
const processElement = /* @__PURE__ */ __name((e) => {
if (e.type === "text" && e.attrs.content) {
let text = e.attrs.content.replace(/<at id=['"]?([0-9]+)['"]?\/>/g, (_, userId) => {
userId && imageInfos.push({ userId });
return " ";
}).replace(/<img[^>]*src=['"]([^'"]+)['"][^>]*\/?>/g, (_, src) => {
src && imageInfos.push({ src });
return " ";
});
allText += text + " ";
} else if (e.type === "at" && e.attrs.id) imageInfos.push({ userId: e.attrs.id });
else if (e.type === "img" && e.attrs.src) imageInfos.push({ src: e.attrs.src });
e.children?.length && e.children.forEach(processElement);
}, "processElement");
args.forEach(processElement);
if (allText.trim()) {
const tokens = allText.trim().match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
tokens.forEach((token) => {
if (token.startsWith("-")) {
const optMatch = token.match(/^-([a-zA-Z0-9_-]+)(?:=(.*))?$/);
if (optMatch) {
const [, key2, rawValue = "true"] = optMatch;
let value = rawValue;
if (rawValue === "true") value = true;
else if (rawValue === "false") value = false;
else if (/^-?\d+$/.test(rawValue)) value = parseInt(rawValue, 10);
else if (/^-?\d+\.\d+$/.test(rawValue)) value = parseFloat(rawValue);
options[key2] = value;
}
} else if (token.startsWith("<at") || token.startsWith("@")) {
const userId = token.startsWith("<at") ? parseTarget(token) : token.match(/@(\d+)/)?.[1];
userId ? imageInfos.push({ userId }) : token.startsWith("@") && texts.push(token);
} else {
texts.push(token.replace(/^(['"])(.*)\1$/, "$2"));
}
});
}
const properties = templateInfo?.params_type?.args_type?.args_model?.properties || {};
Object.entries(properties).forEach(([key2, prop]) => {
const typedProp = prop;
if (key2 in options && key2 !== "user_infos") {
const value = options[key2];
if (typedProp.type === "integer" && typeof value !== "number")
options[key2] = parseInt(String(value), 10);
else if (typedProp.type === "number" && typeof value !== "number")
options[key2] = parseFloat(String(value));
else if (typedProp.type === "boolean" && typeof value !== "boolean")
options[key2] = value === "true" || value === "1" || value === 1;
}
});
let origImageInfos = [...imageInfos];
let origTexts = [...texts];
const needSelfAvatar = min_images === 1 && !origImageInfos.length || origImageInfos.length && origImageInfos.length + 1 === min_images;
if (needSelfAvatar) origImageInfos = [{ userId: session.userId }, ...origImageInfos];
if (!origTexts.length && default_texts.length) origTexts = [...default_texts];
const checkCount = /* @__PURE__ */ __name((count, min, max, type) => {
if (min != null && count < min || max != null && count > max) {
const rangeText = min === max ? min : min != null && max != null ? `${min}~${max}` : min != null ? `至少${min}` : `最多${max}`;
throw new Error(`当前${count}${type === "图片" ? "张" : "条"}${type},需要${rangeText}${type === "图片" ? "张" : "条"}${type}`);
}
}, "checkCount");
checkCount(origImageInfos.length, min_images, max_images, "图片");
checkCount(origTexts.length, min_texts, max_texts, "文本");
const images = [];
const userInfos = [];
await Promise.all(origImageInfos.map(async (info, index) => {
try {
const url = "src" in info ? info.src : await getUserAvatar(session, info.userId);
const userInfo = "userId" in info ? { name: info.userId } : {};
const response = await fetch(url, { signal: AbortSignal.timeout(5e3) });
if (!response.ok) throw new Error(`HTTP状态码 ${response.status}`);
const contentType = response.headers.get("content-type") || "image/png";
const buffer = Buffer.from(await response.arrayBuffer());
images[index] = new Blob([buffer], { type: contentType });
userInfos[index] = userInfo;
} catch {
images[index] = new Blob([], { type: "image/png" });
userInfos[index] = "userId" in info ? { name: info.userId } : {};
}
}));
const formData = new FormData();
origTexts.forEach((text) => formData.append("texts", text));
images.forEach((img) => formData.append("images", img));
formData.append("args", JSON.stringify({
user_infos: userInfos,
...options
}));
const imageBuffer = await apiRequest(
`${this.apiUrl}/memes/${tempId}/`,
{ method: "post", formData, responseType: "arraybuffer", timeout: 1e4 },
this.logger
);
if (!imageBuffer) return autoRecall(session, "生成表情包失败:未获取到 API 数据");
return (0, import_koishi4.h)("image", { url: `data:image/png;base64,${Buffer.from(imageBuffer).toString("base64")}` });
} catch (e) {
return autoRecall(session, e.message);
}
}
};
// src/index.ts
var name = "memes";
var inject = { optional: ["puppeteer"] };
var logger = new import_koishi5.Logger("memes");
var usage = `
<div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<h2 style="margin-top: 0; color: #4a6ee0;">📌 插件说明</h2>
<p>📖 <strong>使用文档</strong>:请点击左上角的 <strong>插件主页</strong> 查看插件使用文档</p>
<p>🔍 <strong>更多插件</strong>:可访问 <a href="https://github.com/YisRime" style="color:#4a6ee0;text-decoration:none;">苡淞的 GitHub</a> 查看本人的所有插件</p>
</div>
<div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<h2 style="margin-top: 0; color: #e0574a;">❤️ 支持与反馈</h2>
<p>🌟 喜欢这个插件?请在 <a href="https://github.com/YisRime" style="color:#e0574a;text-decoration:none;">GitHub</a> 上给我一个 Star!</p>
<p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
</div>
`;
var Config = import_koishi5.Schema.object({
loadApi: import_koishi5.Schema.boolean().description("开启自定义 API 生成").default(false),
loadInternal: import_koishi5.Schema.boolean().description("开启内置图片生成").default(false),
genUrl: import_koishi5.Schema.string().description("MemeGenerator API 配置").default("http://localhost:2233"),
useMiddleware: import_koishi5.Schema.boolean().description("开启关键词匹配中间件").default(false),
requirePrefix: import_koishi5.Schema.boolean().description("开启关键词匹配指令前缀").default(true),
blacklist: import_koishi5.Schema.string().description("禁止生成黑名单(英文逗号分隔)").role("textarea")
});
function apply(ctx, config) {
const apiUrl = config.genUrl?.trim().replace(/\/+$/, "") || "";
const memeGenerator = new MemeGenerator(ctx, logger, apiUrl);
const memeMaker = new MemeMaker(ctx);
let keywordMap = /* @__PURE__ */ new Map();
const blacklistArr = (config.blacklist || "").split(",").map((s) => s.trim()).filter(Boolean);
const meme = ctx.command("memes [page:string]", "表情生成").usage("可通过 MemeGenerator 生成表情\n也可自定义 API 生成表情").example("memes - 查看所有表情模板").example("memes 2 - 仅在文本模式下查看第2页模板列表").action(async ({ session }, page) => {
if (typeof page === "string" && page.trim().toLowerCase() === "make") return "请使用 memes.make 来生成表情";
try {
let keys = memeGenerator["memeCache"].length > 0 ? memeGenerator["memeCache"].map((t) => t.id) : await apiRequest(`${apiUrl}/memes/keys`, {}, logger) || [];
const allTemplates = await Promise.all(keys.map(async (key) => {
const cachedTemplate = memeGenerator["memeCache"].find((t) => t.id === key);
if (cachedTemplate) {
const { id, keywords = [], tags = [], params_type: pt = {} } = cachedTemplate;
const formatReq = /* @__PURE__ */ __name((min, max, type = "") => {
if (min === max && min) return `${type}${min}`;
if (min != null || max != null) return `${type}${min || 0}-${max || "∞"}`;
return "";
}, "formatReq");
return {
id,
keywords: Array.isArray(keywords) ? keywords : [keywords].filter(Boolean),
imgReq: formatReq(pt.min_images, pt.max_images, "图片"),
textReq: formatReq(pt.min_texts, pt.max_texts, "文本"),
tags: Array.isArray(tags) ? tags : []
};
}
try {
const info = await apiRequest(`${apiUrl}/memes/${key}/info`, {}, logger);
if (!info) return { id: key, keywords: [], imgReq: "", textReq: "", tags: [] };
const { keywords = [], tags = [], params_type: pt = {} } = info;
const formatReq = /* @__PURE__ */ __name((min, max, type = "") => {
if (min === max && min) return `${type}${min}`;
if (min != null || max != null) return `${type}${min || 0}-${max || "∞"}`;
return "";
}, "formatReq");
return {
id: key,
keywords: Array.isArray(keywords) ? keywords : [keywords].filter(Boolean),
imgReq: formatReq(pt?.min_images, pt?.max_images, "图片"),
textReq: formatReq(pt?.min_texts, pt?.max_texts, "文本"),
tags: Array.isArray(tags) ? tags : []
};
} catch {
return { id: key, keywords: [], imgReq: "", textReq: "", tags: [] };
}
}));
if (ctx.puppeteer) {
try {
const pageTitle = `表情模板列表`;
allTemplates.sort((a, b) => {
const keyA = a.keywords[0] || a.id;
const keyB = b.keywords[0] || b.id;
return keyA.localeCompare(keyB, "zh-CN");
});
return renderTemplateListAsImage(ctx, pageTitle, allTemplates).then(
(buffer) => (0, import_koishi5.h)("image", { url: `data:image/png;base64,${buffer.toString("base64")}` })
);
} catch (err) {
logger.error("渲染模板列表图片失败:", err);
}
}
const allKeywords = [];
allTemplates.forEach((template) => {
if (template.keywords.length > 0) allKeywords.push(...template.keywords);
else allKeywords.push(template.id);
});
const lines = [];
let currentLine = "";
for (const keyword of allKeywords) {
const separator = currentLine ? " " : "";
let displayWidth = 0;
const testStr = currentLine + separator + keyword;
for (let i = 0; i < testStr.length; i++) {
displayWidth += /[\u4e00-\u9fa5\uff00-\uffff]/.test(testStr[i]) ? 2 : 1;
}
if (displayWidth <= 36) {
currentLine += separator + keyword;
} else {
lines.push(currentLine);
currentLine = keyword;
}
}
if (currentLine) lines.push(currentLine);
const LINES_PER_PAGE = 10;
const showAll = page === "all";
const pageNum = typeof page === "string" ? parseInt(page) || 1 : 1;
const totalPages = Math.ceil(lines.length / LINES_PER_PAGE);
const validPage = Math.max(1, Math.min(pageNum, totalPages));
const displayLines = showAll ? lines : lines.slice((validPage - 1) * LINES_PER_PAGE, validPage * LINES_PER_PAGE);
const header = showAll ? `表情模板列表(共${allTemplates.length}个)
` : totalPages > 1 ? `表情模板列表(${validPage}/${totalPages}页)
` : `表情模板列表(共${allTemplates.length}个)
`;
return header + displayLines.join("\n");
} catch (err) {
return autoRecall(session, `获取模板列表失败: ${err.message}`);
}
});
meme.subcommand(".make <key:string> [args:text]", "Meme 表情生成").usage('使用关键词或模板ID生成表情\n可添加文本、用户头像、图片等内容\n可用"-参数=值"来设置参数').example('memes.make ba_say 你好 -character=1 - 使用"ba_say"生成角色"心奈"的表情').example('memes.make 摸 @用户 - 使用"摸"生成表情').action(async ({ session }, key, args) => {
if (!key) return autoRecall(session, "请提供模板ID或关键词");
if (blacklistArr.includes(key)) return autoRecall(session, `已禁用生成该表情`);
const elements = args ? [(0, import_koishi5.h)("text", { content: args })] : [];
return memeGenerator.generateMeme(session, key, elements);
});
meme.subcommand(".info [key:string]", "获取模板信息").usage("查看指定模板的详细信息和参数\n包括需要的图片和文本数量和可选参数及示例").example('memes.info ba_say - 查看"ba_say"模板的详细信息').example('memes.info 摸 - 查看"摸"模板的详细信息').action(async ({ session }, key) => {
if (!key) return autoRecall(session, "请提供模板ID或关键词");
try {
const template = await memeGenerator.findTemplate(key);
if (!template) return autoRecall(session, `未找到表情模板"${key}"`);
const templateId = template.id;
let previewImageBuffer = null;
let previewImageBase64 = null;
try {
previewImageBuffer = await apiRequest(
`${apiUrl}/memes/${templateId}/preview`,
{ responseType: "arraybuffer", timeout: 8e3 },
logger
);
if (previewImageBuffer) {
previewImageBase64 = `data:image/png;base64,${Buffer.from(previewImageBuffer).toString("base64")}`;
}
} catch (err) {
logger.warn(`获取预览图失败: ${templateId}`);
}
if (ctx.puppeteer) {
try {
const infoImage = await renderTemplateInfoAsImage(ctx, template, previewImageBase64);
return (0, import_koishi5.h)("image", { url: `data:image/png;base64,${infoImage.toString("base64")}` });
} catch (err) {
logger.error("渲染模板信息图片失败:", err);
}
}
const response = [];
if (previewImageBuffer) {
response.push((0, import_koishi5.h)("image", { url: previewImageBase64 }));
}
const output = [];
const keywords = Array.isArray(template.keywords) ? template.keywords : [template.keywords].filter(Boolean);
output.push(`模板"${keywords.join(", ")}(${template.id})"详细信息:`);
if (template.tags?.length) output.push(`标签: ${template.tags.join(", ")}`);
const pt = template.params_type || {};
output.push("需要参数:");
output.push(`- 图片: ${pt.min_images || 0}${pt.max_images !== pt.min_images ? `-${pt.max_images}` : ""}张`);
output.push(`- 文本: ${pt.min_texts || 0}${pt.max_texts !== pt.min_texts ? `-${pt.max_texts}` : ""}条`);
if (pt.default_texts?.length) output.push(`- 默认文本: ${pt.default_texts.join(", ")}`);
if (pt.args_type?.args_model?.properties) {
output.push("其他参数:");
const properties = pt.args_type.args_model.properties;
const definitions = pt.args_type.args_model.$defs || {};
for (const key2 in properties) {
if (key2 === "user_infos") continue;
const prop = properties[key2];
let desc = `- ${key2}`;
if (prop.type) {
let typeStr = prop.type;
if (prop.type === "array" && prop.items?.$ref) {
const refType = prop.items.$ref.replace("#/$defs/", "").split("/")[0];
typeStr = `${prop.type}<${refType}>`;
}
desc += ` (${typeStr})`;
}
if (prop.default !== void 0) desc += ` 默认值: ${JSON.stringify(prop.default)}`;
if (prop.description) desc += ` - ${prop.description}`;
if (prop.enum?.length) desc += ` [可选值: ${prop.enum.join(", ")}]`;
output.push(desc);
}
if (Object.keys(definitions).length) {
output.push("类型定义:");
for (const typeName in definitions) {
output.push(`- ${typeName}:`);
const typeDef = definitions[typeName];
if (typeDef.properties) {
for (const propName in typeDef.properties) {
const prop = typeDef.properties[propName];
let propDesc = ` • ${propName}`;
if (prop.type) propDesc += ` (${prop.type})`;
if (prop.default !== void 0) propDesc += ` 默认值: ${JSON.stringify(prop.default)}`;
if (prop.description) propDesc += ` - ${prop.description}`;
if (prop.enum?.length) propDesc += ` [可选值: ${prop.enum.join(", ")}]`;
output.push(propDesc);
}
}
}
}
}
if (pt.args_type?.parser_options?.length) {
output.push("命令行参数:");
pt.args_type.parser_options.forEach((opt) => {
let desc = `- ${opt.names.join(", ")}`;
if (opt.args?.length) {
const argsText = opt.args.map((arg) => {
let argDesc = arg.name;
if (arg.value) argDesc += `:${arg.value}`;
if (arg.default !== null && arg.default !== void 0) argDesc += `=${arg.default}`;
return argDesc;
}).join(" ");
desc += ` ${argsText}`;
}
if (opt.help_text) desc += ` - ${opt.help_text}`;
output.push(desc);
});
}
if (pt.args_type?.args_examples?.length) {
output.push("参数示例:");
pt.args_type.args_examples.forEach((example, i) => {
output.push(`- 示例${i + 1}: ${JSON.stringify(example)}`);
});
}
if (template.shortcuts?.length) {
output.push("快捷指令:");
template.shortcuts.forEach((shortcut) => {
output.push(`- ${shortcut.humanized || shortcut.key}${shortcut.args?.length ? ` (参数: ${shortcut.args.join(" ")})` : ""}`);
});
}
if (template.date_created || template.date_modified) {
output.push(`创建时间: ${template.date_created}
修改时间: ${template.date_modified}`);
}
response.push((0, import_koishi5.h)("text", { content: output.join("\n") }));
return response;
} catch (err) {
return autoRecall(session, `获取模板信息失败: ${err.message}`);
}
});
meme.subcommand(".search <keyword:string>", "搜索模板表情").usage("根据关键词搜索表情模板\n可搜索模板ID、关键词或标签").example('memes.search 吃 - 搜索包含"吃"的表情模板').action(async ({ session }, keyword) => {
if (!keyword) return autoRecall(session, "请提供关键词");
try {
const results = await memeGenerator.matchTemplates(keyword);
if (!results?.length) return autoRecall(session, `未找到有关"${keyword}"的表情模板`);
const resultLines = results.map((t) => {
const keywords = Array.isArray(t.keywords) ? t.keywords.join(", ") : t.keywords || "";
let line = `${keywords}(${t.id})`;
if (t.tags?.length) line += ` #${t.tags.join("#")}`;
return line;
});
return `搜索结果(共${results.length}项):
${resultLines.join("\n")}`;
} catch (err) {
return autoRecall(session, `搜索失败: ${err.message}`);
}
});
meme.subcommand(".reload", "刷新模板缓存", { authority: 3 }).usage("刷新模板缓存,重新获取模板信息").action(async ({ session }) => {
try {
const result = await memeGenerator.refreshCache();
if (config.useMiddleware) keywordMap.clear();
logger.info(`已刷新缓存文件(${result.length}项)`);
return `已刷新缓存文件(${result.length}项)`;
} catch (err) {
return autoRecall(session, `刷新缓存失败:${err.message}`);
}
});
if (config.useMiddleware) {
ctx.on("message", async (session) => {
if (keywordMap.size === 0) {
keywordMap = memeGenerator.getAllKeywordMappings();
}
const rawContent = session.content;
if (!rawContent) return;
const elements = import_koishi5.h.parse(rawContent);
const firstTextElement = elements.find((el) => el.type === "text");
if (!firstTextElement?.attrs?.content) return;
let content = firstTextElement.attrs.content.trim();
if (config.requirePrefix) {
const prefixes = [].concat(ctx.root.config.prefix).filter(Boolean);
if (prefixes.length) {
const matched = prefixes.find((p) => content.startsWith(p));
if (!matched) return;
content = content.slice(matched.length).trim();
}
}
const spaceIndex = content.indexOf(" ");
const key = spaceIndex === -1 ? content : content.substring(0, spaceIndex);
if (blacklistArr.includes(key)) return;
const templateId = keywordMap.get(key);
if (!templateId) return;
const paramElements = [];
if (spaceIndex !== -1) {
const remainingText = content.substring(spaceIndex + 1).trim();
if (remainingText) paramElements.push((0, import_koishi5.h)("text", { content: remainingText }));
}
elements.forEach((element) => {
if (element !== firstTextElement) paramElements.push(element);
});
await session.send(await memeGenerator.generateMeme(session,