Initial qiwei secondary development handoff
This commit is contained in:
222
helper/after_sales_file.go
Normal file
222
helper/after_sales_file.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
afterSalesFileContentLimit = 6000
|
||||
afterSalesFilePromptLimit = 1800
|
||||
|
||||
afterSalesFileStatusReady = "parsed"
|
||||
afterSalesFileStatusSaved = "saved"
|
||||
afterSalesFileStatusUnsupported = "unsupported"
|
||||
afterSalesFileStatusMissing = "missing"
|
||||
afterSalesFileStatusDownloadFail = "download_failed"
|
||||
afterSalesFileStatusExtractFailed = "extract_failed"
|
||||
)
|
||||
|
||||
func extractAfterSalesFileFromMessage(msg autoReplyMessage, raw map[string]interface{}, sourceMessageID string) AfterSalesFileAttachment {
|
||||
if !looksLikeAfterSalesFileMessage(msg, raw) {
|
||||
return AfterSalesFileAttachment{}
|
||||
}
|
||||
attachment := AfterSalesFileAttachment{
|
||||
Name: strings.TrimSpace(msg.MediaFileName),
|
||||
Ref: firstNonEmpty(strings.TrimSpace(msg.MediaURL), strings.TrimSpace(msg.MediaFileID)),
|
||||
SourceMessageID: strings.TrimSpace(sourceMessageID),
|
||||
}
|
||||
for _, value := range []string{msg.MediaLocalPath, firstLocalMediaPathFromValue(raw)} {
|
||||
if path := normalizedExistingFilePath(value); path != "" {
|
||||
attachment.Path = path
|
||||
break
|
||||
}
|
||||
}
|
||||
if attachment.Path == "" {
|
||||
if path, err := ensureAutoReplyMediaLocalPath(msg); err == nil {
|
||||
attachment.Path = normalizedExistingFilePath(path)
|
||||
} else {
|
||||
attachment.ExtractStatus = afterSalesFileStatusDownloadFail
|
||||
}
|
||||
}
|
||||
if attachment.Name == "" {
|
||||
attachment.Name = firstNonEmpty(filepath.Base(attachment.Path), filepath.Base(strings.TrimSpace(msg.MediaURL)), strings.TrimSpace(msg.MediaFileID), "客户附件")
|
||||
}
|
||||
if attachment.Path == "" {
|
||||
if attachment.ExtractStatus == "" {
|
||||
attachment.ExtractStatus = afterSalesFileStatusDownloadFail
|
||||
}
|
||||
return attachment
|
||||
}
|
||||
content, status := extractAfterSalesFileContent(attachment.Path)
|
||||
attachment.Content = content
|
||||
attachment.ExtractStatus = status
|
||||
return normalizeAfterSalesFileAttachment(attachment)
|
||||
}
|
||||
|
||||
func looksLikeAfterSalesFileMessage(msg autoReplyMessage, raw map[string]interface{}) bool {
|
||||
if strings.TrimSpace(msg.MediaKind) == "file" || msg.RawType == 11045 {
|
||||
return true
|
||||
}
|
||||
if raw != nil {
|
||||
if typeVal, ok := raw["type"]; ok && intFromAny(typeVal) == 11045 {
|
||||
return true
|
||||
}
|
||||
if event := strings.TrimSpace(stringFromAny(raw["event"])); event == "20005" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizedExistingFilePath(value string) string {
|
||||
value = strings.Trim(strings.TrimSpace(value), "\"'")
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if filepath.IsAbs(value) {
|
||||
if _, err := os.Stat(value); err == nil {
|
||||
return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
candidate := resolveAutoReplyPath(value)
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractAfterSalesFileContent(path string) (string, string) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return "", afterSalesFileStatusMissing
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", afterSalesFileStatusMissing
|
||||
}
|
||||
if info.IsDir() {
|
||||
return "", afterSalesFileStatusUnsupported
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext == ".zip" {
|
||||
return "", "zip_not_parsed"
|
||||
}
|
||||
switch ext {
|
||||
case ".txt", ".md", ".csv", ".xlsx", ".docx", ".pdf":
|
||||
default:
|
||||
return "", afterSalesFileStatusUnsupported
|
||||
}
|
||||
blocks, err := parseKnowledgeFile(path, filepath.Dir(path))
|
||||
if err != nil {
|
||||
var warning knowledgeParseWarning
|
||||
if !errorAs(err, &warning) {
|
||||
return "", afterSalesFileStatusExtractFailed + ": " + truncateText(err.Error(), 160)
|
||||
}
|
||||
}
|
||||
parts := make([]string, 0, len(blocks))
|
||||
for _, block := range blocks {
|
||||
text := strings.TrimSpace(block.Content)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(block.Title) != "" {
|
||||
text = strings.TrimSpace(block.Title) + "\n" + text
|
||||
}
|
||||
parts = append(parts, text)
|
||||
}
|
||||
content := truncateText(strings.TrimSpace(strings.Join(parts, "\n\n")), afterSalesFileContentLimit)
|
||||
if content == "" {
|
||||
return "", afterSalesFileStatusSaved
|
||||
}
|
||||
return content, afterSalesFileStatusReady
|
||||
}
|
||||
|
||||
func normalizeAfterSalesFileAttachment(item AfterSalesFileAttachment) AfterSalesFileAttachment {
|
||||
item.Name = strings.TrimSpace(item.Name)
|
||||
item.Path = strings.Trim(strings.TrimSpace(item.Path), "\"'")
|
||||
item.Ref = strings.TrimSpace(item.Ref)
|
||||
item.Content = truncateText(strings.TrimSpace(item.Content), afterSalesFileContentLimit)
|
||||
item.ExtractStatus = strings.TrimSpace(item.ExtractStatus)
|
||||
item.SourceMessageID = strings.TrimSpace(item.SourceMessageID)
|
||||
if item.Name == "" {
|
||||
item.Name = firstNonEmpty(filepath.Base(item.Path), filepath.Base(item.Ref), "客户附件")
|
||||
}
|
||||
if item.ExtractStatus == "" {
|
||||
if item.Content != "" {
|
||||
item.ExtractStatus = afterSalesFileStatusReady
|
||||
} else if item.Path != "" {
|
||||
item.ExtractStatus = afterSalesFileStatusSaved
|
||||
} else {
|
||||
item.ExtractStatus = afterSalesFileStatusDownloadFail
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func normalizeAfterSalesFileAttachments(items []AfterSalesFileAttachment) []AfterSalesFileAttachment {
|
||||
seen := make(map[string]struct{})
|
||||
result := make([]AfterSalesFileAttachment, 0, len(items))
|
||||
for _, item := range items {
|
||||
item = normalizeAfterSalesFileAttachment(item)
|
||||
if item.Path == "" && item.Ref == "" && item.Name == "" && item.Content == "" {
|
||||
continue
|
||||
}
|
||||
key := firstNonEmpty(item.SourceMessageID, item.Path, item.Ref, item.Name+"|"+item.Content)
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func afterSalesFilePromptText(files []AfterSalesFileAttachment, limit int) string {
|
||||
if limit <= 0 {
|
||||
limit = afterSalesFilePromptLimit
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, file := range normalizeAfterSalesFileAttachments(files) {
|
||||
if b.Len() > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("文件:")
|
||||
b.WriteString(firstNonEmpty(file.Name, filepath.Base(file.Path), file.Ref))
|
||||
if file.ExtractStatus != "" {
|
||||
b.WriteString(" 状态:")
|
||||
b.WriteString(file.ExtractStatus)
|
||||
}
|
||||
if file.Content != "" {
|
||||
b.WriteString("\n内容:\n")
|
||||
b.WriteString(truncateText(file.Content, limit))
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func formatAfterSalesFileAttachmentsForExcel(files []AfterSalesFileAttachment, includeContent bool) string {
|
||||
files = normalizeAfterSalesFileAttachments(files)
|
||||
if len(files) == 0 {
|
||||
return ""
|
||||
}
|
||||
lines := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
line := firstNonEmpty(file.Name, filepath.Base(file.Path), file.Ref, "客户附件")
|
||||
if file.Path != "" {
|
||||
line += " | " + file.Path
|
||||
} else if file.Ref != "" {
|
||||
line += " | " + file.Ref
|
||||
}
|
||||
if file.ExtractStatus != "" {
|
||||
line += " | " + file.ExtractStatus
|
||||
}
|
||||
if includeContent && file.Content != "" {
|
||||
line += fmt.Sprintf("\n%s", truncateText(file.Content, 1200))
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return strings.Join(lines, "\n\n")
|
||||
}
|
||||
Reference in New Issue
Block a user