# 修复剧本文件解析时的下载问题 **日期**: 2026-02-06 **类型**: Bug 修复 **影响**: 剧本文件上传和解析功能 --- ## 📋 问题描述 在实施 RFC 140(文件 URL 存储优化)后,剧本文件解析一直失败,出现以下错误: ``` FileNotFoundError: [Errno 2] No such file or directory: 'screenplays/019c2d43-59a9-7e62-b325-b2bd786624d5/xxx.pdf' ``` ### 症状 - ✅ 文件上传成功 - ✅ 附件记录创建成功(存储相对路径) - ❌ Celery 解析任务失败 - ⏳ API 一直显示 "parsing" 状态 --- ## 🔍 根本原因 `ScreenplayFileParserService._download_file()` 方法只处理两种情况: 1. HTTP/HTTPS URL → 下载 2. 本地文件路径 → 直接使用 **缺少第三种情况**:OSS 相对路径(新数据格式) ### 问题代码 ```python # app/services/screenplay_file_parser_service.py (旧版本) async def _download_file(self, file_path: str) -> str: if file_path.startswith('http://') or file_path.startswith('https://'): # 下载 HTTP URL ... else: return file_path # ❌ 直接返回相对路径作为本地文件路径 ``` ### 数据流 ``` 1. 用户上传 PDF → attachment.file_url = "screenplays/xxx.pdf" (相对路径) 2. Celery 任务触发 → parse_file(file_path="screenplays/xxx.pdf") 3. _download_file() → 返回 "screenplays/xxx.pdf" (误以为是本地路径) 4. _parse_pdf() → 尝试打开本地文件 ❌ FileNotFoundError ``` --- ## ✅ 修复方案 ### 1. 扩展 `_download_file()` 支持 OSS 相对路径 ```python # app/services/screenplay_file_parser_service.py async def _download_file(self, file_path: str) -> str: """下载文件到临时目录(支持 HTTP URL 和 OSS 相对路径)""" # 场景 1:HTTP/HTTPS URL(旧数据或外部 URL) if file_path.startswith('http://') or file_path.startswith('https://'): # 通过 HTTP 下载 ... # 场景 2:本地文件路径(绝对路径) elif file_path.startswith('/') or Path(file_path).is_absolute(): return file_path # 场景 3:OSS 相对路径(新数据)✅ else: logger.debug("从 OSS 下载文件 | 对象路径: %s", file_path) from app.core.storage import StorageService storage = StorageService() # 从 OSS 下载文件内容 file_content = await storage.download_file(file_path) # 保存到临时文件 suffix = Path(file_path).suffix with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file: tmp_file.write(file_content) return tmp_file.name ``` ### 2. 添加 `StorageService.download_file()` 方法 ```python # app/core/storage.py class StorageService: async def download_file(self, object_name: str) -> bytes: """从对象存储下载文件内容 Args: object_name: 对象名称(存储路径,相对路径) Returns: bytes: 文件内容 """ try: response = self.client.get_object( Bucket=self.bucket_name, Key=object_name ) return response['Body'].read() except ClientError as e: raise StorageError(f"下载文件失败: {str(e)}") ``` --- ## 🎯 验证步骤 ### 1. 重启 Celery Worker ```bash docker compose restart celery-worker-ai ``` ### 2. 上传测试文件 ```bash curl -X POST http://localhost:8000/api/v1/screenplays/upload \ -F "file=@test.pdf" \ -F "projectId=xxx" ``` ### 3. 检查日志 ```bash docker compose logs celery-worker-ai --tail 50 ``` **预期输出**: ``` [INFO] 从 OSS 下载文件 | 对象路径: screenplays/xxx.pdf [INFO] 文件已从 OSS 下载到临时目录 | 路径: /tmp/xxx.pdf [INFO] 开始解析剧本文件 | 剧本ID: xxx | 类型: application/pdf [INFO] 剧本文件解析成功 | 字数: 1234 ``` --- ## 📊 影响范围 ### 修改的文件 1. `server/app/services/screenplay_file_parser_service.py` - 修改 `_download_file()` 方法 2. `server/app/core/storage.py` - 新增 `download_file()` 方法 ### 影响的功能 - ✅ 剧本文件上传和解析(PDF/DOCX) - ✅ 文件去重功能 - ✅ 后台 Celery 任务 ### 向后兼容性 ✅ **完全兼容**: - HTTP/HTTPS URL(旧数据)仍然可用 - 本地文件路径仍然可用 - OSS 相对路径(新数据)现在正常工作 --- ## 🔗 相关文档 - [RFC 140 - Screenplay 文件存储重构](../rfcs/140-screenplay-file-storage-refactor.md) - [URL 迁移指南](../guides/url-migration.md) - [Changelog 2026-02-06](./2026-02-06-screenplay-file-storage-refactor.md)