diff --git a/app/api/v1/files.py b/app/api/v1/files.py
new file mode 100644
index 0000000..28ba336
--- /dev/null
+++ b/app/api/v1/files.py
@@ -0,0 +1,199 @@
+"""
+文件管理API路由
+提供CAD文件列表和下载功能
+"""
+from fastapi import APIRouter, HTTPException
+from fastapi.responses import FileResponse, StreamingResponse
+from typing import List
+from pathlib import Path
+import os
+import zipfile
+import io
+from app.config import settings, software_config
+
+router = APIRouter()
+
+# CAD文件扩展名映射
+CAD_EXTENSIONS = {
+ 'creo': ['.prt', '.asm', '.drw'], # Creo文件,包括.prt.1等版本文件
+ 'pdms': ['.rvm', '.dri'], # PDMS文件
+ 'revit': ['.rvt', '.rfa', '.rte'] # Revit文件
+}
+
+
+def get_cad_files_path() -> Path:
+ """获取CAD文件存储路径"""
+ cad_path = software_config.get_cad_files_path()
+ path = Path(cad_path)
+
+ if not path.exists():
+ raise HTTPException(status_code=404, detail=f"CAD文件路径不存在: {cad_path}")
+
+ return path
+
+
+def is_cad_file(filename: str) -> bool:
+ """判断文件是否为CAD文件(包括版本号后缀)"""
+ filename_lower = filename.lower()
+
+ # 检查所有CAD扩展名
+ for software, extensions in CAD_EXTENSIONS.items():
+ for ext in extensions:
+ # 检查标准扩展名
+ if filename_lower.endswith(ext):
+ return True
+ # 检查带版本号的扩展名(如 .prt.1, .prt.2 等)
+ if ext in filename_lower and '.' in filename_lower.split(ext)[-1]:
+ # 验证版本号部分是否为数字
+ version_part = filename_lower.split(ext)[-1]
+ if version_part.startswith('.') and version_part[1:].isdigit():
+ return True
+
+ return False
+
+
+def scan_cad_files(base_path: Path) -> List[dict]:
+ """扫描目录下的所有CAD文件"""
+ cad_files = []
+
+ try:
+ for root, dirs, files in os.walk(base_path):
+ for file in files:
+ if is_cad_file(file):
+ file_path = Path(root) / file
+ relative_path = file_path.relative_to(base_path)
+
+ # 获取文件信息
+ stat = file_path.stat()
+
+ cad_files.append({
+ 'filename': file,
+ 'relative_path': str(relative_path).replace('\\', '/'),
+ 'absolute_path': str(file_path),
+ 'size': stat.st_size,
+ 'modified_time': stat.st_mtime,
+ 'extension': get_file_extension(file)
+ })
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"扫描文件失败: {str(e)}")
+
+ return cad_files
+
+
+def get_file_extension(filename: str) -> str:
+ """获取文件扩展名(包括版本号)"""
+ filename_lower = filename.lower()
+
+ # 检查是否有版本号
+ for software, extensions in CAD_EXTENSIONS.items():
+ for ext in extensions:
+ if ext in filename_lower:
+ idx = filename_lower.find(ext)
+ return filename[idx:]
+
+ # 如果没有匹配,返回标准扩展名
+ return Path(filename).suffix
+
+
+@router.get("/files/list")
+async def list_cad_files():
+ """
+ 获取CAD文件列表
+ 返回所有Creo、PDMS、Revit格式的文件
+ """
+ base_path = get_cad_files_path()
+ files = scan_cad_files(base_path)
+
+ return {
+ 'success': True,
+ 'base_path': str(base_path),
+ 'total_count': len(files),
+ 'files': files
+ }
+
+
+@router.get("/files/download/{file_path:path}")
+async def download_file(file_path: str):
+ """
+ 下载单个文件
+
+ Args:
+ file_path: 文件的相对路径
+ """
+ base_path = get_cad_files_path()
+ full_path = base_path / file_path
+
+ # 安全检查:确保文件在允许的目录内
+ try:
+ full_path = full_path.resolve()
+ base_path = base_path.resolve()
+
+ if not str(full_path).startswith(str(base_path)):
+ raise HTTPException(status_code=403, detail="访问被拒绝")
+ except Exception:
+ raise HTTPException(status_code=400, detail="无效的文件路径")
+
+ # 检查文件是否存在
+ if not full_path.exists():
+ raise HTTPException(status_code=404, detail="文件不存在")
+
+ if not full_path.is_file():
+ raise HTTPException(status_code=400, detail="不是有效的文件")
+
+ # 检查是否为CAD文件
+ if not is_cad_file(full_path.name):
+ raise HTTPException(status_code=403, detail="只能下载CAD文件")
+
+ return FileResponse(
+ path=str(full_path),
+ filename=full_path.name,
+ media_type='application/octet-stream'
+ )
+
+
+@router.post("/files/download/batch")
+async def download_batch_files(file_paths: List[str]):
+ """
+ 批量下载文件(打包为ZIP)
+
+ Args:
+ file_paths: 文件相对路径列表
+ """
+ if not file_paths:
+ raise HTTPException(status_code=400, detail="文件列表不能为空")
+
+ base_path = get_cad_files_path()
+
+ # 创建内存中的ZIP文件
+ zip_buffer = io.BytesIO()
+
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
+ for file_path in file_paths:
+ full_path = base_path / file_path
+
+ # 安全检查
+ try:
+ full_path = full_path.resolve()
+ base_path_resolved = base_path.resolve()
+
+ if not str(full_path).startswith(str(base_path_resolved)):
+ continue # 跳过不安全的路径
+ except Exception:
+ continue
+
+ # 检查文件是否存在且为CAD文件
+ if full_path.exists() and full_path.is_file() and is_cad_file(full_path.name):
+ # 使用相对路径作为ZIP内的路径
+ arcname = file_path.replace('\\', '/')
+ zip_file.write(str(full_path), arcname=arcname)
+
+ # 重置缓冲区位置
+ zip_buffer.seek(0)
+
+ return StreamingResponse(
+ zip_buffer,
+ media_type='application/zip',
+ headers={
+ 'Content-Disposition': 'attachment; filename="cad_files.zip"'
+ }
+ )
diff --git a/app/api/v1/websocket.py b/app/api/v1/websocket.py
index 636c6fe..01eaa51 100644
--- a/app/api/v1/websocket.py
+++ b/app/api/v1/websocket.py
@@ -25,6 +25,14 @@ class WSMessageType:
GET_LOG_STATS = "get_log_stats"
CLEANUP_LOGS = "cleanup_logs"
GET_OPERATION_TYPES = "get_operation_types"
+ # 文件管理相关消息类型
+ GET_FILE_LIST = "get_file_list"
+ DOWNLOAD_FILE = "download_file"
+ DOWNLOAD_BATCH = "download_batch"
+ # 文件重命名相关消息类型
+ GET_RENAME_STRATEGIES = "get_rename_strategies"
+ PREVIEW_RENAME = "preview_rename"
+ RENAME_FILES = "rename_files"
router = APIRouter()
@@ -116,6 +124,12 @@ async def handle_client_message(message: dict, client_id: str, user_id: str):
- get_log_stats: 获取日志统计信息
- cleanup_logs: 清理过期日志
- get_operation_types: 获取操作类型列表
+ - get_file_list: 获取CAD文件列表
+ - download_file: 获取单个文件下载URL
+ - download_batch: 获取批量文件下载URL
+ - get_rename_strategies: 获取可用的重命名策略列表
+ - preview_rename: 预览重命名结果
+ - rename_files: 执行批量重命名
"""
from app.core.software_manager import software_manager
from app.core.log_manager import log_manager
@@ -499,6 +513,255 @@ async def handle_client_message(message: dict, client_id: str, user_id: str):
"message": f"获取操作类型失败: {str(e)}",
"timestamp": websocket_manager._get_timestamp()
}, client_id)
+
+ elif message_type == WSMessageType.GET_FILE_LIST:
+ # 获取CAD文件列表
+ try:
+ from app.api.v1.files import get_cad_files_path, scan_cad_files
+
+ base_path = get_cad_files_path()
+ files = scan_cad_files(base_path)
+
+ await websocket_manager.send_personal_message({
+ "type": MessageType.INFO,
+ "message": "获取文件列表成功",
+ "data": {
+ "base_path": str(base_path),
+ "total_count": len(files),
+ "files": files
+ },
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ except Exception as e:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": f"获取文件列表失败: {str(e)}",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ elif message_type == WSMessageType.DOWNLOAD_FILE:
+ # 获取单个文件下载URL
+ file_path = message.get("file_path")
+ if not file_path:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": "缺少参数: file_path",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+ return
+
+ try:
+ from app.api.v1.files import get_cad_files_path, is_cad_file
+ from pathlib import Path
+
+ base_path = get_cad_files_path()
+ full_path = base_path / file_path
+
+ # 安全检查
+ full_path = full_path.resolve()
+ base_path = base_path.resolve()
+
+ if not str(full_path).startswith(str(base_path)):
+ raise ValueError("访问被拒绝")
+
+ if not full_path.exists():
+ raise FileNotFoundError("文件不存在")
+
+ if not is_cad_file(full_path.name):
+ raise ValueError("只能下载CAD文件")
+
+ # 返回下载URL(使用HTTP接口)
+ download_url = f"/api/v1/files/download/{file_path}"
+
+ await websocket_manager.send_personal_message({
+ "type": MessageType.INFO,
+ "message": "文件下载链接已生成",
+ "data": {
+ "file_path": file_path,
+ "filename": full_path.name,
+ "download_url": download_url,
+ "file_size": full_path.stat().st_size
+ },
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ except Exception as e:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": f"生成下载链接失败: {str(e)}",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ elif message_type == WSMessageType.DOWNLOAD_BATCH:
+ # 获取批量文件下载URL
+ file_paths = message.get("file_paths")
+ if not file_paths or not isinstance(file_paths, list):
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": "缺少参数: file_paths (必须是数组)",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+ return
+
+ try:
+ # 返回批量下载URL(使用HTTP接口)
+ download_url = "/api/v1/files/download/batch"
+
+ await websocket_manager.send_personal_message({
+ "type": MessageType.INFO,
+ "message": "批量下载链接已生成",
+ "data": {
+ "file_count": len(file_paths),
+ "file_paths": file_paths,
+ "download_url": download_url,
+ "method": "POST",
+ "note": "请使用POST方法,将file_paths作为JSON数组发送到此URL"
+ },
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ except Exception as e:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": f"生成批量下载链接失败: {str(e)}",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ elif message_type == WSMessageType.GET_RENAME_STRATEGIES:
+ # 获取可用的重命名策略列表
+ try:
+ from app.core.rename_manager import rename_manager
+
+ strategies = rename_manager.get_available_strategies()
+
+ await websocket_manager.send_personal_message({
+ "type": MessageType.INFO,
+ "message": "获取重命名策略成功",
+ "data": {
+ "strategies": strategies,
+ "total_count": len(strategies)
+ },
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ except Exception as e:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": f"获取重命名策略失败: {str(e)}",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ elif message_type == WSMessageType.PREVIEW_RENAME:
+ # 预览重命名结果
+ file_paths = message.get("file_paths")
+ strategy_name = message.get("strategy")
+ params = message.get("params", {})
+
+ if not file_paths or not isinstance(file_paths, list):
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": "缺少参数: file_paths (必须是数组)",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+ return
+
+ if not strategy_name:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": "缺少参数: strategy",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+ return
+
+ try:
+ from app.core.rename_manager import rename_manager
+ from pathlib import Path
+
+ # 提取文件名
+ filenames = [Path(fp).name for fp in file_paths]
+
+ # 预览重命名
+ preview_results = rename_manager.preview_rename(
+ filenames=filenames,
+ strategy_name=strategy_name,
+ params=params
+ )
+
+ await websocket_manager.send_personal_message({
+ "type": MessageType.INFO,
+ "message": "重命名预览生成成功",
+ "data": {
+ "strategy": strategy_name,
+ "params": params,
+ "preview": preview_results,
+ "total_count": len(preview_results)
+ },
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ except Exception as e:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": f"预览重命名失败: {str(e)}",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ elif message_type == WSMessageType.RENAME_FILES:
+ # 执行批量重命名
+ file_paths = message.get("file_paths")
+ strategy_name = message.get("strategy")
+ params = message.get("params", {})
+
+ if not file_paths or not isinstance(file_paths, list):
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": "缺少参数: file_paths (必须是数组)",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+ return
+
+ if not strategy_name:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": "缺少参数: strategy",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+ return
+
+ try:
+ from app.core.rename_manager import rename_manager
+ from app.api.v1.files import get_cad_files_path
+
+ base_path = get_cad_files_path()
+
+ # 执行重命名
+ results = rename_manager.execute_rename(
+ base_path=base_path,
+ file_paths=file_paths,
+ strategy_name=strategy_name,
+ params=params
+ )
+
+ await websocket_manager.send_personal_message({
+ "type": MessageType.INFO,
+ "message": f"批量重命名完成: 成功 {results['success_count']} 个, 失败 {results['failed_count']} 个",
+ "data": {
+ "strategy": strategy_name,
+ "params": params,
+ "success_count": results["success_count"],
+ "failed_count": results["failed_count"],
+ "results": results["results"]
+ },
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
+
+ except Exception as e:
+ await websocket_manager.send_personal_message({
+ "type": MessageType.ERROR,
+ "message": f"批量重命名失败: {str(e)}",
+ "timestamp": websocket_manager._get_timestamp()
+ }, client_id)
else:
# 未知消息类型
diff --git a/app/config.py b/app/config.py
index 08042a3..3c44596 100644
--- a/app/config.py
+++ b/app/config.py
@@ -35,6 +35,9 @@ class Settings(BaseSettings):
software_config_path: str = str(BASE_DIR / "configs" / "software_config.yaml")
users_config_path: str = str(BASE_DIR / "configs" / "users.json")
+ # CAD文件存储路径
+ cad_files_path: str = r"C:\Users\Public\Documents"
+
# CORS配置
cors_origins: List[str] = ["*"]
@@ -82,6 +85,15 @@ class SoftwareConfig:
def validate_software_exists(self, software_id: str) -> bool:
"""验证软件是否存在于配置中"""
return software_id in self.get_software_list()
+
+ def get_cad_files_path(self) -> str:
+ """获取CAD文件存储路径"""
+ if not self._config:
+ self.load_config()
+
+ file_storage = self._config.get('file_storage', {})
+ return file_storage.get('cad_files_path', r'C:\Users\Public\Documents')
+
# 创建全局配置实例
diff --git a/app/core/rename_manager.py b/app/core/rename_manager.py
new file mode 100644
index 0000000..5b9de3c
--- /dev/null
+++ b/app/core/rename_manager.py
@@ -0,0 +1,366 @@
+"""
+文件重命名管理器
+提供批量重命名策略和执行功能
+"""
+from pathlib import Path
+from typing import List, Dict, Optional, Callable
+from datetime import datetime
+import re
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class RenameStrategy:
+ """重命名策略基类"""
+
+ def __init__(self, name: str, description: str):
+ self.name = name
+ self.description = description
+
+ def apply(self, filename: str, index: int, params: dict) -> str:
+ """
+ 应用重命名策略
+
+ Args:
+ filename: 原文件名
+ index: 文件索引(从0开始)
+ params: 策略参数
+
+ Returns:
+ 新文件名
+ """
+ raise NotImplementedError
+
+
+class AddPrefixStrategy(RenameStrategy):
+ """添加前缀策略"""
+
+ def __init__(self):
+ super().__init__(
+ name="add_prefix",
+ description="在文件名前添加前缀"
+ )
+
+ def apply(self, filename: str, index: int, params: dict) -> str:
+ prefix = params.get("prefix", "")
+ return f"{prefix}{filename}"
+
+
+class AddSuffixStrategy(RenameStrategy):
+ """添加后缀策略(在扩展名前)"""
+
+ def __init__(self):
+ super().__init__(
+ name="add_suffix",
+ description="在扩展名前添加后缀"
+ )
+
+ def apply(self, filename: str, index: int, params: dict) -> str:
+ suffix = params.get("suffix", "")
+ # 分离文件名和扩展名(包括版本号)
+ name_part, ext_part = self._split_name_ext(filename)
+ return f"{name_part}{suffix}{ext_part}"
+
+ @staticmethod
+ def _split_name_ext(filename: str) -> tuple:
+ """分离文件名和扩展名(保留版本号)"""
+ # 处理如 file.prt.1 这样的文件
+ parts = filename.split('.')
+ if len(parts) > 1:
+ # 检查最后一部分是否为数字(版本号)
+ if parts[-1].isdigit() and len(parts) > 2:
+ # 有版本号:file.prt.1 -> file, .prt.1
+ name = parts[0]
+ ext = '.' + '.'.join(parts[1:])
+ else:
+ # 无版本号:file.prt -> file, .prt
+ name = '.'.join(parts[:-1])
+ ext = '.' + parts[-1]
+ else:
+ name = filename
+ ext = ''
+ return name, ext
+
+
+class SequenceStrategy(RenameStrategy):
+ """序号重命名策略"""
+
+ def __init__(self):
+ super().__init__(
+ name="sequence",
+ description="使用序号重命名文件"
+ )
+
+ def apply(self, filename: str, index: int, params: dict) -> str:
+ base_name = params.get("base_name", "file")
+ start_number = params.get("start_number", 1)
+ digits = params.get("digits", 3)
+ separator = params.get("separator", "_")
+
+ # 保留扩展名
+ _, ext_part = AddSuffixStrategy._split_name_ext(filename)
+
+ # 生成序号
+ number = start_number + index
+ number_str = str(number).zfill(digits)
+
+ return f"{base_name}{separator}{number_str}{ext_part}"
+
+
+class ReplaceTextStrategy(RenameStrategy):
+ """文本替换策略"""
+
+ def __init__(self):
+ super().__init__(
+ name="replace_text",
+ description="替换文件名中的文本"
+ )
+
+ def apply(self, filename: str, index: int, params: dict) -> str:
+ search_text = params.get("search", "")
+ replace_text = params.get("replace", "")
+ case_sensitive = params.get("case_sensitive", True)
+
+ if not search_text:
+ return filename
+
+ if case_sensitive:
+ return filename.replace(search_text, replace_text)
+ else:
+ # 不区分大小写的替换
+ pattern = re.compile(re.escape(search_text), re.IGNORECASE)
+ return pattern.sub(replace_text, filename)
+
+
+class AddDateTimeStrategy(RenameStrategy):
+ """添加日期时间策略"""
+
+ def __init__(self):
+ super().__init__(
+ name="add_datetime",
+ description="添加日期时间戳"
+ )
+
+ def apply(self, filename: str, index: int, params: dict) -> str:
+ format_str = params.get("format", "%Y%m%d_%H%M%S")
+ position = params.get("position", "suffix") # prefix or suffix
+ separator = params.get("separator", "_")
+
+ timestamp = datetime.now().strftime(format_str)
+ name_part, ext_part = AddSuffixStrategy._split_name_ext(filename)
+
+ if position == "prefix":
+ return f"{timestamp}{separator}{name_part}{ext_part}"
+ else:
+ return f"{name_part}{separator}{timestamp}{ext_part}"
+
+
+class ChangeCaseStrategy(RenameStrategy):
+ """大小写转换策略"""
+
+ def __init__(self):
+ super().__init__(
+ name="change_case",
+ description="转换文件名大小写"
+ )
+
+ def apply(self, filename: str, index: int, params: dict) -> str:
+ case_type = params.get("case_type", "lower") # lower, upper, title
+
+ name_part, ext_part = AddSuffixStrategy._split_name_ext(filename)
+
+ if case_type == "lower":
+ name_part = name_part.lower()
+ elif case_type == "upper":
+ name_part = name_part.upper()
+ elif case_type == "title":
+ name_part = name_part.title()
+
+ return f"{name_part}{ext_part}"
+
+
+class RenameManager:
+ """重命名管理器"""
+
+ def __init__(self):
+ self.strategies: Dict[str, RenameStrategy] = {}
+ self._register_default_strategies()
+
+ def _register_default_strategies(self):
+ """注册默认策略"""
+ strategies = [
+ AddPrefixStrategy(),
+ AddSuffixStrategy(),
+ SequenceStrategy(),
+ ReplaceTextStrategy(),
+ AddDateTimeStrategy(),
+ ChangeCaseStrategy()
+ ]
+
+ for strategy in strategies:
+ self.strategies[strategy.name] = strategy
+
+ def get_available_strategies(self) -> List[Dict[str, str]]:
+ """获取可用的重命名策略列表"""
+ return [
+ {
+ "name": strategy.name,
+ "description": strategy.description
+ }
+ for strategy in self.strategies.values()
+ ]
+
+ def preview_rename(
+ self,
+ filenames: List[str],
+ strategy_name: str,
+ params: dict
+ ) -> List[Dict[str, str]]:
+ """
+ 预览重命名结果
+
+ Args:
+ filenames: 文件名列表
+ strategy_name: 策略名称
+ params: 策略参数
+
+ Returns:
+ 预览结果列表,包含原文件名和新文件名
+ """
+ if strategy_name not in self.strategies:
+ raise ValueError(f"未知的重命名策略: {strategy_name}")
+
+ strategy = self.strategies[strategy_name]
+ results = []
+
+ for index, filename in enumerate(filenames):
+ try:
+ new_name = strategy.apply(filename, index, params)
+ results.append({
+ "original": filename,
+ "new": new_name,
+ "success": True
+ })
+ except Exception as e:
+ results.append({
+ "original": filename,
+ "new": filename,
+ "success": False,
+ "error": str(e)
+ })
+
+ return results
+
+ def execute_rename(
+ self,
+ base_path: Path,
+ file_paths: List[str],
+ strategy_name: str,
+ params: dict
+ ) -> Dict[str, any]:
+ """
+ 执行批量重命名
+
+ Args:
+ base_path: 基础路径
+ file_paths: 相对文件路径列表
+ strategy_name: 策略名称
+ params: 策略参数
+
+ Returns:
+ 执行结果
+ """
+ if strategy_name not in self.strategies:
+ raise ValueError(f"未知的重命名策略: {strategy_name}")
+
+ strategy = self.strategies[strategy_name]
+ results = {
+ "success_count": 0,
+ "failed_count": 0,
+ "results": []
+ }
+
+ for index, file_path in enumerate(file_paths):
+ try:
+ # 获取完整路径
+ full_path = base_path / file_path
+
+ # 安全检查
+ full_path = full_path.resolve()
+ base_path_resolved = base_path.resolve()
+
+ if not str(full_path).startswith(str(base_path_resolved)):
+ raise ValueError("访问被拒绝")
+
+ if not full_path.exists():
+ raise FileNotFoundError("文件不存在")
+
+ # 生成新文件名
+ original_filename = full_path.name
+ new_filename = strategy.apply(original_filename, index, params)
+
+ # 处理文件名冲突
+ new_path = full_path.parent / new_filename
+ new_path = self._resolve_conflict(new_path)
+
+ # 执行重命名
+ full_path.rename(new_path)
+
+ results["results"].append({
+ "original_path": file_path,
+ "original_name": original_filename,
+ "new_name": new_path.name,
+ "new_path": str(new_path.relative_to(base_path_resolved)).replace('\\', '/'),
+ "success": True
+ })
+ results["success_count"] += 1
+
+ logger.info(f"重命名成功: {original_filename} -> {new_path.name}")
+
+ except Exception as e:
+ results["results"].append({
+ "original_path": file_path,
+ "original_name": Path(file_path).name,
+ "success": False,
+ "error": str(e)
+ })
+ results["failed_count"] += 1
+
+ logger.error(f"重命名失败: {file_path}, 错误: {str(e)}")
+
+ return results
+
+ def _resolve_conflict(self, path: Path) -> Path:
+ """
+ 解决文件名冲突
+
+ Args:
+ path: 目标路径
+
+ Returns:
+ 解决冲突后的路径
+ """
+ if not path.exists():
+ return path
+
+ # 分离文件名和扩展名
+ name_part, ext_part = AddSuffixStrategy._split_name_ext(path.name)
+ parent = path.parent
+
+ # 添加数字后缀
+ counter = 1
+ while True:
+ new_name = f"{name_part}_{counter}{ext_part}"
+ new_path = parent / new_name
+ if not new_path.exists():
+ return new_path
+ counter += 1
+
+ # 防止无限循环
+ if counter > 1000:
+ raise ValueError("无法解决文件名冲突")
+
+
+# 全局实例
+rename_manager = RenameManager()
diff --git a/app/main.py b/app/main.py
index b609eae..7a25aa4 100644
--- a/app/main.py
+++ b/app/main.py
@@ -3,7 +3,7 @@ FastAPI主入口文件
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
-from app.api.v1 import websocket
+from app.api.v1 import websocket, files
from app.core.websocket_manager import websocket_manager
from app.core.software_manager import software_manager
from app.core.log_manager import log_manager
@@ -41,6 +41,8 @@ app.add_middleware(
# 注册API路由
app.include_router(websocket.router, prefix="/api/v1/ws", tags=["WebSocket"])
+app.include_router(files.router, prefix="/api/v1", tags=["Files"])
+
@app.get("/")
diff --git a/configs/software_config.yaml b/configs/software_config.yaml
index a906b07..b42ecf5 100644
--- a/configs/software_config.yaml
+++ b/configs/software_config.yaml
@@ -1,3 +1,7 @@
+# 文件存储路径配置
+file_storage:
+ cad_files_path: "C:\\Users\\Public\\Documents"
+
software:
creo:
name: "PTC Creo"
diff --git a/websocket-file-api-docs.md b/websocket-file-api-docs.md
new file mode 100644
index 0000000..4b359e5
--- /dev/null
+++ b/websocket-file-api-docs.md
@@ -0,0 +1,1007 @@
+# WebSocket 文件管理API文档
+
+## 概述
+通过WebSocket连接实现CAD文件的列表查询和下载功能。支持Creo、PDMS、Revit三种CAD软件的文件格式。
+
+## WebSocket连接
+
+**连接地址**: `ws://localhost:8000/api/v1/ws/connect`
+
+**连接参数**:
+- `client_id`: 客户端ID(可选,不提供会自动生成)
+- `user_id`: 用户ID(可选)
+
+**连接示例**:
+```javascript
+const ws = new WebSocket('ws://localhost:8000/api/v1/ws/connect?user_id=user123');
+```
+
+---
+
+## 支持的文件格式
+
+### 📐 Creo 文件
+- `.prt` - 零件文件(Part)
+- `.asm` - 装配文件(Assembly)
+- `.drw` - 工程图文件(Drawing)
+- **特别支持版本号后缀**:`.prt.1`, `.prt.2`, `.asm.1` 等
+
+### 🏭 PDMS 文件
+- `.rvm` - 3D模型文件
+- `.dri` - 图纸文件
+
+### 🏗️ Revit 文件
+- `.rvt` - Revit项目文件
+- `.rfa` - Revit族文件(Family)
+- `.rte` - Revit模板文件
+
+---
+
+## WebSocket消息格式
+
+### 1. 获取CAD文件列表
+
+**发送消息**:
+```json
+{
+ "type": "get_file_list"
+}
+```
+
+**响应消息**:
+```json
+{
+ "type": "info",
+ "message": "获取文件列表成功",
+ "data": {
+ "base_path": "C:\\Users\\Public\\Documents",
+ "total_count": 15,
+ "files": [
+ {
+ "filename": "part1.prt.1",
+ "relative_path": "creo/part1.prt.1",
+ "absolute_path": "C:\\Users\\Public\\Documents\\creo\\part1.prt.1",
+ "size": 1024000,
+ "modified_time": 1706950800.0,
+ "extension": ".prt.1"
+ },
+ {
+ "filename": "building.rvt",
+ "relative_path": "revit/building.rvt",
+ "absolute_path": "C:\\Users\\Public\\Documents\\revit\\building.rvt",
+ "size": 5120000,
+ "modified_time": 1706951000.0,
+ "extension": ".rvt"
+ }
+ ]
+ },
+ "timestamp": "2026-02-04T10:30:00"
+}
+```
+
+---
+
+### 2. 获取单个文件下载链接
+
+**发送消息**:
+```json
+{
+ "type": "download_file",
+ "file_path": "creo/part1.prt.1"
+}
+```
+
+**响应消息**:
+```json
+{
+ "type": "info",
+ "message": "文件下载链接已生成",
+ "data": {
+ "file_path": "creo/part1.prt.1",
+ "filename": "part1.prt.1",
+ "download_url": "/api/v1/files/download/creo/part1.prt.1",
+ "file_size": 1024000
+ },
+ "timestamp": "2026-02-04T10:30:00"
+}
+```
+
+**使用下载链接**:
+```javascript
+// 获取到download_url后,使用HTTP GET请求下载
+const downloadUrl = `http://localhost:8000${data.download_url}`;
+window.open(downloadUrl, '_blank');
+```
+
+---
+
+### 3. 获取批量下载链接
+
+**发送消息**:
+```json
+{
+ "type": "download_batch",
+ "file_paths": [
+ "creo/part1.prt.1",
+ "creo/assembly.asm",
+ "revit/building.rvt"
+ ]
+}
+```
+
+**响应消息**:
+```json
+{
+ "type": "info",
+ "message": "批量下载链接已生成",
+ "data": {
+ "file_count": 3,
+ "file_paths": [
+ "creo/part1.prt.1",
+ "creo/assembly.asm",
+ "revit/building.rvt"
+ ],
+ "download_url": "/api/v1/files/download/batch",
+ "method": "POST",
+ "note": "请使用POST方法,将file_paths作为JSON数组发送到此URL"
+ },
+ "timestamp": "2026-02-04T10:30:00"
+}
+```
+
+**使用批量下载**:
+```javascript
+// 使用fetch API进行POST请求
+const response = await fetch('http://localhost:8000/api/v1/files/download/batch', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data.file_paths)
+});
+
+const blob = await response.blob();
+const url = window.URL.createObjectURL(blob);
+const a = document.createElement('a');
+a.href = url;
+a.download = 'cad_files.zip';
+a.click();
+```
+
+---
+
+## 完整前端示例
+
+### JavaScript/WebSocket
+
+```javascript
+class CADFileManager {
+ constructor(serverUrl = 'ws://localhost:8000/api/v1/ws/connect') {
+ this.ws = null;
+ this.serverUrl = serverUrl;
+ this.httpBaseUrl = 'http://localhost:8000';
+ }
+
+ // 连接WebSocket
+ connect(userId = 'user123') {
+ return new Promise((resolve, reject) => {
+ this.ws = new WebSocket(`${this.serverUrl}?user_id=${userId}`);
+
+ this.ws.onopen = () => {
+ console.log('WebSocket连接成功');
+ resolve();
+ };
+
+ this.ws.onerror = (error) => {
+ console.error('WebSocket错误:', error);
+ reject(error);
+ };
+
+ this.ws.onmessage = (event) => {
+ const message = JSON.parse(event.data);
+ this.handleMessage(message);
+ };
+ });
+ }
+
+ // 处理服务器消息
+ handleMessage(message) {
+ console.log('收到消息:', message);
+
+ if (message.type === 'info' && message.data) {
+ // 处理不同类型的响应
+ if (message.data.files) {
+ this.onFileListReceived(message.data);
+ } else if (message.data.download_url) {
+ this.onDownloadUrlReceived(message.data);
+ }
+ } else if (message.type === 'error') {
+ console.error('错误:', message.message);
+ }
+ }
+
+ // 获取文件列表
+ getFileList() {
+ this.ws.send(JSON.stringify({
+ type: 'get_file_list'
+ }));
+ }
+
+ // 文件列表回调(可以被覆盖)
+ onFileListReceived(data) {
+ console.log('文件列表:', data.files);
+ console.log('总数:', data.total_count);
+ }
+
+ // 请求单个文件下载
+ requestDownload(filePath) {
+ this.ws.send(JSON.stringify({
+ type: 'download_file',
+ file_path: filePath
+ }));
+ }
+
+ // 下载URL回调
+ onDownloadUrlReceived(data) {
+ const fullUrl = `${this.httpBaseUrl}${data.download_url}`;
+ console.log('下载链接:', fullUrl);
+
+ // 自动触发下载
+ window.open(fullUrl, '_blank');
+ }
+
+ // 请求批量下载
+ async requestBatchDownload(filePaths) {
+ // 批量下载直接使用HTTP POST
+ const response = await fetch(`${this.httpBaseUrl}/api/v1/files/download/batch`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(filePaths)
+ });
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'cad_files.zip';
+ a.click();
+ window.URL.revokeObjectURL(url);
+ }
+
+ // 断开连接
+ disconnect() {
+ if (this.ws) {
+ this.ws.close();
+ }
+ }
+}
+
+// 使用示例
+async function main() {
+ const manager = new CADFileManager();
+
+ // 连接
+ await manager.connect('user123');
+
+ // 自定义文件列表回调
+ manager.onFileListReceived = (data) => {
+ console.log(`找到 ${data.total_count} 个文件`);
+ data.files.forEach(file => {
+ console.log(`- ${file.filename} (${(file.size / 1024).toFixed(2)} KB)`);
+ });
+ };
+
+ // 获取文件列表
+ manager.getFileList();
+
+ // 下载单个文件
+ setTimeout(() => {
+ manager.requestDownload('creo/part1.prt.1');
+ }, 2000);
+
+ // 批量下载
+ setTimeout(async () => {
+ await manager.requestBatchDownload([
+ 'creo/part1.prt.1',
+ 'revit/building.rvt'
+ ]);
+ }, 4000);
+}
+
+main();
+```
+
+---
+
+### React示例
+
+```jsx
+import React, { useState, useEffect, useRef } from 'react';
+
+function CADFileManager() {
+ const [files, setFiles] = useState([]);
+ const [selectedFiles, setSelectedFiles] = useState([]);
+ const [connected, setConnected] = useState(false);
+ const wsRef = useRef(null);
+
+ useEffect(() => {
+ // 连接WebSocket
+ const ws = new WebSocket('ws://localhost:8000/api/v1/ws/connect?user_id=user123');
+
+ ws.onopen = () => {
+ console.log('WebSocket连接成功');
+ setConnected(true);
+
+ // 连接成功后立即获取文件列表
+ ws.send(JSON.stringify({ type: 'get_file_list' }));
+ };
+
+ ws.onmessage = (event) => {
+ const message = JSON.parse(event.data);
+
+ if (message.type === 'info' && message.data?.files) {
+ setFiles(message.data.files);
+ } else if (message.type === 'info' && message.data?.download_url) {
+ // 自动打开下载链接
+ window.open(`http://localhost:8000${message.data.download_url}`, '_blank');
+ }
+ };
+
+ wsRef.current = ws;
+
+ return () => {
+ ws.close();
+ };
+ }, []);
+
+ // 刷新文件列表
+ const refreshFileList = () => {
+ if (wsRef.current && connected) {
+ wsRef.current.send(JSON.stringify({ type: 'get_file_list' }));
+ }
+ };
+
+ // 下载单个文件
+ const downloadFile = (filePath) => {
+ if (wsRef.current && connected) {
+ wsRef.current.send(JSON.stringify({
+ type: 'download_file',
+ file_path: filePath
+ }));
+ }
+ };
+
+ // 批量下载
+ const downloadBatch = async () => {
+ const response = await fetch('http://localhost:8000/api/v1/files/download/batch', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(selectedFiles)
+ });
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'cad_files.zip';
+ a.click();
+ window.URL.revokeObjectURL(url);
+ };
+
+ // 切换文件选择
+ const toggleFileSelection = (filePath) => {
+ setSelectedFiles(prev =>
+ prev.includes(filePath)
+ ? prev.filter(f => f !== filePath)
+ : [...prev, filePath]
+ );
+ };
+
+ return (
+
+
CAD文件管理器
+
+
+ 连接状态: {connected ? '✅ 已连接' : '❌ 未连接'}
+
+
+
+
+
+
文件列表 (共 {files.length} 个)
+
+
+
+ );
+}
+
+export default CADFileManager;
+```
+
+---
+
+## 错误处理
+
+**错误响应格式**:
+```json
+{
+ "type": "error",
+ "message": "错误描述",
+ "timestamp": "2026-02-04T10:30:00"
+}
+```
+
+**常见错误**:
+- `"缺少参数: file_path"` - 未提供文件路径
+- `"缺少参数: file_paths (必须是数组)"` - 批量下载参数格式错误
+- `"访问被拒绝"` - 文件路径不安全
+- `"文件不存在"` - 请求的文件不存在
+- `"只能下载CAD文件"` - 尝试下载非CAD格式文件
+- `"获取文件列表失败"` - 扫描文件系统失败
+
+---
+
+## 注意事项
+
+1. **WebSocket用于控制,HTTP用于下载**
+ - 文件列表通过WebSocket返回
+ - 文件下载使用HTTP接口(因为WebSocket不适合传输大文件)
+
+2. **路径安全**
+ - 所有文件路径都会进行安全检查
+ - 只能访问配置目录内的文件
+
+3. **文件格式限制**
+ - 只能下载Creo、PDMS、Revit格式的文件
+ - 自动识别带版本号的Creo文件
+
+4. **批量下载**
+ - 文件打包为ZIP格式
+ - 保持原有目录结构
+ - 自动跳过不存在或不安全的文件
+
+5. **配置路径**
+ - 默认路径:`C:\Users\Public\Documents`
+ - 可在 `configs/software_config.yaml` 中修改
+
+---
+
+## 文件批量重命名功能
+
+### 4. 获取重命名策略列表
+
+**发送消息**:
+```json
+{
+ "type": "get_rename_strategies"
+}
+```
+
+**响应消息**:
+```json
+{
+ "type": "info",
+ "message": "获取重命名策略成功",
+ "data": {
+ "strategies": [
+ {
+ "name": "add_prefix",
+ "description": "在文件名前添加前缀"
+ },
+ {
+ "name": "add_suffix",
+ "description": "在扩展名前添加后缀"
+ },
+ {
+ "name": "sequence",
+ "description": "使用序号重命名文件"
+ },
+ {
+ "name": "replace_text",
+ "description": "替换文件名中的文本"
+ },
+ {
+ "name": "add_datetime",
+ "description": "添加日期时间戳"
+ },
+ {
+ "name": "change_case",
+ "description": "转换文件名大小写"
+ }
+ ],
+ "total_count": 6
+ },
+ "timestamp": "2026-02-04T11:10:00"
+}
+```
+
+---
+
+### 5. 预览重命名结果
+
+**发送消息**:
+```json
+{
+ "type": "preview_rename",
+ "file_paths": [
+ "creo/part1.prt.1",
+ "creo/assembly.asm",
+ "revit/building.rvt"
+ ],
+ "strategy": "add_prefix",
+ "params": {
+ "prefix": "NEW_"
+ }
+}
+```
+
+**响应消息**:
+```json
+{
+ "type": "info",
+ "message": "重命名预览生成成功",
+ "data": {
+ "strategy": "add_prefix",
+ "params": {
+ "prefix": "NEW_"
+ },
+ "preview": [
+ {
+ "original": "part1.prt.1",
+ "new": "NEW_part1.prt.1",
+ "success": true
+ },
+ {
+ "original": "assembly.asm",
+ "new": "NEW_assembly.asm",
+ "success": true
+ },
+ {
+ "original": "building.rvt",
+ "new": "NEW_building.rvt",
+ "success": true
+ }
+ ],
+ "total_count": 3
+ },
+ "timestamp": "2026-02-04T11:10:00"
+}
+```
+
+---
+
+### 6. 执行批量重命名
+
+**发送消息**:
+```json
+{
+ "type": "rename_files",
+ "file_paths": [
+ "creo/part1.prt.1",
+ "creo/assembly.asm"
+ ],
+ "strategy": "sequence",
+ "params": {
+ "base_name": "component",
+ "start_number": 1,
+ "digits": 3,
+ "separator": "_"
+ }
+}
+```
+
+**响应消息**:
+```json
+{
+ "type": "info",
+ "message": "批量重命名完成: 成功 2 个, 失败 0 个",
+ "data": {
+ "strategy": "sequence",
+ "params": {
+ "base_name": "component",
+ "start_number": 1,
+ "digits": 3,
+ "separator": "_"
+ },
+ "success_count": 2,
+ "failed_count": 0,
+ "results": [
+ {
+ "original_path": "creo/part1.prt.1",
+ "original_name": "part1.prt.1",
+ "new_name": "component_001.prt.1",
+ "new_path": "creo/component_001.prt.1",
+ "success": true
+ },
+ {
+ "original_path": "creo/assembly.asm",
+ "original_name": "assembly.asm",
+ "new_name": "component_002.asm",
+ "new_path": "creo/component_002.asm",
+ "success": true
+ }
+ ]
+ },
+ "timestamp": "2026-02-04T11:10:00"
+}
+```
+
+---
+
+## 重命名策略参数说明
+
+### 1. add_prefix(添加前缀)
+```json
+{
+ "strategy": "add_prefix",
+ "params": {
+ "prefix": "NEW_"
+ }
+}
+```
+
+### 2. add_suffix(添加后缀)
+```json
+{
+ "strategy": "add_suffix",
+ "params": {
+ "suffix": "_backup"
+ }
+}
+```
+
+### 3. sequence(序号重命名)
+```json
+{
+ "strategy": "sequence",
+ "params": {
+ "base_name": "file",
+ "start_number": 1,
+ "digits": 3,
+ "separator": "_"
+ }
+}
+```
+- `base_name`: 基础文件名
+- `start_number`: 起始序号(默认1)
+- `digits`: 序号位数(默认3,如001)
+- `separator`: 分隔符(默认下划线)
+
+### 4. replace_text(文本替换)
+```json
+{
+ "strategy": "replace_text",
+ "params": {
+ "search": "old",
+ "replace": "new",
+ "case_sensitive": true
+ }
+}
+```
+- `search`: 要搜索的文本
+- `replace`: 替换为的文本
+- `case_sensitive`: 是否区分大小写(默认true)
+
+### 5. add_datetime(添加日期时间)
+```json
+{
+ "strategy": "add_datetime",
+ "params": {
+ "format": "%Y%m%d_%H%M%S",
+ "position": "suffix",
+ "separator": "_"
+ }
+}
+```
+- `format`: 日期时间格式(默认:`%Y%m%d_%H%M%S`)
+- `position`: 位置,`prefix`或`suffix`(默认suffix)
+- `separator`: 分隔符(默认下划线)
+
+### 6. change_case(大小写转换)
+```json
+{
+ "strategy": "change_case",
+ "params": {
+ "case_type": "lower"
+ }
+}
+```
+- `case_type`: 转换类型,可选值:`lower`(小写)、`upper`(大写)、`title`(标题格式)
+
+---
+
+## 重命名功能使用示例
+
+### JavaScript示例
+
+```javascript
+// 扩展之前的 CADFileManager 类
+class CADFileManager {
+ // ... 之前的代码 ...
+
+ // 获取重命名策略列表
+ getRenameStrategies() {
+ this.ws.send(JSON.stringify({
+ type: 'get_rename_strategies'
+ }));
+ }
+
+ // 预览重命名
+ previewRename(filePaths, strategy, params) {
+ this.ws.send(JSON.stringify({
+ type: 'preview_rename',
+ file_paths: filePaths,
+ strategy: strategy,
+ params: params
+ }));
+ }
+
+ // 执行批量重命名
+ renameFiles(filePaths, strategy, params) {
+ this.ws.send(JSON.stringify({
+ type: 'rename_files',
+ file_paths: filePaths,
+ strategy: strategy,
+ params: params
+ }));
+ }
+}
+
+// 使用示例
+async function renameExample() {
+ const manager = new CADFileManager();
+ await manager.connect('user123');
+
+ // 1. 获取策略列表
+ manager.getRenameStrategies();
+
+ // 2. 预览重命名(添加前缀)
+ setTimeout(() => {
+ manager.previewRename(
+ ['creo/part1.prt.1', 'creo/assembly.asm'],
+ 'add_prefix',
+ { prefix: 'V2_' }
+ );
+ }, 1000);
+
+ // 3. 执行重命名(序号重命名)
+ setTimeout(() => {
+ manager.renameFiles(
+ ['creo/part1.prt.1', 'creo/part2.prt.1'],
+ 'sequence',
+ {
+ base_name: 'component',
+ start_number: 1,
+ digits: 3,
+ separator: '_'
+ }
+ );
+ }, 3000);
+}
+```
+
+### React示例
+
+```jsx
+function CADFileRenamer() {
+ const [files, setFiles] = useState([]);
+ const [selectedFiles, setSelectedFiles] = useState([]);
+ const [strategies, setStrategies] = useState([]);
+ const [selectedStrategy, setSelectedStrategy] = useState('');
+ const [renameParams, setRenameParams] = useState({});
+ const [previewResults, setPreviewResults] = useState([]);
+ const wsRef = useRef(null);
+
+ useEffect(() => {
+ const ws = new WebSocket('ws://localhost:8000/api/v1/ws/connect?user_id=user123');
+
+ ws.onopen = () => {
+ // 获取文件列表和策略列表
+ ws.send(JSON.stringify({ type: 'get_file_list' }));
+ ws.send(JSON.stringify({ type: 'get_rename_strategies' }));
+ };
+
+ ws.onmessage = (event) => {
+ const message = JSON.parse(event.data);
+
+ if (message.type === 'info') {
+ if (message.data?.files) {
+ setFiles(message.data.files);
+ } else if (message.data?.strategies) {
+ setStrategies(message.data.strategies);
+ } else if (message.data?.preview) {
+ setPreviewResults(message.data.preview);
+ } else if (message.data?.results) {
+ // 重命名完成,刷新文件列表
+ ws.send(JSON.stringify({ type: 'get_file_list' }));
+ setPreviewResults([]);
+ setSelectedFiles([]);
+ }
+ }
+ };
+
+ wsRef.current = ws;
+ return () => ws.close();
+ }, []);
+
+ // 预览重命名
+ const handlePreview = () => {
+ if (wsRef.current && selectedFiles.length > 0 && selectedStrategy) {
+ wsRef.current.send(JSON.stringify({
+ type: 'preview_rename',
+ file_paths: selectedFiles,
+ strategy: selectedStrategy,
+ params: renameParams
+ }));
+ }
+ };
+
+ // 执行重命名
+ const handleRename = () => {
+ if (wsRef.current && selectedFiles.length > 0 && selectedStrategy) {
+ if (confirm(`确定要重命名 ${selectedFiles.length} 个文件吗?`)) {
+ wsRef.current.send(JSON.stringify({
+ type: 'rename_files',
+ file_paths: selectedFiles,
+ strategy: selectedStrategy,
+ params: renameParams
+ }));
+ }
+ }
+ };
+
+ return (
+
+
CAD文件批量重命名
+
+ {/* 策略选择 */}
+
+
+
+
+
+ {/* 参数输入(根据策略动态显示) */}
+ {selectedStrategy === 'add_prefix' && (
+
+
+ setRenameParams({...renameParams, prefix: e.target.value})}
+ />
+
+ )}
+
+ {/* 操作按钮 */}
+
+
+
+
+
+ {/* 预览结果 */}
+ {previewResults.length > 0 && (
+
+
预览结果
+ {previewResults.map((result, idx) => (
+
+ {result.original} → {result.new}
+
+ ))}
+
+ )}
+
+ {/* 文件列表 */}
+
文件列表
+
+
+ );
+}
+```
+
+---
+
+## 重命名功能注意事项
+
+1. **文件名冲突处理**
+ - 如果重命名后的文件名已存在,系统会自动添加数字后缀(如 `file_1`, `file_2`)
+ - 最多尝试1000次,避免无限循环
+
+2. **扩展名保留**
+ - 所有重命名策略都会保留原有的文件扩展名
+ - 支持带版本号的扩展名(如 `.prt.1`, `.prt.2`)
+
+3. **预览功能**
+ - 建议在执行重命名前先使用预览功能
+ - 预览不会修改实际文件,只返回重命名结果
+
+4. **错误处理**
+ - 批量重命名时,单个文件失败不会影响其他文件
+ - 返回结果中会详细说明每个文件的成功/失败状态
+
+5. **安全性**
+ - 只能重命名配置目录内的CAD文件
+ - 路径安全检查防止目录遍历攻击