feat: update auto reply and packaging
This commit is contained in:
@@ -10,9 +10,12 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const autoReplyMaterialRepeatWindow = 30 * time.Minute
|
||||
|
||||
type AutoReplyMaterial struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@@ -56,9 +59,6 @@ func (e *AutoReplyEngine) matchMaterials(userQuery string, searchContext string,
|
||||
if !cfg.Materials.AutoSendEnabled {
|
||||
return nil
|
||||
}
|
||||
if isBroadAllMaterialRequest(userQuery) {
|
||||
return nil
|
||||
}
|
||||
materials, err := loadAutoReplyMaterials(cfg.Materials.IndexPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
@@ -74,7 +74,23 @@ func (e *AutoReplyEngine) matchMaterials(userQuery string, searchContext string,
|
||||
|
||||
requestedTypes := requestedMaterialTypes(userQuery)
|
||||
hasSendIntent := hasMaterialSendIntent(userQuery)
|
||||
if hasSendIntent && isGenericMaterialRequest(userQuery) && !materialQueryHasSpecificSignal(userQuery, materials) && strings.TrimSpace(searchContext) == strings.TrimSpace(userQuery) {
|
||||
explicitAll := isBroadAllMaterialRequest(userQuery)
|
||||
hasSpecificSignal := materialQueryHasSpecificSignal(userQuery, materials)
|
||||
if !hasSendIntent && !explicitAll {
|
||||
return nil
|
||||
}
|
||||
if explicitAll {
|
||||
if hasSpecificSignal {
|
||||
queryText := buildMaterialSearchText(userQuery, "", nil, false)
|
||||
return e.collectMaterialMatches(materials, cfg.Materials.Directory, requestedTypesForExplicitAll(userQuery, requestedTypes), queryText, true)
|
||||
}
|
||||
filteredTypes := requestedTypesForExplicitAll(userQuery, requestedTypes)
|
||||
if len(filteredTypes) > 0 {
|
||||
return e.collectMaterialMatchesByType(materials, cfg.Materials.Directory, filteredTypes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if hasSendIntent && isGenericMaterialRequest(userQuery) && !hasSpecificSignal && strings.TrimSpace(searchContext) == strings.TrimSpace(userQuery) {
|
||||
return nil
|
||||
}
|
||||
queryText := buildMaterialSearchText(userQuery, "", nil, false)
|
||||
@@ -122,6 +138,86 @@ func (e *AutoReplyEngine) collectMaterialMatches(materials []AutoReplyMaterial,
|
||||
return matches
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) collectMaterialMatchesByType(materials []AutoReplyMaterial, root string, requestedTypes map[string]bool) []autoReplyMaterialMatch {
|
||||
if len(requestedTypes) == 0 {
|
||||
return nil
|
||||
}
|
||||
matches := make([]autoReplyMaterialMatch, 0, len(materials))
|
||||
for _, material := range materials {
|
||||
if !requestedTypes[material.MaterialType] {
|
||||
continue
|
||||
}
|
||||
path := resolveAutoReplyMaterialPath(root, material.Path)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
e.setLastErrorWithScope(autoReplyErrorScopeKnowledge, fmt.Sprintf("material file missing: %s", path))
|
||||
continue
|
||||
}
|
||||
matches = append(matches, autoReplyMaterialMatch{Material: material, Path: path, Score: 1})
|
||||
}
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
if matches[i].Material.Priority != matches[j].Material.Priority {
|
||||
return matches[i].Material.Priority > matches[j].Material.Priority
|
||||
}
|
||||
return strings.ToLower(matches[i].Material.Path) < strings.ToLower(matches[j].Material.Path)
|
||||
})
|
||||
return matches
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) filterRecentlySentMaterials(msg autoReplyMessage, matches []autoReplyMaterialMatch) []autoReplyMaterialMatch {
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
e.mu.Lock()
|
||||
if e.materialSent == nil {
|
||||
e.materialSent = make(map[string]time.Time)
|
||||
}
|
||||
for key, ts := range e.materialSent {
|
||||
if now.Sub(ts) > autoReplyMaterialRepeatWindow {
|
||||
delete(e.materialSent, key)
|
||||
}
|
||||
}
|
||||
filtered := make([]autoReplyMaterialMatch, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
key := materialSentFingerprint(msg, match)
|
||||
if key == "" {
|
||||
filtered = append(filtered, match)
|
||||
continue
|
||||
}
|
||||
if ts, ok := e.materialSent[key]; ok && now.Sub(ts) <= autoReplyMaterialRepeatWindow {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, match)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) rememberSentMaterial(msg autoReplyMessage, match autoReplyMaterialMatch) {
|
||||
key := materialSentFingerprint(msg, match)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
e.mu.Lock()
|
||||
if e.materialSent == nil {
|
||||
e.materialSent = make(map[string]time.Time)
|
||||
}
|
||||
e.materialSent[key] = time.Now()
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func materialSentFingerprint(msg autoReplyMessage, match autoReplyMaterialMatch) string {
|
||||
conversationID := strings.TrimSpace(msg.ConversationID)
|
||||
path := strings.ToLower(filepath.Clean(strings.TrimSpace(match.Path)))
|
||||
if path == "." || path == "" {
|
||||
path = strings.ToLower(filepath.Clean(strings.TrimSpace(match.Material.Path)))
|
||||
}
|
||||
if conversationID == "" || path == "" || path == "." {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{msg.stableRobotID(), conversationID, path}, "|")
|
||||
}
|
||||
|
||||
func limitMaterialMatches(matches []autoReplyMaterialMatch, maxPerReply int) []autoReplyMaterialMatch {
|
||||
limit := maxPerReply
|
||||
if limit <= 0 {
|
||||
@@ -192,6 +288,29 @@ func requestedMaterialTypes(query string) map[string]bool {
|
||||
return result
|
||||
}
|
||||
|
||||
func requestedTypesForExplicitAll(query string, requestedTypes map[string]bool) map[string]bool {
|
||||
if len(requestedTypes) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(requestedTypes) == 1 && requestedTypes["file"] && !explicitlyRequestsOnlyFiles(query) {
|
||||
return nil
|
||||
}
|
||||
return requestedTypes
|
||||
}
|
||||
|
||||
func explicitlyRequestsOnlyFiles(query string) bool {
|
||||
text := normalizeGreetingText(query)
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
for _, token := range []string{"文档", "表格", "手册", "说明书", "ppt", "pdf", "doc", "docx", "xls", "xlsx"} {
|
||||
if strings.Contains(text, normalizeGreetingText(token)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsAnyMaterialIntent(text string, keywords []string) bool {
|
||||
for _, keyword := range keywords {
|
||||
if strings.Contains(text, keyword) {
|
||||
@@ -632,30 +751,37 @@ func inferMaterialType(path string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) sendMaterials(msg autoReplyMessage, matches []autoReplyMaterialMatch, reason string, timings autoReplyTimings) error {
|
||||
func (e *AutoReplyEngine) sendMaterials(msg autoReplyMessage, matches []autoReplyMaterialMatch, reason string, timings autoReplyTimings, tutorialText string) error {
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
captions := make([]string, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
if caption := customMaterialCaptionForSend(match.Material); caption != "" {
|
||||
captions = append(captions, caption)
|
||||
}
|
||||
}
|
||||
if len(captions) == 0 {
|
||||
captions = append(captions, combinedMaterialCaption(matches))
|
||||
}
|
||||
caption := strings.Join(uniqueMaterialStrings(captions), "\n")
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, caption); err != nil {
|
||||
return err
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, caption)
|
||||
sent := make([]string, 0, len(matches))
|
||||
if text := strings.TrimSpace(tutorialText); text != "" {
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, text); err != nil {
|
||||
return err
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, text)
|
||||
}
|
||||
for _, match := range matches {
|
||||
caption := materialCaptionForSend(match.Material)
|
||||
captionAfter := materialCaptionShouldFollowMaterial(match.Material)
|
||||
if !captionAfter {
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, caption); err != nil {
|
||||
return err
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, caption)
|
||||
}
|
||||
if err := sendAutoReplyMaterial(uint32(msg.ClientID), msg.ConversationID, match.Material.MaterialType, match.Path); err != nil {
|
||||
return fmt.Errorf("send material %s failed: %w", match.Path, err)
|
||||
}
|
||||
e.rememberSentMaterial(msg, match)
|
||||
sent = append(sent, fmt.Sprintf("%s:%s", match.Material.MaterialType, match.Path))
|
||||
if captionAfter {
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, caption); err != nil {
|
||||
return err
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, caption)
|
||||
}
|
||||
}
|
||||
e.markCooldown(msg)
|
||||
e.incStatus("replied")
|
||||
@@ -689,6 +815,15 @@ func (e *AutoReplyEngine) sendMaterials(msg autoReplyMessage, matches []autoRepl
|
||||
return nil
|
||||
}
|
||||
|
||||
func materialCaptionShouldFollowMaterial(material AutoReplyMaterial) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(material.MaterialType)) {
|
||||
case "image", "video", "gif":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func materialCaptionForSend(material AutoReplyMaterial) string {
|
||||
if caption := customMaterialCaptionForSend(material); caption != "" {
|
||||
return caption
|
||||
@@ -696,6 +831,45 @@ func materialCaptionForSend(material AutoReplyMaterial) string {
|
||||
return defaultMaterialCaption(material.MaterialType)
|
||||
}
|
||||
|
||||
func materialTutorialTextFromHits(hits []KnowledgeChunk) string {
|
||||
items := make([]string, 0, 3)
|
||||
for _, hit := range hits {
|
||||
content := strings.TrimSpace(hit.Content)
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
content = compactMaterialTutorialContent(content)
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, content)
|
||||
if len(items) >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "我也把相关排查说明整理给您:\n" + strings.Join(items, "\n")
|
||||
}
|
||||
|
||||
func compactMaterialTutorialContent(content string) string {
|
||||
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||
result := make([]string, 0, 4)
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(strings.Trim(line, "#>*- \t"))
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, line)
|
||||
if len(result) >= 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
text := strings.Join(result, "\n")
|
||||
return truncateText(text, 280)
|
||||
}
|
||||
|
||||
func customMaterialCaptionForSend(material AutoReplyMaterial) string {
|
||||
caption := strings.TrimSpace(material.Caption)
|
||||
if caption != "" && !isLegacyGenericMaterialCaption(caption) {
|
||||
@@ -718,19 +892,19 @@ func isLegacyGenericMaterialCaption(caption string) bool {
|
||||
func defaultMaterialCaption(materialType string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(materialType)) {
|
||||
case "image":
|
||||
return "我把图片发你。"
|
||||
return "我把图片发给您。"
|
||||
case "video":
|
||||
return "我把视频发你。"
|
||||
return "我把视频发给您。"
|
||||
case "gif":
|
||||
return "我把动图发你。"
|
||||
return "我把动图发给您。"
|
||||
default:
|
||||
return "我把文件发你。"
|
||||
return "我把文件发给您。"
|
||||
}
|
||||
}
|
||||
|
||||
func combinedMaterialCaption(matches []autoReplyMaterialMatch) string {
|
||||
if len(matches) == 0 {
|
||||
return "我把文件发你。"
|
||||
return "我把文件发给您。"
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
labels := make([]string, 0, 4)
|
||||
@@ -755,7 +929,7 @@ func combinedMaterialCaption(matches []autoReplyMaterialMatch) string {
|
||||
if len(labels) == 1 {
|
||||
return defaultMaterialCaption(matches[0].Material.MaterialType)
|
||||
}
|
||||
return "我把" + strings.Join(labels, "和") + "发你。"
|
||||
return "我把" + strings.Join(labels, "和") + "发给您。"
|
||||
}
|
||||
|
||||
func uniqueMaterialStrings(items []string) []string {
|
||||
@@ -805,6 +979,13 @@ func sendAutoReplyMaterialRequest(clientID uint32, conversationID string, materi
|
||||
"file": path,
|
||||
},
|
||||
}
|
||||
if messageType == 11031 {
|
||||
if fileName := strings.TrimSpace(filepath.Base(path)); fileName != "" && fileName != "." {
|
||||
data := request["data"].(map[string]interface{})
|
||||
data["fileName"] = fileName
|
||||
data["file_name"] = fileName
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user