13 KiB
Changelog: 剧本文件解析器实现
日期: 2026-02-03 模块: server (后端) 类型: Feature Implementation 影响范围: Screenplay Service, API Routes, Schemas
概述
完成剧本文件解析器功能的实现,支持多种文件格式(TXT、Markdown、DOCX、PDF、RTF、DOC)的上传和解析,实现智能路由策略(同步/异步解析)和完整的状态跟踪机制。
变更详情
1. 🐛 Bug 修复
ScreenplayFileParserService Repository 初始化问题
问题描述:
ScreenplayFileParserService.__init__ 方法中 self.repository 被设置为 None,导致在 parse_file() 和 parse_file_sync() 方法中调用 self.repository.update() 时抛出 AttributeError: 'NoneType' object has no attribute 'update'。
修复方案:
# 修复前
def __init__(self, db: AsyncSession):
self.db = db
self.repository = None # ❌ Repository 将在需要时创建
# 修复后
from app.repositories.screenplay_repository import ScreenplayRepository
def __init__(self, db: Optional[AsyncSession]):
self.db = db
self.repository = ScreenplayRepository(db) if db else None # ✅ 正确初始化
影响:
- 修复了文件解析功能无法更新数据库状态的关键问题
- 确保解析状态(parsing_status)能够正确更新为 completed/failed
文件: server/app/services/screenplay_file_parser_service.py:42-44
2. ✨ 新增 Schema 定义
FileUploadResponse
用途: 文件上传响应模型,支持同步和异步解析场景
字段:
class FileUploadResponse(BaseModel):
screenplay_id: UUID # 剧本 ID
name: str # 剧本名称
type: str # 剧本类型(text/file)
file_url: str # 文件 URL
file_name: Optional[str] # 原始文件名
file_size: Optional[int] # 文件大小(字节)
mime_type: Optional[str] # 文件 MIME 类型
content: Optional[str] # 剧本内容(同步解析时返回)
word_count: Optional[int] # 字数统计(同步解析时返回)
parsing_status: str # 解析状态: idle/pending/parsing/completed/failed
parsed_at: Optional[datetime] # 解析完成时间
task_id: Optional[str] # Celery 任务 ID(异步解析时返回)
特点:
- 支持同步解析场景(返回 content 和 word_count)
- 支持异步解析场景(返回 task_id)
- 统一的响应格式,便于前端处理
ParseFileRequest
用途: 手动触发文件解析请求模型
字段:
class ParseFileRequest(BaseModel):
force: bool = Field(False, description="是否强制重新解析")
应用场景:
- 解析失败后重试
- 强制重新解析已解析的文件
ParseStatusResponse
用途: 解析状态查询响应模型
字段:
class ParseStatusResponse(BaseModel):
screenplay_id: UUID # 剧本 ID
parsing_status: str # 解析状态
progress: Optional[int] # 解析进度(0-100)
content: Optional[str] # 剧本内容(解析完成时返回)
word_count: Optional[int] # 字数统计(解析完成时返回)
parsing_error: Optional[str] # 解析错误信息(失败时返回)
message: str # 状态描述信息
特点:
- 提供进度指示(parsing: 50%, completed: 100%, failed: 0%)
- 根据状态返回不同内容(completed 返回 content,failed 返回 error)
- 友好的状态描述信息
文件: server/app/schemas/screenplay.py:199-229
3. 🚀 新增 API 端点
POST /api/v1/screenplays/upload-and-parse
功能: 上传并解析剧本文件,智能路由到同步/异步解析
请求参数:
project_id(Form): 项目 IDname(Form): 剧本名称file(File): 剧本文件
智能路由策略:
# 同步解析(立即返回内容)
SYNC_PARSE_TYPES = {
'text/plain', # TXT
'text/markdown' # Markdown
}
# 异步解析(返回任务 ID)
ASYNC_PARSE_TYPES = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', # DOCX
'application/msword', # DOC
'application/pdf', # PDF
'application/rtf', # RTF
'text/rtf' # RTF (alternative)
}
响应示例:
// 同步解析
{
"code": 200,
"message": "文件上传成功",
"data": {
"screenplay_id": "uuid",
"content": "剧本内容...",
"word_count": 1500,
"parsing_status": "completed"
}
}
// 异步解析
{
"code": 200,
"message": "文件上传成功,正在解析...",
"data": {
"screenplay_id": "uuid",
"parsing_status": "parsing",
"task_id": "celery-task-id"
}
}
权限要求: 需要项目编辑权限(editor)
文件: server/app/api/v1/screenplays.py:373-489
POST /api/v1/screenplays/{screenplay_id}/parse-file
功能: 手动触发文件解析任务
请求参数:
screenplay_id(Path): 剧本 IDforce(Body, Optional): 是否强制重新解析
应用场景:
- 解析失败后重试
- 强制重新解析已解析的文件
验证逻辑:
# 检查是否为文件类型剧本
if screenplay.type != 2: # ScreenplayType.FILE
raise ValidationError("只能解析文件类型的剧本")
# 检查是否已经在解析中
if screenplay.parsing_status == 'parsing' and not force:
raise ValidationError("剧本正在解析中,请稍后再试")
响应示例:
{
"code": 200,
"message": "文件解析任务已提交",
"data": {
"screenplay_id": "uuid",
"parsing_status": "parsing",
"task_id": "celery-task-id"
}
}
权限要求: 需要项目编辑权限(editor)
文件: server/app/api/v1/screenplays.py:491-582
GET /api/v1/screenplays/{screenplay_id}/parse-status
功能: 查询剧本文件解析状态和进度
请求参数:
screenplay_id(Path): 剧本 ID
状态映射:
status_messages = {
'idle': '未开始解析',
'pending': '等待解析',
'parsing': '正在解析文件...',
'completed': '文件解析完成',
'failed': '文件解析失败'
}
进度计算:
parsing: 50%completed: 100%failed: 0%
响应示例:
// 解析中
{
"code": 200,
"data": {
"screenplay_id": "uuid",
"parsing_status": "parsing",
"progress": 50,
"message": "正在解析文件..."
}
}
// 解析完成
{
"code": 200,
"data": {
"screenplay_id": "uuid",
"parsing_status": "completed",
"progress": 100,
"content": "剧本内容...",
"word_count": 1500,
"message": "文件解析完成"
}
}
// 解析失败
{
"code": 200,
"data": {
"screenplay_id": "uuid",
"parsing_status": "failed",
"progress": 0,
"parsing_error": "不支持的文件格式",
"message": "文件解析失败"
}
}
权限要求: 需要项目查看权限(viewer)
文件: server/app/api/v1/screenplays.py:584-661
技术实现细节
文件上传与去重
使用 FileStorageService 实现基于 SHA256 的文件去重:
# 1. 计算文件校验和
checksum = hashlib.sha256(file_content).hexdigest()
# 2. 检查是否已存在
existing = await checksum_repo.get_by_checksum(checksum)
if existing:
# 文件已存在,增加引用计数
await increase_reference_count(existing.id)
return existing.file_url
# 3. 上传新文件到 MinIO
object_name = f"screenplays/{user_id}/{checksum}{extension}"
file_url = await storage.upload_bytes(data=file_content, object_name=object_name)
优势:
- 避免重复存储相同文件
- 节省存储空间
- 提高上传速度(重复文件直接返回)
异步解析任务
使用 Celery 实现异步文件解析,支持重试机制:
@shared_task(bind=True, max_retries=3)
def parse_screenplay_file_task(self, screenplay_id: str, file_path: str, mime_type: str):
try:
result = asyncio.run(_parse_file_async(screenplay_id, file_path, mime_type))
return result
except Exception as e:
if self.request.retries < self.max_retries:
raise self.retry(exc=e, countdown=60) # 1 分钟后重试
else:
return {'screenplay_id': screenplay_id, 'status': 'failed', 'error': str(e)}
特点:
- 最多重试 3 次
- 重试间隔 60 秒
- 达到最大重试次数后标记为失败
文件解析支持
| 文件类型 | MIME Type | 解析方式 | 解析库 |
|---|---|---|---|
| TXT | text/plain | 同步 | 内置 |
| Markdown | text/markdown | 同步 | 内置 |
| DOCX | application/vnd.openxmlformats-officedocument.wordprocessingml.document | 异步 | python-docx |
| DOC | application/msword | 异步 | python-docx |
| application/pdf | 异步 | pdfplumber | |
| RTF | application/rtf, text/rtf | 异步 | striprtf |
字数统计算法
支持中英文混合统计:
def _count_words(self, text: str) -> int:
# 统计中文字符
chinese_chars = re.findall(r'[\u4e00-\u9fff]', text)
chinese_count = len(chinese_chars)
# 移除中文字符后统计英文单词
text_without_chinese = re.sub(r'[\u4e00-\u9fff]', '', text)
english_words = re.findall(r'\b[a-zA-Z]+\b', text_without_chinese)
english_count = len(english_words)
return chinese_count + english_count
数据库变更
无新增表或字段,使用现有 screenplays 表的以下字段:
file_url: 文件访问 URLfile_size: 文件大小(字节)mime_type: 文件 MIME 类型checksum: 文件 SHA256 校验和storage_path: 文件存储路径parsing_status: 解析状态(idle/pending/parsing/completed/failed)parsing_error: 解析错误信息parsed_at: 解析完成时间content: 解析后的文本内容word_count: 字数统计
测试建议
单元测试
-
ScreenplayFileParserService
- 测试
should_parse_async()方法的 MIME 类型判断 - 测试
parse_file_sync()同步解析逻辑 - 测试
_count_words()中英文混合统计 - 测试各种文件格式的解析(TXT, DOCX, PDF, RTF)
- 测试
-
Schema 验证
- 测试
FileUploadResponse的字段验证 - 测试
ParseStatusResponse的条件字段返回
- 测试
集成测试
-
上传并解析流程
# 测试同步解析(TXT) response = await client.post( "/api/v1/screenplays/upload-and-parse", data={"project_id": project_id, "name": "test.txt"}, files={"file": ("test.txt", b"content", "text/plain")} ) assert response.json()["data"]["parsing_status"] == "completed" assert "content" in response.json()["data"] # 测试异步解析(DOCX) response = await client.post( "/api/v1/screenplays/upload-and-parse", data={"project_id": project_id, "name": "test.docx"}, files={"file": ("test.docx", docx_bytes, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")} ) assert response.json()["data"]["parsing_status"] == "parsing" assert "task_id" in response.json()["data"] -
状态查询流程
# 查询解析状态 response = await client.get(f"/api/v1/screenplays/{screenplay_id}/parse-status") assert response.json()["data"]["parsing_status"] in ["idle", "pending", "parsing", "completed", "failed"] -
手动触发解析
# 测试强制重新解析 response = await client.post( f"/api/v1/screenplays/{screenplay_id}/parse-file", json={"force": True} ) assert response.status_code == 202
边界测试
- 大文件上传(> 10MB)
- 不支持的文件格式
- 损坏的文件
- 空文件
- 并发上传相同文件(测试去重)
- 解析中重复触发解析(测试防重)
依赖项
无新增依赖,使用现有库:
python-docx: DOCX 文件解析pdfplumber: PDF 文件解析striprtf: RTF 文件解析aiofiles: 异步文件操作httpx: HTTP 客户端(下载文件)celery: 异步任务队列
后续优化建议
-
性能优化
- 实现文件解析进度实时推送(WebSocket)
- 优化大文件解析性能(分块处理)
- 添加解析结果缓存
-
功能增强
- 支持更多文件格式(PPTX, ODT, Pages)
- 支持文件预览(前 N 行)
- 支持批量上传和解析
-
监控与告警
- 添加解析任务监控(成功率、耗时)
- 添加文件存储监控(空间使用率)
- 添加异常告警(解析失败率过高)
-
安全加固
- 添加文件类型白名单验证
- 添加文件大小限制
- 添加病毒扫描
相关文档
- 需求文档:
docs/requirements/backend/04-services/project/screenplay-file-parser-service.md - API 文档:
server/app/api/v1/screenplays.py - Service 文档:
server/app/services/screenplay_file_parser_service.py - Schema 文档:
server/app/schemas/screenplay.py
作者
- 实现者: Claude Sonnet 4.5
- 审核者: 待定
- 日期: 2026-02-03