feat(文档解析): 新增Word文档图片解析支持并上传至MinIO

新增对Word文档中图片的解析支持,能够提取文档中的图片并上传至MinIO存储,同时生成包含图片链接的Markdown格式文本。已支持Docx格式的Word文档处理,完善了文档解析功能。
This commit is contained in:
Wenjie Zhang 2025-11-26 18:14:07 +08:00
parent 0ff771dc19
commit 18d46ca04a
2 changed files with 87 additions and 3 deletions

View File

@ -11,7 +11,7 @@
- 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" />
- 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
- 集成 neo4j mcp (或者自己构建工具)
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接(还有word等)
### Bugs
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
@ -28,6 +28,7 @@
- 新增基于知识库文件生成示例问题功能([#335](https://github.com/xerrors/Yuxi-Know/pull/335#issuecomment-3530976425)
- 新增知识库支持文件夹/压缩包上传的功能([#335](https://github.com/xerrors/Yuxi-Know/pull/335#issuecomment-3530976425)
- 新增自定义模型支持、新增 dashscope rerank/embeddings 模型的支持
- 新增文档解析的图片支持,已支持 MinerU Officical、Docs、Markdown Zip格式
### 修复
- 修复重排序模型实际未生效的问题

View File

@ -2,6 +2,7 @@ import asyncio
import os
import re
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
from langchain_community.document_loaders import (
@ -72,6 +73,85 @@ def _extract_word_text(file_path: Path) -> str:
raise ValueError(f"无法解析 Word 文档: {file_path.name}") from unstructured_error
def _extract_docx_markdown_with_images(file_path: Path, params: dict | None = None) -> str:
params = params or {}
db_id = params.get("db_id") or "word-docs"
minio_client = get_minio_client()
bucket_name = "kb-images"
minio_client.ensure_bucket_exists(bucket_name)
file_id = hashstr(file_path.name, length=16)
with zipfile.ZipFile(file_path, "r") as zf:
rels_path = "word/_rels/document.xml.rels"
rid_to_target: dict[str, str] = {}
try:
rels_xml = zf.read(rels_path).decode("utf-8")
rels_root = ET.fromstring(rels_xml)
for rel in list(rels_root):
rid = rel.attrib.get("Id")
target = rel.attrib.get("Target")
rtype = rel.attrib.get("Type")
if rid and target and rtype and rtype.endswith("/image"):
rid_to_target[rid] = target
except Exception:
rid_to_target = {}
doc_xml = zf.read("word/document.xml").decode("utf-8")
ns = {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
root = ET.fromstring(doc_xml)
md_lines: list[str] = []
for p in root.findall(".//w:p", ns):
texts = [t.text or "" for t in p.findall(".//w:t", ns)]
para_text = "".join(texts).strip()
image_urls: list[tuple[str, str]] = []
for blip in p.findall(".//a:blip", ns):
rid = blip.attrib.get(f"{{{ns['r']}}}embed")
if not rid:
continue
target = rid_to_target.get(rid)
if not target:
continue
media_path = target if target.startswith("word/") else f"word/{target}"
try:
data = zf.read(media_path)
object_name = f"{db_id}/{file_id}/images/{Path(target).name}"
suffix = Path(target).suffix.lower()
content_type = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".tif": "image/tiff",
".tiff": "image/tiff",
}.get(suffix, "image/jpeg")
result = minio_client.upload_file(
bucket_name=bucket_name,
object_name=object_name,
data=data,
content_type=content_type,
)
image_urls.append((Path(target).name, result.url))
except Exception as e:
logger.error(f"上传图片失败 {Path(target).name}: {e}")
continue
line = para_text
for name, url in image_urls:
line = f"{line}\n![{name}]({url})"
if line:
md_lines.append(line)
return "\n\n".join(md_lines)
def chunk_with_parser(file_path, params=None):
"""
使用文件解析器将文件切分成固定大小的块
@ -294,8 +374,11 @@ async def process_file_to_markdown(file_path: str, params: dict | None = None) -
content = f.read()
return f"# {file_path_obj.name}\n\n{content}"
elif file_ext in [".doc", ".docx"]:
# 处理 Word 文档
elif file_ext == ".docx":
text = _extract_docx_markdown_with_images(file_path_obj, params=params)
return f"# {file_path_obj.name}\n\n" + text
elif file_ext == ".doc":
text = _extract_word_text(file_path_obj)
return f"# {file_path_obj.name}\n\n{text}"