feat: simplify SOP to immediate-effect configuration
This commit is contained in:
197
internal/knowledge/content.go
Normal file
197
internal/knowledge/content.go
Normal file
@@ -0,0 +1,197 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user