diff --git a/packages/plugin-eval/tests/index.spec.ts b/packages/plugin-eval/tests/index.spec.ts index a61308ccc3..2938e54431 100644 --- a/packages/plugin-eval/tests/index.spec.ts +++ b/packages/plugin-eval/tests/index.spec.ts @@ -82,6 +82,8 @@ describe('Eval Plugin', () => { await ses.shouldReply('demo', '123') await ses.shouldReply('# ^repeat:(.+) ${"$1".repeat(3)} -x', '问答已添加,编号为 2。') await ses.shouldReply('repeat:123', '123123123') + await ses.shouldReply('# ^我.+ 对,${"$0".replace(/我/, "你")} -x', '问答已添加,编号为 3。') + await ses.shouldReply('我是伞兵', '对,你是伞兵') }) it('global', async () => { diff --git a/packages/plugin-teach/src/database/mongo.ts b/packages/plugin-teach/src/database/mongo.ts index 92c6b1b7aa..e18ff946cd 100644 --- a/packages/plugin-teach/src/database/mongo.ts +++ b/packages/plugin-teach/src/database/mongo.ts @@ -70,29 +70,24 @@ export default function apply(ctx: Context) { ctx.on('dialogue/mongo', ({ regexp, answer, question, original }, conditionals) => { if (regexp) { if (answer) conditionals.push({ answer: { $regex: new RegExp(answer, 'i') } }) - if (question) conditionals.push({ question: { $regex: new RegExp(original, 'i') } }) + if (original) conditionals.push({ original: { $regex: new RegExp(original, 'i') } }) return } if (answer) conditionals.push({ answer }) - if (question) { - if (regexp === false) { - conditionals.push({ question }) - } else { - const $expr = { - body(field: string, question: string, original: string) { - const regex = new RegExp(field, 'i') - return regex.test(question) || regex.test(original) - }, - args: ['$name', question, original], - lang: 'js', - } - conditionals.push({ - $or: [ - { flag: { $bitsAllClear: Dialogue.Flag.regexp }, question }, - { flag: { $bitsAllSet: Dialogue.Flag.regexp }, $expr }, - ], - }) + if (regexp === false) { + if (question) conditionals.push({ question }) + } else if (original) { + const $expr = { + body(field: string, original: string) { + const regex = new RegExp(field, 'i') + return regex.test(original) + }, + args: ['$name', original], + lang: 'js', } + const conds = [{ flag: { $bitsAllSet: Dialogue.Flag.regexp }, $expr } as FilterQuery] + if (question) conds.push({ flag: { $bitsAllClear: Dialogue.Flag.regexp }, question }) + conditionals.push({ $or: conds }) } }) diff --git a/packages/plugin-teach/src/database/mysql.ts b/packages/plugin-teach/src/database/mysql.ts index 36b357ddb1..05340b5a0a 100644 --- a/packages/plugin-teach/src/database/mysql.ts +++ b/packages/plugin-teach/src/database/mysql.ts @@ -84,22 +84,17 @@ export default function apply(ctx: Context) { if (regexp) { if (answer) conditionals.push('`answer` REGEXP ' + escape(answer)) - if (question) conditionals.push('`question` REGEXP ' + escape(original)) + if (original) conditionals.push('`original` REGEXP ' + escape(original)) return } if (answer) conditionals.push('`answer` = ' + escape(answer)) - if (question) { - if (regexp === false) { - conditionals.push('`question` = ' + escape(question)) - } else { - conditionals.push(`(\ - !(\`flag\` & ${Dialogue.Flag.regexp}) && \`question\` = ${escape(question)} ||\ - \`flag\` & ${Dialogue.Flag.regexp} && (\ - ${escape(question)} REGEXP \`question\` || ${escape(original)} REGEXP \`question\`\ - )\ - )`) - } + if (regexp === false) { + if (question) conditionals.push('`question` = ' + escape(question)) + } else if (original) { + const conds = [`\`flag\` & ${Dialogue.Flag.regexp} && ${escape(original)} REGEXP \`original\``] + if (question) conds.push(`!(\`flag\` & ${Dialogue.Flag.regexp}) && \`question\` = ${escape(question)}`) + conditionals.push(`(${conds.join(' || ')})`) } }) diff --git a/packages/plugin-teach/src/internal.ts b/packages/plugin-teach/src/internal.ts index fcd73b50bd..b39294ddc5 100644 --- a/packages/plugin-teach/src/internal.ts +++ b/packages/plugin-teach/src/internal.ts @@ -45,11 +45,11 @@ export default function apply(ctx: Context, config: Dialogue.Config) { } else if (/\[CQ:(?!face)/.test(question)) { return template('teach.prohibited-cq-code') } - const { unprefixed, prefixed, appellative } = options.regexp - ? { unprefixed: question, prefixed: question, appellative: false } + const { unprefixed, appellative } = options.regexp + ? { unprefixed: question, appellative: false } : config._stripQuestion(question) defineProperty(options, 'appellative', appellative) - defineProperty(options, '_original', prefixed) + defineProperty(options, '_original', question) defineProperty(options, 'original', question) args[0] = unprefixed args[1] = answer diff --git a/packages/plugin-teach/src/receiver.ts b/packages/plugin-teach/src/receiver.ts index e87566c7c2..8aee50ea97 100644 --- a/packages/plugin-teach/src/receiver.ts +++ b/packages/plugin-teach/src/receiver.ts @@ -163,6 +163,10 @@ tokenizer.interpolate('$n', '', (rest) => { return { rest, tokens: [], source: '' } }) +const halfWidth = ',,.~?!()[]' +const fullWidth = ',、。~?!()【】' +const fullWidthRegExp = new RegExp(`[${fullWidth}]`) + export async function triggerDialogue(ctx: Context, session: Session, next: NextFunction = noop) { const state = ctx.getSessionState(session) state.next = next @@ -203,8 +207,7 @@ export async function triggerDialogue(ctx: Context, session: Session, next: Next .replace(/\$0/g, escapeAnswer(session.content)) if (dialogue.flag & Dialogue.Flag.regexp) { - const capture = dialogue._capture || new RegExp(dialogue.question, 'i').exec(state.test.question) - if (!capture) console.log(dialogue.question, state.test.question) + const capture = dialogue._capture || new RegExp(dialogue.original, 'i').exec(state.test.original) capture.map((segment, index) => { if (index && index <= 9) { state.answer = state.answer.replace(new RegExp(`\\$${index}`, 'g'), escapeAnswer(segment || '')) @@ -245,7 +248,17 @@ export default function apply(ctx: Context, config: Dialogue.Config) { ctx.app._dialogueStates = {} config._stripQuestion = (source) => { - source = prepareSource(source) + source = segment.transform(source, { + text: ({ content }, index, chain) => { + let message = simplify(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 original = source const capture = nicknameRE.exec(source) if (capture) source = source.slice(capture[0].length) @@ -297,12 +310,11 @@ export default function apply(ctx: Context, config: Dialogue.Config) { ctx.on('dialogue/receive', ({ session, test }) => { if (session.content.includes('[CQ:image,')) return true - const { unprefixed, prefixed, appellative, activated } = config._stripQuestion(session.content) + const { unprefixed, appellative, activated } = config._stripQuestion(session.content) test.question = unprefixed - test.original = prefixed + test.original = session.content test.activated = activated test.appellative = appellative - if (!test.question) return true }) // 预判要获取的用户字段 @@ -322,27 +334,3 @@ export default function apply(ctx: Context, config: Dialogue.Config) { return triggerDialogue(ctx, session, next) }) } - -function prepareSource(source: string) { - return segment.transform(source, { - text: ({ content }, index, chain) => { - let message = simplify(segment.unescape('' + content)) - .toLowerCase() - .replace(/\s+/g, '') - .replace(/,/g, ',') - .replace(/、/g, ',') - .replace(/。/g, '.') - .replace(/?/g, '?') - .replace(/!/g, '!') - .replace(/(/g, '(') - .replace(/)/g, ')') - .replace(/【/g, '[') - .replace(/】/g, ']') - .replace(/~/g, '~') - .replace(/…/g, '...') - if (index === 0) message = message.replace(/^[()\[\]]*/, '') - if (index === chain.length - 1) message = message.replace(/[\.,?!()\[\]~]*$/, '') - return message - }, - }) -} diff --git a/packages/plugin-teach/src/search.ts b/packages/plugin-teach/src/search.ts index 0e312beaac..88e091eab7 100644 --- a/packages/plugin-teach/src/search.ts +++ b/packages/plugin-teach/src/search.ts @@ -165,7 +165,7 @@ async function showSearch(argv: Dialogue.Argv) { const { regexp, page = 1, original, pipe, recursive, autoMerge } = options const { itemsPerPage = 30, mergeThreshold = 5 } = argv.config - const test: DialogueTest = { question, answer, regexp, original: options._original } + const test: DialogueTest = { question, answer, regexp, original } if (app.bail('dialogue/before-search', argv, test)) return const dialogues = await app.database.getDialoguesByTest(test) @@ -182,14 +182,14 @@ async function showSearch(argv: Dialogue.Argv) { await argv.app.parallel('dialogue/search', argv, test, dialogues) } - if (!question && !answer) { + if (!original && !answer) { if (!dialogues.length) return '没有搜索到任何回答,尝试切换到其他环境。' return sendResult('全部问答如下', formatQuestionAnswers(argv, dialogues)) } if (!options.regexp) { const suffix = options.regexp !== false ? ',请尝试使用正则表达式匹配' : '' - if (!question) { + if (!original) { if (!dialogues.length) return session.send(`没有搜索到回答“${answer}”${suffix}。`) const output = dialogues.map(d => `${formatPrefix(argv, d)}${d.original}`) return sendResult(`回答“${answer}”的问题如下`, output) @@ -227,7 +227,7 @@ async function showSearch(argv: Dialogue.Argv) { }) } - if (!question) { + if (!original) { if (!dialogues.length) return `没有搜索到含有正则表达式“${answer}”的回答。` return sendResult(`回答正则表达式“${answer}”的搜索结果如下`, output) } else if (!answer) { diff --git a/packages/plugin-teach/tests/basic.spec.ts b/packages/plugin-teach/tests/basic.spec.ts index 8fdb92889c..91a34e9bcd 100644 --- a/packages/plugin-teach/tests/basic.spec.ts +++ b/packages/plugin-teach/tests/basic.spec.ts @@ -3,169 +3,169 @@ import { install, InstalledClock } from '@sinonjs/fake-timers' import createEnvironment from './environment' import jest from 'jest-mock' -describe('Teach Plugin - Basic', () => { - describe('Basic Support', () => { - const { app } = createEnvironment({ - mergeThreshold: 1, - }) - - const session1 = app.session('123', '456') - const session2 = app.session('321', '456') - - before(async () => { - await app.start() - await app.database.initUser('123', 3) - await app.database.initUser('321', 2) - await app.database.initChannel('456') - }) - - it('create', async () => { - await session1.shouldNotReply('foo') - await session1.shouldReply('# foo', '缺少问题或回答,请检查指令语法。') - await session1.shouldReply('# foo bar', '问答已添加,编号为 1。') - await session1.shouldReply('# foo bar baz', '存在多余的参数,请检查指令语法或将含有空格或换行的问答置于一对引号内。') - await session1.shouldReply('foo', 'bar') - }) - - it('validate', async () => { - await session1.shouldReply('# [CQ:image] bar', '问题必须是纯文本。') - await session1.shouldReply('# foo[foo bar -x', '问题含有错误的或不支持的正则表达式语法。') - }) - - it('modify', async () => { - await session1.shouldReply('# foo bar', '问答已存在,编号为 1,如要修改请尝试使用 #1 指令。') - await session1.shouldReply('# foo bar -P 1', '修改了已存在的问答,编号为 1。') - await session1.shouldReply('#1 -P 1', '问答 1 没有发生改动。') - await session1.shouldReply('#1 baz', '推测你想修改的是回答而不是问题。发送空行或句号以修改回答,使用 -I 选项以忽略本提示。') - await session1.shouldReply('#1 baz', '推测你想修改的是回答而不是问题。发送空行或句号以修改回答,使用 -I 选项以忽略本提示。') - await session1.shouldReply('.', '问答 1 已成功修改。') - await session1.shouldReply('foo', 'baz') - }) - - it('search 1', async () => { - await session1.shouldReply('## foo', '问题“foo”的回答如下:\n1. [P=1] baz') - await session1.shouldReply('## baz', '没有搜索到问题“baz”,请尝试使用正则表达式匹配。') - await session1.shouldReply('## baz -x', '没有搜索到含有正则表达式“baz”的问题。') - await session1.shouldReply('## ~ baz', '回答“baz”的问题如下:\n1. [P=1] foo') - await session1.shouldReply('## ~ foo', '没有搜索到回答“foo”,请尝试使用正则表达式匹配。') - await session1.shouldReply('## ~ foo -x', '没有搜索到含有正则表达式“foo”的回答。') - await session1.shouldReply('## foo baz', '“foo”“baz”匹配的回答如下:\n1') - await session1.shouldReply('## foo bar', '没有搜索到问答“foo”“bar”,请尝试使用正则表达式匹配。') - await session1.shouldReply('## foo bar -x', '没有搜索到含有正则表达式“foo”“bar”的问答。') - }) - - it('search 2', async () => { - await session1.shouldReply('# foo bar', '问答已添加,编号为 2。') - await session1.shouldReply('# goo bar', '问答已添加,编号为 3。') - await session1.shouldReply('##', '共收录了 2 个问题和 3 个回答。') - await session1.shouldReply('## fo -x', '问题正则表达式“fo”的搜索结果如下:\n1. [P=1] 问题:foo,回答:baz\n2. 问题:foo,回答:bar') - await session1.shouldReply('## ~ ar -x', '回答正则表达式“ar”的搜索结果如下:\n2. 问题:foo,回答:bar\n3. 问题:goo,回答:bar') - await session1.shouldReply('## fo ar -x', '问答正则表达式“fo”“ar”的搜索结果如下:\n2. 问题:foo,回答:bar') - await session1.shouldReply('### oo', '问题正则表达式“oo”的搜索结果如下:\nfoo (共 2 个回答)\ngoo (#3)') - await session1.shouldReply('### ~ ba', '回答正则表达式“ba”的搜索结果如下:\nbaz (#1)\nbar (共 2 个问题)') - }) - - it('miscellaneous', async () => { - await session1.shouldNotReply('.foo') - await session1.shouldReply('#') - await session2.shouldReply('#') - }) - }) - - describe('Internal', () => { - const { u3g1 } = createEnvironment({}) - - let clock: InstalledClock - const randomReal = jest.spyOn(Random, 'real') - - before(() => { - clock = install({ shouldAdvanceTime: true, advanceTimeDelta: 5 }) - randomReal.mockReturnValue(1 - Number.EPSILON) - }) - - after(() => { - clock.uninstall() - randomReal.mockRestore() - }) - - it('appellative', async () => { - await u3g1.shouldReply('# koishi,foo bar', '问答已添加,编号为 1。') - await u3g1.shouldNotReply('foo') - await u3g1.shouldReply('koishi, foo', 'bar') - await u3g1.shouldReply('satori, foo', 'bar') - // TODO support at-trigger - // await u3g1.shouldReply(`[CQ:at,id=${app.selfId}] foo`, 'bar') - await u3g1.shouldReply('#1', '编号为 1 的问答信息:\n问题:koishi,foo\n回答:bar\n触发权重:p=0, P=1') - await u3g1.shouldReply('## foo', '问题“foo”的回答如下:\n1. [p=0, P=1] bar') - }) - - it('activated', async () => { - await u3g1.shouldReply('# koishi ?', '问答已添加,编号为 2。') - await u3g1.shouldReply('koishi', '?') - await u3g1.shouldReply('foo', 'bar') - - // due to mocked Random.real - await u3g1.shouldReply('# satori ! -p 0.5', '问答已添加,编号为 3。') - await u3g1.shouldNotReply('satori') - }) - - it('regular expression', async () => { - clock.runAll() - await u3g1.shouldReply('# foo baz -xP 0.5', '问答已添加,编号为 4。') - await u3g1.shouldNotReply('foo') - await u3g1.shouldReply('koishi, fooo', 'baz') - await u3g1.shouldReply('#4 -p 0.5 -P 1', '问答 4 已成功修改。') - await u3g1.shouldReply('koishi, fooo', 'baz') - }) - }) - - describe('Interpolate', () => { - const { u3g1, app } = createEnvironment({}) - - app.command('bar').action(() => 'hello') - app.command('baz').action(({ session }) => session.sendQueued('hello')) - app.command('report [text]').action(async ({ session }, text) => { - await session.sendQueued(text) - await session.sendQueued('end') - }) - - it('basic support', async () => { - await u3g1.shouldReply('# foo $(bar)', '问答已添加,编号为 1。') - await u3g1.shouldReply('foo', ['hello']) - await u3g1.shouldReply('#1 ~ 1$(bar)2', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['1hello2']) - await u3g1.shouldReply('#1 ~ 1$(bar)2$(bar)3', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['1hello2hello3']) - await u3g1.shouldReply('#1 ~ 1$(barrr)2', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['12']) - await u3g1.shouldReply('#1 ~ $(barrr)', '问答 1 已成功修改。') - await u3g1.shouldNotReply('foo') - await u3g1.shouldReply('#1 -r', '问答 1 已成功删除。') - }) - - it('queued messages', async () => { - await u3g1.shouldReply('# foo $(baz)', '问答已添加,编号为 1。') - await u3g1.shouldReply('foo', ['hello']) - await u3g1.shouldReply('#1 ~ 1$(baz)2', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['1hello', '2']) - await u3g1.shouldReply('#1 ~ $(bar)$(baz)', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['hellohello']) - await u3g1.shouldReply('#1 ~ $(baz)$(bar)', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['hello', 'hello']) - await u3g1.shouldReply('#1 ~ 1$n$(bar)$n2', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['1', 'hello', '2']) - await u3g1.shouldReply('#1 ~ 1$n$(baz)$n2', '问答 1 已成功修改。') - await u3g1.shouldReply('foo', ['1', 'hello', '2']) - await u3g1.shouldReply('#1 -r', '问答 1 已成功删除。') - }) - - it('capturing groups', async () => { - await u3g1.shouldReply('# ^foo(.*) $(report $1) -x', '问答已添加,编号为 1。') - await u3g1.shouldReply('foobar', ['bar', 'end']) - await u3g1.shouldReply('foo', ['end']) - await u3g1.shouldReply('#1 ~ foo$0', '问答 1 已成功修改。') - await u3g1.shouldReply('foobar', ['foofoobar']) - await u3g1.shouldReply('#1 -r', '问答 1 已成功删除。') - }) +describe('Teach Plugin - Basic Support', () => { + const { app } = createEnvironment({ + mergeThreshold: 1, + }) + + const session1 = app.session('123', '456') + const session2 = app.session('321', '456') + + before(async () => { + await app.start() + await app.database.initUser('123', 3) + await app.database.initUser('321', 2) + await app.database.initChannel('456') + }) + + it('create', async () => { + await session1.shouldNotReply('foo') + await session1.shouldReply('# foo', '缺少问题或回答,请检查指令语法。') + await session1.shouldReply('# foo bar', '问答已添加,编号为 1。') + await session1.shouldReply('# foo bar baz', '存在多余的参数,请检查指令语法或将含有空格或换行的问答置于一对引号内。') + await session1.shouldReply('foo', 'bar') + }) + + it('validate', async () => { + await session1.shouldReply('# [CQ:image] bar', '问题必须是纯文本。') + await session1.shouldReply('# foo[foo bar -x', '问题含有错误的或不支持的正则表达式语法。') + }) + + it('modify', async () => { + await session1.shouldReply('# foo bar', '问答已存在,编号为 1,如要修改请尝试使用 #1 指令。') + await session1.shouldReply('# foo bar -P 1', '修改了已存在的问答,编号为 1。') + await session1.shouldReply('#1 -P 1', '问答 1 没有发生改动。') + await session1.shouldReply('#1 baz', '推测你想修改的是回答而不是问题。发送空行或句号以修改回答,使用 -I 选项以忽略本提示。') + await session1.shouldReply('#1 baz', '推测你想修改的是回答而不是问题。发送空行或句号以修改回答,使用 -I 选项以忽略本提示。') + await session1.shouldReply('.', '问答 1 已成功修改。') + await session1.shouldReply('foo', 'baz') + }) + + it('search 1', async () => { + await session1.shouldReply('## foo', '问题“foo”的回答如下:\n1. [P=1] baz') + await session1.shouldReply('## baz', '没有搜索到问题“baz”,请尝试使用正则表达式匹配。') + await session1.shouldReply('## baz -x', '没有搜索到含有正则表达式“baz”的问题。') + await session1.shouldReply('## ~ baz', '回答“baz”的问题如下:\n1. [P=1] foo') + await session1.shouldReply('## ~ foo', '没有搜索到回答“foo”,请尝试使用正则表达式匹配。') + await session1.shouldReply('## ~ foo -x', '没有搜索到含有正则表达式“foo”的回答。') + await session1.shouldReply('## foo baz', '“foo”“baz”匹配的回答如下:\n1') + await session1.shouldReply('## foo bar', '没有搜索到问答“foo”“bar”,请尝试使用正则表达式匹配。') + await session1.shouldReply('## foo bar -x', '没有搜索到含有正则表达式“foo”“bar”的问答。') + }) + + it('search 2', async () => { + await session1.shouldReply('# foo bar', '问答已添加,编号为 2。') + await session1.shouldReply('# goo bar', '问答已添加,编号为 3。') + await session1.shouldReply('##', '共收录了 2 个问题和 3 个回答。') + await session1.shouldReply('## fo -x', '问题正则表达式“fo”的搜索结果如下:\n1. [P=1] 问题:foo,回答:baz\n2. 问题:foo,回答:bar') + await session1.shouldReply('## ~ ar -x', '回答正则表达式“ar”的搜索结果如下:\n2. 问题:foo,回答:bar\n3. 问题:goo,回答:bar') + await session1.shouldReply('## fo ar -x', '问答正则表达式“fo”“ar”的搜索结果如下:\n2. 问题:foo,回答:bar') + await session1.shouldReply('### oo', '问题正则表达式“oo”的搜索结果如下:\nfoo (共 2 个回答)\ngoo (#3)') + await session1.shouldReply('### ~ ba', '回答正则表达式“ba”的搜索结果如下:\nbaz (#1)\nbar (共 2 个问题)') + }) + + it('miscellaneous', async () => { + await session1.shouldNotReply('.foo') + await session1.shouldReply('#') + await session2.shouldReply('#') + }) +}) + +describe('Teach Plugin - Appellative', () => { + const { u3g1 } = createEnvironment({}) + + let clock: InstalledClock + const randomReal = jest.spyOn(Random, 'real') + + before(() => { + clock = install({ shouldAdvanceTime: true, advanceTimeDelta: 5 }) + randomReal.mockReturnValue(1 - Number.EPSILON) + }) + + after(() => { + clock.uninstall() + randomReal.mockRestore() + }) + + it('appellative', async () => { + await u3g1.shouldReply('# koishi,foo bar', '问答已添加,编号为 1。') + await u3g1.shouldNotReply('foo') + // should strip spaces + await u3g1.shouldReply('koishi, foo', 'bar') + // should strip punctuations + await u3g1.shouldReply('satori, foo?', 'bar') + // TODO support at-trigger + // await u3g1.shouldReply(`[CQ:at,id=${app.selfId}] foo`, 'bar') + await u3g1.shouldReply('#1', '编号为 1 的问答信息:\n问题:koishi,foo\n回答:bar\n触发权重:p=0, P=1') + await u3g1.shouldReply('## foo', '问题“foo”的回答如下:\n1. [p=0, P=1] bar') + }) + + it('activated', async () => { + await u3g1.shouldReply('# koishi ?', '问答已添加,编号为 2。') + await u3g1.shouldReply('koishi', '?') + await u3g1.shouldReply('foo', 'bar') + + // due to mocked Random.real + await u3g1.shouldReply('# satori ! -p 0.5', '问答已添加,编号为 3。') + await u3g1.shouldNotReply('satori') + }) + + it('regular expression', async () => { + clock.runAll() + await u3g1.shouldReply('# foo baz -xP 0.5', '问答已添加,编号为 4。') + await u3g1.shouldNotReply('foo') + await u3g1.shouldReply('koishi, fooo', 'baz') + await u3g1.shouldReply('#4 -p 0.5 -P 1', '问答 4 已成功修改。') + await u3g1.shouldReply('koishi, fooo', 'baz') + }) +}) + +describe('Teach Plugin - Interpolate', () => { + const { u3g1, app } = createEnvironment({}) + + app.command('bar').action(() => 'hello') + app.command('baz').action(({ session }) => session.sendQueued('hello')) + app.command('report [text]').action(async ({ session }, text) => { + await session.sendQueued(text) + await session.sendQueued('end') + }) + + it('basic support', async () => { + await u3g1.shouldReply('# foo $(bar)', '问答已添加,编号为 1。') + await u3g1.shouldReply('foo', ['hello']) + await u3g1.shouldReply('#1 ~ 1$(bar)2', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['1hello2']) + await u3g1.shouldReply('#1 ~ 1$(bar)2$(bar)3', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['1hello2hello3']) + await u3g1.shouldReply('#1 ~ 1$(barrr)2', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['12']) + await u3g1.shouldReply('#1 ~ $(barrr)', '问答 1 已成功修改。') + await u3g1.shouldNotReply('foo') + await u3g1.shouldReply('#1 -r', '问答 1 已成功删除。') + }) + + it('queued messages', async () => { + await u3g1.shouldReply('# foo $(baz)', '问答已添加,编号为 1。') + await u3g1.shouldReply('foo', ['hello']) + await u3g1.shouldReply('#1 ~ 1$(baz)2', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['1hello', '2']) + await u3g1.shouldReply('#1 ~ $(bar)$(baz)', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['hellohello']) + await u3g1.shouldReply('#1 ~ $(baz)$(bar)', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['hello', 'hello']) + await u3g1.shouldReply('#1 ~ 1$n$(bar)$n2', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['1', 'hello', '2']) + await u3g1.shouldReply('#1 ~ 1$n$(baz)$n2', '问答 1 已成功修改。') + await u3g1.shouldReply('foo', ['1', 'hello', '2']) + await u3g1.shouldReply('#1 -r', '问答 1 已成功删除。') + }) + + it('capturing groups', async () => { + await u3g1.shouldReply('# ^foo(.*) $(report $1) -x', '问答已添加,编号为 1。') + await u3g1.shouldReply('foobar', ['bar', 'end']) + await u3g1.shouldReply('foo', ['end']) + await u3g1.shouldReply('#1 ~ foo$0', '问答 1 已成功修改。') + await u3g1.shouldReply('foobar', ['foofoobar']) + await u3g1.shouldReply('#1 -r', '问答 1 已成功删除。') }) }) diff --git a/packages/plugin-teach/tests/environment.ts b/packages/plugin-teach/tests/environment.ts index 2c18b2268c..131967c2c9 100644 --- a/packages/plugin-teach/tests/environment.ts +++ b/packages/plugin-teach/tests/environment.ts @@ -1,6 +1,6 @@ import { Database, Context } from 'koishi-core' import { defineProperty, Observed, clone, intersection } from 'koishi-utils' -import { Dialogue, DialogueTest, equal, Config, apply } from 'koishi-plugin-teach' +import { Dialogue, DialogueTest, equal, apply } from 'koishi-plugin-teach' import { App } from 'koishi-test-utils' declare module 'koishi-core' { @@ -59,15 +59,19 @@ export function memory(ctx: Context) { ctx.on('dialogue/memory', (data, { regexp, answer, question, original }) => { if (regexp) { if (answer && !new RegExp(answer, 'i').test(data.answer)) return true - if (question && !new RegExp(question, 'i').test(data.question)) return true + if (original && !new RegExp(original, 'i').test(data.original)) return true return } if (answer && answer !== data.answer) return true - if (question) { - if (regexp === false || !(data.flag & Dialogue.Flag.regexp)) return question !== data.question - const questionRegExp = new RegExp(data.question, 'i') - return !questionRegExp.test(question) && !questionRegExp.test(original) + if (regexp === false) { + if (question) return question !== data.question + } else if (original) { + if (data.flag & Dialogue.Flag.regexp) { + return !new RegExp(data.original, 'i').test(original) + } else { + return question !== data.question + } } }) @@ -106,7 +110,7 @@ function getProduct({ startTime, endTime }: Dialogue, time: number) { return (startTime - time) * (time - endTime) * (endTime - startTime) } -export default function (config: Config) { +export default function (config: Dialogue.Config) { const app = new App({ nickname: ['koishi', 'satori'], mockDatabase: true,