You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

4.6 KiB

修复剧本文件解析时的下载问题

日期: 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 相对路径(新数据格式)

问题代码

# 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 相对路径

# 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() 方法

# 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

docker compose restart celery-worker-ai

2. 上传测试文件

curl -X POST http://localhost:8000/api/v1/screenplays/upload \
  -F "file=@test.pdf" \
  -F "projectId=xxx"

3. 检查日志

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 相对路径(新数据)现在正常工作

🔗 相关文档