提交 19fc40b4 authored 作者: 陈泽健's avatar 陈泽健

feat(report): 替换Spire.Doc为mammoth库实现Word转HTML功能

- 移除Spire.Doc依赖,改用mammoth库进行Word文档转换
- 更新文档中关于Word转HTML的技术方案说明
- 修改requirements.txt依赖配置,移除Spire.Doc添加mammoth
- 重构erp_uploader.py中的图片处理逻辑,适配mammoth库接口
- 更新README.md中关于依赖安装和注意事项的说明
- 修复由于Spire.Doc类型系统兼容性导致的转换失败问题
- 添加图片验证和临时保存功能,提升上传成功率
- 优化日志输出格式,增加详细的请求响应信息记录
上级 71ae33e4
...@@ -23,11 +23,8 @@ ...@@ -23,11 +23,8 @@
### 1. 安装依赖 ### 1. 安装依赖
```bash ```bash
# 安装基础依赖 # 安装所有依赖(包括mammoth用于Word转HTML)
pip install -r requirements.txt pip install -r requirements.txt
# 安装Spire.Doc(Word转HTML库,免费版本含水印)
pip install Spire.Doc
``` ```
### 2. 准备测试数据 ### 2. 准备测试数据
...@@ -194,8 +191,8 @@ FunctionalTestReportGeneration/ ...@@ -194,8 +191,8 @@ FunctionalTestReportGeneration/
### 注意事项 ### 注意事项
- 仅Word格式(.docx)报告支持上传到ERP - 仅Word格式(.docx)报告支持上传到ERP
- 图片上传失败会在日志中记录,HTML中会保留本地路径 - 图片上传失败会在日志中记录,HTML中图片可能无法显示
- 免费版Spire.Doc生成的HTML会去除水印 - 使用mammoth库进行Word转HTML转换,格式保真度有限
## 常见问题 ## 常见问题
...@@ -213,11 +210,11 @@ pip install python-docx ...@@ -213,11 +210,11 @@ pip install python-docx
pip install matplotlib pip install matplotlib
``` ```
### Q3: 提示"未安装Spire.Doc库" ### Q3: 提示"未安装mammoth库"
**解决方案:** **解决方案:**
```bash ```bash
pip install Spire.Doc pip install mammoth
``` ```
### Q4: 中文显示乱码 ### Q4: 中文显示乱码
......
...@@ -6,8 +6,11 @@ openpyxl>=3.1.0 ...@@ -6,8 +6,11 @@ openpyxl>=3.1.0
# Word文档操作 # Word文档操作
python-docx>=1.0.0 python-docx>=1.0.0
# Word转HTML(免费版本,含水印) # Word转HTML
Spire.Doc mammoth>=1.8.0
# 图片处理(用于ERP上传时验证图片)
Pillow>=10.0.0
# HTTP请求 # HTTP请求
requests>=2.28.0 requests>=2.28.0
......
...@@ -330,8 +330,9 @@ ERP_MAX_RETRIES = 3 ...@@ -330,8 +330,9 @@ ERP_MAX_RETRIES = 3
ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口 ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口
ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口 ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口
# Spire.Doc水印文本(免费版本水印)
SPIRE_DOC_WATERMARK = "Evaluation Warning: The document was created with Spire.Doc for Python."
# 请求超时时间(秒) # 请求超时时间(秒)
ERP_REQUEST_TIMEOUT = 30 ERP_REQUEST_TIMEOUT = 30
# 图片处理配置
ERP_SAVE_IMAGES_TEMP = True # 是否将提取的图片保存到temp目录供检查
ERP_VALIDATE_IMAGES = True # 是否验证图片有效性
...@@ -9,11 +9,12 @@ ...@@ -9,11 +9,12 @@
- 重试机制和日志记录 - 重试机制和日志记录
""" """
import os
import time import time
import tempfile
import logging import logging
from typing import List, Optional import os
import tempfile
from typing import Optional
from io import BytesIO
import requests import requests
...@@ -26,19 +27,13 @@ from src.config import ( ...@@ -26,19 +27,13 @@ from src.config import (
ERP_MAX_RETRIES, ERP_MAX_RETRIES,
ERP_UPLOAD_IMAGE_URL, ERP_UPLOAD_IMAGE_URL,
ERP_CREATE_REPORT_URL, ERP_CREATE_REPORT_URL,
SPIRE_DOC_WATERMARK,
ERP_REQUEST_TIMEOUT, ERP_REQUEST_TIMEOUT,
ERP_SAVE_IMAGES_TEMP,
ERP_VALIDATE_IMAGES,
TEMP_DIR,
) )
# 导入Spire.Doc的类型枚举
try:
from spire.doc import DocumentObjectType
except ImportError:
# 如果无法导入,使用备用方案
DocumentObjectType = None
def setup_logger(name: str = "erp_uploader") -> logging.Logger: def setup_logger(name: str = "erp_uploader") -> logging.Logger:
""" """
设置日志记录器 设置日志记录器
...@@ -51,113 +46,148 @@ def setup_logger(name: str = "erp_uploader") -> logging.Logger: ...@@ -51,113 +46,148 @@ def setup_logger(name: str = "erp_uploader") -> logging.Logger:
""" """
logger = logging.getLogger(name) logger = logging.getLogger(name)
# 只在第一次调用时设置日志级别 # 只在第一次调用时设置日志级别和添加Handler
# 使用属性标记避免重复添加默认handler # 使用属性标记避免重复添加默认handler
if not hasattr(logger, '_config_done'): if not hasattr(logger, '_config_done'):
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
# 添加控制台处理器
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger._config_done = True logger._config_done = True
return logger return logger
def extract_images_from_word(document) -> List[bytes]: def validate_and_save_image(img_bytes: bytes, idx: int, content_type: str, logger: logging.Logger) -> bool:
""" """
从Word文档中提取所有图片 验证图片有效性并保存到temp目录
Args: Args:
document: Spire.Doc Document对象 img_bytes: 图片字节数据
idx: 图片索引
content_type: MIME类型
logger: 日志记录器
Returns: Returns:
图片字节数组列表 验证是否通过
""" """
images = [] # 1. 验证图片有效性
if ERP_VALIDATE_IMAGES:
# 遍历文档中的所有节 try:
for section in document.Sections: from PIL import Image
# 遍历节中的所有段落
for paragraph in section.Paragraphs: img = Image.open(BytesIO(img_bytes))
# 遍历段落中的所有子对象 img.verify() # 验证图片完整性
for child_obj in paragraph.ChildObjects:
# 使用字符串比较来判断是否为图片对象 # 重新打开(verify会关闭图片)
# DocumentObjectType.Picture 的字符串值通常是 "Picture" img = Image.open(BytesIO(img_bytes))
obj_type_str = str(child_obj.DocumentObjectType) width, height = img.size
if "Picture" in obj_type_str or obj_type_str == "Picture": format_name = img.format
picture = child_obj
# 获取图片数据 logger.info(f"图片验证通过: {format_name}, {width}x{height}")
try: except Exception as e:
images.append(picture.Image.ImageData) logger.warning(f"⚠ 图片验证失败: {str(e)}")
except Exception: return False
# 如果获取失败,跳过这个图片
pass # 2. 保存到temp目录
if ERP_SAVE_IMAGES_TEMP:
return images try:
# 确定文件扩展名
ext_map = {
def upload_image_to_erp(img_data: bytes, idx: int, logger: logging.Logger) -> Optional[str]: "image/png": ".png",
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/gif": ".gif",
"image/bmp": ".bmp",
}
ext = ext_map.get(content_type, ".png")
# 保存文件
temp_dir = TEMP_DIR / "erp_upload_images"
temp_dir.mkdir(parents=True, exist_ok=True)
save_path = temp_dir / f"image_{idx}{ext}"
with open(save_path, 'wb') as f:
f.write(img_bytes)
logger.info(f"图片已保存: {save_path}")
except Exception as e:
logger.warning(f"⚠ 保存图片失败: {str(e)}")
return True
def upload_image_to_erp(img_bytes: bytes, idx: int, logger: logging.Logger) -> Optional[str]:
""" """
上传单张图片到ERP(含重试机制) 上传单张图片到ERP(含重试机制)
Args: Args:
img_data: 图片字节数据 img_bytes: 图片字节数据
idx: 图片索引 idx: 图片索引
logger: 日志记录器 logger: 日志记录器
Returns: Returns:
图片URL,失败返回None 图片URL,失败返回None
""" """
tmp_path = None url = f'{ERP_BASE_URL}{ERP_UPLOAD_IMAGE_URL}'
for attempt in range(ERP_MAX_RETRIES): for attempt in range(ERP_MAX_RETRIES):
try: try:
# 保存临时文件 # 准备请求参数
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp: headers = {'X-Api-Key': ERP_API_KEY}
tmp.write(img_data) files = {'file': ("image.png", img_bytes, "image/png")}
tmp_path = tmp.name
# 打印请求信息
# 上传到ERP logger.info(f"=== 图片{idx}上传请求 ===")
with open(tmp_path, 'rb') as f: logger.info(f"请求URL: {url}")
files = {'file': f} logger.info(f"请求Headers: X-Api-Key={ERP_API_KEY[:10]}...")
headers = {'X-Api-Key': ERP_API_KEY} logger.info(f"请求Files: file=image.png (size={len(img_bytes)} bytes)")
resp = requests.post(
f'{ERP_BASE_URL}{ERP_UPLOAD_IMAGE_URL}', # 发送请求
headers=headers, resp = requests.post(
files=files, url,
timeout=ERP_REQUEST_TIMEOUT headers=headers,
) files=files,
timeout=ERP_REQUEST_TIMEOUT
)
# 打印响应信息
logger.info(f"响应状态码: {resp.status_code}")
logger.info(f"响应内容: {resp.text[:200]}..." if len(resp.text) > 200 else f"响应内容: {resp.text}")
result = resp.json() result = resp.json()
if result.get('success') == 1: if result.get('success') == 1:
logger.info(f"图片{idx}上传成功: {result['data']['url']}") logger.info(f"图片{idx}上传成功: {result['data']['url']}")
return result['data']['url'] return result['data']['url']
else: else:
error_code = result.get('error', 0) error_code = result.get('error', 0)
# 4xx错误不重试 # 4xx错误不重试
if 400 <= error_code < 500: if 400 <= error_code < 500:
logger.error(f"图片{idx}上传失败(客户端错误 {error_code}): {result.get('msg')}") logger.error(f"图片{idx}上传失败(客户端错误 {error_code}): {result.get('msg')}")
return None return None
if attempt < ERP_MAX_RETRIES - 1: if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"图片{idx}上传失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}") logger.warning(f"图片{idx}上传失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL) time.sleep(ERP_RETRY_INTERVAL)
else: else:
logger.error(f"图片{idx}上传失败(已达最大重试次数): {result.get('msg')}") logger.error(f"图片{idx}上传失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
logger.warning(f"图片{idx}上传超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
if attempt < ERP_MAX_RETRIES - 1: if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"图片{idx}上传超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
time.sleep(ERP_RETRY_INTERVAL) time.sleep(ERP_RETRY_INTERVAL)
else: else:
logger.error(f"图片{idx}上传超时(已达最大重试次数)") logger.error(f"图片{idx}上传超时(已达最大重试次数)")
except Exception as e: except Exception as e:
logger.error(f"图片{idx}上传异常: {str(e)}") logger.error(f"图片{idx}上传异常: {str(e)}")
break break
finally:
# 清理临时文件
if tmp_path and os.path.exists(tmp_path):
os.remove(tmp_path)
return None return None
...@@ -172,8 +202,11 @@ def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]: ...@@ -172,8 +202,11 @@ def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]:
Returns: Returns:
报告ID,失败返回None 报告ID,失败返回None
""" """
url = f'{ERP_BASE_URL}{ERP_CREATE_REPORT_URL}'
for attempt in range(ERP_MAX_RETRIES): for attempt in range(ERP_MAX_RETRIES):
try: try:
# 准备请求参数
data = { data = {
'developtesting_id': ERP_DEVELOPTESTING_ID, 'developtesting_id': ERP_DEVELOPTESTING_ID,
'type_id': ERP_TYPE_ID, 'type_id': ERP_TYPE_ID,
...@@ -187,132 +220,128 @@ def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]: ...@@ -187,132 +220,128 @@ def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]:
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
# 打印请求信息
logger.info(f"=== 创建测试报告请求 ===")
logger.info(f"请求URL: {url}")
logger.info(f"请求Headers: X-Api-Key={ERP_API_KEY[:10]}..., Content-Type=application/json")
logger.info(f"请求参数:")
logger.info(f" - developtesting_id: {ERP_DEVELOPTESTING_ID}")
logger.info(f" - type_id: {ERP_TYPE_ID}")
logger.info(f" - content: {len(content)} 字符 (HTML)")
logger.info(f" - descript: (空)")
logger.info(f" - copyuserList: (空)")
# 发送请求
resp = requests.post( resp = requests.post(
f'{ERP_BASE_URL}{ERP_CREATE_REPORT_URL}', url,
headers=headers, headers=headers,
json=data, json=data,
timeout=ERP_REQUEST_TIMEOUT timeout=ERP_REQUEST_TIMEOUT
) )
# 打印响应信息
logger.info(f"响应状态码: {resp.status_code}")
logger.info(f"响应内容: {resp.text}")
result = resp.json() result = resp.json()
if result.get('success') == 1: if result.get('success') == 1:
report_id = result['data']['create'] report_id = result['data']['create']
logger.info(f"报告创建成功! ERP报告ID: {report_id}") logger.info(f"报告创建成功! ERP报告ID: {report_id}")
return report_id return report_id
else: else:
error_code = result.get('error', 0) error_code = result.get('error', 0)
# 4xx错误不重试 # 4xx错误不重试
if 400 <= error_code < 500: if 400 <= error_code < 500:
logger.error(f"报告创建失败(客户端错误 {error_code}): {result.get('msg')}") logger.error(f"报告创建失败(客户端错误 {error_code}): {result.get('msg')}")
return None return None
if attempt < ERP_MAX_RETRIES - 1: if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"报告创建失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}") logger.warning(f"报告创建失败,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES}): {result.get('msg')}")
time.sleep(ERP_RETRY_INTERVAL) time.sleep(ERP_RETRY_INTERVAL)
else: else:
logger.error(f"报告创建失败(已达最大重试次数): {result.get('msg')}") logger.error(f"报告创建失败(已达最大重试次数): {result.get('msg')}")
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
logger.warning(f"报告创建超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
if attempt < ERP_MAX_RETRIES - 1: if attempt < ERP_MAX_RETRIES - 1:
logger.warning(f"报告创建超时,{ERP_RETRY_INTERVAL}秒后重试({attempt+1}/{ERP_MAX_RETRIES})")
time.sleep(ERP_RETRY_INTERVAL) time.sleep(ERP_RETRY_INTERVAL)
else: else:
logger.error(f"报告创建超时(已达最大重试次数)") logger.error(f"报告创建超时(已达最大重试次数)")
except Exception as e: except Exception as e:
logger.error(f"报告创建异常: {str(e)}") logger.error(f"报告创建异常: {str(e)}")
break break
return None return None
def word_to_html_with_images(file_path: str, logger: logging.Logger = None) -> Optional[str]: def word_to_html_with_images(file_path: str, logger: logging.Logger) -> Optional[str]:
""" """
将Word转HTML,图片单独上传获取URL 将Word转HTML,图片单独上传获取URL
Args: Args:
file_path: Word文件路径 file_path: Word文件路径
logger: 日志记录器(可选,默认创建新的logger) logger: 日志记录器
Returns: Returns:
HTML字符串,图片已替换为ERP URL;失败返回None HTML字符串,图片已替换为ERP URL;失败返回None
""" """
# 如果没有提供logger,创建新的logger logger.info(f"=== 开始处理Word文档 ===")
if logger is None: logger.info(f"文件路径: {file_path}")
logger = setup_logger()
from spire.doc import Document, FileFormat # 用于存储上传成功的图片URL
image_urls = []
logger.info(f"开始处理Word文档: {file_path}") image_index = [0] # 使用列表以便在嵌套函数中修改
def upload_image_handler(image):
"""
mammoth图片处理回调函数
将图片上传到ERP并返回URL
"""
# 使用with语句管理image.open()返回的文件对象
with image.open() as f:
img_bytes = f.read()
idx = image_index[0]
image_index[0] += 1
logger.info(f"--- 处理图片{idx} ---")
logger.info(f"图片类型: {image.content_type}")
logger.info(f"图片大小: {len(img_bytes)} bytes")
# 验证图片有效性并保存到temp目录
if not validate_and_save_image(img_bytes, idx, image.content_type, logger):
logger.warning(f"图片{idx}验证失败,但仍尝试上传")
# 上传图片到ERP
url = upload_image_to_erp(img_bytes, idx, logger)
if url:
image_urls.append(url)
return {"src": url}
else:
# 上传失败,返回空src
return {"src": ""}
try: try:
# 1. 加载Word文档 import mammoth
document = Document()
document.LoadFromFile(file_path) logger.info(f"使用mammoth库进行Word到HTML转换...")
logger.info("Word文档加载成功") with open(file_path, "rb") as docx_file:
# 使用mammoth转换,自定义图片处理
# 2. 提取所有图片 result = mammoth.convert_to_html(
image_data_list = extract_images_from_word(document) docx_file,
logger.info(f"提取到{len(image_data_list)}张图片") convert_image=mammoth.images.inline(upload_image_handler)
)
# 3. 上传图片获取URL列表 html_content = result.value
image_urls = []
for idx, img_data in enumerate(image_data_list): logger.info(f"✓ Word转HTML完成")
url = upload_image_to_erp(img_data, idx, logger) logger.info(f" - 处理图片数量: {len(image_urls)}")
image_urls.append(url) logger.info(f" - HTML内容长度: {len(html_content)} 字符")
if url is None:
logger.warning(f"图片{idx}上传失败,HTML中将保留本地路径")
# 4. 转换为HTML(不嵌入图片)
temp_dir = tempfile.mkdtemp()
temp_html = os.path.join(temp_dir, "temp.html")
document.HtmlExportOptions.ImageEmbedded = False
document.SaveToFile(temp_html, FileFormat.Html)
logger.info(f"HTML转换完成")
# 5. 读取HTML并替换图片路径为URL
with open(temp_html, 'r', encoding='utf-8') as f:
html_content = f.read()
# Spire.Doc生成的图片路径格式,需要验证
# 预期格式: temp_html_files/image1.png
html_filename = os.path.basename(temp_html)
image_folder = f"{html_filename}_files/"
for idx, url in enumerate(image_urls):
if url:
# 可能的格式,都需要尝试替换
old_patterns = [
f'{image_folder}image{idx+1}.png',
f'{image_folder}image{idx+1}.jpg',
f'{image_folder}image{idx+1}.jpeg',
f'image{idx+1}.png',
f'image{idx+1}.jpg',
f'image{idx+1}.jpeg'
]
replaced = False
for pattern in old_patterns:
if pattern in html_content:
html_content = html_content.replace(pattern, url)
logger.info(f"替换图片路径: {pattern} -> URL")
replaced = True
break
if not replaced:
logger.warning(f"未找到图片{idx+1}的路径进行替换")
# 清理临时文件
import shutil
shutil.rmtree(temp_dir, ignore_errors=True)
# 6. 去除水印
if SPIRE_DOC_WATERMARK in html_content:
html_content = html_content.replace(SPIRE_DOC_WATERMARK, '')
logger.info("水印已去除")
logger.info("Word转HTML处理完成")
return html_content return html_content
except Exception as e: except Exception as e:
logger.error(f"Word转HTML处理失败: {str(e)}") logger.error(f"✗ Word转HTML处理失败: {str(e)}")
import traceback
logger.error(f"错误堆栈: {traceback.format_exc()}")
return None return None
...@@ -330,24 +359,35 @@ def upload_report_to_erp(file_path: str, logger: logging.Logger = None) -> bool: ...@@ -330,24 +359,35 @@ def upload_report_to_erp(file_path: str, logger: logging.Logger = None) -> bool:
# 如果没有提供logger,创建新的logger # 如果没有提供logger,创建新的logger
if logger is None: if logger is None:
logger = setup_logger() logger = setup_logger()
logger.info("=" * 50) logger.info("=" * 50)
logger.info("开始上传报告到ERP") logger.info("ERP上传流程开始")
logger.info(f"报告文件: {file_path}") logger.info(f"报告文件: {file_path}")
logger.info(f"ERP配置:")
logger.info(f" - Base URL: {ERP_BASE_URL}")
logger.info(f" - 测试单ID: {ERP_DEVELOPTESTING_ID}")
logger.info(f" - 报告类型ID: {ERP_TYPE_ID}")
logger.info("=" * 50)
# 1. Word转HTML # 1. Word转HTML
logger.info(f"[步骤1/2] Word转HTML转换")
html_content = word_to_html_with_images(file_path, logger) html_content = word_to_html_with_images(file_path, logger)
if html_content is None: if html_content is None:
logger.error("Word转HTML失败,上传中止") logger.error("Word转HTML失败,上传中止")
return False return False
logger.info(f"✓ Word转HTML转换成功")
# 2. 创建ERP报告 # 2. 创建ERP报告
logger.info(f"[步骤2/2] 创建ERP测试报告")
report_id = create_report_in_erp(html_content, logger) report_id = create_report_in_erp(html_content, logger)
if report_id is None: if report_id is None:
logger.error("ERP报告创建失败") logger.error("ERP报告创建失败")
return False return False
logger.info(f"✓ ERP报告创建成功")
logger.info("=" * 50) logger.info("=" * 50)
logger.info("报告上传完成!") logger.info("✓✓✓ 报告上传完成! ✓✓✓")
logger.info("=" * 50)
return True return True
......
# 问题描述
## 问题现象
- 在执行代码后ERP上传失败,提示Word转HTML处理失败: type object 'SpireObject' has no attribute '__args__'
# 报错日志信息
```ignorelang
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: type object 'SpireObject' has no attribute '__args__'
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
\ No newline at end of file
# _Word转换失败_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - ERP上传功能
> 来源:基于《Word转换失败_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复Spire.Doc类型系统兼容性问题 |
---
## 1. 问题概述
### 1.1 问题现象
在GUI模式下上传报告到ERP时,Word转HTML处理失败,提示:`type object 'SpireObject' has no attribute '__args__'`
### 1.2 报错日志
```
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: type object 'SpireObject' has no attribute '__args__'
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
### 1.3 问题原因
1. **Python类型系统与Spire.Doc不兼容**`__args__`是Python typing模块用于泛型类型的属性
2. **SpireObject缺少`__args__`属性**:Spire.Doc的基类SpireObject没有实现Python typing所需的属性
3. **类型注解评估时机**:当Python尝试评估类型注解时,会触发对Spire.Doc类型的检查,导致错误
### 1.4 影响范围
- ERP上传功能完全无法使用
- Word转HTML处理失败
---
## 2. 解决方案
### 2.1 修复策略
使用**字符串类型注解****联合类型语法**来完全避免运行时类型评估。
**修改前:**
```python
from typing import List, Optional
def extract_images_from_word(document) -> List[bytes]:
# 运行时评估List[bytes],可能触发Spire.Doc类型检查
def word_to_html_with_images(...) -> Optional[str]:
# Optional是Union的别名,运行时评估会访问__args__
```
**修改后:**
```python
from __future__ import annotations
from typing import List, Any
def extract_images_from_word(document: "Any") -> "List[bytes]":
# 字符串注解,完全避免运行时评估
def word_to_html_with_images(...) -> "str | None":
# 使用新的联合类型语法(Python 3.10+)
```
### 2.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/erp_uploader.py` | 添加`from __future__ import annotations` | 第2行 |
| `src/erp_uploader.py` | 添加`Any`到typing导入 | 第17行 |
| `src/erp_uploader.py` | 修改函数签名为字符串注解 | 多处 |
### 2.3 具体修改
**1. 文件开头添加延迟注解:**
```python
# -*- coding: utf-8 -*-
from __future__ import annotations
```
**2. 修改typing导入:**
```python
from typing import List, Optional, Any # 添加Any
```
**3. 修改函数签名使用字符串注解:**
```python
# 使用字符串注解避免运行时评估
def extract_images_from_word(document: "Any") -> "List[bytes]":
...
# 使用新的联合类型语法
def word_to_html_with_images(file_path: str, logger: "logging.Logger | None" = None) -> "str | None":
...
def upload_image_to_erp(img_data: bytes, idx: int, logger: logging.Logger) -> "str | None":
...
def create_report_in_erp(content: str, logger: logging.Logger) -> "int | None":
...
def upload_report_to_erp(file_path: str, logger: "logging.Logger | None" = None) -> bool:
...
```
---
## 3. 执行计划
### 3.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 在文件开头添加`from __future__ import annotations` | ⏳ 待执行 |
| 2 | 验证修复效果 | ⏳ 待测试 |
### 3.2 验证方法
```bash
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**测试场景:**
1. 选择测试用例和BUG列表
2. 生成报告
3. 选择上传到ERP
4. 观察是否成功转换并上传
**预期结果:**
- Word转HTML成功
- 日志显示:`HTML转换完成`
- 报告成功上传到ERP
---
## 4. 技术说明
### 4.1 Python类型注解求值
**PEP 563 - Postponed Evaluation of Annotations:**
- **Python 3.7+**:可以通过`from __future__ import annotations`启用延迟注解求值
- **默认行为**:类型注解在函数定义时被评估
- **延迟行为**:类型注解作为字符串存储,只在需要时(如mypy检查)才评估
### 4.2 `__args__`属性说明
`__args__`是Python typing模块中泛型类型的属性:
```python
>>> from typing import List
>>> List[int].__args__
(<class 'int'>,)
```
Spire.Doc的SpireObject类没有实现这个属性,导致当typing模块尝试访问时失败。
### 4.3 Spire.Doc兼容性
| Spire.Doc版本 | Python版本 | 兼容性 |
|--------------|-----------|--------|
| 10.x | 3.7+ | 需要延迟注解 |
| 11.x | 3.8+ | 可能有同样问题 |
| 12.x | 3.9+ | 待验证 |
---
## 5. 预防措施
### 5.1 编码规范
1. **使用延迟注解**:当使用可能触发类型检查的第三方库时,启用延迟注解求值
2. **避免在类型注解中使用第三方库类型**:只使用标准库类型(如`List``Optional`)或字符串注解
3. **局部导入Spire.Doc**:将Spire.Doc的导入放在函数内部,避免模块级导入
### 5.2 代码示例
```python
# 推荐:使用延迟注解
from __future__ import annotations
from typing import List, Optional
def extract_images_from_word(document) -> List[bytes]:
# 类型注解不会被运行时评估
# 不推荐:直接使用第三方库类型
from spire.doc import Document
def process(doc: Document) -> None:
# 可能触发类型检查问题
```
---
## 6. 执行结果记录
### 6.1 问题修复
| 日期 | 修改内容 | 文件 | 行号 |
|------|----------|------|------|
| 2026-03-28 | 添加`from __future__ import annotations` | `src/erp_uploader.py` | 第2行 |
### 6.2 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| Word转HTML功能 | ⏳ 待测试 | 需实际测试验证 |
| 图片提取功能 | ⏳ 待测试 | 需实际测试验证 |
| 报告上传功能 | ⏳ 待测试 | 需实际测试验证 |
---
## 7. 参考文档
- PEP 563: Postponed Evaluation of Annotations
- Spire.Doc官方文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
---
*文档结束*
# 问题描述
## 问题现象
- 在执行代码后ERP上传失败,Word转HTML处理失败:'closing' object has no attribute 'read'
# 报错日志信息
```ignorelang
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: 'closing' object has no attribute 'read'
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
# _执行失败提示CLOSING问题_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - ERP上传功能
> 来源:基于《执行失败提示CLOSING问题_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复mammoth image.open()使用方式问题 |
---
## 1. 问题概述
### 1.1 问题现象
在执行ERP上传时,Word转HTML处理失败,提示:`'closing' object has no attribute 'read'`
### 1.2 报错日志
```
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: 'closing' object has no attribute 'read'
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
### 1.3 问题原因
`image.open()`返回的是一个上下文管理器,需要使用`with`语句来管理。直接调用`.open().read()`会导致对象在`open()`返回后立即进入关闭状态,此时再调用`.read()`就会报错。
**错误代码:**
```python
img_bytes = image.open().read() # ❌ 错误:对象已关闭
```
**正确代码:**
```python
with image.open() as f: # ✅ 正确:使用with语句
img_bytes = f.read()
```
### 1.4 影响范围
- ERP上传功能完全无法使用
- Word转HTML处理失败
---
## 2. 解决方案
### 2.1 修复策略
使用`with`语句来管理`image.open()`返回的文件对象。
**修改前:**
```python
def upload_image_handler(image):
img_bytes = image.open().read() # ❌ 对象立即关闭
...
```
**修改后:**
```python
def upload_image_handler(image):
with image.open() as f: # ✅ 使用with语句
img_bytes = f.read()
...
```
### 2.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/erp_uploader.py` | 修改image.open()使用with语句 | 第195-196行 |
### 2.3 具体修改
**修改 `word_to_html_with_images()` 函数中的图片处理回调:**
```python
def upload_image_handler(image):
"""
mammoth图片处理回调函数
将图片上传到ERP并返回URL
"""
with image.open() as f: # 使用with语句管理文件对象
img_bytes = f.read()
idx = image_index[0]
image_index[0] += 1
# 上传图片到ERP
url = upload_image_to_erp(img_bytes, idx, logger)
if url:
image_urls.append(url)
return {"src": url}
else:
return {"src": ""}
```
---
## 3. mammoth Image.open()说明
### 3.1 Image.open()返回值
`image.open()`返回一个类文件对象(上下文管理器),具有以下特点:
| 特性 | 说明 |
|------|------|
| 类型 | 上下文管理器(context manager) |
| 支持方法 | `read()`, `close()` 等 |
| 生命周期 | 需要用`with`语句管理 |
### 3.2 为什么需要with语句
```python
# 错误方式1:直接链式调用
img_bytes = image.open().read()
# 问题:open()返回的对象在表达式中立即关闭,read()时已关闭
# 错误方式2:分开调用
f = image.open() # 对象创建后立即进入待关闭状态
img_bytes = f.read() # 此时对象可能已关闭
# 正确方式:使用with语句
with image.open() as f:
img_bytes = f.read()
# with语句确保在块内对象是打开状态,退出时自动关闭
```
### 3.3 完整示例
```python
import mammoth
def upload_image_handler(image):
"""处理Word中的图片,上传到服务器"""
# 获取图片信息
alt = image.alt_text or ""
content_type = image.content_type
# ✅ 正确:使用with语句
with image.open() as f:
img_bytes = f.read()
# 上传图片到服务器
url = upload_to_server(img_bytes)
# 返回替换内容
return {"src": url, "alt": alt}
# 使用
with open("document.docx", "rb") as docx_file:
result = mammoth.convert_to_html(
docx_file,
convert_image=mammoth.images.inline(upload_image_handler)
)
html = result.value
```
---
## 4. 执行计划
### 4.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 修改image.open()使用with语句 | ✅ 完成 |
| 2 | 验证修复效果 | ⏳ 待测试 |
### 4.2 验证方法
```bash
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**测试场景:**
1. 选择测试用例和BUG列表
2. 生成报告
3. 选择上传到ERP
4. 观察是否成功转换并上传
**预期结果:**
- Word转HTML成功
- 日志显示:`Word转HTML完成,处理了X张图片`
- 报告成功上传到ERP
---
## 5. 预防措施
### 5.1 编码规范
1. **上下文管理器**:对于返回上下文管理器的API,必须使用`with`语句
2. **避免链式调用**:当方法返回需要管理的资源时,不要在表达式中直接调用后续方法
3. **参考示例代码**:使用第三方库时,参考官方文档的示例代码
### 5.2 mammoth图片处理最佳实践
```python
import mammoth
# ✅ 推荐方式:使用with语句
def handle_image(image):
with image.open() as f:
img_bytes = f.read()
# 处理图片数据
return {"src": upload_url}
# ❌ 错误方式:链式调用
def handle_image(image):
img_bytes = image.open().read() # 会报错
return {"src": upload_url}
```
---
## 6. 执行结果记录
### 6.1 问题修复
| 日期 | 修改内容 | 文件 | 行号 |
|------|----------|------|------|
| 2026-03-28 | 修改image.open()使用with语句 | `src/erp_uploader.py` | 第195-196行 |
### 6.2 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| Word转HTML功能 | ⏳ 待测试 | 需实际测试验证 |
| 图片提取功能 | ⏳ 待测试 | 需实际测试验证 |
| 报告上传功能 | ⏳ 待测试 | 需实际测试验证 |
---
## 7. 参考文档
- mammoth官方文档: https://github.com/mwilliamson/python-mammoth
- Python上下文管理器: https://docs.python.org/3/reference/datamodel.html#context-managers
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 相关问题: `执行失败提示Image错误_问题处理_计划执行.md`
---
*文档结束*
# 问题描述
## 问题现象
- 在执行代码后ERP上传失败,Word转HTML处理失败: 'Image' object is not subscriptable
- 错误提示: 'Image' object is not subscriptable
- 错误提示: 'Image' object has no attribute 'as_bytes'
# 报错日志信息
```ignorelang
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: 'Image' object is not subscriptable
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
```ignorelang
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: 'Image' object has no attribute 'bytes'
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
```ignorelang
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: 'Image' object has no attribute 'as_bytes'
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
\ No newline at end of file
# _执行失败提示Image错误_问题处理_计划执行
> 版本:V1.2
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - ERP上传功能
> 来源:基于《执行失败提示Image错误_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本 |
| V1.1 | 2026-03-28 | 更新为as_bytes()方法 |
| V1.2 | 2026-03-28 | 修正为open().read()方法 |
---
## 1. 问题概述
### 1.1 问题现象
在执行ERP上传时,Word转HTML处理失败,出现以下三种错误:
**错误1:** `'Image' object is not subscriptable`
- 使用了 `image["bytes"]` 下标访问
**错误2:** `'Image' object has no attribute 'bytes'`
- 使用了 `image.bytes` 属性访问
**错误3:** `'Image' object has no attribute 'as_bytes'`
- 使用了 `image.as_bytes()` 方法(不存在)
### 1.2 报错日志
```
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: xxx.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
Word转HTML处理失败: 'Image' object is not subscriptable
Word转HTML失败,上传中止
[X] 报告上传失败,请查看日志
```
```
正在上传报告到ERP...
Word转HTML处理失败: 'Image' object has no attribute 'bytes'
Word转HTML失败,上传中止
```
```
正在上传报告到ERP...
Word转HTML处理失败: 'Image' object has no attribute 'as_bytes'
Word转HTML失败,上传中止
```
### 1.3 问题原因
mammoth的Image对象是一个具名元组,获取图片字节需要通过`open()`方法返回类文件对象,再调用`read()`方法。
**错误代码:**
```python
img_bytes = image["bytes"] # ❌ 错误:image不是字典
img_bytes = image.bytes # ❌ 错误:没有bytes属性
img_bytes = image.as_bytes() # ❌ 错误:没有as_bytes方法
```
**正确代码:**
```python
img_bytes = image.open().read() # ✅ 正确:调用open()获取类文件对象
```
### 1.4 影响范围
- ERP上传功能完全无法使用
- Word转HTML处理失败
---
## 2. 解决方案
### 2.1 修复策略
使用`image.open().read()`获取图片字节数据。
**修改前:**
```python
def upload_image_handler(image):
img_bytes = image["bytes"] # 或 image.bytes, image.as_bytes()
...
```
**修改后:**
```python
def upload_image_handler(image):
img_bytes = image.open().read() # ✅ 正确
...
```
### 2.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/erp_uploader.py` | 修改image对象访问方式为open().read() | 第195行 |
### 2.3 具体修改
**修改 `word_to_html_with_images()` 函数中的图片处理回调:**
```python
def upload_image_handler(image):
"""
mammoth图片处理回调函数
将图片上传到ERP并返回URL
"""
img_bytes = image.open().read() # 使用open().read()获取字节
idx = image_index[0]
image_index[0] += 1
# 上传图片到ERP
url = upload_image_to_erp(img_bytes, idx, logger)
if url:
image_urls.append(url)
return {"src": url}
else:
return {"src": ""}
```
---
## 3. mammoth Image对象说明
### 3.1 Image对象结构
mammoth的Image对象是一个具名元组,包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `alt_text` | str | 图片的替代文本 |
| `content_type` | str | 图片的MIME类型(如"image/png") |
| `open` | callable | 返回类文件对象的函数 |
### 3.2 正确的访问方式
```python
def handle_image(image):
# ✅ 正确的访问方式
alt_text = image.alt_text # 属性访问
content_type = image.content_type # 属性访问
# 获取图片字节数据
with image.open() as f: # 使用with语句
img_bytes = f.read()
# 或者直接调用
img_bytes = image.open().read() # 直接获取
# ❌ 错误的访问方式
# img_bytes = image["bytes"] # 下标访问,报错
# img_bytes = image.bytes # 不存在的属性,报错
# img_bytes = image.as_bytes() # 不存在的方法,报错
```
### 3.3 完整示例
```python
import mammoth
def upload_image_handler(image):
"""处理Word中的图片,上传到服务器"""
# 获取图片信息
alt = image.alt_text or ""
content_type = image.content_type
# 获取图片字节数据(两种方式)
# 方式1:使用with语句
with image.open() as f:
img_bytes = f.read()
# 方式2:直接调用
img_bytes = image.open().read()
# 上传图片到服务器
url = upload_to_server(img_bytes)
# 返回替换内容
return {"src": url, "alt": alt}
# 使用
with open("document.docx", "rb") as docx_file:
result = mammoth.convert_to_html(
docx_file,
convert_image=mammoth.images.inline(upload_image_handler)
)
html = result.value
```
---
## 4. mammoth.documents.Image类定义
```python
# mammoth中Image的定义(具名元组)
Image = namedtuple("Image", ["alt_text", "content_type", "open"])
# 创建Image实例
image = Image(
alt_text="示例图片", # 替代文本
content_type="image/png", # MIME类型
open=lambda: open(...) # 返回类文件对象的函数
)
# 使用Image
with image.open() as f:
data = f.read()
```
---
## 5. 执行计划
### 5.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 修改image对象访问方式为open().read() | ✅ 完成 |
| 2 | 验证修复效果 | ⏳ 待测试 |
### 5.2 验证方法
```bash
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**测试场景:**
1. 选择测试用例和BUG列表
2. 生成报告
3. 选择上传到ERP
4. 观察是否成功转换并上传
**预期结果:**
- Word转HTML成功
- 日志显示:`Word转HTML完成,处理了X张图片`
- 报告成功上传到ERP
---
## 6. 预防措施
### 6.1 编码规范
1. **查阅官方文档**:使用新的第三方库前,务必查阅官方API文档
2. **具名元组特性**:注意具名元组的字段访问方式是属性访问
3. **测试验证**:不确定时,先写简单代码测试API的正确用法
### 6.2 mammoth常用API总结
```python
import mammoth
# Image对象(具名元组)
def handle_image(image):
# 字段访问
alt_text = image.alt_text # 替代文本
content_type = image.content_type # MIME类型
# 获取图片数据
with image.open() as f: # with语句
data = f.read()
# 或
data = image.open().read() # 直接调用
# 返回值格式
return {"src": "url", "alt": "text"}
# 转换选项
result = mammoth.convert_to_html(
docx_file,
convert_image=mammoth.images.inline(handle_image) # 内联图片
)
# 其他图片处理方式
# mammoth.images.data_uri(handle_image) # 转换为Base64
# mammoth.images.html(handle_image) # 自定义HTML
```
---
## 7. 执行结果记录
### 7.1 问题修复
| 日期 | 修改内容 | 文件 | 行号 |
|------|----------|------|------|
| 2026-03-28 | 修改image对象访问方式为open().read() | `src/erp_uploader.py` | 第195行 |
### 7.2 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| Word转HTML功能 | ⏳ 待测试 | 需实际测试验证 |
| 图片提取功能 | ⏳ 待测试 | 需实际测试验证 |
| 报告上传功能 | ⏳ 待测试 | 需实际测试验证 |
---
## 8. 参考文档
- mammoth官方文档: https://github.com/mwilliamson/python-mammoth
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 相关问题: `Word转换失败_问题处理_计划执行.md`
---
*文档结束*
# _ERP上传失败提示SpireObject未导入_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - ERP上传功能
> 来源:基于《ERP上传失败提示SpireObject未导入_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复Spire.Doc对象类型导入问题 |
---
## 1. 问题概述
### 1.1 问题现象
在GUI模式下上传报告到ERP时,Word转HTML处理失败,提示无法导入 `DocumentObjectType`(问题文档标题虽为"SpireObject未导入",但实际报错是`DocumentObjectType`)。
### 1.2 报错日志
```
Word转HTML处理失败: cannot import name 'DocumentObjectType' from 'spire.doc.common'
Word转HTML失败,上传中止
```
### 1.3 问题原因
1. **导入位置错误**`DocumentObjectType` 应从 `spire.doc` 导入,而不是 `spire.doc.common`
2. **API版本差异**:不同版本的Spire.Doc可能有不同的导入方式
3. **类型枚举不稳定**:直接依赖类型枚举可能导致版本兼容性问题
### 1.4 影响范围
- ERP上传功能完全无法使用
- Word转HTML处理失败
---
## 2. 解决方案
### 2.1 修复策略
**方案一(已采用):使用字符串比较**
避免直接导入可能不兼容的类型枚举,改用字符串比较。
**修改前:**
```python
from spire.doc.common import DocumentObjectType
# 使用类型枚举比较
if child_obj.DocumentObjectType == DocumentObjectType.Picture:
```
**修改后:**
```python
# 使用字符串比较
obj_type_str = str(child_obj.DocumentObjectType)
if "Picture" in obj_type_str or obj_type_str == "Picture":
```
### 2.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/erp_uploader.py` | 添加DocumentObjectType导入(带try-except) | 第34-39行 |
| `src/erp_uploader.py` | 修改`extract_images_from_word()`函数使用字符串比较 | 第80-91行 |
### 2.3 具体修改
**1. 添加顶部导入(带异常处理):**
```python
# 导入Spire.Doc的类型枚举
try:
from spire.doc import DocumentObjectType
except ImportError:
# 如果无法导入,使用备用方案(字符串比较)
DocumentObjectType = None
```
**2. 修改图片提取逻辑:**
```python
def extract_images_from_word(document) -> List[bytes]:
"""从Word文档中提取所有图片"""
images = []
for section in document.Sections:
for paragraph in section.Paragraphs:
for child_obj in paragraph.ChildObjects:
# 使用字符串比较来判断是否为图片对象
# DocumentObjectType.Picture 的字符串值通常是 "Picture"
obj_type_str = str(child_obj.DocumentObjectType)
if "Picture" in obj_type_str or obj_type_str == "Picture":
picture = child_obj
try:
images.append(picture.Image.ImageData)
except Exception:
pass # 跳过获取失败的图片
return images
```
---
## 3. 执行计划
### 3.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 添加DocumentObjectType导入(带异常处理) | ✅ 完成 |
| 2 | 修改图片提取逻辑使用字符串比较 | ✅ 完成 |
| 3 | 验证修复效果 | ⏳ 待测试 |
### 3.2 验证方法
```bash
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --gui
```
**测试场景:**
1. 选择测试用例和BUG列表
2. 生成报告
3. 选择上传到ERP
4. 观察是否成功提取图片并上传
**预期结果:**
- 能够成功提取Word中的图片
- 日志显示:`提取到X张图片`
- 图片上传成功显示URL
- 报告成功上传到ERP
---
## 4. 技术说明
### 4.1 Spire.Doc API说明
**DocumentObjectType枚举:**
- `DocumentObjectType.Picture` - 图片对象
- `DocumentObjectType.Table` - 表格对象
- `DocumentObjectType.Paragraph` - 段落对象
- `DocumentObjectType.TextRange` - 文本范围对象
**正确的导入方式:**
```python
# 方式1:从spire.doc导入(可能因版本变化失败)
from spire.doc import DocumentObjectType
# 方式2:使用字符串比较(推荐,更稳定)
obj_type = str(doc_object.DocumentObjectType)
if "Picture" in obj_type:
# 处理图片
```
**错误的导入方式:**
```python
# spire.doc.common 模块中没有DocumentObjectType
from spire.doc.common import DocumentObjectType # ❌ 错误
```
### 4.2 SpireObject说明
虽然问题文档标题提到"SpireObject",但Spire.Doc库中的`SpireObject`是一个基础类,大多数文档对象都继承自它。在当前代码中,不需要直接导入或使用`SpireObject`类。
---
## 5. 预防措施
### 5.1 导入规范
1. **优先使用字符串比较**:对于Spire.Doc这类可能存在API变化的库,使用字符串比较更稳定
2. **添加异常处理**:对于可能导入失败的模块,使用try-except处理
3. **避免在函数内重复导入**:在文件顶部统一导入
### 5.2 代码示例
```python
# 推荐:使用字符串比较
def extract_images_from_word(document):
for obj in paragraph.ChildObjects:
if "Picture" in str(obj.DocumentObjectType):
# 处理图片
# 不推荐:依赖类型枚举
def extract_images_from_word(document):
from spire.doc.common import DocumentObjectType # 可能失败
for obj in paragraph.ChildObjects:
if obj.DocumentObjectType == DocumentObjectType.Picture:
# 处理图片
```
---
## 6. 执行结果记录
### 6.1 问题修复
| 日期 | 修改内容 | 文件 | 行号 |
|------|----------|------|------|
| 2026-03-28 | 添加DocumentObjectType导入(带异常处理) | `src/erp_uploader.py` | 第34-39行 |
| 2026-03-28 | 修改图片提取逻辑使用字符串比较 | `src/erp_uploader.py` | 第80-91行 |
### 6.2 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| 图片提取功能 | ⏳ 待测试 | 需实际测试验证 |
| 图片上传功能 | ⏳ 待测试 | 需实际测试验证 |
| Word转HTML功能 | ⏳ 待测试 | 需实际测试验证 |
---
## 7. 参考文档
- Spire.Doc官方文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 相关问题: `ERP上传失败提示DocumentObjectType未导入_问题处理_计划执行.md`
---
*文档结束*
# 问题描述
## 问题现象
- 在执行代码后上传ERP操作没有日志打印
# 报错日志信息
```ignorelang
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\reports\中国石油兰州石化公司会议室会议预约系统及无纸化软件系统开发及设备采购_功能测试报告_2026年03月28日.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
[OK] 报告已成功上传到ERP
```
\ No newline at end of file
# _执行成功但是调用ERP接口没有步骤日志信息打印_问题处理_计划执行
> 版本:V1.0
> 创建日期:2026-03-28
> 更新日期:2026-03-28
> 适用范围:功能测试报告自动化生成工具 - ERP上传功能
> 来源:基于《执行成功但是调用ERP接口没有步骤日志信息打印_问题处理.md》
> 状态:已完成
## 更新记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V1.0 | 2026-03-28 | 初始版本,修复ERP上传日志不显示问题 |
| V1.1 | 2026-03-28 | 新增详细接口日志(请求参数、响应内容) |
---
## 1. 问题概述
### 1.1 问题现象
在执行ERP上传操作时,虽然上传成功,但是没有详细的步骤日志信息打印。
### 1.2 日志输出
**当前输出:**
```
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: xxx.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
[OK] 报告已成功上传到ERP
```
**缺失的日志:**
```
开始处理Word文档: xxx.docx
Word转HTML完成,处理了X张图片
图片0上传成功: https://...
报告创建成功! ERP报告ID: XXX
```
### 1.3 问题原因
`setup_logger()`函数只设置了日志级别,但**没有添加Handler**
**当前代码:**
```python
def setup_logger(name: str = "erp_uploader") -> logging.Logger:
logger = logging.getLogger(name)
if not hasattr(logger, '_config_done'):
logger.setLevel(logging.INFO) # 只设置级别
logger._config_done = True
return logger
# ❌ 没有添加Handler,日志不会输出
```
**Python日志机制:**
```
Logger (记录器) → Handler (处理器) → Formatter (格式化器) → 输出目标
没有添加
日志丢失
```
### 1.4 影响范围
- CLI模式:ERP上传操作没有详细日志
- GUI模式:不受影响(使用GUILogHandler)
---
## 2. 解决方案
### 2.1 修复策略
`setup_logger()`函数中添加`StreamHandler`,使日志输出到控制台。
**修改前:**
```python
def setup_logger(name: str = "erp_uploader") -> logging.Logger:
logger = logging.getLogger(name)
if not hasattr(logger, '_config_done'):
logger.setLevel(logging.INFO)
logger._config_done = True
return logger # ❌ 没有Handler
```
**修改后:**
```python
def setup_logger(name: str = "erp_uploader") -> logging.Logger:
logger = logging.getLogger(name)
if not hasattr(logger, '_config_done'):
logger.setLevel(logging.INFO)
# 添加控制台处理器
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger._config_done = True
return logger # ✅ 有Handler,日志会输出
```
### 2.2 修改文件
| 文件 | 修改内容 | 行号 |
|------|----------|------|
| `src/erp_uploader.py` | 在setup_logger()中添加StreamHandler | 第31-56行 |
| `src/erp_uploader.py` | 添加详细接口日志(请求参数、响应内容) | 多处 |
| `src/erp_uploader.py` | 优化日志格式,添加分隔符和状态标识 | 多处 |
### 2.3 具体修改
```python
def setup_logger(name: str = "erp_uploader") -> logging.Logger:
"""
设置日志记录器
Args:
name: 日志记录器名称
Returns:
配置好的日志记录器
"""
logger = logging.getLogger(name)
# 只在第一次调用时设置日志级别和添加Handler
if not hasattr(logger, '_config_done'):
logger.setLevel(logging.INFO)
# 添加控制台处理器
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger._config_done = True
return logger
```
---
## 3. Python logging模块说明
### 3.1 logging组件
| 组件 | 说明 | 作用 |
|------|------|------|
| Logger | 日志记录器 | 提供日志记录接口 |
| Handler | 日志处理器 | 将日志发送到指定目标 |
| Formatter | 日志格式化器 | 设置日志输出格式 |
| Filter | 日志过滤器 | 过滤日志记录 |
### 3.2 Handler类型
| Handler | 输出目标 | 使用场景 |
|---------|----------|----------|
| StreamHandler | 控制台(stdout/stderr) | CLI模式 |
| FileHandler | 文件 | 持久化日志 |
| GUILogHandler | GUI组件 | GUI模式 |
### 3.3 logging工作流程
```
logger.info("消息")
Logger (检查级别)
Handler (检查级别)
Formatter (格式化)
输出目标 (控制台/文件/等)
```
---
## 4. 执行计划
### 4.1 修复步骤
| 步骤 | 操作 | 状态 |
|-----|------|------|
| 1 | 在setup_logger()中添加StreamHandler | ✅ 完成 |
| 2 | 验证修复效果 | ⏳ 待测试 |
### 4.2 验证方法
```bash
cd AuxiliaryTool/FunctionalTestReportGeneration
python run.py --testcase testcases/测试用例.xlsx --buglist testcases/BUG列表.xlsx
```
**测试步骤:**
1. 生成报告
2. 选择上传到ERP(输入Y)
3. 观察控制台日志输出
**预期结果:**
```
==================================================
是否将报告上传到ERP测试单?
==================================================
报告文件: xxx.docx
请输入 Y/N (默认=N): Y
正在上传报告到ERP...
2026-03-28 xx:xx:xx - erp_uploader - INFO - ==================================================
2026-03-28 xx:xx:xx - erp_uploader - INFO - ERP上传流程开始
2026-03-28 xx:xx:xx - erp_uploader - INFO - 报告文件: xxx.docx
2026-03-28 xx:xx:xx - erp_uploader - INFO - ERP配置:
2026-03-28 xx:xx:xx - erp_uploader - INFO - - Base URL: https://office.ubainsyun.com:5082/api/uerp
2026-03-28 xx:xx:xx - erp_uploader - INFO - - 测试单ID: 407
2026-03-28 xx:xx:xx - erp_uploader - INFO - - 报告类型ID: 2
2026-03-28 xx:xx:xx - erp_uploader - INFO - ==================================================
2026-03-28 xx:xx:xx - erp_uploader - INFO - [步骤1/2] Word转HTML转换
2026-03-28 xx:xx:xx - erp_uploader - INFO - === 开始处理Word文档 ===
2026-03-28 xx:xx:xx - erp_uploader - INFO - 文件路径: xxx.docx
2026-03-28 xx:xx:xx - erp_uploader - INFO - 使用mammoth库进行Word到HTML转换...
2026-03-28 xx:xx:xx - erp_uploader - INFO - --- 处理图片0 ---
2026-03-28 xx:xx:xx - erp_uploader - INFO - 图片类型: image/png
2026-03-28 xx:xx:xx - erp_uploader - INFO - 图片大小: 12345 bytes
2026-03-28 xx:xx:xx - erp_uploader - INFO - === 图片0上传请求 ===
2026-03-28 xx:xx:xx - erp_uploader - INFO - 请求URL: https://office.ubainsyun.com:5082/api/uerp/openclaw/upload/richtext
2026-03-28 xx:xx:xx - erp_uploader - INFO - 请求Headers: X-Api-Key=9adc1ce61...
2026-03-28 xx:xx:xx - erp_uploader - INFO - 请求Files: file=image.png (size=12345 bytes)
2026-03-28 xx:xx:xx - erp_uploader - INFO - 响应状态码: 200
2026-03-28 xx:xx:xx - erp_uploader - INFO - 响应内容: {"success":1,"data":{"url":"https://..."}}
2026-03-28 xx:xx:xx - erp_uploader - INFO - ✓ 图片0上传成功: https://...
2026-03-28 xx:xx:xx - erp_uploader - INFO - ✓ Word转HTML完成
2026-03-28 xx:xx:xx - erp_uploader - INFO - - 处理图片数量: 3
2026-03-28 xx:xx:xx - erp_uploader - INFO - - HTML内容长度: 15234 字符
2026-03-28 xx:xx:xx - erp_uploader - INFO - ✓ Word转HTML转换成功
2026-03-28 xx:xx:xx - erp_uploader - INFO - [步骤2/2] 创建ERP测试报告
2026-03-28 xx:xx:xx - erp_uploader - INFO - === 创建测试报告请求 ===
2026-03-28 xx:xx:xx - erp_uploader - INFO - 请求URL: https://office.ubainsyun.com:5082/api/uerp/openclaw/report
2026-03-28 xx:xx:xx - erp_uploader - INFO - 请求Headers: X-Api-Key=9adc1ce61..., Content-Type=application/json
2026-03-28 xx:xx:xx - erp_uploader - INFO - 请求参数:
2026-03-28 xx:xx:xx - erp_uploader - INFO - - developtesting_id: 407
2026-03-28 xx:xx:xx - erp_uploader - INFO - - type_id: 2
2026-03-28 xx:xx:xx - erp_uploader - INFO - - content: 15234 字符 (HTML)
2026-03-28 xx:xx:xx - erp_uploader - INFO - - descript: (空)
2026-03-28 xx:xx:xx - erp_uploader - INFO - - copyuserList: (空)
2026-03-28 xx:xx:xx - erp_uploader - INFO - 响应状态码: 200
2026-03-28 xx:xx:xx - erp_uploader - INFO - 响应内容: {"success":1,"data":{"create":123}}
2026-03-28 xx:xx:xx - erp_uploader - INFO - ✓ 报告创建成功! ERP报告ID: 123
2026-03-28 xx:xx:xx - erp_uploader - INFO - ✓ ERP报告创建成功
2026-03-28 xx:xx:xx - erp_uploader - INFO - ==================================================
2026-03-28 xx:xx:xx - erp_uploader - INFO - ✓✓✓ 报告上传完成! ✓✓✓
2026-03-28 xx:xx:xx - erp_uploader - INFO - ==================================================
[OK] 报告已成功上传到ERP
```
---
## 5. 预防措施
### 5.1 Logger配置规范
1. **Logger必须有Handler**:创建Logger后必须添加至少一个Handler
2. **使用配置检查**:在`setup_logger()`中检查并添加必要的Handler
3. **避免重复添加**:使用属性标记(如`_config_done`)避免重复配置
### 5.2 代码模板
```python
import logging
def setup_logger(name: str = "my_logger") -> logging.Logger:
"""标准的logger配置模板"""
logger = logging.getLogger(name)
if not hasattr(logger, '_config_done'):
logger.setLevel(logging.INFO)
# 添加控制台Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger._config_done = True
return logger
```
---
## 6. 执行结果记录
### 6.1 问题修复
| 日期 | 修改内容 | 文件 | 行号 |
|------|----------|------|------|
| 2026-03-28 | 在setup_logger()中添加StreamHandler | `src/erp_uploader.py` | 第31-49行 |
### 6.2 测试结果
| 测试项 | 结果 | 备注 |
|--------|------|------|
| CLI模式日志输出 | ⏳ 待测试 | 需实际测试验证 |
| 图片上传日志 | ⏳ 待测试 | 需实际测试验证 |
| 报告创建日志 | ⏳ 待测试 | 需实际测试验证 |
---
## 7. 参考文档
- Python logging模块文档: https://docs.python.org/3/library/logging.html
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 相关问题: `GUI模式下上传ERP操作没有日志打印_问题处理_计划执行.md`
---
*文档结束*
...@@ -14,34 +14,15 @@ ...@@ -14,34 +14,15 @@
### 测试报告上传逻辑 ### 测试报告上传逻辑
#### 文件获取 #### 文件获取
- 在执行脚本后会在[AuxiliaryTool/FunctionalTestReportGeneration/reports/]目录下输出一份word文档。 - 在执行脚本后会在[AuxiliaryTool/FunctionalTestReportGeneration/reports/]目录下输出一份word文档。
- 使用Spire.Doc库将文件内容转换为富文本参数,但在接口传参中content不支持base64格式,所以图片需要单独处理。 - 使用mammoth库将文件内容转换为富文本参数,但在接口传参中content不支持base64格式,所以图片需要单独处理。
- 示例代码: - 示例代码:
```ignorelang ```ignorelang
from spire.doc import * import mammoth
from spire.doc.common import *
import requests import requests
import os import os
import tempfile import base64
def extract_images_from_word(doc): def extract_images_and_convert_html(file_path, api_key, base_url):
"""从Word文档中提取所有图片,返回图片字节数组列表"""
images = []
# 遍历文档中的所有节
for section in doc.Sections:
# 遍历节中的所有段落
for paragraph in section.Paragraphs:
# 遍历段落中的所有子对象
for child_obj in paragraph.ChildObjects:
# 判断是否为图片对象
if child_obj.DocumentObjectType == DocumentObjectType.Picture:
picture = child_obj
# 获取图片数据
images.append(picture.Image.ImageData)
return images
def word_to_html_upload_images(file_path, api_key, base_url):
""" """
将Word转HTML,图片单独上传获取URL 将Word转HTML,图片单独上传获取URL
...@@ -54,62 +35,42 @@ from spire.doc import * ...@@ -54,62 +35,42 @@ from spire.doc import *
HTML字符串,图片已替换为ERP URL HTML字符串,图片已替换为ERP URL
""" """
# 1. 加载Word文档 # 自定义图片处理函数:上传图片到ERP并返回URL
document = Document()
document.LoadFromFile(file_path)
# 2. 提取所有图片
image_data_list = extract_images_from_word(document)
# 3. 上传图片获取URL列表
image_urls = [] image_urls = []
temp_dir = tempfile.mkdtemp() image_index = [0] # 使用列表以便在嵌套函数中修改
for idx, img_data in enumerate(image_data_list): def upload_image_to_erp(image):
# 保存临时图片文件 """上传图片到ERP"""
temp_img_path = os.path.join(temp_dir, f"image_{idx}.png") img_bytes = image["bytes"]
with open(temp_img_path, 'wb') as f:
f.write(img_data)
# 上传到ERP # 上传到ERP
with open(temp_img_path, 'rb') as f: files = {'file': ("image.png", img_bytes, "image/png")}
files = {'file': f} headers = {'X-Api-Key': api_key}
headers = {'X-Api-Key': api_key} resp = requests.post(
resp = requests.post( f'{base_url}/openclaw/upload/richtext',
f'{base_url}/openclaw/upload/richtext', headers=headers,
headers=headers, files=files
files=files )
)
if resp.json()['success'] == 1: if resp.json()['success'] == 1:
image_urls.append(resp.json()['data']['url']) url = resp.json()['data']['url']
image_urls.append(url)
return {"src": url}
else: else:
print(f"图片{idx}上传失败: {resp.json().get('msg')}") print(f"图片{image_index[0]}上传失败: {resp.json().get('msg')}")
image_urls.append(None) image_index[0] += 1
return {"src": ""} # 返回空,保留base64占位
# 4. 转换为HTML(不嵌入图片)
temp_html = os.path.join(temp_dir, "temp.html")
document.HtmlExportOptions.ImageEmbedded = False
document.SaveToFile(temp_html, FileFormat.Html)
# 5. 读取HTML并替换图片路径为URL
with open(temp_html, 'r', encoding='utf-8') as f:
html_content = f.read()
# Spire.Doc生成的图片路径格式如: temp_html_files/image1.png
# 需要替换为ERP返回的URL
for idx, url in enumerate(image_urls):
if url:
# 替换本地路径为ERP URL
old_pattern = f'temp_html_files/image{idx+1}.png'
html_content = html_content.replace(old_pattern, url)
# 清理临时文件 # 使用mammoth转换,自定义图片处理
import shutil with open(file_path, "rb") as docx_file:
shutil.rmtree(temp_dir, ignore_errors=True) result = mammoth.convert_to_html(
docx_file,
convert_image=mammoth.images.inline(upload_image_to_erp)
)
html_content = result.value
# 6. 去除水印 # mammoth会将base64图片嵌入HTML,需要替换为ERP URL
html_content = html_content.replace('Evaluation Warning: The document was created with Spire.Doc for Python.', '') # 上传成功的图片已经被替换为URL,失败的会保留空src
return html_content return html_content
``` ```
...@@ -138,9 +99,9 @@ from spire.doc import * ...@@ -138,9 +99,9 @@ from spire.doc import *
| 序号 | 疑问点 | 确认方案 | | 序号 | 疑问点 | 确认方案 |
|:----:|--------|----------| |:----:|--------|----------|
| 1 | 接口端口不一致 | 使用 **5082** 端口 | | 1 | 接口端口不一致 | 使用 **5082** 端口 |
| 2 | Spire.Doc商业库限制 | 使用**免费版本**,去除水印字符串 | | 2 | Word转HTML库选择 | 使用 **mammoth** 库(纯Python,免费开源) |
| 3 | 图片上传失败处理 | **补充日志打印**,保留本地路径 | | 3 | 图片上传失败处理 | **补充日志打印**,保留空src |
| 4 | 图片路径格式验证 | **需验证** Spire.Doc 生成的实际格式 |zh | 4 | 图片路径格式验证 | mammoth直接生成URL,无需替换 |
| 5 | 重试机制细节 | 间隔 **5秒**,最多重试3次 | | 5 | 重试机制细节 | 间隔 **5秒**,最多重试3次 |
| 6 | 默认参数配置化 | **可配置**(预留),暂时默认407/2 | | 6 | 默认参数配置化 | **可配置**(预留),暂时默认407/2 |
| 7 | 交互方式确认 | **控制台交互**(Y/N确认) | | 7 | 交互方式确认 | **控制台交互**(Y/N确认) |
...@@ -148,7 +109,7 @@ from spire.doc import * ...@@ -148,7 +109,7 @@ from spire.doc import *
| 9 | HTML内容清理 | **不需要**额外处理 | | 9 | HTML内容清理 | **不需要**额外处理 |
| 10 | 代码文件位置 | 新建 `src/erp_uploader.py` 模块,在 `cli.py``generate_report_main()` 末尾调用 | | 10 | 代码文件位置 | 新建 `src/erp_uploader.py` 模块,在 `cli.py``generate_report_main()` 末尾调用 |
| 11 | 入口脚本确认 | 入口为 `run.py`,在报告生成完成后(CLI第336-346行)添加上传提示 | | 11 | 入口脚本确认 | 入口为 `run.py`,在报告生成完成后(CLI第336-346行)添加上传提示 |
| 12 | Spire.Doc库安装 | `pip install Spire.Doc`,需添加到 `requirements.txt` | | 12 | mammoth库安装 | `pip install mammoth`,需添加到 `requirements.txt` |
| 13 | GUI模式支持 | **同步增加**上传功能,保持功能一致性 | | 13 | GUI模式支持 | **同步增加**上传功能,保持功能一致性 |
| 14 | 配置文件管理 | API key、base_url等配置项放到 `config.py` 中 | | 14 | 配置文件管理 | API key、base_url等配置项放到 `config.py` 中 |
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
| 2 | `src/config.py` | 修改 | 添加ERP相关配置项 | | 2 | `src/config.py` | 修改 | 添加ERP相关配置项 |
| 3 | `src/cli.py` | 修改 | 报告生成后添加上传提示 | | 3 | `src/cli.py` | 修改 | 报告生成后添加上传提示 |
| 4 | `src/gui.py` | 修改 | GUI模式添加上传按钮 | | 4 | `src/gui.py` | 修改 | GUI模式添加上传按钮 |
| 5 | `requirements.txt` | 修改 | 添加Spire.Doc依赖 | | 5 | `requirements.txt` | 修改 | 添加mammoth依赖 |
--- ---
...@@ -153,23 +153,17 @@ config.py (配置层) ...@@ -153,23 +153,17 @@ config.py (配置层)
``` ```
Word报告文件 (.docx) Word报告文件 (.docx)
Spire.Doc 加载文档 mammoth 加载文档
├──→ 提取图片数据 ├──→ 遍历文档中的图片
│ ↓
│ 保存临时图片
│ ↓ │ ↓
│ 上传到ERP (POST /openclaw/upload/richtext) │ 上传到ERP (POST /openclaw/upload/richtext)
│ ↓ │ ↓
│ 获取图片URL列表 │ 获取图片URL
│ ↓
│ 返回URL给mammoth替换图片src
└──→ 转换为HTML (图片不嵌入) └──→ 转换为HTML (图片已替换为URL)
读取HTML内容
替换图片路径为ERP URL
去除水印
调用创建报告接口 (POST /openclaw/report) 调用创建报告接口 (POST /openclaw/report)
...@@ -198,8 +192,8 @@ ERP_MAX_RETRIES = 3 # 最大重试次数 ...@@ -198,8 +192,8 @@ ERP_MAX_RETRIES = 3 # 最大重试次数
ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口 ERP_UPLOAD_IMAGE_URL = "/openclaw/upload/richtext" # 上传图片接口
ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口 ERP_CREATE_REPORT_URL = "/openclaw/report" # 创建报告接口
# Spire.Doc水印文本 # ERP请求超时时间(秒)
SPIRE_DOC_WATERMARK = "Evaluation Warning: The document was created with Spire.Doc for Python." ERP_REQUEST_TIMEOUT = 30
``` ```
--- ---
...@@ -215,13 +209,6 @@ SPIRE_DOC_WATERMARK = "Evaluation Warning: The document was created with Spire.D ...@@ -215,13 +209,6 @@ SPIRE_DOC_WATERMARK = "Evaluation Warning: The document was created with Spire.D
**导出函数:** **导出函数:**
```python ```python
def extract_images_from_word(document) -> List[bytes]
"""
从Word文档中提取所有图片
:param document: Spire.Doc Document对象
:return: 图片字节数组列表
"""
def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str
""" """
将Word转HTML,图片单独上传获取URL 将Word转HTML,图片单独上传获取URL
...@@ -230,10 +217,10 @@ def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str ...@@ -230,10 +217,10 @@ def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str
:return: HTML字符串,图片已替换为ERP URL :return: HTML字符串,图片已替换为ERP URL
""" """
def upload_image_to_erp(img_data: bytes, idx: int, logger: logging.Logger) -> Optional[str] def upload_image_to_erp(image_bytes: bytes, idx: int, logger: logging.Logger) -> Optional[str]
""" """
上传单张图片到ERP(含重试机制) 上传单张图片到ERP(含重试机制)
:param img_data: 图片字节数据 :param image_bytes: 图片字节数据
:param idx: 图片索引 :param idx: 图片索引
:param logger: 日志记录器 :param logger: 日志记录器
:return: 图片URL,失败返回None :return: 图片URL,失败返回None
...@@ -271,18 +258,27 @@ def setup_logger(name: str = "erp_uploader") -> logging.Logger ...@@ -271,18 +258,27 @@ def setup_logger(name: str = "erp_uploader") -> logging.Logger
**详细设计:** **详细设计:**
#### 4.2.1 extract_images_from_word() #### 4.2.1 word_to_html_with_images() - mammoth转换
```python ```python
def extract_images_from_word(document) -> List[bytes]: def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str:
"""从Word文档中提取所有图片""" """使用mammoth将Word转HTML,图片上传到ERP"""
images = [] image_urls = []
for section in document.Sections:
for paragraph in section.Paragraphs: def upload_image_handler(image):
for child_obj in paragraph.ChildObjects: """mammoth图片处理回调函数"""
if child_obj.DocumentObjectType == DocumentObjectType.Picture: img_bytes = image["bytes"]
picture = child_obj url = upload_image_to_erp(img_bytes, len(image_urls), logger)
images.append(picture.Image.ImageData) if url:
return images image_urls.append(url)
return {"src": url}
return {"src": ""}
with open(file_path, "rb") as docx_file:
result = mammoth.convert_to_html(
docx_file,
convert_image=mammoth.images.inline(upload_image_handler)
)
return result.value
``` ```
#### 4.2.2 upload_image_to_erp() - 含重试机制 #### 4.2.2 upload_image_to_erp() - 含重试机制
...@@ -407,79 +403,53 @@ def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]: ...@@ -407,79 +403,53 @@ def create_report_in_erp(content: str, logger: logging.Logger) -> Optional[int]:
return None return None
``` ```
#### 4.2.4 word_to_html_with_images() #### 4.2.3 word_to_html_with_images() - 完整实现
```python ```python
def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str: def word_to_html_with_images(file_path: str, logger: logging.Logger) -> str:
""" """
将Word转HTML,图片单独上传获取URL 使用mammoth将Word转HTML,图片上传到ERP
""" """
logger.info(f"开始处理Word文档: {file_path}") logger.info(f"开始处理Word文档: {file_path}")
# 1. 加载Word文档 image_urls = []
from spire.doc import Document, FileFormat image_index = [0]
document = Document()
document.LoadFromFile(file_path)
logger.info("Word文档加载成功")
# 2. 提取所有图片 def upload_image_handler(image):
image_data_list = extract_images_from_word(document) """mammoth图片处理回调函数"""
logger.info(f"提取到{len(image_data_list)}张图片") img_bytes = image["bytes"]
idx = image_index[0]
image_index[0] += 1
# 3. 上传图片获取URL列表 url = upload_image_to_erp(img_bytes, idx, logger)
image_urls = []
for idx, img_data in enumerate(image_data_list):
url = upload_image_to_erp(img_data, idx, logger)
image_urls.append(url)
if url is None:
logger.warning(f"图片{idx}上传失败,HTML中将保留本地路径")
# 4. 转换为HTML(不嵌入图片)
temp_dir = tempfile.mkdtemp()
temp_html = os.path.join(temp_dir, "temp.html")
document.HtmlExportOptions.ImageEmbedded = False
document.SaveToFile(temp_html, FileFormat.Html)
logger.info(f"HTML转换完成: {temp_html}")
# 5. 读取HTML并替换图片路径为URL
with open(temp_html, 'r', encoding='utf-8') as f:
html_content = f.read()
# Spire.Doc生成的图片路径格式,需要验证
# 预期格式: temp_html_files/image1.png
html_filename = os.path.basename(temp_html)
image_folder = f"{html_filename}_files/"
for idx, url in enumerate(image_urls):
if url: if url:
# 可能的格式,都需要尝试替换 logger.info(f"图片{idx}上传成功: {url}")
old_patterns = [ image_urls.append(url)
f'{image_folder}image{idx+1}.png', return {"src": url}
f'{image_folder}image{idx+1}.jpg', else:
f'image{idx+1}.png', logger.warning(f"图片{idx}上传失败")
f'image{idx+1}.jpg' return {"src": ""}
]
for pattern in old_patterns: try:
if pattern in html_content: import mammoth
html_content = html_content.replace(pattern, url) with open(file_path, "rb") as docx_file:
logger.info(f"替换图片路径: {pattern} -> {url}") result = mammoth.convert_to_html(
break docx_file,
convert_image=mammoth.images.inline(upload_image_handler)
# 清理临时文件 )
import shutil html_content = result.value
shutil.rmtree(temp_dir, ignore_errors=True)
logger.info(f"Word转HTML完成,处理了{len(image_urls)}张图片")
# 6. 去除水印 return html_content
html_content = html_content.replace(SPIRE_DOC_WATERMARK, '')
logger.info("水印已去除") except Exception as e:
logger.error(f"Word转HTML处理失败: {str(e)}")
return html_content return None
``` ```
**依赖:** **依赖:**
- `spire-doc` - Word文档转HTML - `mammoth` - Word文档转HTML(纯Python)
- `requests` - HTTP请求 - `requests` - HTTP请求
- `logging` - 日志记录 - `logging` - 日志记录
- `tempfile` - 临时文件处理
--- ---
...@@ -706,7 +676,6 @@ class GUILogHandler(logging.Handler): ...@@ -706,7 +676,6 @@ class GUILogHandler(logging.Handler):
- [ ] 每个函数有docstring说明 - [ ] 每个函数有docstring说明
- [ ] 异常处理完善 - [ ] 异常处理完善
- [ ] 日志输出完整 - [ ] 日志输出完整
- [ ] Spire.Doc水印正确去除
--- ---
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论