From f38062e0ecf648cd8e8407a6f755926ed5399d88 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sat, 29 Aug 2020 15:36:31 +0800 Subject: [PATCH] Pretty fuzzy search, close #122 --- .../description/CommandArgParserBuiltins.kt | 52 ++++++++++++++++--- .../console/internal/command/internal.kt | 35 +++++++++++-- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt index d625c2d98..bc2bf3115 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt @@ -12,10 +12,7 @@ package net.mamoe.mirai.console.command.description import net.mamoe.mirai.Bot import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.internal.command.fuzzySearchMember -import net.mamoe.mirai.contact.Friend -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.contact.User +import net.mamoe.mirai.contact.* import net.mamoe.mirai.getFriendOrNull import net.mamoe.mirai.getGroupOrNull import net.mamoe.mirai.message.data.At @@ -288,9 +285,25 @@ internal interface InternalCommandArgumentParserExtensions : CommandArg fun Group.findMemberOrFail(idOrCard: String): Member { if (idOrCard == "\$") return members.randomOrNull() ?: illegalArgument("当前语境下无法推断随机群员") - return idOrCard.toLongOrNull()?.let { getOrNull(it) } - ?: fuzzySearchMember(idOrCard) - ?: illegalArgument("无法找到目标群员 $idOrCard") + idOrCard.toLongOrNull()?.let { getOrNull(it) }?.let { return it } + this.members.singleOrNull { it.nameCardOrNick.contains(idOrCard) }?.let { return it } + this.members.singleOrNull { it.nameCardOrNick.contains(idOrCard, ignoreCase = true) }?.let { return it } + + val candidates = this.fuzzySearchMember(idOrCard) + candidates.singleOrNull()?.let { + if (it.second == 1.0) return it.first // single match + } + if (candidates.isEmpty()) { + illegalArgument("无法找到成员 $idOrCard") + } else { + var index = 1 + illegalArgument("无法找到成员 $idOrCard。 多个成员满足搜索结果或匹配度不足: \n\n" + + candidates.joinToString("\n", limit = 6) { + val percentage = (it.second * 100).toDecimalPlace(0) + "#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4% + } + ) + } } fun CommandSender.inferBotOrFail(): Bot = @@ -306,3 +319,28 @@ internal interface InternalCommandArgumentParserExtensions : CommandArg fun CommandSender.inferFriendOrFail(): Friend = (this as? FriendCommandSender)?.user ?: illegalArgument("当前语境下无法推断目标好友") } + +internal fun Double.toDecimalPlace(n: Int): String { + return "%.${n}f".format(this) +} + +internal fun String.truncate(lengthLimit: Int, replacement: String = "..."): String = buildString { + var lengthSum = 0 + for (char in this@truncate) { + lengthSum += char.chineseLength() + if (lengthSum > lengthLimit) { + append(replacement) + return toString() + } else append(char) + } + return toString() +} + +internal fun Char.chineseLength(): Int { + return when (this) { + in '\u0000'..'\u007F' -> 1 + in '\u0080'..'\u07FF' -> 2 + in '\u0800'..'\uFFFF' -> 3 + else -> 4 + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt index c664dd0c6..b23671266 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt @@ -13,7 +13,6 @@ import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.description.CommandArgumentParserException import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.contact.nameCardOrNick import kotlin.math.max @@ -97,8 +96,38 @@ internal inline fun Collection.fuzzySearchOnly( } -internal fun Group.fuzzySearchMember(nameCardTarget: String): Member? { - return this.members.fuzzySearch(nameCardTarget) { it.nameCardOrNick } +/** + * @return candidates + */ +internal fun Group.fuzzySearchMember( + nameCardTarget: String, + minRate: Double = 0.5, // 参与判断, 用于提示可能的解 + matchRate: Double = 0.6,// 最终选择的最少需要的匹配率, 减少歧义 + /** + * 如果有多个值超过 [matchRate], 并相互差距小于等于 [disambiguationRate], 则认为有较大歧义风险, 返回可能的解的列表. + */ + disambiguationRate: Double = 0.1, +): List> { + val candidates = (this.members + botAsMember) + .associateWith { it.nameCard.fuzzyMatchWith(nameCardTarget) } + .filter { it.value >= minRate } + .toList() + .sortedByDescending { it.second } + + val bestMatches = candidates.filter { it.second >= matchRate } + + return when { + bestMatches.isEmpty() -> candidates + bestMatches.size == 1 -> listOf(bestMatches.single().first to 1.0) + else -> { + if (bestMatches.first().second - bestMatches.last().second <= disambiguationRate) { + // resolution ambiguity + candidates + } else { + listOf(bestMatches.first().first to 1.0) + } + } + } }