@shangxueink/koishi-plugin-qq-markdown-button
Version:
[<ruby>**QQ机器人按钮菜单**<rp>(</rp><rt>点我查看使用说明</rt><rp>)</rp></ruby>](https://www.npmjs.com/package/@shangxueink/koishi-plugin-qq-markdown-button) 自用小插件咪~ 使用json文件设置你的机器人菜单这样就不需要一堆配置项还很烧脑了。自用插件哦~
261 lines (240 loc) • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.apply = exports.Config = exports.usage = exports.inject = exports.name = void 0;
const { Schema, Logger, h } = require("koishi");
const fs = require('fs');
const path = require('path');
exports.name = "qq-markdown-button";
exports.reusable = true; // 声明此插件可重用
exports.inject = [];
exports.inject = {
// required: [''],
optional: ["database"]
};
exports.usage = `
<div>
<p>本插件可帮助你自定义QQ官方机器人按钮菜单,支持以下三种类型的菜单配置:</p>
<ol>
<li><strong>JSON 按钮</strong>:可以发送带有交互按钮的JSON消息。</li>
<li><strong>被动模板 Markdown</strong>:适用于发送自定义的Markdown模板消息。</li>
<li><strong>原生 Markdown</strong>:支持发送更复杂的原生Markdown消息。</li>
</ol>
<h3>如何配置</h3>
<ul>
<li>在左侧活动栏找到【资源管理器】->【data】->【qq-markdown-button】->【按钮菜单配置1】目录,在该目录下,你会看到对应的文件夹下有<code>.md</code> 和 <code>.json</code> 文件。</li>
<li>根据你选择的菜单类型,编辑对应的 <code>.md</code> 和 <code>.json</code> 文件,修改你的菜单配置。</li>
</ul>
<h3>关于变量替换</h3>
<p>在配置文件(例如 <code>.json</code>)中,你可能会看到一些变量占位符,如:</p>
<ul>
<li><code>\${session.messageId}</code>:运行时会替换为当前会话的消息ID。</li>
<li><code>\${INTERACTION_CREATE}</code>:运行时会替换为当前回调按钮的interaction_id。</li>
<li><code>\${markdown}</code>:会被替换为从对应 <code>.md</code> 文件读取的Markdown内容。</li>
<li><code>\${0}</code>, <code>\${1}</code>, ...:这些数字占位符用于获取命令参数。例如,如果命令是 <code>/mycommand arg1 arg2</code>,那么 <code>\${0}</code> 会被替换为 <code>arg1</code>,<code>\${1}</code> 会被替换为 <code>arg2</code>。</li>
<li>当命令没有提供足够的参数时(例如,命令是 <code>/mycommand arg1</code>,但模板中使用了 <code>\${1}</code>),未提供的数字占位符将自动被替换为字符串 <code>"undefined"</code>。</li>
</ul>
<p>无需手动修改这些变量,它们将在运行时自动替换为相应的真实值。</p>
---
<p>支持重用,你可以开多个这个插件,然后改成不同的指令名称/文件夹名称,以注册多个按钮菜单功能</p>
<p>本插件会自动使用对应的文件夹下的 json / markdown 文件来发送消息<br>使用多重配置时,你通常只需要修改 <code>按钮菜单配置1</code> 那一行</p>
<p>不要手动重命名 json/md文件!</p>
<hr>
<p>赶快选择你需要的配置,开始自定义你的菜单吧!</p>
<p>更多说明 <a href="https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/qq-markdown-button" target="_blank">详见➩项目README</a></p>
<p>相关链接:</p>
<ul>
<li><a href="https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/qq-markdown-button" target="_blank">https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/qq-markdown-button</a></li>
<li><a href="https://forum.koishi.xyz/t/topic/10439" target="_blank">https://forum.koishi.xyz/t/topic/10439</a></li>
</ul>
</div>
`;
exports.Config = Schema.intersect([
Schema.object({
command_name: Schema.string().default('按钮菜单').description('注册的指令名称'),
markdown_id: Schema.string().default('123456789_1234567890').description('markdown模板的ID'),
json_button_id: Schema.string().default('123456789_1234567890').description('按钮模板的ID'),
}).description('基础设置'),
Schema.object({
file_name: Schema.array(String).role('table').description('存储文件的文件夹名称<br>请依次填写 相对于`koishi根目录`的 **文件夹** 路径<br>本插件会自动使用对应的文件夹下的 json / markdown 文件来发送消息<br>使用多重配置时,你通常只需要修改 `按钮菜单配置1` 那一行')
.default([
"data",
"qq-markdown-button",
"按钮菜单配置1"
]),
type_switch: Schema.union([
Schema.const('json').description('json按钮(./json/json.json)'),
Schema.const('markdown').description('被动md,模板md(./markdown/markdown.json)'),
Schema.const('raw').description('原生md(./raw/raw_markdown.json 、 ./raw/raw_markdown.md)'),
]).role('radio').description('选择菜单发送方式。<br>即 使用的json文件'),
}).description('发送设置'),
Schema.object({
Allow_INTERACTION_CREATE: Schema.boolean().default(false).description("是否自动执行所有回调按钮内容(通过`session.execute`)"),
}).description('高级设置'),
Schema.object({
consoleinfo: Schema.boolean().default(false).description("日志调试模式`推荐主动广播时开启,以查看日志错误`"),
}).description('调试设置'),
])
function apply(ctx, config) {
ctx.on('ready', () => {
// 使用配置项中的 file_name 数组构建 baseDir 路径
const baseDirArray = [ctx.baseDir].concat(config.file_name);
const baseDir = path.join(...baseDirArray);
logInfo(baseDir)
// 确保目录存在,如果不存在则创建 (包括子目录)
if (!fs.existsSync(baseDir)) {
fs.mkdirSync(baseDir, { recursive: true });
}
const filesToCopy = {
json: ['json.json'],
markdown: ['markdown.json'],
raw: ['raw_markdown.json', 'raw_markdown.md'],
};
// 复制文件到配置的目录下,并按照新的子目录结构存放
for (const type in filesToCopy) {
filesToCopy[type].forEach(file => {
const srcPath = path.join(__dirname, 'qq', type, file); // 源文件路径,根据新的目录结构调整
const destPath = path.join(baseDir, type, file); // 目标文件路径,保持新的目录结构
// 确保目标目录存在
const destDir = path.dirname(destPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
if (!fs.existsSync(destPath)) {
fs.copyFileSync(srcPath, destPath);
}
});
}
if (config.Allow_INTERACTION_CREATE) {
ctx.on("interaction/button", async session => {
const buttoncontent = session?.event?.button['data'];
if (buttoncontent) {
logInfo(`接收到回调按钮内容:\n${buttoncontent}`)
try {
session.qq.acknowledgeInteraction(session.event._data.id, { code: 0 }).catch(error => { // 非阻塞执行
ctx.logger.error(`执行 acknowledgeInteraction 时出错 (后台任务):`, error);
// 只记录错误
});
await session.execute(`${buttoncontent}`)
} catch (error) {
ctx.logger.error(`执行 acknowledgeInteraction 时出错:`, error);
}
return
}
})
}
ctx.command(`${config.command_name} [...args]`, '发送按钮菜单', { strictOptions: true })
.action(async ({ session }, ...args) => {
if (!(session.platform === "qq" || session.platform === "qqguild")) {
await session.send(`仅支持QQ官方平台使用本指令。`)
return;
}
const type = config.type_switch;
let INTERACTION_CREATE = session.event._data.id
let Menu_message;
try {
let jsonFilePath, mdFilePath;
if (type === 'json') {
jsonFilePath = path.join(baseDir, 'json', 'json.json');
mdFilePath = null; // json 类型不需要 md 文件
} else if (type === 'markdown') {
jsonFilePath = path.join(baseDir, 'markdown', 'markdown.json');
mdFilePath = null; // 被动模板 md 类型也不需要额外的 md 文件,内容在 json 中
} else if (type === 'raw') {
jsonFilePath = path.join(baseDir, 'raw', 'raw_markdown.json');
mdFilePath = path.join(baseDir, 'raw', 'raw_markdown.md');
}
Menu_message = await processMarkdownCommand(jsonFilePath, mdFilePath, session, config, { INTERACTION_CREATE: INTERACTION_CREATE }, args);
logInfo("完整的 Menu_message 内容为:", Menu_message);
await sendsomeMessage(Menu_message, session);
} catch (error) {
ctx.logger.error(`处理命令时出错: ${error}`);
}
});
function logInfo(message, message2) {
if (config.consoleinfo) {
if (message2) {
ctx.logger.info(message, message2)
} else {
ctx.logger.info(message);
}
}
}
async function sendsomeMessage(message, session) {
try {
const { guild, user } = session.event;
const { qq, qqguild, channelId } = session;
if (guild?.id) {
if (qq) {
await qq.sendMessage(channelId, message);
} else if (qqguild) {
await qqguild.sendMessage(channelId, message);
}
} else if (user?.id && qq) {
await qq.sendPrivateMessage(user.id, message);
}
} catch (error) {
ctx.logger.error(`发送markdown消息时出错:`, error);
}
}
function processMarkdownCommand(jsonFilePath, mdFilePath, session, config, variables = {}, args = []) {
try {
const rawJsonData = fs.readFileSync(jsonFilePath, 'utf-8');
let markdownContent = mdFilePath ? fs.readFileSync(mdFilePath, 'utf-8') : '';
const allVariables = {
...variables,
session,
config,
args // 将 args 数组添加到 allVariables 中
};
const replacePlaceholders = (data) => {
if (typeof data === 'string') {
return data.replace(/\$\{([^}]+)\}/g, (_, key) => {
// 尝试直接从 allVariables 中获取
let value = key.split('.').reduce((prev, curr) => prev && prev[curr], allVariables);
// 如果 key 是数字,尝试从 args 数组中获取
if (value === undefined && /^\d+$/.test(key)) {
const index = parseInt(key, 10);
// 如果 args 存在且长度大于 index,则取 args[index]
// 否则,如果 args 不存在或长度不足,则返回字符串 'undefined'
if (args && index >= 0 && index < args.length) {
value = args[index];
} else {
value = 'undefined'; // 当 args 不存在或长度不足时,替换为 'undefined' 字符串
}
}
return value !== undefined ? value : `$\{${key}\}`;
});
} else if (Array.isArray(data)) {
return data.map(replacePlaceholders);
} else if (typeof data === 'object' && data !== null) {
return Object.fromEntries(
Object.entries(data).map(([k, v]) => [k, replacePlaceholders(v)])
);
}
return data;
};
markdownContent = replacePlaceholders(markdownContent).replace(/\n/g, '');
allVariables.markdown = markdownContent;
let rawJsonObject = JSON.parse(rawJsonData);
let replacedJsonObject = replacePlaceholders(rawJsonObject);
// 根据 session.messageId 是否存在,动态删除 JSON 对象中不需要的 ID 字段
if (session.messageId) {
if (replacedJsonObject.msg_id) { // 检查 msg_id 字段是否存在
// session.messageId 存在,删除 event_id
delete replacedJsonObject.event_id;
}
} else {
if (replacedJsonObject.event_id) { // 检查 event_id 字段是否存在
// session.messageId 不存在,删除 msg_id
delete replacedJsonObject.msg_id;
}
}
return replacedJsonObject;
} catch (error) {
ctx.logger.error(`读取或解析文件时出错:`, error);
return '处理文件时出错。';
}
}
});
}
exports.apply = apply;