198 lines
6.1 KiB
Go
198 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|