# 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 (
    <div style={{ padding: '20px' }}>
      <h1>CAD文件管理器</h1>
      
      <div style={{ marginBottom: '20px' }}>
        <span>连接状态: {connected ? '✅ 已连接' : '❌ 未连接'}</span>
        <button onClick={refreshFileList} style={{ marginLeft: '10px' }}>
          刷新列表
        </button>
        <button 
          onClick={downloadBatch} 
          disabled={selectedFiles.length === 0}
          style={{ marginLeft: '10px' }}
        >
          批量下载 ({selectedFiles.length})
        </button>
      </div>
      
      <div>
        <h2>文件列表 (共 {files.length} 个)</h2>
        <table border="1" cellPadding="8" style={{ width: '100%' }}>
          <thead>
            <tr>
              <th>选择</th>
              <th>文件名</th>
              <th>路径</th>
              <th>大小</th>
              <th>扩展名</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {files.map(file => (
              <tr key={file.absolute_path}>
                <td>
                  <input 
                    type="checkbox"
                    checked={selectedFiles.includes(file.relative_path)}
                    onChange={() => toggleFileSelection(file.relative_path)}
                  />
                </td>
                <td>{file.filename}</td>
                <td>{file.relative_path}</td>
                <td>{(file.size / 1024).toFixed(2)} KB</td>
                <td>{file.extension}</td>
                <td>
                  <button onClick={() => downloadFile(file.relative_path)}>
                    下载
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

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 (
    <div style={{ padding: '20px' }}>
      <h1>CAD文件批量重命名</h1>
      
      {/* 策略选择 */}
      <div style={{ marginBottom: '20px' }}>
        <label>重命名策略: </label>
        <select 
          value={selectedStrategy} 
          onChange={(e) => setSelectedStrategy(e.target.value)}
        >
          <option value="">请选择策略</option>
          {strategies.map(s => (
            <option key={s.name} value={s.name}>{s.description}</option>
          ))}
        </select>
      </div>
      
      {/* 参数输入（根据策略动态显示） */}
      {selectedStrategy === 'add_prefix' && (
        <div>
          <label>前缀: </label>
          <input 
            type="text" 
            value={renameParams.prefix || ''} 
            onChange={(e) => setRenameParams({...renameParams, prefix: e.target.value})}
          />
        </div>
      )}
      
      {/* 操作按钮 */}
      <div style={{ marginBottom: '20px' }}>
        <button onClick={handlePreview} disabled={selectedFiles.length === 0}>
          预览重命名
        </button>
        <button onClick={handleRename} disabled={selectedFiles.length === 0} style={{ marginLeft: '10px' }}>
          执行重命名
        </button>
      </div>
      
      {/* 预览结果 */}
      {previewResults.length > 0 && (
        <div style={{ marginBottom: '20px', padding: '10px', background: '#f0f0f0' }}>
          <h3>预览结果</h3>
          {previewResults.map((result, idx) => (
            <div key={idx}>
              {result.original} → {result.new}
            </div>
          ))}
        </div>
      )}
      
      {/* 文件列表 */}
      <h2>文件列表</h2>
      <table border="1" cellPadding="8">
        <thead>
          <tr>
            <th>选择</th>
            <th>文件名</th>
            <th>路径</th>
          </tr>
        </thead>
        <tbody>
          {files.map(file => (
            <tr key={file.absolute_path}>
              <td>
                <input 
                  type="checkbox"
                  checked={selectedFiles.includes(file.relative_path)}
                  onChange={(e) => {
                    if (e.target.checked) {
                      setSelectedFiles([...selectedFiles, file.relative_path]);
                    } else {
                      setSelectedFiles(selectedFiles.filter(f => f !== file.relative_path));
                    }
                  }}
                />
              </td>
              <td>{file.filename}</td>
              <td>{file.relative_path}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

---

## 重命名功能注意事项

1. **文件名冲突处理**
   - 如果重命名后的文件名已存在，系统会自动添加数字后缀（如 `file_1`, `file_2`）
   - 最多尝试1000次，避免无限循环

2. **扩展名保留**
   - 所有重命名策略都会保留原有的文件扩展名
   - 支持带版本号的扩展名（如 `.prt.1`, `.prt.2`）

3. **预览功能**
   - 建议在执行重命名前先使用预览功能
   - 预览不会修改实际文件，只返回重命名结果

4. **错误处理**
   - 批量重命名时，单个文件失败不会影响其他文件
   - 返回结果中会详细说明每个文件的成功/失败状态

5. **安全性**
   - 只能重命名配置目录内的CAD文件
   - 路径安全检查防止目录遍历攻击
