@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
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,
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
});