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