Pin Agent-Native as a submodule so the module builds without the multi-repo sibling path. Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
2.8 KiB
Go
126 lines
2.8 KiB
Go
// Package textextract 从输入材料(txt/md/json/yaml/docx)提取纯文本,
|
||
// 并注入页码锚点便于引用(机制语义对齐 Agent-System internal/textextract,
|
||
// 为独立底座做的纯标准库精简版)。
|
||
package textextract
|
||
|
||
import (
|
||
"archive/zip"
|
||
"bytes"
|
||
"encoding/xml"
|
||
"fmt"
|
||
"io"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
)
|
||
|
||
// ExtractTextWithAnchors 提取文本并注入页码锚点。
|
||
func ExtractTextWithAnchors(filename string, data []byte) (string, error) {
|
||
ext := strings.ToLower(filepath.Ext(filename))
|
||
switch ext {
|
||
case ".txt", ".md", ".json", ".yaml", ".yml", ".csv", ".log":
|
||
return InjectPageAnchors(string(data)), nil
|
||
case ".docx":
|
||
text, err := docxText(data)
|
||
if err != nil {
|
||
return "", fmt.Errorf("docx %s: %w", filename, err)
|
||
}
|
||
return InjectPageAnchors(text), nil
|
||
default:
|
||
// 二进制降级:提取可见 ASCII/UTF-8 片段。
|
||
return InjectPageAnchors(extractVisibleStrings(data)), nil
|
||
}
|
||
}
|
||
|
||
// InjectPageAnchors 每 pageSizeAt 字符(或遇到 \f 换页符)注入 <!-- page N -->。
|
||
func InjectPageAnchors(s string) string {
|
||
if s == "" {
|
||
return s
|
||
}
|
||
var b strings.Builder
|
||
page := 1
|
||
count := 0
|
||
fprintf := func() { fmt.Fprintf(&b, "\n\n<!-- page %d -->\n", page) }
|
||
fprintf()
|
||
for _, r := range s {
|
||
b.WriteRune(r)
|
||
count++
|
||
if r == '\f' {
|
||
page++
|
||
count = 0
|
||
fprintf()
|
||
continue
|
||
}
|
||
if count >= 3000 {
|
||
page++
|
||
count = 0
|
||
fprintf()
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
var xmlTag = regexp.MustCompile(`</w:p>`)
|
||
|
||
func docxText(data []byte) (string, error) {
|
||
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
for _, f := range zr.File {
|
||
if f.Name != "word/document.xml" {
|
||
continue
|
||
}
|
||
rc, err := f.Open()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer rc.Close()
|
||
raw, err := io.ReadAll(rc)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
// 段落结尾转换行,再剥离全部 XML 标签。
|
||
raw = xmlTag.ReplaceAll(raw, []byte("\n"))
|
||
dec := xml.NewDecoder(bytes.NewReader(raw))
|
||
var b strings.Builder
|
||
for {
|
||
tok, err := dec.Token()
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if t, ok := tok.(xml.CharData); ok {
|
||
b.Write(t)
|
||
}
|
||
}
|
||
return strings.TrimSpace(b.String()), nil
|
||
}
|
||
return "", fmt.Errorf("word/document.xml not found")
|
||
}
|
||
|
||
// extractVisibleStrings 从二进制中提取可见可打印字符串(长度>=4)。
|
||
func extractVisibleStrings(data []byte) string {
|
||
var out []string
|
||
start := -1
|
||
for i, b := range data {
|
||
printable := (b >= 0x20 && b < 0x7f) || b >= 0x80
|
||
if printable {
|
||
if start < 0 {
|
||
start = i
|
||
}
|
||
continue
|
||
}
|
||
if start >= 0 && i-start >= 4 {
|
||
out = append(out, string(data[start:i]))
|
||
}
|
||
start = -1
|
||
}
|
||
if start >= 0 && len(data)-start >= 4 {
|
||
out = append(out, string(data[start:]))
|
||
}
|
||
return strings.Join(out, "\n")
|
||
}
|