package agent import ( "archive/zip" "bytes" "context" "encoding/binary" "encoding/xml" "fmt" "io" "sort" "strconv" "strings" "unicode/utf16" "unicode/utf8" "github.com/ledongthuc/pdf" "golang.org/x/text/encoding/simplifiedchinese" ) func decodeAttachmentText(data []byte) (string, error) { data = bytes.TrimPrefix(data, []byte{0xef, 0xbb, 0xbf}) if len(data) >= 2 && (bytes.Equal(data[:2], []byte{0xff, 0xfe}) || bytes.Equal(data[:2], []byte{0xfe, 0xff})) { var order binary.ByteOrder = binary.LittleEndian if data[0] == 0xfe { order = binary.BigEndian } data = data[2:] if len(data)%2 != 0 { return "", fmt.Errorf("UTF-16 文件编码不完整") } units := make([]uint16, len(data)/2) for i := range units { units[i] = order.Uint16(data[i*2:]) } data = []byte(string(utf16.Decode(units))) } else if !utf8.Valid(data) { var err error data, err = simplifiedchinese.GB18030.NewDecoder().Bytes(data) if err != nil || bytes.Contains(data, []byte("\ufffd")) { return "", fmt.Errorf("文本编码无法识别,请转换为 UTF-8") } } for _, b := range data { if b < 0x20 && b != '\t' && b != '\r' && b != '\n' { return "", fmt.Errorf("文件包含二进制内容,请选择文本文件") } } return string(data), nil } func extractPDF(ctx context.Context, data []byte) (string, bool, error) { r, err := pdf.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return "", false, fmt.Errorf("PDF 无法读取,可能损坏或有密码: %w", err) } pages := r.NumPage() var text strings.Builder for i := 1; i <= min(pages, 300); i++ { if err := ctx.Err(); err != nil { return "", false, err } page, err := r.Page(i).GetPlainText(nil) if err != nil { return "", false, fmt.Errorf("读取 PDF 第 %d 页失败: %w", i, err) } if strings.TrimSpace(page) == "" { continue } fmt.Fprintf(&text, "\n[第 %d 页]\n%s\n", i, page) if text.Len() > maxAttachmentText { return text.String(), true, nil } } return text.String(), pages > 300, nil } func officeXML(files []*zip.File, name string) ([]byte, error) { for _, f := range files { if f.Name != name { continue } if f.UncompressedSize64 > 8*1024*1024 { return nil, fmt.Errorf("文档内部 XML 过大,请拆分文件") } r, err := f.Open() if err != nil { return nil, err } defer r.Close() data, err := io.ReadAll(io.LimitReader(r, 8*1024*1024+1)) if err != nil || len(data) > 8*1024*1024 { return nil, fmt.Errorf("文档内部内容读取失败或过大") } return data, nil } return nil, nil } func extractOffice(ctx context.Context, data []byte, sheet bool) (string, error) { z, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil || len(z.File) > 1500 { return "", fmt.Errorf("Office 文件无效或结构过大;请使用 DOCX/XLSX 格式") } var total uint64 for _, f := range z.File { total += f.UncompressedSize64 if total > 50*1024*1024 { return "", fmt.Errorf("Office 文件解压后超过 50 MB,请拆分文件") } } if !sheet { document, err := officeXML(z.File, "word/document.xml") if err != nil { return "", err } if document == nil { return "", fmt.Errorf("文件不是有效的 DOCX 文档") } decoder := xml.NewDecoder(bytes.NewReader(document)) var text strings.Builder for { if err := ctx.Err(); err != nil { return "", err } token, err := decoder.Token() if err == io.EOF { break } if err != nil { return "", fmt.Errorf("DOCX 内容无法解析: %w", err) } switch token := token.(type) { case xml.StartElement: if token.Name.Local == "t" { var value string if err := decoder.DecodeElement(&value, &token); err != nil { return "", err } text.WriteString(value) } if token.Name.Local == "tab" { text.WriteByte('\t') } if token.Name.Local == "br" { text.WriteByte('\n') } case xml.EndElement: if token.Name.Local == "p" || token.Name.Local == "tr" { text.WriteByte('\n') } } if text.Len() > maxAttachmentText { break } } return text.String(), nil } sharedXML, err := officeXML(z.File, "xl/sharedStrings.xml") if err != nil { return "", err } var shared []string if sharedXML != nil { decoder := xml.NewDecoder(bytes.NewReader(sharedXML)) var current strings.Builder for { token, err := decoder.Token() if err == io.EOF { break } if err != nil { return "", err } switch token := token.(type) { case xml.StartElement: if token.Name.Local == "si" { current.Reset() } if token.Name.Local == "t" { var t string if err := decoder.DecodeElement(&t, &token); err != nil { return "", err } current.WriteString(t) } case xml.EndElement: if token.Name.Local == "si" { shared = append(shared, current.String()) if len(shared) > 100000 { return "", fmt.Errorf("Excel 共享字符串过多") } } } } } var names []string for _, f := range z.File { if strings.HasPrefix(f.Name, "xl/worksheets/sheet") && strings.HasSuffix(f.Name, ".xml") { names = append(names, f.Name) } } if len(names) == 0 || len(names) > 50 { return "", fmt.Errorf("Excel 工作表不存在或超过 50 张") } sort.Strings(names) var text strings.Builder for _, name := range names { if err := ctx.Err(); err != nil { return "", err } xmlData, err := officeXML(z.File, name) if err != nil { return "", err } fmt.Fprintf(&text, "\n[%s;数值按文件原始缓存值读取,公式不执行]\n", name) decoder := xml.NewDecoder(bytes.NewReader(xmlData)) for { if err := ctx.Err(); err != nil { return "", err } token, err := decoder.Token() if err == io.EOF { break } if err != nil { return "", err } switch token := token.(type) { case xml.StartElement: if token.Name.Local == "c" { var cell struct { Ref string `xml:"r,attr"` Type string `xml:"t,attr"` Value string `xml:"v"` Formula string `xml:"f"` Inline struct { Text string `xml:"t"` Runs []struct { Text string `xml:"t"` } `xml:"r"` } `xml:"is"` } if err := decoder.DecodeElement(&cell, &token); err != nil { return "", err } value := cell.Value if cell.Type == "s" { n, err := strconv.Atoi(value) if err != nil || n < 0 || n >= len(shared) { return "", fmt.Errorf("Excel 文本索引无效") } value = shared[n] } if cell.Type == "inlineStr" { value = cell.Inline.Text for _, run := range cell.Inline.Runs { value += run.Text } } if cell.Formula != "" && value == "" { value = "[未计算公式] " + cell.Formula } fmt.Fprintf(&text, "%s=%s\t", cell.Ref, value) } case xml.EndElement: if token.Name.Local == "row" { text.WriteByte('\n') } } if text.Len() > maxAttachmentText { return text.String(), nil } } } return text.String(), nil }