package knowledge import ( "encoding/json" "errors" "fmt" "regexp" "strings" ) var contentKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{0,63}$`) var placeholderPattern = regexp.MustCompile(`\{\{\s*((?:input|card)\.[A-Za-z][A-Za-z0-9_]*)\s*\}\}`) type CardField struct { Key string `json:"key"` Name string `json:"name"` Value interface{} `json:"value"` SourceField string `json:"source_field,omitempty"` } type CopyTemplate struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content"` } type Content struct { Fields []CardField `json:"fields,omitempty"` CopyTemplates []CopyTemplate `json:"copy_templates,omitempty"` StandardCopy string `json:"standard_copy,omitempty"` ForbiddenCopy string `json:"forbidden_copy,omitempty"` RiskNote string `json:"risk_note,omitempty"` } type RenderedCopy struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content"` } func ParseContent(raw []byte) (Content, error) { var content Content if err := json.Unmarshal(raw, &content); err != nil { return Content{}, errors.New("知识卡内容必须是 JSON 对象") } return normalizeAndValidate(content) } func ParseContentMap(value map[string]interface{}) (Content, error) { raw, err := json.Marshal(value) if err != nil { return Content{}, err } return ParseContent(raw) } func ValidateReferences(content Content, scenarioFieldKeys map[string]bool) error { cardFieldKeys := make(map[string]bool, len(content.Fields)) for _, field := range content.Fields { cardFieldKeys[field.Key] = true if field.SourceField != "" && !scenarioFieldKeys[field.SourceField] { return fmt.Errorf("知识字段“%s”关联的场景字段“%s”不存在", field.Name, field.SourceField) } } for _, template := range content.CopyTemplates { for _, match := range placeholderPattern.FindAllStringSubmatch(template.Content, -1) { if len(match) != 2 { continue } parts := strings.SplitN(match[1], ".", 2) if len(parts) != 2 { continue } switch parts[0] { case "input": if !scenarioFieldKeys[parts[1]] { return fmt.Errorf("话术“%s”引用的场景字段“%s”不存在", template.Title, parts[1]) } case "card": if !cardFieldKeys[parts[1]] { return fmt.Errorf("话术“%s”引用的知识字段“%s”不存在", template.Title, parts[1]) } } } } return nil } func Render(content Content, answers map[string]interface{}) []RenderedCopy { values := make(map[string]string, len(answers)+len(content.Fields)*2) for key, value := range answers { values["input."+key] = displayValue(value) } for _, field := range content.Fields { value := field.Value if field.SourceField != "" { if answer, ok := answers[field.SourceField]; ok && hasValue(answer) { value = answer } } values["card."+field.Key] = displayValue(value) } copies := make([]RenderedCopy, 0, len(content.CopyTemplates)) for _, template := range content.CopyTemplates { copies = append(copies, RenderedCopy{ID: template.ID, Title: template.Title, Content: placeholderPattern.ReplaceAllStringFunc(template.Content, func(token string) string { matches := placeholderPattern.FindStringSubmatch(token) if len(matches) != 2 || values[matches[1]] == "" { return "未提供" } return values[matches[1]] })}) } return copies } func PrimaryCopy(content Content) string { if len(content.CopyTemplates) > 0 { return content.CopyTemplates[0].Content } return content.StandardCopy } func normalizeAndValidate(content Content) (Content, error) { content.StandardCopy = strings.TrimSpace(content.StandardCopy) content.ForbiddenCopy = strings.TrimSpace(content.ForbiddenCopy) content.RiskNote = strings.TrimSpace(content.RiskNote) if len(content.CopyTemplates) == 0 && content.StandardCopy != "" { content.CopyTemplates = []CopyTemplate{{ID: "default", Title: "标准话术", Content: content.StandardCopy}} } if len(content.CopyTemplates) == 0 { return Content{}, errors.New("请至少填写一条话术模板") } seenFields := map[string]bool{} for i := range content.Fields { field := &content.Fields[i] field.Key = strings.TrimSpace(field.Key) field.Name = strings.TrimSpace(field.Name) field.SourceField = strings.TrimSpace(field.SourceField) if !contentKeyPattern.MatchString(field.Key) || field.Name == "" { return Content{}, errors.New("知识字段需要有效的字段标识和字段名称") } if seenFields[field.Key] { return Content{}, fmt.Errorf("知识字段标识“%s”重复", field.Key) } if field.SourceField != "" && !contentKeyPattern.MatchString(field.SourceField) { return Content{}, fmt.Errorf("知识字段“%s”的关联字段标识不正确", field.Name) } seenFields[field.Key] = true } seenTemplates := map[string]bool{} for i := range content.CopyTemplates { template := &content.CopyTemplates[i] template.ID = strings.TrimSpace(template.ID) template.Title = strings.TrimSpace(template.Title) template.Content = strings.TrimSpace(template.Content) if !contentKeyPattern.MatchString(template.ID) || template.Title == "" || template.Content == "" { return Content{}, errors.New("每条话术需要有效的标识、标题和内容") } if seenTemplates[template.ID] { return Content{}, fmt.Errorf("话术标识“%s”重复", template.ID) } seenTemplates[template.ID] = true } if content.StandardCopy == "" { content.StandardCopy = content.CopyTemplates[0].Content } return content, nil } func hasValue(value interface{}) bool { if value == nil { return false } return strings.TrimSpace(displayValue(value)) != "" } func displayValue(value interface{}) string { switch typed := value.(type) { case nil: return "" case bool: if typed { return "是" } return "否" case []interface{}: items := make([]string, 0, len(typed)) for _, item := range typed { items = append(items, displayValue(item)) } return strings.Join(items, "、") case []string: return strings.Join(typed, "、") default: return fmt.Sprint(value) } }