package intent import ( "strings" "unicode" "unicode/utf8" ) // boundedContains 词界匹配。规则分两边: // - 右缘(必须):命中的右侧不得与词条末字符同类——汉字对汉字、词字符对词字符。 // 「配置」不命中「配置项」(右邻「项」为汉字)、「write」不命中「rewrites」。 // - 左缘(仅拉丁词条):汉字结尾的词条不要求左界——中文没有天然词界, // 「改配置」「系统配置」里的「配置」仍是用户想说那个词; // 拉丁词条按经典 \b 要求两侧(rewrites 两侧的 write 都不算命中)。 func boundedContains(haystack, term string) bool { if term == "" { return false } first, _ := utf8.DecodeRuneInString(term) last, _ := utf8.DecodeLastRuneInString(term) checkLeft := isWord(first) && !isHan(first) // 仅拉丁开头的词条要求左界 for i := strings.Index(haystack, term); i >= 0; { end := i + len(term) if (!checkLeft || leftBounded(haystack, i, first)) && rightBounded(haystack, end, last) { return true } j := strings.Index(haystack[i+1:], term) if j < 0 { return false } i += 1 + j } return false } func leftBounded(s string, i int, termFirst rune) bool { if i == 0 { return true } prev, _ := utf8.DecodeLastRuneInString(s[:i]) return !sameClass(prev, termFirst) } func rightBounded(s string, end int, termLast rune) bool { if end >= len(s) { return true } next, _ := utf8.DecodeRuneInString(s[end:]) return !sameClass(next, termLast) } func sameClass(a, b rune) bool { return (isHan(a) && isHan(b)) || (isWord(a) && isWord(b)) } func isHan(r rune) bool { return unicode.Is(unicode.Han, r) } func isWord(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' }