UNPKG

@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文件设置你的机器人菜单这样就不需要一堆配置项还很烧脑了。自用插件哦~

556 lines (544 loc) 19.8 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, reusable: () => reusable, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_node_path4 = __toESM(require("node:path")); // src/config.ts var import_koishi2 = require("koishi"); // src/template-catalog.ts var import_node_fs2 = __toESM(require("node:fs")); var import_node_path2 = __toESM(require("node:path")); var import_koishi = require("koishi"); // src/files.ts var import_node_fs = __toESM(require("node:fs")); var import_node_path = __toESM(require("node:path")); var templateFiles = { json: ["json.json"], markdown: ["markdown.json"], raw: ["raw-markdown.json", "raw-markdown.md", "raw-without-keyboard.json", "raw-without-keyboard.md"] }; var LEGACY_ROOT_NAME = "qq-markdown-button"; var CURRENT_ROOT_NAME = "qq-markdown-button-v2"; function resolveBaseDir(rootDir, fileName) { const normalizedFileName = fileName.map((segment, index) => { if (index === 1 && segment === LEGACY_ROOT_NAME) { return CURRENT_ROOT_NAME; } return segment; }); return import_node_path.default.join(rootDir, ...normalizedFileName); } __name(resolveBaseDir, "resolveBaseDir"); function ensureTemplateFiles(baseDir, templateRoot) { if (!import_node_fs.default.existsSync(baseDir)) { import_node_fs.default.mkdirSync(baseDir, { recursive: true }); } for (const [type, files] of Object.entries(templateFiles)) { for (const file of files) { const sourcePath = import_node_path.default.join(templateRoot, type, file); const targetPath = import_node_path.default.join(baseDir, type, file); const targetDir = import_node_path.default.dirname(targetPath); if (!import_node_fs.default.existsSync(targetDir)) { import_node_fs.default.mkdirSync(targetDir, { recursive: true }); } if (!import_node_fs.default.existsSync(targetPath)) { import_node_fs.default.copyFileSync(sourcePath, targetPath); } } } } __name(ensureTemplateFiles, "ensureTemplateFiles"); function ensureTemplateDirs(baseDir) { for (const type of Object.keys(templateFiles)) { const dirPath = import_node_path.default.join(baseDir, type); if (!import_node_fs.default.existsSync(dirPath)) { import_node_fs.default.mkdirSync(dirPath, { recursive: true }); } } } __name(ensureTemplateDirs, "ensureTemplateDirs"); // src/template-catalog.ts var TEMPLATE_SCHEMA_KEY = "qq-markdown-button.templates"; var SEND_SEQUENCE_SCHEMA_KEY = "qq-markdown-button.send-sequence"; function listJsonTemplates(baseDir, type) { const dirPath = import_node_path2.default.join(baseDir, type); if (!import_node_fs2.default.existsSync(dirPath)) return []; return import_node_fs2.default.readdirSync(dirPath, { withFileTypes: true }).filter((entry) => entry.isFile() && import_node_path2.default.extname(entry.name).toLowerCase() === ".json").map((entry) => { const name2 = import_node_path2.default.basename(entry.name, ".json"); return { type, name: name2, value: `${type}/${name2}`, label: `${type}/${name2}` }; }); } __name(listJsonTemplates, "listJsonTemplates"); function listRawTemplates(baseDir) { const dirPath = import_node_path2.default.join(baseDir, "raw"); if (!import_node_fs2.default.existsSync(dirPath)) return []; const jsonNames = /* @__PURE__ */ new Set(); const markdownNames = /* @__PURE__ */ new Set(); for (const entry of import_node_fs2.default.readdirSync(dirPath, { withFileTypes: true })) { if (!entry.isFile()) continue; const extName = import_node_path2.default.extname(entry.name).toLowerCase(); const baseName = import_node_path2.default.basename(entry.name, extName); if (extName === ".json") { jsonNames.add(baseName); } else if (extName === ".md") { markdownNames.add(baseName); } } const candidates = []; for (const name2 of Array.from(jsonNames).sort((left, right) => left.localeCompare(right))) { if (!markdownNames.has(name2)) continue; candidates.push({ type: "raw", name: name2, value: `raw/${name2}`, label: `raw/${name2}` }); } return candidates; } __name(listRawTemplates, "listRawTemplates"); function createTemplateSchema(candidates) { if (!candidates.length) { return import_koishi.Schema.union([ import_koishi.Schema.const("").description("当前目录下没有可用模板") ]); } return import_koishi.Schema.union( candidates.map((candidate) => import_koishi.Schema.const(candidate.value).description(candidate.label)) ); } __name(createTemplateSchema, "createTemplateSchema"); function createSendSequenceSchema(candidates) { return import_koishi.Schema.array(createTemplateSchema(candidates)).role("table").description("按顺序发送多个模板,支持排序和混合类型").default([candidates[0]?.value ?? ""]); } __name(createSendSequenceSchema, "createSendSequenceSchema"); function scanTemplateCandidates(baseDir) { return [ ...listJsonTemplates(baseDir, "json"), ...listJsonTemplates(baseDir, "markdown"), ...listRawTemplates(baseDir) ]; } __name(scanTemplateCandidates, "scanTemplateCandidates"); function setupTemplateCatalog(ctx, baseDir, logger) { ensureTemplateDirs(baseDir); const watcherList = []; let disposeDebounceTimer; const refreshSchema = /* @__PURE__ */ __name(() => { const candidates = scanTemplateCandidates(baseDir); logger.debug("刷新模板候选项:", candidates.map((candidate) => candidate.value)); ctx.schema.set(TEMPLATE_SCHEMA_KEY, createTemplateSchema(candidates)); ctx.schema.set(SEND_SEQUENCE_SCHEMA_KEY, createSendSequenceSchema(candidates)); }, "refreshSchema"); const scheduleRefresh = /* @__PURE__ */ __name(() => { if (disposeDebounceTimer) { disposeDebounceTimer(); } disposeDebounceTimer = ctx.setTimeout(() => { disposeDebounceTimer = void 0; refreshSchema(); }, 150); }, "scheduleRefresh"); const watchDir = /* @__PURE__ */ __name((type) => { const dirPath = import_node_path2.default.join(baseDir, type); try { const watcher = import_node_fs2.default.watch(dirPath, () => { scheduleRefresh(); }); watcherList.push(watcher); } catch (error) { logger.error(`监听 ${dirPath} 目录时出错`, error); } }, "watchDir"); refreshSchema(); watchDir("json"); watchDir("markdown"); watchDir("raw"); return () => { for (const watcher of watcherList) { watcher.close(); } if (disposeDebounceTimer) { disposeDebounceTimer(); } }; } __name(setupTemplateCatalog, "setupTemplateCatalog"); // src/config.ts var usage = ` <div> <p>详细使用说明请查看 npm 页面和 README。</p> <p>README 中包含模板类型说明、DAU 说明、<code>send_sequence</code> 用法,以及 <code>raw-without-keyboard</code> 推荐方案。</p> <p><a href="https://www.npmjs.com/package/@shangxueink/koishi-plugin-qq-markdown-button" target="_blank">打开 npm 包页面</a></p> </div> `; var Config = import_koishi2.Schema.intersect([ import_koishi2.Schema.object({ command_name: import_koishi2.Schema.string().default("按钮菜单").description("注册的指令名称"), file_name_v2: import_koishi2.Schema.array(String).role("table").description("存储文件的文件夹名称<br>请依次填写 相对于koishi根目录的 **文件夹** 路径<br>本插件会自动使用对应的文件夹下的 json / markdown / raw 文件来发送消息").default([ "data", "qq-markdown-button-v2", "按钮菜单配置1" ]) }).description("基础设置"), import_koishi2.Schema.object({ send_sequence: import_koishi2.Schema.dynamic(SEND_SEQUENCE_SCHEMA_KEY).description("启用插件后会按当前目录加载模板候选项,并显示可排序表格;未启用时这里只显示占位项。") }).description("发送设置"), import_koishi2.Schema.object({ Allow_INTERACTION_CREATE: import_koishi2.Schema.boolean().default(false).description("是否自动执行所有回调按钮内容(通过`session.execute`)") }).description("高级设置"), import_koishi2.Schema.object({ consoleinfo: import_koishi2.Schema.boolean().default(false).description("日志调试模式,推荐主动广播时开启,用于查看日志错误") }).description("调试设置") ]); // src/logger.ts function createLogger(ctx, enabled) { return { debug(message, detail) { if (!enabled) return; if (detail === void 0) { ctx.logger.info(message); return; } ctx.logger.info(message, detail); }, error(message, error) { if (error === void 0) { ctx.logger.error(message); return; } ctx.logger.error(message, error); } }; } __name(createLogger, "createLogger"); // src/session.ts function isRecord(value) { return typeof value === "object" && value !== null; } __name(isRecord, "isRecord"); function getNestedValue(source, paths) { let current = source; for (const key of paths) { if (!isRecord(current) || !(key in current)) { return void 0; } current = current[key]; } return current; } __name(getNestedValue, "getNestedValue"); function isSupportedPlatform(session) { return session.platform === "qq" || session.platform === "qqguild"; } __name(isSupportedPlatform, "isSupportedPlatform"); function getInteractionId(session) { const interactionId = getNestedValue(session.event, ["_data", "id"]); return typeof interactionId === "string" ? interactionId : ""; } __name(getInteractionId, "getInteractionId"); function getButtonData(session) { const buttonData = getNestedValue(session.event, ["button", "data"]); return typeof buttonData === "string" ? buttonData : void 0; } __name(getButtonData, "getButtonData"); // src/template.ts var import_node_fs3 = __toESM(require("node:fs")); var import_node_path3 = __toESM(require("node:path")); function isRecord2(value) { return typeof value === "object" && value !== null; } __name(isRecord2, "isRecord"); function parseTemplateRef(templateRef) { const separatorIndex = templateRef.indexOf("/"); if (separatorIndex <= 0 || separatorIndex >= templateRef.length - 1) { throw new Error(`模板引用格式无效:${templateRef}`); } const type = templateRef.slice(0, separatorIndex); const name2 = templateRef.slice(separatorIndex + 1); if (type !== "json" && type !== "markdown" && type !== "raw") { throw new Error(`模板类型无效:${templateRef}`); } return { type, name: name2 }; } __name(parseTemplateRef, "parseTemplateRef"); function getTemplatePaths(baseDir, templateRef) { const { type, name: name2 } = parseTemplateRef(templateRef); if (type === "json") { return { jsonFilePath: import_node_path3.default.join(baseDir, "json", `${name2}.json`), markdownFilePath: null }; } if (type === "markdown") { return { jsonFilePath: import_node_path3.default.join(baseDir, "markdown", `${name2}.json`), markdownFilePath: null }; } return { jsonFilePath: import_node_path3.default.join(baseDir, "raw", `${name2}.json`), markdownFilePath: import_node_path3.default.join(baseDir, "raw", `${name2}.md`) }; } __name(getTemplatePaths, "getTemplatePaths"); function getPlaceholderValue(key, variables, args) { if (/^\d+$/.test(key)) { const index = Number.parseInt(key, 10); return args[index] ?? "undefined"; } const paths = key.split("."); let current = variables; for (const pathKey of paths) { if (!isRecord2(current) || !(pathKey in current)) { return void 0; } current = current[pathKey]; } return current === void 0 ? void 0 : String(current); } __name(getPlaceholderValue, "getPlaceholderValue"); function replacePlaceholders(data, variables, args) { if (typeof data === "string") { return data.replace(/\$\{([^}]+)\}/g, (_, key) => { const value = getPlaceholderValue(key, variables, args); return value === void 0 ? `\${${key}}` : value; }); } if (Array.isArray(data)) { return data.map((item) => replacePlaceholders(item, variables, args)); } if (isRecord2(data)) { const result = {}; for (const [key, value] of Object.entries(data)) { result[key] = replacePlaceholders(value, variables, args); } return result; } return data; } __name(replacePlaceholders, "replacePlaceholders"); function buildMenuMessage(baseDir, templateRef, session, config, interactionId, args) { const { jsonFilePath, markdownFilePath } = getTemplatePaths(baseDir, templateRef); const rawJsonData = import_node_fs3.default.readFileSync(jsonFilePath, "utf-8"); const variables = { INTERACTION_CREATE: interactionId, session, config, args }; if (markdownFilePath) { const markdownContent = import_node_fs3.default.readFileSync(markdownFilePath, "utf-8"); variables.markdown = replacePlaceholders(markdownContent, variables, args); } const rawJsonObject = JSON.parse(rawJsonData); const replacedJsonObject = replacePlaceholders(rawJsonObject, variables, args); if (isRecord2(replacedJsonObject)) { if (session.messageId) { delete replacedJsonObject.event_id; } else { delete replacedJsonObject.msg_id; } } return replacedJsonObject; } __name(buildMenuMessage, "buildMenuMessage"); // src/sender.ts function isRecord3(value) { return typeof value === "object" && value !== null; } __name(isRecord3, "isRecord"); function isSendableMessage(message) { return isRecord3(message); } __name(isSendableMessage, "isSendableMessage"); function withMessageSequence(message, sequence) { if (!isSendableMessage(message)) { return message; } return { ...message, msg_seq: sequence }; } __name(withMessageSequence, "withMessageSequence"); function getErrorCode(error) { if (!isRecord3(error)) { return void 0; } const response = error.response; if (!isRecord3(response)) { return void 0; } const data = response.data; if (!isRecord3(data)) { return void 0; } if (typeof data.err_code === "number") { return data.err_code; } if (typeof data.code === "number") { return data.code; } return void 0; } __name(getErrorCode, "getErrorCode"); async function sendMenuMessage(session, message) { const guildId = session.event.guild?.id; const userId = session.event.user?.id; if (guildId) { if (session.qq) { await session.qq.sendMessage(session.channelId, message); return; } if (session.qqguild) { await session.qqguild.sendMessage(session.channelId, message); return; } } if (userId && session.qq) { await session.qq.sendPrivateMessage(userId, message); return; } throw new Error("当前会话没有可用的发送目标。"); } __name(sendMenuMessage, "sendMenuMessage"); async function sendMenuSequence(options, logger) { const { baseDir, session, config, args, interactionId } = options; let nextSequence = 1; for (const [index, template] of config.send_sequence.entries()) { try { const message = buildMenuMessage(baseDir, template, session, config, interactionId, args); logger.debug(`第 ${index + 1} 步生成的消息内容:`, message); let preparedMessage = withMessageSequence(message, nextSequence); try { await sendMenuMessage(session, preparedMessage); } catch (error) { if (getErrorCode(error) !== 40054005) { throw error; } nextSequence += 1; preparedMessage = withMessageSequence(message, nextSequence); logger.debug(`第 ${index + 1} 步遇到去重,使用新的 msg_seq 重试:${nextSequence}`); await sendMenuMessage(session, preparedMessage); } nextSequence += 1; } catch (error) { logger.error(`发送第 ${index + 1}${template} 模板时出错`, error); } } } __name(sendMenuSequence, "sendMenuSequence"); // src/index.ts var name = "qq-markdown-button"; var reusable = true; var inject = { optional: ["database"] }; function apply(ctx, config) { const logger = createLogger(ctx, config.consoleinfo); const baseDir = resolveBaseDir(ctx.baseDir, config.file_name_v2); const templateRoot = import_node_path4.default.resolve(__dirname, "..", "qq"); try { ensureTemplateFiles(baseDir, templateRoot); logger.debug(`模板目录:${baseDir}`); } catch (error) { logger.error("初始化模板文件时出错", error); } const disposeTemplateCatalog = setupTemplateCatalog(ctx, baseDir, logger); ctx.effect(() => { return () => { disposeTemplateCatalog(); }; }); if (config.Allow_INTERACTION_CREATE) { ctx.on("interaction/button", async (session) => { const buttonData = getButtonData(session); if (!buttonData) return; logger.debug(`接收到回调按钮内容:${buttonData}`); const interactionId = getInteractionId(session); if (session.qq && interactionId) { void session.qq.acknowledgeInteraction(interactionId, { code: 0 }).catch((error) => { logger.error("执行 acknowledgeInteraction 时出错", error); }); } try { await session.execute(buttonData); } catch (error) { logger.error("执行回调按钮内容时出错", error); } }); } ctx.command(`${config.command_name} [...args]`, "发送按钮菜单", { strictOptions: true }).action(async ({ session }, ...args) => { if (!session) return; if (!isSupportedPlatform(session)) { await session.send("仅支持QQ官方平台使用本指令。"); return; } if (!config.send_sequence.length) { await session.send("当前未配置发送步骤。"); return; } try { await sendMenuSequence({ baseDir, session, config, args, interactionId: getInteractionId(session) }, logger); } catch (error) { logger.error("处理指令时出错", error); } }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name, reusable, usage });