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

feat(cli): 新增文档处理后PDF转换和网盘上传功能

- 在文档优化和翻译完成后,新增交互式PDF转换询问功能
- 使用win32com调用Microsoft Word COM接口实现PDF转换
- 新增交互式网盘上传功能,支持UNC路径和本地盘符路径
- 实现批量上传多个文件,用户可选择上传特定文件
- 添加文件冲突处理,自动备份同名文件并生成时间戳
- 上传成功后自动清理本地对应文件,失败时保留文件
- 更新版本号从1.3.1到1.4.0,新增网盘上传配置项
- 修复PowerShell中MySQL查询语句的引号转义问题
- 新增PDF转换和网盘上传的需求文档及实现计划文档
上级 09dffb83
......@@ -24,6 +24,8 @@ from src.config import (
)
from src.optimizer import optimize_document
from src.internationalizer import internationalize_document
from src.uploader import upload_to_network, batch_upload
from src.converter import convert_to_pdf
logger = getLogger(__name__)
......@@ -218,6 +220,160 @@ def parse_command_line() -> argparse.Namespace:
return parser.parse_args()
def _prompt_pdf_convert(docx_path: str) -> list:
"""
交互式询问用户是否将文档转换为PDF格式
转换成功后返回所有可用文件路径列表(.docx + .pdf),
转换失败时仅返回原始 .docx 路径
Args:
docx_path: 原始 .docx 文件路径
Returns:
可用文件路径列表
"""
print()
try:
choice = input("是否将文档转换为PDF格式?(Y/N): ").strip()
except (KeyboardInterrupt, EOFError):
print("\n已跳过PDF转换")
return [docx_path]
if choice.upper() not in ('Y', 'YES'):
print("已跳过PDF转换")
return [docx_path]
# 执行PDF转换
print("\n正在转换为PDF...")
try:
pdf_path = convert_to_pdf(docx_path)
print(f"✓ PDF转换成功!")
print(f" PDF文件: {pdf_path}")
# 返回两种格式的文件路径
return [docx_path, pdf_path]
except (FileNotFoundError, RuntimeError) as e:
print(f"✗ PDF转换失败")
# 打印失败原因和解决指引
for line in str(e).split('\n'):
print(f" {line}")
print(" 原始 .docx 文件已保留")
logger.error("PDF转换失败: %s", e)
# 转换失败,仅返回原始 .docx
return [docx_path]
def _prompt_upload(file_paths: list) -> None:
"""
交互式询问用户是否上传文件至网盘
支持单文件和多文件(批量)上传场景,由用户选择目标路径和上传范围
Args:
file_paths: 待上传的本地文件路径列表
"""
if not file_paths:
return
# 询问是否上传
print()
try:
choice = input("是否将文档上传至网盘?(Y/N): ").strip()
except (KeyboardInterrupt, EOFError):
print("\n已跳过上传")
return
if choice.upper() not in ('Y', 'YES'):
print("已跳过上传")
return
# 输入网盘目标路径
try:
target_dir = input("请输入网盘目标路径: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n已取消上传")
return
if not target_dir:
print("未输入路径,已取消上传")
return
# 去除路径两端引号(用户从资源管理器复制路径时可能带引号)
target_dir = target_dir.strip('"').strip("'")
# 单文件上传
if len(file_paths) == 1:
file_path = file_paths[0]
file_name = Path(file_path).name
try:
print(f"\n正在上传: {file_name}")
upload_path = upload_to_network(file_path, target_dir)
print(f"✓ 上传成功!")
print(f" 文件地址: {upload_path}")
# 上传成功后清理本地文件
from src.uploader import _cleanup_local_file
_cleanup_local_file(file_path)
print(" 本地文件已清理")
except (FileNotFoundError, PermissionError, OSError) as e:
print(f"✗ 上传失败: {e}")
print(" 本地文件已保留")
logger.error("网盘上传失败: %s", e)
return
# 多文件(批量)上传 - 用户选择上传范围
print(f"\n待上传文件列表:")
for i, fp in enumerate(file_paths, 1):
print(f" {i}. {Path(fp).name}")
try:
selection = input(
f"\n请选择上传文件(输入序号,多个用逗号分隔,输入 A 全选): "
).strip()
except (KeyboardInterrupt, EOFError):
print("\n已取消上传")
return
# 解析用户选择
selected_files = []
if selection.upper() == 'A':
# 全选
selected_files = file_paths
else:
# 解析序号
try:
indices = [int(idx.strip()) for idx in selection.split(',') if idx.strip()]
for idx in indices:
if 1 <= idx <= len(file_paths):
selected_files.append(file_paths[idx - 1])
else:
print(f" 警告: 序号 {idx} 超出范围,已跳过")
except ValueError:
print("输入格式无效,已取消上传")
return
if not selected_files:
print("未选择任何文件,已取消上传")
return
# 执行批量上传
print(f"\n正在批量上传 {len(selected_files)} 个文件...")
results = batch_upload(selected_files, target_dir)
# 打印汇总结果
print(f"\n批量上传完成!(成功: {len(results['success'])}/{len(selected_files)})")
if results['failed']:
print(f"失败 {len(results['failed'])} 个文件,本地文件已保留")
def run_cli(args: argparse.Namespace) -> int:
"""
运行命令行程序
......@@ -286,6 +442,12 @@ def run_cli(args: argparse.Namespace) -> int:
print(f" 输出文档: {result_path}")
print("=" * 60)
# 询问是否转换为PDF格式
available_files = _prompt_pdf_convert(result_path)
# 询问是否上传至网盘
_prompt_upload(available_files)
return 0
except FileNotFoundError as e:
......@@ -314,6 +476,12 @@ def run_cli(args: argparse.Namespace) -> int:
print(f" 输出文档: {result_path}")
print("=" * 60)
# 询问是否转换为PDF格式
available_files = _prompt_pdf_convert(result_path)
# 询问是否上传至网盘
_prompt_upload(available_files)
return 0
except FileNotFoundError as e:
......
......@@ -154,10 +154,16 @@ MAX_RETRY = 3
# 翻译超时时间(秒)
TRANSLATE_TIMEOUT = 10
# ================================
# 网盘上传配置
# ================================
# 备份文件命名中的时间戳格式
BACKUP_TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
# ================================
# 版本信息
# ================================
__version__ = "1.3.1"
__version__ = "1.4.0"
__author__ = "Document Optimizer"
__description__ = "文档自动优化校准工具"
......
# -*- coding: utf-8 -*-
"""
文档自动优化校准工具 - PDF转换模块
本模块提供将Word文档(.docx)转换为PDF格式的功能,
通过 win32com 调用 Microsoft Word COM 接口执行转换。
"""
import os
from pathlib import Path
from logging import getLogger
logger = getLogger(__name__)
def _check_word_available() -> tuple:
"""
检测本机是否具备PDF转换条件
检测 pywin32 库是否已安装,以及 Microsoft Word COM 接口是否可调用
Returns:
元组 (是否可用: bool, 错误信息: str)
可用时返回 (True, ""),不可用时返回 (False, 错误原因)
"""
# 检测 pywin32 库
try:
import win32com.client
except ImportError:
error_msg = "未安装 pywin32 库"
logger.error("环境检测失败: %s", error_msg)
return False, error_msg
# 检测 Word COM 接口是否可调用
try:
word = win32com.client.Dispatch("Word.Application")
word.Quit()
logger.info("环境检测通过: Microsoft Word 可用")
return True, ""
except Exception as e:
error_msg = f"Microsoft Word COM 接口调用失败: {e}"
logger.error("环境检测失败: %s", error_msg)
return False, error_msg
def convert_to_pdf(docx_path: str) -> str:
"""
将Word文档转换为PDF格式
使用 win32com 调用 Microsoft Word COM 接口,通过 ExportAsFixedFormat 导出 PDF。
PDF 文件与源 .docx 文件保存在同一目录,文件名相同仅扩展名变更。
Args:
docx_path: Word文档路径(.docx文件)
Returns:
转换后的 PDF 文件绝对路径
Raises:
FileNotFoundError: 源文件不存在
RuntimeError: 转换环境不满足或转换过程失败
"""
source = Path(docx_path)
# 验证源文件存在
if not source.exists():
raise FileNotFoundError(f"源文件不存在: {docx_path}")
# 验证文件格式
if source.suffix.lower() != '.docx':
raise RuntimeError(f"仅支持 .docx 格式文件,当前文件: {source.suffix}")
# 构建 PDF 输出路径(与源文件同目录,仅扩展名变更)
pdf_path = str(source.with_suffix('.pdf'))
logger.info("开始PDF转换: %s -> %s", source.name, Path(pdf_path).name)
# 执行转换
word = None
doc = None
try:
import win32com.client
# 启动 Word 应用
word = win32com.client.Dispatch("Word.Application")
word.Visible = False # 不显示 Word 窗口
word.DisplayAlerts = 0 # 禁用警告弹窗
# 打开文档(需使用绝对路径)
abs_docx = os.path.abspath(str(source))
doc = word.Documents.Open(abs_docx)
# 导出为 PDF(wdExportFormatPDF = 17)
abs_pdf = os.path.abspath(pdf_path)
doc.ExportAsFixedFormat(
OutputFileName=abs_pdf,
ExportFormat=17, # wdExportFormatPDF
OpenAfterExport=False, # 导出后不打开
OptimizeFor=0, # wdExportOptimizeForPrint(打印质量)
CreateBookmarks=1 # wdExportCreateHeadingBookmarks(根据标题创建书签)
)
logger.info("PDF转换成功: %s", pdf_path)
return pdf_path
except ImportError:
raise RuntimeError(
"PDF转换失败:未安装 pywin32 库\n"
" 解决方法:\n"
" 1. 执行 pip install pywin32 安装依赖\n"
" 2. 确认已安装 Microsoft Word"
)
except Exception as e:
error_detail = str(e)
# 根据错误类型生成解决指引
guidance = _get_error_guidance(error_detail)
raise RuntimeError(
f"PDF转换失败:{error_detail}\n"
f" 解决方法:\n"
f"{guidance}"
)
finally:
# 确保 Word 文档和应用始终被关闭
if doc is not None:
try:
doc.Close(False) # 不保存更改
except Exception:
pass
if word is not None:
try:
word.Quit()
except Exception:
pass
def _get_error_guidance(error_detail: str) -> str:
"""
根据错误信息生成解决指引
Args:
error_detail: 异常的错误信息
Returns:
格式化的解决指引字符串
"""
error_lower = error_detail.lower()
if "could not find" in error_lower or "找不到" in error_lower:
return (
" 1. 确认已安装 Microsoft Word\n"
" 2. 尝试修复 Office 安装\n"
" 3. 确认 Word 可以正常手动打开 .docx 文件"
)
elif "being used by another process" in error_lower or "正在使用" in error_lower:
return (
" 1. 关闭其他正在使用该文件的程序(如 Word、WPS)\n"
" 2. 等待片刻后重试\n"
" 3. 重启计算机后重试"
)
elif "permission" in error_lower or "权限" in error_lower:
return (
" 1. 以管理员身份运行本程序\n"
" 2. 检查输出目录的写入权限\n"
" 3. 确认文件未被设为只读"
)
else:
return (
" 1. 确认已安装 Microsoft Word\n"
" 2. 执行 pip install pywin32 安装依赖\n"
" 3. 确认文件未被其他程序占用\n"
" 4. 尝试手动用 Word 打开该文件并另存为 PDF"
)
# -*- coding: utf-8 -*-
"""
文档自动优化校准工具 - 网盘上传模块
本模块提供将处理后的文档上传至网盘(SMB共享目录)的功能,
支持 UNC 路径和本地盘符路径,自动备份冲突文件。
"""
import os
import shutil
from datetime import datetime
from pathlib import Path
from logging import getLogger
from src.config import BACKUP_TIMESTAMP_FORMAT
logger = getLogger(__name__)
def _validate_network_path(path: str) -> bool:
"""
验证网盘路径是否可访问
兼容 UNC 路径(\\\\192.168.9.9\\共享目录)和本地盘符路径(Z:\\目录)
Args:
path: 网盘目标路径
Returns:
True 表示路径可访问,False 表示不可访问
"""
target = Path(path)
try:
# 尝试访问目标路径,检查是否存在且可写
if target.exists():
# 路径已存在,检查写权限
if os.access(str(target), os.W_OK):
logger.info("网盘路径验证通过: %s", path)
return True
else:
logger.error("网盘路径无写入权限: %s", path)
return False
else:
# 路径不存在,尝试创建(验证父目录权限)
try:
target.mkdir(parents=True, exist_ok=True)
logger.info("网盘目录已创建: %s", path)
return True
except PermissionError:
logger.error("网盘路径无创建权限: %s", path)
return False
except (OSError, PermissionError) as e:
logger.error("网盘路径验证失败: %s, 错误: %s", path, e)
return False
def _backup_existing_file(file_path: Path) -> bool:
"""
备份网盘上的同名文件
备份命名规则:原文件名_备份_时间戳.扩展名
Args:
file_path: 网盘上已存在的文件路径
Returns:
True 表示备份成功或无需备份,False 表示备份失败
"""
if not file_path.exists():
# 文件不存在,无需备份
return True
try:
timestamp = datetime.now().strftime(BACKUP_TIMESTAMP_FORMAT)
backup_name = f"{file_path.stem}_备份_{timestamp}{file_path.suffix}"
backup_path = file_path.parent / backup_name
shutil.copy2(str(file_path), str(backup_path))
logger.info("已备份网盘文件: %s -> %s", file_path.name, backup_name)
print(f" 已备份原有文件: {backup_name}")
return True
except (OSError, PermissionError) as e:
logger.error("备份文件失败: %s, 错误: %s", file_path, e)
return False
def upload_to_network(file_path: str, target_dir: str) -> str:
"""
上传文件至网盘
将本地文件复制到网盘目标目录,自动备份同名冲突文件
Args:
file_path: 本地文件路径
target_dir: 网盘目标目录路径
Returns:
上传后的完整文件路径
Raises:
FileNotFoundError: 源文件不存在
PermissionError: 目标目录无写入权限
OSError: 网络不可达或其他IO错误
"""
source = Path(file_path)
# 验证源文件存在
if not source.exists():
raise FileNotFoundError(f"源文件不存在: {file_path}")
logger.info("开始上传文件: %s -> %s", source.name, target_dir)
# 验证目标目录可访问
if not _validate_network_path(target_dir):
raise PermissionError(f"目标目录不可访问或无写入权限: {target_dir}")
# 构建目标文件路径
target = Path(target_dir) / source.name
# 备份同名文件(如有冲突)
if target.exists():
if not _backup_existing_file(target):
raise OSError(f"备份网盘同名文件失败,终止上传: {target}")
# 执行文件复制
try:
shutil.copy2(str(source), str(target))
logger.info("文件上传成功: %s", target)
return str(target)
except (OSError, PermissionError) as e:
logger.error("文件上传失败: %s -> %s, 错误: %s", source, target, e)
raise
def _cleanup_local_file(file_path: str) -> bool:
"""
清理本地已上传的文件
仅在上传成功后调用,删除本地 reports/ 目录中对应的文件
Args:
file_path: 本地文件路径
Returns:
True 表示清理成功,False 表示清理失败
"""
local_file = Path(file_path)
try:
if local_file.exists():
local_file.unlink()
logger.info("本地文件已清理: %s", file_path)
return True
else:
logger.warning("本地文件不存在,跳过清理: %s", file_path)
return True
except OSError as e:
logger.error("清理本地文件失败: %s, 错误: %s", file_path, e)
return False
def batch_upload(file_paths: list, target_dir: str) -> dict:
"""
批量上传多个文件至网盘
逐个上传文件列表中的文件,汇总返回每个文件的上传结果
Args:
file_paths: 本地文件路径列表
target_dir: 网盘目标目录路径
Returns:
上传结果字典: {
"success": [成功上传的文件路径列表],
"failed": [(文件路径, 错误信息) 元组列表],
"upload_paths": [网盘上的完整路径列表]
}
"""
results = {
"success": [],
"failed": [],
"upload_paths": []
}
logger.info("开始批量上传,共 %d 个文件", len(file_paths))
for i, file_path in enumerate(file_paths, 1):
file_name = Path(file_path).name
print(f"\n [{i}/{len(file_paths)}] 正在上传: {file_name}")
try:
upload_path = upload_to_network(file_path, target_dir)
results["success"].append(file_path)
results["upload_paths"].append(upload_path)
# 上传成功后清理本地文件
_cleanup_local_file(file_path)
print(f" ✓ {file_name} 上传成功")
print(f" 地址: {upload_path}")
except (FileNotFoundError, PermissionError, OSError) as e:
results["failed"].append((file_path, str(e)))
print(f" ✗ {file_name} 上传失败: {e}")
logger.error("批量上传中文件失败: %s, 错误: %s", file_path, e)
logger.info("批量上传完成: 成功 %d, 失败 %d",
len(results["success"]), len(results["failed"]))
return results
......@@ -1259,13 +1259,15 @@ function Test-MySQLDeepCheck {
$actualContainer = (@($checkResult.Output)[0].ToString().Trim() -replace "`r","")
Write-Log -Level "INFO" -Message "[MySQL深度] 容器: $actualContainer"
# 辅助:通过 docker exec mysql -e 方式执行 SQL 查询(与已有 MySQL 连接检测一致)
# 密码用双引号保护特殊字符(如 & ),使用 -e 直接传递 SQL 语句避免管道引号问题
# 辅助:通过 docker exec mysql -e 方式执行 SQL 查询
# 密码用双引号保护特殊字符(如 & ),SQL 用单引号包裹(避免 plink 传参时引号冲突)
# SQL 内部字符串用双引号(MySQL 同时支持单引号和双引号作为字符串界定符)
# PowerShell 中: `" 产生 " , "" 产生 " , ' 是字面量
$mysqlExec = "docker exec $actualContainer mysql -uroot -p`"$mysqlPassword`" -N"
# --- 1. 缓冲池命中率 ---
try {
$bpCmd = "$mysqlExec -e `"SHOW STATUS LIKE 'Innodb_buffer_pool_read%'`""
$bpCmd = "$mysqlExec -e 'SHOW STATUS LIKE ""Innodb_buffer_pool_read%""'"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: $bpCmd"
$bpResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $bpCmd
......@@ -1300,12 +1302,12 @@ function Test-MySQLDeepCheck {
# --- 2. 慢查询状态 ---
try {
$slowCmd = "$mysqlExec -e `"SHOW VARIABLES LIKE 'slow_query_log'`""
$slowCmd = "$mysqlExec -e 'SHOW VARIABLES LIKE ""slow_query_log""'"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: $slowCmd"
$slowResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $slowCmd
# 同时获取慢查询数量
$slowCountCmd = "$mysqlExec -e `"SHOW STATUS LIKE 'Slow_queries'`""
$slowCountCmd = "$mysqlExec -e 'SHOW STATUS LIKE ""Slow_queries""'"
$slowCountResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $slowCountCmd
if ($slowResult.ExitCode -eq 0 -and $slowResult.Output) {
......@@ -1337,11 +1339,11 @@ function Test-MySQLDeepCheck {
# --- 3. 连接使用率 ---
try {
$connCmd = "$mysqlExec -e `"SHOW STATUS LIKE 'Threads_connected'`""
$connCmd = "$mysqlExec -e 'SHOW STATUS LIKE ""Threads_connected""'"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: $connCmd"
$connResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $connCmd
$maxConnCmd = "$mysqlExec -e `"SHOW VARIABLES LIKE 'max_connections'`""
$maxConnCmd = "$mysqlExec -e 'SHOW VARIABLES LIKE ""max_connections""'"
$maxConnResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $maxConnCmd
if ($connResult.ExitCode -eq 0 -and $connResult.Output) {
......@@ -1379,7 +1381,7 @@ function Test-MySQLDeepCheck {
# --- 4. 主从复制状态 ---
try {
# 使用 -E 参数获取竖向输出格式(等效于 \G)
$replCmd = "$mysqlExec -E -e `"SHOW SLAVE STATUS`""
$replCmd = "$mysqlExec -E -e 'SHOW SLAVE STATUS'"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: $replCmd"
$replResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $replCmd
......@@ -1422,7 +1424,8 @@ function Test-MySQLDeepCheck {
# --- 5. TOP20 大表 ---
try {
$topTableCmd = "$mysqlExec -e `"SELECT table_name,ROUND(data_length/1024/1024,2) AS data_mb,ROUND(index_length/1024/1024,2) AS idx_mb,table_rows FROM information_schema.TABLES WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys') ORDER BY data_length DESC LIMIT 20`""
# SQL 内部字符串用双引号(MySQL 支持),外层用单引号包裹避免 plink 引号冲突
$topTableCmd = "$mysqlExec -e 'SELECT table_name,ROUND(data_length/1024/1024,2) AS data_mb,ROUND(index_length/1024/1024,2) AS idx_mb,table_rows FROM information_schema.TABLES WHERE table_schema NOT IN (""mysql"",""information_schema"",""performance_schema"",""sys"") ORDER BY data_length DESC LIMIT 20'"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: $topTableCmd"
$topTableResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $topTableCmd
......@@ -1448,11 +1451,11 @@ function Test-MySQLDeepCheck {
# --- 6. QPS/TPS 性能基线 ---
try {
$qpsCmd = "$mysqlExec -e `"SHOW STATUS LIKE 'Queries'`""
$qpsCmd = "$mysqlExec -e 'SHOW STATUS LIKE ""Queries""'"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: $qpsCmd"
$qpsResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $qpsCmd
$uptimeCmd = "$mysqlExec -e `"SHOW STATUS LIKE 'Uptime'`""
$uptimeCmd = "$mysqlExec -e 'SHOW STATUS LIKE ""Uptime""'"
$uptimeResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $uptimeCmd
if ($qpsResult.ExitCode -eq 0 -and $qpsResult.Output) {
......
# 新增PDF格式转换功能
## 代码路径
- 代码路径:[AuxiliaryTool/DocumentAutoOptimizationCalibration]
## 功能需求
### 功能目标
**目标:** 新增将word文档转换为PDF格式文档功能。
### 需求描述
#### 文档路径获取
- 当文档校正或国际化翻译完成后,询问用户是否需要将生成后的文档转换为PDF格式。
#### 功能实现方式
- 通过将新生成的文档转换为PDF格式,转换完成后询问用户是否需要上传至网盘。
- 使用 `win32com` 调用 Microsoft Word COM 接口执行转换(格式还原度最高,项目中已有 `win32com` 使用先例)。
- **前提条件**:运行环境需安装 Microsoft Word。
#### 用户交互方式
- 文档处理完成后,通过**交互式输入**询问用户是否需要转换为PDF格式。
- 转换完成后询问上传时,**由用户选择上传哪个格式的文件**
- 仅上传 PDF
- 仅上传 .docx
- 两者都上传
#### PDF 输出路径
- 转换后的 PDF 文件与原始 .docx 文件保存在同一目录(`reports/`)。
- 命名规则:与 .docx 同名,仅扩展名变更(如 `文档_优化版.docx``文档_优化版.pdf`)。
#### 本地文件清理
- 上传成功后,**自动清理本地对应的文件**(.docx 和 PDF 均会清理)。
- 复用已有的 `_cleanup_local_file` 清理函数。
#### 转换失败处理
- 若 PDF 转换失败,**保留原始 .docx 文件不删除**
- 在终端打印**失败原因**
- 提供解决指引(如:请确认已安装 Microsoft Word、检查文件是否被其他程序占用等)。
## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
# 新增PDF格式转换功能_实现计划
## 1. 背景与目标
### 背景
文档自动优化校准工具已支持文档格式优化、国际化翻译、网盘上传功能。用户经常需要将处理后的 Word 文档转为 PDF 格式分发,目前需要手动转换。
### 目标
在文档处理完成后,新增交互式 PDF 转换功能,通过 `win32com` 调用 Microsoft Word COM 接口将 .docx 转换为 PDF,并支持转换后直接上传至网盘。
---
## 2. 项目结构(变更部分)
```
AuxiliaryTool/DocumentAutoOptimizationCalibration/
├── src/
│ ├── config.py # 配置模块(不变)
│ ├── converter.py # ★ 新增:PDF转换模块
│ ├── uploader.py # 网盘上传模块(不变)
│ ├── cli.py # 命令行接口(集成转换交互逻辑)
│ ├── optimizer.py # 文档优化核心模块(不变)
│ ├── internationalizer.py # 文档国际化模块(不变)
│ └── ...
└── ...
```
---
## 3. 功能模块设计
### 3.1 新增模块:PDF 转换 (src/converter.py)
核心功能函数:
| 函数名 | 功能描述 |
|--------|---------|
| `convert_to_pdf(docx_path)` | 主函数:将 .docx 转换为 PDF |
| `_check_word_available()` | 检测本机是否安装 Word 及 pywin32 |
### 3.2 修改模块:命令行接口 (src/cli.py)
调整处理完成后的交互流程为:
```
文档优化/翻译完成
→ 询问是否转换为PDF (Y/N)
→ Y: 执行转换,获得 PDF 文件
→ N: 跳过转换
→ 询问是否上传至网盘 (Y/N)
→ 展示可用文件列表(.docx / .pdf / 两者)
→ 用户选择上传哪些文件
```
---
## 4. 详细实现步骤
### 4.1 实现 PDF 转换模块 (src/converter.py)
- [ ] 实现 `_check_word_available()` 环境检测函数
- [ ] 检测 pywin32 库是否已安装
- [ ] 检测 Microsoft Word COM 接口是否可调用
- [ ] 实现 `convert_to_pdf(docx_path)` 转换主函数
- [ ] 验证源文件存在且为 .docx 格式
- [ ] 使用 win32com 调用 Word COM 接口打开文档
- [ ] 调用 `ExportAsFixedFormat` 导出为 PDF
- [ ] 关闭文档并退出 Word 进程
- [ ] 返回 PDF 文件路径
- [ ] 异常处理:Word 未安装、COM 调用失败、文件占用
- [ ] 转换失败时打印原因和解决指引
### 4.2 集成转换功能到 CLI (src/cli.py)
- [ ] 封装 `_prompt_pdf_convert(docx_path)` 交互函数
- [ ] 询问用户是否转换为 PDF (Y/N)
- [ ] 调用 converter 模块执行转换
- [ ] 转换失败时打印原因和解决指引,保留 .docx
- [ ] 修改 `_prompt_upload(file_paths)` 支持多格式文件选择
- [ ] 展示可用文件列表(含 .docx 和 .pdf)
- [ ] 用户选择上传哪些文件
- [ ] 调整 `run_cli()` 流程顺序
- [ ] 优化模式:完成 → 询问转PDF → 询问上传
- [ ] 翻译模式:完成 → 询问转PDF → 询问上传
---
## 5. 关键技术要点
### 5.1 win32com PDF 转换
```python
import win32com.client
import os
word = win32com.client.Dispatch("Word.Application")
word.Visible = False
doc = word.Documents.Open(os.path.abspath(docx_path))
# wdExportFormatPDF = 17
doc.ExportAsFixedFormat(
OutputPDFPath,
ExportFormat=17,
OpenAfterExport=False,
OptimizeFor=0,
CreateBookmarks=1
)
doc.Close()
word.Quit()
```
### 5.2 环境检测与错误指引
```python
try:
import win32com.client
except ImportError:
print("转换失败:未安装 pywin32 库")
print("解决方法:执行 pip install pywin32")
```
### 5.3 COM 进程安全退出
使用 try/finally 确保 Word 进程始终被关闭,避免残留进程。
---
## 6. 交互流程设计
### 6.1 优化模式完整流程
```
文档优化完成!
============================
输入文档: xxx.docx
输出文档: reports/xxx_优化版.docx
============================
是否将文档转换为PDF格式?(Y/N): Y
正在转换为PDF...
✓ PDF转换成功!
PDF文件: reports/xxx_优化版.pdf
是否将文档上传至网盘?(Y/N): Y
请输入网盘目标路径: \\192.168.9.9\发布版本\03服务器部署
可上传文件:
1. xxx_优化版.docx
2. xxx_优化版.pdf
请选择上传文件(输入序号,多个用逗号分隔,输入 A 全选): 2
正在上传: xxx_优化版.pdf
✓ 上传成功!
文件地址: \\192.168.9.9\...\xxx_优化版.pdf
本地文件已清理
```
### 6.2 转换失败降级流程
```
文档优化完成!
============================
输出文档: reports/xxx_优化版.docx
============================
是否将文档转换为PDF格式?(Y/N): Y
正在转换为PDF...
✗ PDF转换失败:未安装 pywin32 库
解决方法:
1. 执行 pip install pywin32 安装依赖
2. 确认已安装 Microsoft Word
3. 确认文件未被其他程序占用
原始 .docx 文件已保留
是否将文档上传至网盘?(Y/N): ...
```
---
## 7. 验证测试
### 测试步骤
1. 运行优化模式,完成后选择转 PDF,再选择上传
2. 运行翻译模式,完成后选择不转 PDF,直接上传 .docx
3. 模拟 Word 未安装环境,验证失败提示和解决指引
4. 模拟文件被占用场景,验证失败提示
5. 选择同时上传 .docx 和 PDF,验证批量上传
### 验收标准
- [ ] PDF 转换成功,格式还原度与 Word 另存为 PDF 一致
- [ ] 转换失败时打印原因和解决指引,保留 .docx
- [ ] 用户可选择上传 PDF / .docx / 两者
- [ ] 上传成功后 .docx 和 PDF 均自动清理
- [ ] Word 进程无残留
---
## 8. 参考文件
| 文件路径 | 用途 |
|---------|------|
| `src/internationalizer.py` | win32com 使用参考(_update_fields_via_win32com) |
| `src/uploader.py` | 上传和清理函数复用 |
| `src/cli.py` | 现有 CLI 交互逻辑 |
---
## 9. 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| v1.5.0 | 2026-06-07 | 新增 PDF 转换功能 |
---
## 10. 实现状态
### 已完成功能
- [x] PDF 转换核心模块 (converter.py)
- [x] CLI 交互集成 (cli.py)
### 待验证功能
- [ ] 功能测试验证(需在安装 Word 的环境中测试)
# 新增上传网盘操作
## 代码路径
- 代码路径:[AuxiliaryTool/DocumentAutoOptimizationCalibration]
## 功能需求
### 功能目标
**目标:** 新增上传网盘的功能,减少人工上传。
### 需求描述
#### 文档路径获取
- 当文档校正或国际化翻译完成后,询问用户是否需要将生成后的文档上传至网盘。
#### 功能实现方式
- 通过将新生成的文档上传至网盘,并返回上传的网盘链接。
- 网盘路径如:`\\192.168.9.9\发布版本\03服务器部署\15新统一平台`,实际路径由用户自行填写。
- **不依赖网络驱动器映射**,直接通过 UNC 路径(`\\IP\共享目录\...`)进行文件操作。
- 需同时兼容两种路径格式:
- UNC 路径:`\\192.168.9.9\发布版本\03服务器部署\15新统一平台`
- 已映射的本地盘符:`Z:\发布版本\03服务器部署\15新统一平台`
- **前提条件**:当前 Windows 用户具有该共享目录的访问权限(域账号或本地账号授权)。
#### 用户交互方式
- 文档处理完成后,通过**交互式输入**询问用户是否需要上传至网盘。
- 用户确认上传后,**每次手动输入**网盘目标路径。
- 支持翻译模式下的**批量上传**:当生成多个语言版本文档时,由用户选择上传哪些文件(可全部上传或部分上传)。
#### 文件冲突处理
- 上传前检测目标路径是否已存在同名文件。
- 若存在同名文件,**先备份原有文件**(备份命名规则:`原文件名_备份_时间戳.docx`),再执行上传操作。
#### 上传结果展示
- 上传成功后,在终端打印文件上传的完整地址(**网盘路径 + 文件名称**)。
- 示例输出:`\\192.168.9.9\发布版本\03服务器部署\15新统一平台\文档名_优化版.docx`
#### 本地文件清理
- 上传成功后,**自动清理本地 `reports/` 目录中对应的上传文件**
- 若上传失败,保留本地文件不进行清理。
## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
# 新增上传网盘操作_实现计划
## 1. 背景与目标
### 背景
文档自动优化校准工具已完成文档格式优化和国际化翻译功能,但处理后的文档仅保存在本地 `reports/` 目录,需人工手动复制到公司网盘(SMB 共享目录),效率较低。
### 目标
在文档处理完成后,新增交互式网盘上传功能,支持将生成的文档直接上传至 SMB 网络共享目录,减少人工操作。
---
## 2. 项目结构(变更部分)
```
AuxiliaryTool/DocumentAutoOptimizationCalibration/
├── src/
│ ├── config.py # 配置模块(新增上传相关配置)
│ ├── uploader.py # ★ 新增:网盘上传模块
│ ├── cli.py # 命令行接口(集成上传交互逻辑)
│ ├── optimizer.py # 文档优化核心模块(不变)
│ ├── internationalizer.py # 文档国际化模块(不变)
│ └── ...
└── ...
```
---
## 3. 功能模块设计
### 3.1 新增模块:网盘上传 (src/uploader.py)
核心功能函数:
| 函数名 | 功能描述 |
|--------|---------|
| `upload_to_network(file_path, target_dir)` | 主函数:上传文件至网盘 |
| `_validate_network_path(path)` | 验证网盘路径是否可访问 |
| `_backup_existing_file(file_path)` | 备份网盘上的同名文件 |
| `_cleanup_local_file(file_path)` | 清理本地已上传的文件 |
| `batch_upload(file_paths, target_dir)` | 批量上传多个文件 |
### 3.2 修改模块:配置模块 (src/config.py)
新增配置项:
```python
# ================================
# 网盘上传配置
# ================================
# 备份文件命名中的时间戳格式
BACKUP_TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
```
### 3.3 修改模块:命令行接口 (src/cli.py)
`run_cli()` 函数中,优化模式和翻译模式完成后的位置,新增交互式上传询问逻辑。
---
## 4. 详细实现步骤
### 4.1 新增网盘上传配置 (src/config.py)
- [x] 添加 `BACKUP_TIMESTAMP_FORMAT` 时间戳格式常量
### 4.2 实现网盘上传模块 (src/uploader.py)
- [x] 实现 `_validate_network_path(path)` 路径验证函数
- [x] 兼容 UNC 路径(`\\192.168.9.9\...`)和本地盘符(`Z:\...`
- [x] 检测路径是否可访问(读写权限)
- [x] 实现 `_backup_existing_file(file_path)` 备份函数
- [x] 检测目标路径是否存在同名文件
- [x] 备份命名规则:`原文件名_备份_时间戳.docx`
- [x] 备份到同一目录下
- [x] 实现 `upload_to_network(file_path, target_dir)` 上传主函数
- [x] 验证源文件存在
- [x] 验证目标目录可访问
- [x] 备份同名文件(如有冲突)
- [x] 使用 `shutil.copy2` 复制文件(保留元数据)
- [x] 返回上传后的完整路径
- [x] 异常处理:网络不可达、权限不足、磁盘空间不足
- [x] 实现 `_cleanup_local_file(file_path)` 本地清理函数
- [x] 删除本地 `reports/` 中已成功上传的文件
- [x] 日志记录清理操作
- [x] 实现 `batch_upload(file_paths, target_dir)` 批量上传函数
- [x] 遍历文件列表逐个上传
- [x] 返回每个文件的上传结果(成功/失败)
- [x] 汇总打印上传结果
### 4.3 集成上传功能到 CLI (src/cli.py)
- [x] 在优化模式完成后,新增交互式询问
- [x] 询问用户是否上传至网盘(Y/N)
- [x] 用户输入网盘目标路径
- [x] 调用上传函数
- [x] 上传成功后清理本地文件
- [x] 终端打印上传后的完整地址
- [x] 在翻译模式完成后,新增交互式询问
- [x] 询问用户是否上传至网盘(Y/N)
- [x] 用户输入网盘目标路径
- [x] 展示待上传的文件列表(可能多个语言版本)
- [x] 由用户选择上传哪些文件(全部/部分)
- [x] 调用批量上传函数
- [x] 上传成功后清理已上传的本地文件
- [x] 终端打印每个文件的上传地址
- [x] 封装公共交互函数 `_prompt_upload(file_paths)` 避免重复代码
---
## 5. 关键技术要点
### 5.1 UNC 路径文件操作
Windows 下 Python 原生支持 UNC 路径,无需映射网络驱动器:
```python
import shutil
# 直接使用 UNC 路径复制文件
shutil.copy2(src_file, r"\\192.168.9.9\发布版本\目标目录\文件.docx")
```
### 5.2 路径兼容处理
同时兼容 UNC 路径和本地盘符路径:
```python
from pathlib import Path, PureWindowsPath
# Path 对象可直接处理两种格式
target = Path(r"\\192.168.9.9\共享目录") # UNC 路径
target = Path(r"Z:\共享目录") # 盘符路径
```
### 5.3 备份文件命名
```python
from datetime import datetime
timestamp = datetime.now().strftime(BACKUP_TIMESTAMP_FORMAT)
backup_name = f"{stem}_备份_{timestamp}{suffix}"
# 示例:文档_优化版_备份_20260607_143025.docx
```
### 5.4 上传后本地清理
```python
# 仅在上传成功后清理,失败则保留
try:
upload_to_network(file_path, target_dir)
os.remove(file_path)
logger.info("本地文件已清理: %s", file_path)
except Exception as e:
logger.warning("上传失败,保留本地文件: %s", file_path)
```
---
## 6. 交互流程设计
### 6.1 优化模式上传流程
```
文档优化完成!
============================
输入文档: xxx.docx
输出文档: reports/xxx_优化版.docx
============================
是否将文档上传至网盘?(Y/N): Y
请输入网盘目标路径: \\192.168.9.9\发布版本\03服务器部署\15新统一平台
正在上传...
✓ 上传成功!
文件地址: \\192.168.9.9\发布版本\03服务器部署\15新统一平台\xxx_优化版.docx
本地文件已清理
```
### 6.2 翻译模式批量上传流程
```
文档翻译完成!
============================
输入文档: xxx.docx
输出文档: reports/xxx_英文版.docx
============================
是否将文档上传至网盘?(Y/N): Y
请输入网盘目标路径: \\192.168.9.9\发布版本\03服务器部署\15新统一平台
待上传文件列表:
1. xxx_英文版.docx
2. xxx_韩文版.docx
3. xxx_法文版.docx
请选择上传文件(输入序号,多个用逗号分隔,输入 A 全选): A
正在批量上传...
✓ xxx_英文版.docx 上传成功
地址: \\192.168.9.9\...\xxx_英文版.docx
✓ xxx_韩文版.docx 上传成功
地址: \\192.168.9.9\...\xxx_韩文版.docx
✓ xxx_法文版.docx 上传成功
地址: \\192.168.9.9\...\xxx_法文版.docx
全部上传完成!(3/3)
已清理本地文件
```
---
## 7. 验证测试
### 测试步骤
1. 运行优化模式:`python run.py --input 文档.docx`,完成后选择上传
2. 运行翻译模式:`python run.py --translate --input 文档.docx`,完成后选择批量上传
3. 测试网盘路径不可达时的错误提示
4. 测试同名文件冲突时的备份行为
5. 测试上传失败时本地文件是否保留
### 验收标准
- [ ] 上传成功后终端打印完整网盘地址
- [ ] 同名文件冲突时自动备份
- [ ] 上传成功后本地文件自动清理
- [ ] 上传失败时本地文件保留
- [ ] UNC 路径和盘符路径均可正常使用
- [ ] 翻译模式支持用户选择部分文件上传
---
## 8. 参考文件
| 文件路径 | 用途 |
|---------|------|
| `AuxiliaryTool/DocumentAutoOptimizationCalibration/src/cli.py` | 现有 CLI 交互逻辑 |
| `AuxiliaryTool/DocumentAutoOptimizationCalibration/src/config.py` | 现有配置模块 |
| `AuxiliaryTool/ScriptTool/远程容器更新/upload_to_nas.sh` | NAS 上传脚本参考 |
---
## 9. 优化功能回填
- [ ] 支持配置文件预设默认网盘路径
- [ ] 支持上传历史记录
---
## 10. 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| v1.4.0 | 2026-06-07 | 新增网盘上传功能 |
---
## 11. 实现状态
### 已完成功能
- [x] 网盘上传配置项 (config.py)
- [x] 网盘上传核心模块 (uploader.py)
- [x] CLI 交互集成 (cli.py)
### 待验证功能
- [ ] 功能测试验证(需在实际网盘环境中测试)
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论