c89cad6643
- Rename UI/prompt labels: "品牌主问题" → "优化关键词", "搜索问题" → "搜索词" - Add brand/competitor entity exclusion filter in question expansion (AI distill path) - Refactor combination question text normalization, remove manual question-mark appending - Update prompts to emphasize commercial-intent search queries over editorial phrasing - Sync prompt changes across server/configs, deploy, and k3s configs - Update i18n, frontend views (BrandQuestionCreate, TemplateWizard), and test fixtures Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
152 lines
4.1 KiB
Go
152 lines
4.1 KiB
Go
package app
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
const (
|
||
QuestionLayerL1 = "L1"
|
||
QuestionLayerL2 = "L2"
|
||
QuestionLayerL3 = "L3"
|
||
QuestionLayerL4 = "L4"
|
||
QuestionLayerL5 = "L5"
|
||
|
||
QuestionIntentInformational = "informational"
|
||
QuestionIntentEvaluative = "evaluative"
|
||
QuestionIntentDecisional = "decisional"
|
||
|
||
QuestionSourceManual = "manual"
|
||
QuestionSourceCombination = "combination"
|
||
QuestionSourceAIDistill = "ai_distill"
|
||
)
|
||
|
||
type ClassifiedQuestion struct {
|
||
Text string `json:"text"`
|
||
Layer string `json:"layer"`
|
||
Intent string `json:"intent"`
|
||
}
|
||
|
||
func normalizeQuestionText(text string) string {
|
||
trimmed := strings.TrimSpace(text)
|
||
if trimmed == "" {
|
||
return ""
|
||
}
|
||
return strings.Map(func(r rune) rune {
|
||
if unicode.IsPunct(r) {
|
||
return -1
|
||
}
|
||
return r
|
||
}, trimmed)
|
||
}
|
||
|
||
func normalizeQuestionKey(text string) string {
|
||
return strings.ToLower(normalizeQuestionText(text))
|
||
}
|
||
|
||
func isValidQuestionLayer(value string) bool {
|
||
switch strings.TrimSpace(value) {
|
||
case QuestionLayerL1, QuestionLayerL2, QuestionLayerL3, QuestionLayerL4, QuestionLayerL5:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func isValidQuestionIntent(value string) bool {
|
||
switch strings.TrimSpace(value) {
|
||
case QuestionIntentInformational, QuestionIntentEvaluative, QuestionIntentDecisional:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func isValidExternalQuestionSource(value string) bool {
|
||
switch strings.TrimSpace(value) {
|
||
case QuestionSourceManual, QuestionSourceCombination, QuestionSourceAIDistill:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func inferQuestionIntent(text string) string {
|
||
normalized := strings.ToLower(strings.TrimSpace(text))
|
||
if containsAny(normalized, []string{"哪家好", "哪个好", "怎么选", "推荐", "哪里买", "适合谁", "多少钱", "报价", "购买"}) {
|
||
return QuestionIntentDecisional
|
||
}
|
||
if containsAny(normalized, []string{"对比", "区别", "哪个更", "优劣", "测评", "排名", "值不值", "替代", "竞品"}) {
|
||
return QuestionIntentEvaluative
|
||
}
|
||
return QuestionIntentInformational
|
||
}
|
||
|
||
func inferQuestionLayer(text, brandName string, competitorNames []string) string {
|
||
normalized := strings.ToLower(strings.TrimSpace(text))
|
||
brandHits := 0
|
||
if containsFold(normalized, brandName) {
|
||
brandHits++
|
||
}
|
||
for _, competitor := range competitorNames {
|
||
if containsFold(normalized, competitor) {
|
||
brandHits++
|
||
}
|
||
}
|
||
if brandHits >= 2 || containsAny(normalized, []string{"对比", "区别", "哪个更", "优劣", "竞品", "替代"}) {
|
||
return QuestionLayerL5
|
||
}
|
||
if brandHits == 1 {
|
||
return QuestionLayerL1
|
||
}
|
||
if containsAny(normalized, []string{"北京", "上海", "广州", "深圳", "杭州", "成都", "合肥", "华东", "华南", "华北", "中小企业", "企业", "团队", "老板", "新手", "预算", "价格", "费用", "场景", "本地"}) {
|
||
return QuestionLayerL4
|
||
}
|
||
if containsAny(normalized, []string{"痛点", "问题", "踩坑", "解决", "失败", "难", "风险", "怎么办"}) {
|
||
return QuestionLayerL3
|
||
}
|
||
return QuestionLayerL2
|
||
}
|
||
|
||
func classifyQuestionText(text, brandName string, competitorNames []string) ClassifiedQuestion {
|
||
trimmed := normalizeQuestionText(text)
|
||
return ClassifiedQuestion{
|
||
Text: trimmed,
|
||
Layer: inferQuestionLayer(trimmed, brandName, competitorNames),
|
||
Intent: inferQuestionIntent(trimmed),
|
||
}
|
||
}
|
||
|
||
func questionLooksValid(text string) bool {
|
||
trimmed := normalizeQuestionText(text)
|
||
if utf8.RuneCountInString(trimmed) < 4 {
|
||
return false
|
||
}
|
||
hasLetterOrNumber := false
|
||
for _, r := range trimmed {
|
||
if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
||
hasLetterOrNumber = true
|
||
break
|
||
}
|
||
}
|
||
if !hasLetterOrNumber {
|
||
return false
|
||
}
|
||
return containsAny(strings.ToLower(trimmed), []string{"?", "?", "什么", "如何", "怎么", "哪", "为什么", "是否", "能不能", "适合", "区别", "对比", "推荐"})
|
||
}
|
||
|
||
func containsAny(value string, needles []string) bool {
|
||
for _, needle := range needles {
|
||
if strings.Contains(value, strings.ToLower(strings.TrimSpace(needle))) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func containsFold(value, needle string) bool {
|
||
needle = strings.ToLower(strings.TrimSpace(needle))
|
||
return needle != "" && strings.Contains(value, needle)
|
||
}
|