UNPKG

koishi-plugin-teach

Version:
1,288 lines (1,277 loc) 74.9 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); var __export = (target, all) => { __markAsModule(target); for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __reExport = (target, module2, desc) => { if (module2 && typeof module2 === "object" || typeof module2 === "function") { for (let key of __getOwnPropNames(module2)) if (!__hasOwnProp.call(target, key) && key !== "default") __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable }); } return target; }; var __toModule = (module2) => { return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2); }; // packages/plugin-teach/src/index.ts __export(exports, { Dialogue: () => Dialogue, MessageBuffer: () => MessageBuffer, RE_DIALOGUES: () => RE_DIALOGUES, RE_GROUPS: () => RE_GROUPS, apply: () => apply11, create: () => create, disposable: () => disposable, equal: () => equal, escapeAnswer: () => escapeAnswer, formatAnswer: () => formatAnswer, formatAnswers: () => formatAnswers, formatDetails: () => formatDetails, formatQuestionAnswers: () => formatQuestionAnswers, getDetails: () => getDetails, getTotalWeight: () => getTotalWeight, isHours: () => isHours, isPositiveInteger: () => isPositiveInteger, isZeroToOne: () => isZeroToOne, name: () => name, prepareTargets: () => prepareTargets, sendResult: () => sendResult, split: () => split, triggerDialogue: () => triggerDialogue, unescapeAnswer: () => unescapeAnswer, update: () => update }); var import_koishi_core4 = __toModule(require("koishi-core")); var import_koishi_utils7 = __toModule(require("koishi-utils")); // packages/plugin-teach/src/utils.ts var import_koishi_core = __toModule(require("koishi-core")); var import_koishi_utils = __toModule(require("koishi-utils")); import_koishi_core.Tables.extend("dialogue", { type: "incremental", fields: { id: "unsigned", flag: "unsigned(4)", probS: { type: "decimal", precision: 4, scale: 3, initial: 1 }, probA: { type: "decimal", precision: 4, scale: 3, initial: 0 }, startTime: "integer", endTime: "integer", groups: "list(255)", original: "string(255)", question: "string(255)", answer: "text", predecessors: "list(255)", successorTimeout: "unsigned", writer: "string(255)" } }); var Dialogue; (function(Dialogue2) { let Flag; (function(Flag2) { Flag2[Flag2["frozen"] = 1] = "frozen"; Flag2[Flag2["regexp"] = 2] = "regexp"; Flag2[Flag2["context"] = 4] = "context"; Flag2[Flag2["substitute"] = 8] = "substitute"; Flag2[Flag2["complement"] = 16] = "complement"; })(Flag = Dialogue2.Flag || (Dialogue2.Flag = {})); async function get(ctx, test, fields) { if (Array.isArray(test)) { const dialogues = await ctx.database.get("dialogue", test, fields); dialogues.forEach((d) => (0, import_koishi_utils.defineProperty)(d, "_backup", (0, import_koishi_utils.clone)(d))); return dialogues; } else { const query = { $and: [] }; ctx.emit("dialogue/test", test, query); const dialogues = await ctx.database.get("dialogue", query); dialogues.forEach((d) => (0, import_koishi_utils.defineProperty)(d, "_backup", (0, import_koishi_utils.clone)(d))); return dialogues.filter((data) => { if (!test.groups || test.partial) return true; return !(data.flag & 16) === test.reversed || !equal(test.groups, data.groups); }); } } Dialogue2.get = get; async function update2(dialogues, argv) { const data = []; const fields = new Set(["id"]); for (const { _diff } of dialogues) { for (const key in _diff) { fields.add(key); } } for (const dialogue of dialogues) { if (!Object.keys(dialogue._diff).length) { argv.skipped.push(dialogue.id); } else { dialogue._diff = {}; argv.updated.push(dialogue.id); data.push((0, import_koishi_utils.pick)(dialogue, fields)); addHistory(dialogue._backup, "修改", argv, false); } } await argv.app.database.update("dialogue", data); } Dialogue2.update = update2; async function stats(ctx) { return ctx.database.aggregate("dialogue", { dialogues: { $count: "id" }, questions: { $count: "question" } }); } Dialogue2.stats = stats; async function remove(dialogues, argv, revert3 = false) { const ids = dialogues.map((d) => d.id); argv.app.database.remove("dialogue", ids); for (const id of ids) { addHistory(argv.dialogueMap[id], "删除", argv, revert3); } return ids; } Dialogue2.remove = remove; async function revert2(dialogues, argv) { const created = dialogues.filter((d) => d._type === "添加"); const edited = dialogues.filter((d) => d._type !== "添加"); await remove(created, argv, true); await recover(edited, argv); return `问答 ${dialogues.map((d) => d.id).sort((a, b) => a - b)} 已回退完成。`; } Dialogue2.revert = revert2; async function recover(dialogues, argv) { await argv.app.database.update("dialogue", dialogues); for (const dialogue of dialogues) { addHistory(dialogue, "修改", argv, true); } } Dialogue2.recover = recover; function addHistory(dialogue, type, argv, revert3) { var _a; if (revert3) return delete argv.app.teachHistory[dialogue.id]; argv.app.teachHistory[dialogue.id] = dialogue; const time = Date.now(); (0, import_koishi_utils.defineProperty)(dialogue, "_timestamp", time); (0, import_koishi_utils.defineProperty)(dialogue, "_operator", argv.session.userId); (0, import_koishi_utils.defineProperty)(dialogue, "_type", type); setTimeout(() => { var _a2; if (((_a2 = argv.app.teachHistory[dialogue.id]) == null ? void 0 : _a2._timestamp) === time) { delete argv.app.teachHistory[dialogue.id]; } }, (_a = argv.config.historyAge) != null ? _a : 6e5); } Dialogue2.addHistory = addHistory; })(Dialogue || (Dialogue = {})); function sendResult(argv, prefix, suffix) { const { session, options, uneditable, unknown, skipped, updated, target, config } = argv; const { remove, revert: revert2, create: create2 } = options; const output = []; if (prefix) output.push(prefix); if (updated.length) { output.push(create2 ? `修改了已存在的问答,编号为 ${updated.join(", ")}。` : `问答 ${updated.join(", ")} 已成功修改。`); } if (skipped.length) { output.push(create2 ? `问答已存在,编号为 ${target.join(", ")},如要修改请尝试使用 ${config.prefix}${skipped.join(",")} 指令。` : `问答 ${skipped.join(", ")} 没有发生改动。`); } if (uneditable.length) { output.push(`问答 ${uneditable.join(", ")} 因权限过低无法${revert2 ? "回退" : remove ? "删除" : "修改"}。`); } if (unknown.length) { output.push(`${revert2 ? "最近无人修改过" : "没有搜索到"}编号为 ${unknown.join(", ")} 的问答。`); } if (suffix) output.push(suffix); return session.send(output.join("\n")); } function split(source) { if (!source) return []; return source.split(",").flatMap((value) => { if (!value.includes("..")) return +value; const capture = value.split(".."); const start = +capture[0], end = +capture[1]; if (end < start) return []; return new Array(end - start + 1).fill(0).map((_, index) => start + index); }); } function equal(array1, array2) { return array1.slice().sort().join() === array2.slice().sort().join(); } function prepareTargets(argv, dialogues = argv.dialogues) { const targets = dialogues.filter((dialogue) => { return !argv.app.bail("dialogue/permit", argv, dialogue); }); argv.uneditable.unshift(...(0, import_koishi_utils.difference)(dialogues, targets).map((d) => d.id)); return targets.map((data) => (0, import_koishi_utils.observe)(data, `dialogue ${data.id}`)); } function isPositiveInteger(source) { const n = +source; if ((0, import_koishi_utils.isInteger)(n) && n > 0) return n; throw new Error("应为正整数。"); } function isZeroToOne(source) { const n = +source; if (n >= 0 && n <= 1) return n; throw new Error("应为不超过 1 的正数。"); } var RE_DIALOGUES = /^\d+(\.\.\d+)?(,\d+(\.\.\d+)?)*$/; // packages/plugin-teach/src/internal.ts var import_koishi_core3 = __toModule(require("koishi-core")); // packages/plugin-teach/src/update.ts var import_koishi_utils3 = __toModule(require("koishi-utils")); // packages/plugin-teach/src/receiver.ts var import_koishi_core2 = __toModule(require("koishi-core")); var import_koishi_utils2 = __toModule(require("koishi-utils")); function escapeAnswer(message) { return message.replace(/\$/g, "@@__PLACEHOLDER__@@"); } function unescapeAnswer(message) { return message.replace(/@@__PLACEHOLDER__@@/g, "$"); } import_koishi_core2.Context.prototype.getSessionState = function(session) { const { channelId, userId, app } = session; if (!app._dialogueStates[channelId]) { this.emit("dialogue/state", app._dialogueStates[channelId] = { channelId }); } const state = Object.create(app._dialogueStates[channelId]); state.session = session; state.userId = userId; return state; }; async function getTotalWeight(ctx, state) { const { session, dialogues } = state; ctx.app.emit(session, "dialogue/prepare", state); const userFields = new Set(["name", "flag"]); ctx.app.emit(session, "dialogue/before-attach-user", state, userFields); await session.observeUser(userFields); if (ctx.app.bail(session, "dialogue/attach-user", state)) return 0; return dialogues.reduce((prev, curr) => prev + curr._weight, 0); } var MessageBuffer = class { constructor(session) { this.session = session; this.buffer = ""; this.original = false; this.hasData = false; this.send = session.send.bind(session); this.sendQueued = session.sendQueued.bind(session); session.send = async (message) => { if (!message) return; this.hasData = true; if (this.original) { return this.send(message); } this.buffer += message; }; session.sendQueued = async (message, delay) => { if (!message) return; this.hasData = true; if (this.original) { return this.sendQueued(message, delay); } return this._flush(this.buffer + message, delay); }; } write(message) { if (!message) return; this.hasData = true; this.buffer += message; } async _flush(message, delay) { this.original = true; message = message.trim(); await this.sendQueued(message, delay); this.buffer = ""; this.original = false; } flush() { return this._flush(this.buffer); } async execute(argv) { this.original = false; const send = this.session.send; const sendQueued = this.session.sendQueued; await this.session.execute(argv); this.session.sendQueued = sendQueued; this.session.send = send; this.original = true; } async end(message = "") { this.write(message); await this.flush(); this.original = true; delete this.session.send; delete this.session.sendQueued; } }; var tokenizer = new import_koishi_core2.Argv.Tokenizer(); tokenizer.interpolate("$n", "", (rest) => { return { rest, tokens: [], source: "" }; }); var halfWidth = ",,.~?!()[]"; var fullWidth = ",、。~?!()【】"; var fullWidthRegExp = new RegExp(`[${fullWidth}]`); async function triggerDialogue(ctx, session, next = import_koishi_utils2.noop) { const state = ctx.getSessionState(session); state.next = next; state.test = {}; if (ctx.bail("dialogue/receive", state)) return next(); const logger = ctx.logger("dialogue"); logger.debug("[receive]", session.messageId, session.content); const dialogues = state.dialogues = await Dialogue.get(ctx, state.test); let dialogue; const total = await getTotalWeight(ctx, state); if (!total) return next(); const target = import_koishi_utils2.Random.real(Math.max(1, total)); let pointer = 0; for (const _dialogue of dialogues) { pointer += _dialogue._weight; if (target < pointer) { dialogue = _dialogue; break; } } if (!dialogue) return next(); logger.debug("[attach]", session.messageId); state.dialogue = dialogue; state.dialogues = [dialogue]; state.answer = dialogue.answer.replace(/\$\$/g, "@@__PLACEHOLDER__@@").replace(/\$A/g, (0, import_koishi_utils2.segment)("at", { type: "all" })).replace(/\$a/g, (0, import_koishi_utils2.segment)("at", { id: session.userId })).replace(/\$m/g, (0, import_koishi_utils2.segment)("at", { id: session.selfId })).replace(/\$s/g, () => escapeAnswer(session.username)).replace(/\$0/g, escapeAnswer(session.content)); if (dialogue.flag & Dialogue.Flag.regexp) { const capture = dialogue._capture || new RegExp(dialogue.original, "i").exec(state.test.original); if (!capture) return; capture.map((segment3, index2) => { if (index2 && index2 <= 9) { state.answer = state.answer.replace(new RegExp(`\\$${index2}`, "g"), escapeAnswer(segment3 || "")); } }); } if (await ctx.app.serial(session, "dialogue/before-send", state)) return; logger.debug("[send]", session.messageId, "->", dialogue.answer); const buffer = new MessageBuffer(session); session._redirected = (session._redirected || 0) + 1; let index; const { content, inters } = tokenizer.parseToken(unescapeAnswer(state.answer)); while (inters.length) { const argv = inters.shift(); buffer.write(content.slice(index, argv.pos)); if (argv.initiator === "$n") { await buffer.flush(); } else { await buffer.execute(argv); } index = argv.pos; } await buffer.end(content.slice(index)); await ctx.app.parallel(session, "dialogue/send", state); } function apply(ctx, config) { const { nickname = ctx.app.options.nickname, maxRedirections = 3 } = config; const nicknames = (0, import_koishi_utils2.makeArray)(nickname).map(import_koishi_utils2.escapeRegExp); const nicknameRE = new RegExp(`^((${nicknames.join("|")})[,,]?\\s*)+`); const ctx2 = ctx.group(); ctx.app._dialogueStates = {}; config._stripQuestion = (source) => { const original = import_koishi_utils2.segment.unescape(source); source = import_koishi_utils2.segment.transform(source, { text: ({ content }, index, chain) => { let message = (0, import_koishi_utils2.simplify)(import_koishi_utils2.segment.unescape("" + content)).toLowerCase().replace(/\s+/g, "").replace(fullWidthRegExp, ($0) => halfWidth[fullWidth.indexOf($0)]); if (index === 0) message = message.replace(/^[()\[\]]*/, ""); if (index === chain.length - 1) message = message.replace(/[\.,?!()\[\]~]*$/, ""); return message; } }); const capture = nicknameRE.exec(source); const unprefixed = capture ? source.slice(capture[0].length) : source; return { original, parsed: unprefixed || source, appellative: unprefixed && unprefixed !== source, activated: !unprefixed && unprefixed !== source }; }; ctx.before("attach", (session) => { if (session.parsed.appel) return; const { activated } = ctx.getSessionState(session); if (activated[session.userId]) session.parsed.appel = true; }); ctx2.middleware(async (session, next) => { return triggerDialogue(ctx, session, next); }); ctx.on("notice/poke", async (session) => { if (session.targetId !== session.selfId) return; const { flag } = await session.observeChannel(["flag"]); if (flag & import_koishi_core2.Channel.Flag.ignore) return; session.content = "hook:poke"; triggerDialogue(ctx, session); }); async function triggerNotice(name2, session) { const { flag, assignee } = await session.observeChannel(["flag", "assignee"]); if (assignee !== session.selfId) return; if (flag & import_koishi_core2.Channel.Flag.ignore) return; session.content = "hook:" + name2 + (session.userId === session.selfId ? ":self" : ":others"); triggerDialogue(ctx, session); } ctx.on("notice/honor", async (session) => { await triggerNotice(session.subsubtype, session); }); ctx.on("group-member-added", triggerNotice.bind(null, "join")); ctx.on("group-member-deleted", triggerNotice.bind(null, "leave")); ctx.on("dialogue/receive", ({ session }) => { var _a; if (((_a = session.user) == null ? void 0 : _a.authority) < config.authority.receive) return true; }); ctx.on("dialogue/receive", ({ session, test }) => { if (session.content.includes("[CQ:image,")) return true; const { original, parsed, appellative, activated } = config._stripQuestion(session.content); test.question = parsed; test.original = original; test.activated = activated; test.appellative = appellative; }); ctx.before("dialogue/attach-user", ({ dialogues, session }, userFields) => { for (const data of dialogues) { const { inters } = tokenizer.parseToken(data.answer); for (const argv of inters) { session.collect("user", argv, userFields); } } }); ctx2.command("dialogue <message:text>", "触发教学对话").action(async ({ session, next }, message = "") => { if (session._redirected > maxRedirections) return next(); session.content = message; return triggerDialogue(ctx, session, next); }); } // packages/plugin-teach/src/search.ts function apply2(ctx) { ctx.command("teach.status").action(async () => { const { questions, dialogues } = await Dialogue.stats(ctx); return `共收录了 ${questions} 个问题和 ${dialogues} 个回答。`; }); ctx.command("teach").option("search", "搜索已有问答", { notUsage: true }).option("page", "/ <page> 设置搜索结果的页码", { type: isPositiveInteger }).option("autoMerge", "自动合并相同的问题和回答").option("recursive", "-R 禁用递归查询", { value: false }).option("pipe", "| <op:text> 对每个搜索结果执行操作"); ctx.on("dialogue/execute", (argv) => { const { search } = argv.options; if (search) return showSearch(argv); }); ctx.on("dialogue/list", ({ _redirections }, output, prefix, argv) => { if (!_redirections) return; output.push(...formatAnswers(argv, _redirections, prefix + "= ")); }); ctx.on("dialogue/detail-short", ({ flag }, output) => { if (flag & Dialogue.Flag.regexp) { output.questionType = "正则"; } }); ctx.before("dialogue/search", ({ options }, test) => { test.noRecursive = options.recursive === false; }); ctx.before("dialogue/search", ({ options }, test) => { test.appellative = options.appellative; }); ctx.on("dialogue/search", async (argv, test, dialogues) => { if (!argv.questionMap) { argv.questionMap = { [test.question]: dialogues }; } for (const dialogue of dialogues) { const { answer } = dialogue; if (!answer.startsWith("%{dialogue ")) continue; const { original, parsed } = argv.config._stripQuestion(answer.slice(11, -1).trimStart()); if (parsed in argv.questionMap) continue; const dialogues2 = argv.questionMap[parsed] = await Dialogue.get(ctx, __spreadProps(__spreadValues({}, test), { regexp: null, question: parsed, original })); Object.defineProperty(dialogue, "_redirections", { writable: true, value: dialogues2 }); await argv.app.parallel("dialogue/search", argv, test, dialogues2); } }); } function formatAnswer(source, { maxAnswerLength = 100 }) { let trimmed = false; const lines = source.split(/(\r?\n|\$n)/g); if (lines.length > 1) { trimmed = true; source = lines[0].trim(); } source = source.replace(/\[CQ:image,[^\]]+\]/g, "[图片]"); if (source.length > maxAnswerLength) { trimmed = true; source = source.slice(0, maxAnswerLength); } if (trimmed && !source.endsWith("……")) { if (source.endsWith("…")) { source += "…"; } else { source += "……"; } } return source; } function getDetails(argv, dialogue) { const details = []; argv.app.emit("dialogue/detail-short", dialogue, details, argv); return details; } function formatDetails(dialogue, details) { return `${dialogue.id}. ${details.length ? `[${details.join(", ")}] ` : ""}`; } function formatPrefix(argv, dialogue, showAnswerType = false) { const details = getDetails(argv, dialogue); let result = formatDetails(dialogue, details); if (details.questionType) result += `[${details.questionType}] `; if (showAnswerType && details.answerType) result += `[${details.answerType}] `; return result; } function formatAnswers(argv, dialogues, prefix = "") { return dialogues.map((dialogue) => { const { answer } = dialogue; const output = [`${prefix}${formatPrefix(argv, dialogue, true)}${formatAnswer(answer, argv.config)}`]; argv.app.emit("dialogue/list", dialogue, output, prefix, argv); return output.join("\n"); }); } function formatQuestionAnswers(argv, dialogues, prefix = "") { return dialogues.map((dialogue) => { const details = getDetails(argv, dialogue); const { questionType = "问题", answerType = "回答" } = details; const { original, answer } = dialogue; const output = [`${prefix}${formatDetails(dialogue, details)}${questionType}:${original},${answerType}:${formatAnswer(answer, argv.config)}`]; argv.app.emit("dialogue/list", dialogue, output, prefix, argv); return output.join("\n"); }); } async function showSearch(argv) { const { app, session, options, args: [question, answer] } = argv; const { regexp, page = 1, original, pipe, recursive, autoMerge } = options; const { itemsPerPage = 30, mergeThreshold = 5 } = argv.config; const test = { question, answer, regexp, original }; if (app.bail("dialogue/before-search", argv, test)) return; const dialogues = await Dialogue.get(app, test); if (pipe) { if (!dialogues.length) return "没有搜索到任何问答。"; const command = app.command("teach"); const argv2 = __spreadProps(__spreadValues({}, command.parse(pipe)), { session, command }); const target = argv2.options["target"] = dialogues.map((d) => d.id).join(","); argv2.source = `#${target} ${pipe}`; return command.execute(argv2); } if (recursive !== false && !autoMerge) { await argv.app.parallel("dialogue/search", argv, test, dialogues); } if (!original && !answer) { if (!dialogues.length) return "没有搜索到任何回答,尝试切换到其他环境。"; return sendResult2("全部问答如下", formatQuestionAnswers(argv, dialogues)); } if (!options.regexp) { const suffix = options.regexp !== false ? ",请尝试使用正则表达式匹配" : ""; if (!original) { if (!dialogues.length) return session.send(`没有搜索到回答“${answer}”${suffix}。`); const output2 = dialogues.map((d) => `${formatPrefix(argv, d)}${d.original}`); return sendResult2(`回答“${answer}”的问题如下`, output2); } else if (!answer) { if (!dialogues.length) return session.send(`没有搜索到问题“${original}”${suffix}。`); const output2 = formatAnswers(argv, dialogues); const state = app.getSessionState(session); state.isSearch = true; state.test = test; state.dialogues = dialogues; const total = await getTotalWeight(app, state); return sendResult2(`问题“${original}”的回答如下`, output2, dialogues.length > 1 ? `实际触发概率:${+Math.min(total, 1).toFixed(3)}` : ""); } else { if (!dialogues.length) return session.send(`没有搜索到问答“${original}”“${answer}”${suffix}。`); const output2 = [dialogues.map((d) => d.id).join(", ")]; return sendResult2(`“${original}”“${answer}”匹配的回答如下`, output2); } } let output; if (!autoMerge || question && answer) { output = formatQuestionAnswers(argv, dialogues); } else { const idMap = {}; for (const dialogue of dialogues) { const key = question ? dialogue.original : dialogue.answer; if (!idMap[key]) idMap[key] = []; idMap[key].push(dialogue.id); } output = Object.keys(idMap).map((key) => { const { length } = idMap[key]; return length <= mergeThreshold ? `${key} (#${idMap[key].join(", #")})` : `${key} (共 ${length} 个${question ? "回答" : "问题"})`; }); } if (!original) { if (!dialogues.length) return `没有搜索到含有正则表达式“${answer}”的回答。`; return sendResult2(`回答正则表达式“${answer}”的搜索结果如下`, output); } else if (!answer) { if (!dialogues.length) return `没有搜索到含有正则表达式“${original}”的问题。`; return sendResult2(`问题正则表达式“${original}”的搜索结果如下`, output); } else { if (!dialogues.length) return `没有搜索到含有正则表达式“${original}”“${answer}”的问答。`; return sendResult2(`问答正则表达式“${original}”“${answer}”的搜索结果如下`, output); } function sendResult2(title, output2, suffix) { if (output2.length <= itemsPerPage) { output2.unshift(title + ":"); if (suffix) output2.push(suffix); } else { const pageCount = Math.ceil(output2.length / itemsPerPage); output2 = output2.slice((page - 1) * itemsPerPage, page * itemsPerPage); output2.unshift(title + `(第 ${page}/${pageCount} 页):`); if (suffix) output2.push(suffix); output2.push("可以使用 /+页码 以调整输出的条目页数。"); } return output2.join("\n"); } } // packages/plugin-teach/src/update.ts function apply3(ctx) { ctx.command("teach").option("review", "-v 查看最近的修改").option("revert", "-V 回退最近的修改").option("includeLast", "-l [count] 包含最近的修改数量", { type: isIntegerOrInterval }).option("excludeLast", "-L [count] 排除最近的修改数量", { type: isIntegerOrInterval }).option("target", "<ids> 查看或修改已有问题", { type: RE_DIALOGUES }).option("remove", "-r 彻底删除问答"); ctx.on("dialogue/execute", (argv) => { const { remove, revert: revert2, target } = argv.options; if (!target) return; argv.target = (0, import_koishi_utils3.deduplicate)(split(target)); delete argv.options.target; try { return update(argv); } catch (err) { ctx.logger("teach").warn(err); return argv.session.send(`${revert2 ? "回退" : remove ? "删除" : "修改"}问答时出现问题。`); } }); ctx.on("dialogue/execute", (argv) => { const { options, session } = argv; const { includeLast, excludeLast } = options; if (!options.review && !options.revert) return; const now = Date.now(), includeTime = import_koishi_utils3.Time.parseTime(includeLast), excludeTime = import_koishi_utils3.Time.parseTime(excludeLast); const dialogues = Object.values(argv.app.teachHistory).filter((dialogue) => { if (dialogue._operator !== session.userId) return; const offset = now - dialogue._timestamp; if (includeTime && offset >= includeTime) return; if (excludeTime && offset < excludeTime) return; return true; }).sort((d1, d2) => d2._timestamp - d1._timestamp).filter((_, index, temp) => { if (!includeTime && includeLast && index >= +includeLast) return; if (!excludeTime && excludeLast && index < temp.length - +excludeLast) return; return true; }); if (!dialogues.length) return session.send("没有搜索到满足条件的教学操作。"); return options.review ? review(dialogues, argv) : revert(dialogues, argv); }, true); ctx.before("dialogue/detail", async (argv) => { if (argv.options.modify) return; await argv.app.parallel("dialogue/search", argv, {}, argv.dialogues); }); ctx.on("dialogue/detail-short", ({ _type, _timestamp }, output) => { if (_type) { output.unshift(`${_type}-${import_koishi_utils3.Time.formatTimeShort(Date.now() - _timestamp)}`); } }); ctx.on("dialogue/detail", ({ original, answer, flag, _type, _timestamp }, output) => { if (flag & Dialogue.Flag.regexp) { output.push(`正则:${original}`); } else { output.push(`问题:${original}`); } output.push(`回答:${answer}`); if (_type) { output.push(`${_type}于:${import_koishi_utils3.Time.formatTime(Date.now() - _timestamp)}前`); } }); } function isIntegerOrInterval(source) { const n = +source; if (n * 0 === 0) { isPositiveInteger(source); return source; } else { if (import_koishi_utils3.Time.parseTime(source)) return source; throw new Error(); } } function review(dialogues, argv) { const { session } = argv; const output = dialogues.map((d) => { const details = getDetails(argv, d); const { questionType = "问题", answerType = "回答" } = details; const { original, answer } = d; return `${formatDetails(d, details)}${questionType}:${original},${answerType}:${formatAnswer(answer, argv.config)}`; }); output.unshift("近期执行的教学操作有:"); return session.send(output.join("\n")); } async function revert(dialogues, argv) { try { return argv.session.send(await Dialogue.revert(dialogues, argv)); } catch (err) { argv.app.logger("teach").warn(err); return argv.session.send("回退问答中出现问题。"); } } async function update(argv) { const { app, session, options, target, config, args } = argv; const { maxPreviews = 10, previewDelay = 500 } = config; const { revert: revert2, review: review2, remove, search } = options; options.modify = !review2 && !search && (Object.keys(options).length || args.length); if (!options.modify && !search && target.length > maxPreviews) { return session.send(`一次最多同时预览 ${maxPreviews} 个问答。`); } argv.uneditable = []; argv.updated = []; argv.skipped = []; const dialogues = argv.dialogues = revert2 || review2 ? Object.values((0, import_koishi_utils3.pick)(argv.app.teachHistory, target)).filter(Boolean) : await Dialogue.get(app, target, null); argv.dialogueMap = Object.fromEntries(dialogues.map((d) => [d.id, __spreadValues({}, d)])); if (search) { return session.send(formatQuestionAnswers(argv, dialogues).join("\n")); } const actualIds = argv.dialogues.map((d) => d.id); argv.unknown = (0, import_koishi_utils3.difference)(target, actualIds); await app.serial("dialogue/before-detail", argv); if (!options.modify) { if (argv.unknown.length) { await session.send(`${review2 ? "最近无人修改过" : "没有搜索到"}编号为 ${argv.unknown.join(", ")} 的问答。`); } for (let index = 0; index < dialogues.length; index++) { const output = [`编号为 ${dialogues[index].id} 的${review2 ? "历史版本" : "问答信息"}:`]; await app.serial("dialogue/detail", dialogues[index], output, argv); if (index) await (0, import_koishi_utils3.sleep)(previewDelay); await session.send(output.join("\n")); } return; } const targets = prepareTargets(argv); if (revert2) { const message = targets.length ? await Dialogue.revert(targets, argv) : ""; return sendResult(argv, message); } if (remove) { let message = ""; if (targets.length) { const editable = await Dialogue.remove(targets, argv); message = `问答 ${editable.join(", ")} 已成功删除。`; } await app.serial("dialogue/after-modify", argv); return sendResult(argv, message); } if (targets.length) { const result = await app.serial("dialogue/before-modify", argv); if (typeof result === "string") return result; for (const dialogue of targets) { app.emit("dialogue/modify", argv, dialogue); } await Dialogue.update(targets, argv); await app.serial("dialogue/after-modify", argv); } return sendResult(argv); } async function create(argv) { const { app, options, args: [question, answer] } = argv; options.create = options.modify = true; argv.unknown = []; argv.uneditable = []; argv.updated = []; argv.skipped = []; argv.dialogues = await Dialogue.get(app, { question, answer, regexp: false }); await app.serial("dialogue/before-detail", argv); const result = await app.serial("dialogue/before-modify", argv); if (typeof result === "string") return result; if (argv.dialogues.length) { argv.target = argv.dialogues.map((d) => d.id); argv.dialogueMap = Object.fromEntries(argv.dialogues.map((d) => [d.id, d])); const targets = prepareTargets(argv); if (options.remove) { let message = ""; if (targets.length) { const editable = await Dialogue.remove(targets, argv); message = `问答 ${editable.join(", ")} 已成功删除。`; } await app.serial("dialogue/after-modify", argv); return sendResult(argv, message); } for (const dialogue2 of targets) { app.emit("dialogue/modify", argv, dialogue2); } await Dialogue.update(targets, argv); await app.serial("dialogue/after-modify", argv); return sendResult(argv); } const dialogue = { flag: 0 }; if (app.bail("dialogue/permit", argv, dialogue)) { return argv.session.send("该问答因权限过低无法添加。"); } try { app.emit("dialogue/modify", argv, dialogue); const created = await app.database.create("dialogue", dialogue); Dialogue.addHistory(dialogue, "添加", argv, false); argv.dialogues = [created]; await app.serial("dialogue/after-modify", argv); return sendResult(argv, `问答已添加,编号为 ${argv.dialogues[0].id}。`); } catch (err) { await argv.session.send("添加问答时遇到错误。"); throw err; } } // packages/plugin-teach/src/internal.ts var import_fastest_levenshtein = __toModule(require("fastest-levenshtein")); import_koishi_core3.template.set("teach", { "too-many-arguments": "存在多余的参数,请检查指令语法或将含有空格或换行的问答置于一对引号内。", "missing-question-or-answer": "缺少问题或回答,请检查指令语法。", "prohibited-command": "禁止在教学回答中插值调用 {0} 指令。", "prohibited-cq-code": "问题必须是纯文本。", "illegal-regexp": "问题含有错误的或不支持的正则表达式语法。", "probably-modify-answer": "推测你想修改的是回答而不是问题。发送空行或句号以修改回答,使用 -I 选项以忽略本提示。", "probably-regexp": "推测你想{0}的问题是正则表达式。发送空行或句号以添加 -x 选项,使用 -I 选项以忽略本提示。" }); function apply4(ctx, config) { (0, import_koishi_core3.defineProperty)(ctx.app, "teachHistory", {}); ctx.command("teach").option("ignoreHint", "-I 忽略智能提示").option("regexp", "-x 使用正则表达式匹配", { authority: config.authority.regExp }).option("regexp", "-X 取消使用正则表达式匹配", { value: false }).option("redirect", "=> <answer:string> 重定向到其他问答").check(({ options, args }) => { function parseArgument() { if (!args.length) return ""; const [arg] = args.splice(0, 1); if (!arg || arg === "~" || arg === "~") return ""; return arg.trim(); } const question = parseArgument(); const answer = options.redirect ? `$(dialogue ${options.redirect})` : parseArgument(); if (args.length) { return (0, import_koishi_core3.template)("teach.too-many-arguments"); } else if (/\[CQ:(?!face)/.test(question)) { return (0, import_koishi_core3.template)("teach.prohibited-cq-code"); } const { original, parsed, appellative } = options.regexp ? { original: import_koishi_core3.segment.unescape(question), parsed: question, appellative: false } : config._stripQuestion(question); (0, import_koishi_core3.defineProperty)(options, "appellative", appellative); (0, import_koishi_core3.defineProperty)(options, "original", original); args[0] = parsed; args[1] = answer; if (!args[0] && !args[1]) args.splice(0, Infinity); }); function maybeAnswer(question, dialogues) { return dialogues.every((dialogue) => { const dist = (0, import_fastest_levenshtein.distance)(question, dialogue.answer); return dist < dialogue.answer.length / 2 && dist < (0, import_fastest_levenshtein.distance)(question, dialogue.question); }); } function maybeRegExp(question) { return question.startsWith("^") || question.endsWith("$"); } ctx.before("dialogue/modify", async (argv) => { const { options, session, target, dialogues, args } = argv; const { ignoreHint, regexp } = options; const [question, answer] = args; function applySuggestion(argv2) { return argv2.target ? update(argv2) : create(argv2); } if (target && !ignoreHint && question && !answer && maybeAnswer(question, dialogues)) { const dispose = session.middleware(({ content }, next) => { dispose(); content = content.trim(); if (content && content !== "." && content !== "。") return next(); args[1] = options.original; args[0] = ""; return applySuggestion(argv); }); return (0, import_koishi_core3.template)("teach.probably-modify-answer"); } if (question && !regexp && maybeRegExp(question) && !ignoreHint && (!target || !dialogues.every((d) => d.flag & Dialogue.Flag.regexp))) { const dispose = session.middleware(({ content }, next) => { dispose(); content = content.trim(); if (content && content !== "." && content !== "。") return next(); options.regexp = true; return applySuggestion(argv); }); return (0, import_koishi_core3.template)("teach.probably-regexp", target ? "修改" : "添加"); } if (regexp || regexp !== false && question && dialogues.some((d) => d.flag & Dialogue.Flag.regexp)) { const questions = question ? [question] : dialogues.map((d) => d.question); try { questions.forEach((q) => new RegExp(q)); } catch (error) { return (0, import_koishi_core3.template)("teach.illegal-regexp"); } } }); ctx.before("dialogue/modify", async ({ options, target, args }) => { if (options.create && !target && !(args[0] && args[1])) { return (0, import_koishi_core3.template)("teach.missing-question-or-answer"); } }); ctx.on("dialogue/modify", ({ options, args }, data) => { if (args[1]) { data.answer = args[1]; } if (options.regexp !== void 0) { data.flag &= ~Dialogue.Flag.regexp; data.flag |= +options.regexp * Dialogue.Flag.regexp; } if (args[0]) { data.question = args[0]; data.original = options.original; } }); ctx.on("dialogue/detail", async (dialogue, output, argv) => { var _a; if ((_a = dialogue._redirections) == null ? void 0 : _a.length) { output.push("重定向到:", ...formatQuestionAnswers(argv, dialogue._redirections)); } }); ctx.on("dialogue/flag", (flag) => { ctx.before("dialogue/search", ({ options }, test) => { test[flag] = options[flag]; }); ctx.on("dialogue/modify", ({ options }, data) => { if (options[flag] !== void 0) { data.flag &= ~Dialogue.Flag[flag]; data.flag |= +options[flag] * Dialogue.Flag[flag]; } }); ctx.on("dialogue/test", (test, query) => { if (test[flag] === void 0) return; query.$and.push({ flag: { [test[flag] ? "$bitsAllSet" : "$bitsAllClear"]: Dialogue.Flag[flag] } }); }); }); ctx.before("command", ({ command, session }) => { if (command.config.noInterp && session._redirected) { return (0, import_koishi_core3.template)("teach.prohibited-command", command.name); } }); ctx.before("dialogue/modify", async ({ args }) => { if (!args[1]) return; try { args[1] = await ctx.transformAssets(args[1]); } catch (error) { ctx.logger("teach").warn(error.message); return "上传图片时发生错误。"; } }); ctx.on("dialogue/test", ({ regexp, answer, question, original }, query) => { if (regexp) { if (answer) query.answer = { $regex: new RegExp(answer, "i") }; if (original) query.original = { $regex: new RegExp(original, "i") }; return; } if (answer) query.answer = answer; if (regexp === false) { if (question) query.question = question; } else if (original) { const $or = [{ flag: { $bitsAllSet: Dialogue.Flag.regexp }, original: { $regexFor: original } }]; if (question) $or.push({ flag: { $bitsAllClear: Dialogue.Flag.regexp }, question }); query.$and.push({ $or }); } }); } // packages/plugin-teach/src/plugins/context.ts var import_koishi_utils4 = __toModule(require("koishi-utils")); var RE_GROUPS = /^\d+(,\d+)*$/; function apply5(ctx, config) { if (config.useContext === false) return; const authority = config.authority.context; ctx.command("teach").option("disable", "-d 在当前环境下禁用问答").option("disableGlobal", "-D 在所有环境下禁用问答", { authority }).option("enable", "-e 在当前环境下启用问答").option("enableGlobal", "-E 在所有环境下启用问答", { authority }).option("groups", "-g <gids:string> 设置具体的生效环境", { authority, type: RE_GROUPS }).option("global", "-G 无视上下文搜索").action(({ options, session }) => { if (options.disable && options.enable) { return "选项 -d, -e 不能同时使用。"; } else if (options.disableGlobal && options.enableGlobal) { return "选项 -D, -E 不能同时使用。"; } else if (options.disableGlobal && options.disable) { return "选项 -D, -d 不能同时使用。"; } else if (options.enable && options.enableGlobal) { return "选项 -E, -e 不能同时使用。"; } let noContextOptions = false; let reversed, partial, groups; if (options.disable) { reversed = true; partial = !options.enableGlobal; groups = [session.cid]; } else if (options.disableGlobal) { reversed = !!options.groups; partial = false; groups = options.enable ? [session.cid] : []; } else if (options.enableGlobal) { reversed = !options.groups; partial = false; groups = []; } else { noContextOptions = !options.enable; if (options["target"] ? options.enable : !options.global) { reversed = false; partial = true; groups = [session.cid]; } } (0, import_koishi_utils4.defineProperty)(options, "reversed", reversed); (0, import_koishi_utils4.defineProperty)(options, "partial", partial); if ("groups" in options) { if (noContextOptions) { return "选项 -g, --groups 必须与 -d/-D/-e/-E 之一同时使用。"; } else { (0, import_koishi_utils4.defineProperty)(options, "groups", options.groups ? options.groups.split(",").map((id) => `${session.platform}:${id}`) : []); } } else if (session.subtype !== "group" && options["partial"]) { return "非群聊上下文中请使用 -E/-D 进行操作或指定 -g, --groups 选项。"; } else { (0, import_koishi_utils4.defineProperty)(options, "groups", groups); } }); ctx.on("dialogue/modify", ({ options }, data) => { const { groups, partial, reversed } = options; if (!groups) return; if (!data.groups) data.groups = []; if (partial) { const newGroups = !(data.flag & Dialogue.Flag.complement) === reversed ? (0, import_koishi_utils4.difference)(data.groups, groups) : (0, import_koishi_utils4.union)(data.groups, groups); if (!equal(data.groups, newGroups)) { data.groups = newGroups.sort(); } } else { data.flag = data.flag & ~Dialogue.Flag.complement | +reversed * Dialogue.Flag.complement; if (!equal(data.groups, groups)) { data.groups = groups.sort(); } } }); ctx.before("dialogue/search", ({ options }, test) => { test.partial = options.partial; test.reversed = options.reversed; test.groups = options.groups; }); ctx.on("dialogue/detail", ({ groups, flag }, output, { session }) => { const thisGroup = session.subtype === "group" && groups.includes(session.cid); output.push(`生效环境:${flag & Dialogue.Flag.complement ? thisGroup ? groups.length - 1 ? `除本群等 ${groups.length} 个群外的所有群` : "除本群" : groups.length ? `除 ${groups.length} 个群外的所有群` : "全局" : thisGroup ? groups.length - 1 ? `本群等 ${groups.length} 个群` : "本群" : groups.length ? `${groups.length} 个群` : "全局禁止"}`); }); ctx.on("dialogue/detail-short", ({ groups, flag }, output, { session, options }) => { if (!options.groups && session.subtype === "group") { const isReversed = flag & Dialogue.Flag.complement; const hasGroup = groups.includes(session.cid); output.unshift(!isReversed === hasGroup ? isReversed ? "E" : "e" : isReversed ? "d" : "D"); } }); ctx.on("dialogue/receive", ({ session, test }) => { test.partial = true; test.reversed = false; test.groups = [session.cid]; }); ctx.on("dialogue/test", (test, query) => { if (!test.groups || !test.groups.length) return; query.$and.push({ $or: [{ flag: { [test.reversed ? "$bitsAllSet" : "$bitsAllClear"]: Dialogue.Flag.complement }, $and: test.groups.map(($el) => ({ groups: { $el } })) }, { flag: { [test.reversed ? "$bitsAllClear" : "$bitsAllSet"]: Dialogue.Flag.complement }, $not: { groups: { $el: test.groups } } }] }); }); } // packages/plugin-teach/src/plugins/throttle.ts var import_koishi_utils5 = __toModule(require("koishi-utils")); function apply6(ctx, config) { const throttleConfig = (0, import_koishi_utils5.makeArray)(config.throttle); const counters = {}; for (const { interval, responses } of throttleConfig) { counters[interval] = responses; } ctx.on("dialogue/state", (state) => { state.counters = __spreadValues({}, counters); }); ctx.on("dialogue/receive", ({ counters: counters2, session }) => { if (session._redirected) return; for (const interval in counters2) { if (counters2[interval] <= 0) return true; } }); ctx.before("dialogue/send", ({ counters: counters2, session }) => { if (session._redirected) return; for (const { interval } of throttleConfig) { counters2[interval]--; setTimeout(() => counters2[interval]++, interval); } }); const { preventLoop } = config; const preventLoopConfig = !preventLoop ? [] : typeof preventLoop === "number" ? [{ length: preventLoop, participants: 1 }] : (0, import_koishi_utils5.makeArray)(preventLoop); const initiatorCount = Math.max(0, ...preventLoopConfig.map((c) => c.length)); ctx.on("dialogue/state", (state) => { state.initiators = []; }); ctx.on("dialogue/receive", (state) => { if (state.session._redirected) return; const timestamp = Date.now(); for (const { participants, length, debounce } of preventLoopConfig) { if (state.initiators.length < length) break; const initiators = new Set(state.initiators.slice(0, length)); if (initiators.size <= participants && initiators.has(state.userId) && !(debounce > timestamp - state.loopTimestamp)) { state.loopTimestamp = timestamp; return true; } } }); ctx.before("dialogue/send", (state) => { if (state.session._redirected) return; state.initiators.unshift(state.userId); state.initiators.splice(initiatorCount, Infinity); state.loopTimestamp = null; }); } // packages/plugin-teach/src/plugins/probability.ts function apply7(ctx, config) { const { appellationTimeout = 2e4 } = config; ctx.command("teach").option("probabilityStrict", "-p <prob> 设置问题的触发权重", { type: isZeroToOne }).option("probabilityAppellative", "-P <prob> 设置被称呼时问题的触发权重", { type: isZeroToOne }); ctx.on("dialogue/modify", ({ options }, data) => { var _a, _b; if (options.create) { data.probS = (_a = options.probabilityStrict) != null ? _a : 1 - +options.appellative; data.probA = (_b = options.probabilityAppellative) != null ? _b : +options.appellative; } else { if (options.probabilityStrict !== void 0) { data.probS = options.probabilityStrict; } if (options.probabilityAppellative !== void 0) { data.probA = options.probabilityAppellative; } } }); ctx.on("dialogue/state", (state) => { state.activated = {}; }); ctx.on("dialogue/prepare", ({ test, userId, dialogues, activated }) => { const hasNormal = dialogues.some((d) => !(d.flag & Dialogue.Flag.regexp)); dialogues.forEach((dialogue) => { if (hasNormal && dialogue.flag & Dialogue.Flag.regexp) { dialogue._weight = 0; } else if (userId in activated) { dialogue._weight = Math.max(dialogue.probS, dialogue.probA); } else if (!test.appellative || !(dialogue.flag & Dialogue.Flag.regexp)) { dialogue._weight = test.appellative ? dialogue.probA : dialogue.probS; } else { const regexp = new RegExp(dialogue.question); const queue = dialogue.probS >= dialogue.probA ? [[test.original, dialogue.probS], [test.question, dialogue.probA]] : [[test.question, dialogue.probA], [test.original, dialogue.probS]]; for (const [question, weight] of queue) { dialogue._capture = regexp.exec(question); dialogue._weight = weight; if (dialogue._capture) break; } } }); }); ctx.before("dialogue/send", ({ test, activated, userId }) => { if (!test.activated) return; const time = activated[userId] = Date.now(); setTimeout(() => { if (activated[userId] === time) { delete activated[userId]; } }, appellationTimeout); }); ctx.on("dialogue/detail", ({ probS, probA }, output) => { if (probS < 1 || probA > 0) output.push(`触发权重:p=${probS}, P=${probA}`); }); ctx.on("dialogue/detail-short", ({ pr