koishi-plugin-teach
Version:
Teach plugin for Koishi
1,288 lines (1,277 loc) • 74.9 kB
JavaScript
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