feat: simplify SOP to immediate-effect configuration

This commit is contained in:
Eric 1549169735@qq.com
2026-08-18 16:27:02 +08:00
parent c5ab886b70
commit 8de48fb05e
150 changed files with 6764 additions and 1626 deletions

View 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)
}
}