2025-10-30 11:14:43 +08:00
|
|
|
"""章节相关的Pydantic模型"""
|
2026-01-09 17:13:19 +08:00
|
|
|
from pydantic import BaseModel, Field, ConfigDict
|
2025-11-27 17:29:23 +08:00
|
|
|
from typing import Optional, List, Dict, Any
|
2025-10-30 11:14:43 +08:00
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChapterBase(BaseModel):
|
|
|
|
|
"""章节基础模型"""
|
|
|
|
|
title: str = Field(..., description="章节标题")
|
|
|
|
|
chapter_number: int = Field(..., description="章节序号")
|
|
|
|
|
content: Optional[str] = Field(None, description="章节内容")
|
|
|
|
|
summary: Optional[str] = Field(None, description="章节摘要")
|
|
|
|
|
word_count: Optional[int] = Field(0, description="字数")
|
|
|
|
|
status: Optional[str] = Field("draft", description="章节状态")
|
2025-11-18 22:14:55 +08:00
|
|
|
outline_id: Optional[str] = Field(None, description="关联的大纲ID")
|
|
|
|
|
sub_index: Optional[int] = Field(1, description="大纲下的子章节序号")
|
|
|
|
|
expansion_plan: Optional[str] = Field(None, description="展开规划详情(JSON)")
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChapterCreate(BaseModel):
|
|
|
|
|
"""创建章节的请求模型"""
|
|
|
|
|
project_id: str = Field(..., description="所属项目ID")
|
|
|
|
|
title: str = Field(..., description="章节标题")
|
|
|
|
|
chapter_number: int = Field(..., description="章节序号")
|
|
|
|
|
content: Optional[str] = Field(None, description="章节内容")
|
|
|
|
|
summary: Optional[str] = Field(None, description="章节摘要")
|
|
|
|
|
status: Optional[str] = Field("draft", description="章节状态")
|
2025-11-18 22:14:55 +08:00
|
|
|
outline_id: Optional[str] = Field(None, description="关联的大纲ID")
|
|
|
|
|
sub_index: Optional[int] = Field(1, description="大纲下的子章节序号")
|
|
|
|
|
expansion_plan: Optional[str] = Field(None, description="展开规划详情(JSON)")
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChapterUpdate(BaseModel):
|
|
|
|
|
"""更新章节的请求模型"""
|
|
|
|
|
title: Optional[str] = None
|
|
|
|
|
content: Optional[str] = None
|
|
|
|
|
# chapter_number 不允许修改,只能通过大纲的重排序来调整
|
|
|
|
|
summary: Optional[str] = None
|
|
|
|
|
# word_count 自动计算,不允许手动修改
|
|
|
|
|
status: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChapterResponse(BaseModel):
|
|
|
|
|
"""章节响应模型"""
|
|
|
|
|
id: str
|
|
|
|
|
project_id: str
|
|
|
|
|
title: str
|
|
|
|
|
chapter_number: int
|
|
|
|
|
content: Optional[str] = None
|
|
|
|
|
summary: Optional[str] = None
|
|
|
|
|
word_count: int = 0
|
|
|
|
|
status: str
|
2025-11-18 22:14:55 +08:00
|
|
|
outline_id: Optional[str] = None
|
|
|
|
|
sub_index: Optional[int] = 1
|
|
|
|
|
expansion_plan: Optional[str] = None
|
|
|
|
|
outline_title: Optional[str] = None # 大纲标题(从Outline表联查)
|
|
|
|
|
outline_order: Optional[int] = None # 大纲排序序号(从Outline表联查)
|
2025-10-30 11:14:43 +08:00
|
|
|
created_at: datetime
|
|
|
|
|
updated_at: datetime
|
|
|
|
|
|
2026-01-09 17:13:19 +08:00
|
|
|
model_config = ConfigDict(from_attributes=True)
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChapterListResponse(BaseModel):
|
|
|
|
|
"""章节列表响应模型"""
|
|
|
|
|
total: int
|
2025-10-31 17:23:25 +08:00
|
|
|
items: list[ChapterResponse]
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 16:27:49 +08:00
|
|
|
class AnalysisTaskStatusResponse(BaseModel):
|
|
|
|
|
"""单章节分析任务状态响应"""
|
|
|
|
|
has_task: bool
|
|
|
|
|
task_id: Optional[str] = None
|
|
|
|
|
chapter_id: str
|
|
|
|
|
status: str
|
|
|
|
|
progress: int = 0
|
|
|
|
|
error_message: Optional[str] = None
|
|
|
|
|
auto_recovered: bool = False
|
|
|
|
|
created_at: Optional[str] = None
|
|
|
|
|
started_at: Optional[str] = None
|
|
|
|
|
completed_at: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchAnalysisStatusRequest(BaseModel):
|
|
|
|
|
"""批量查询分析状态请求"""
|
|
|
|
|
chapter_ids: Optional[List[str]] = Field(None, description="待查询章节ID列表;为空时查询项目下全部章节")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchAnalysisStatusResponse(BaseModel):
|
|
|
|
|
"""批量查询分析状态响应"""
|
|
|
|
|
project_id: str
|
|
|
|
|
total: int
|
|
|
|
|
items: Dict[str, AnalysisTaskStatusResponse]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchAnalyzeUnanalyzedRequest(BaseModel):
|
|
|
|
|
"""一键分析未分析章节请求"""
|
|
|
|
|
chapter_ids: Optional[List[str]] = Field(None, description="可选:限定待分析章节ID列表;为空则自动识别项目内全部未分析章节")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchAnalyzeUnanalyzedResponse(BaseModel):
|
|
|
|
|
"""一键分析未分析章节响应"""
|
|
|
|
|
project_id: str
|
|
|
|
|
total_candidates: int = Field(0, description="候选章节总数(有内容章节)")
|
|
|
|
|
total_started: int = Field(0, description="本次已启动分析任务数")
|
|
|
|
|
total_skipped_no_content: int = Field(0, description="跳过:无内容章节数")
|
|
|
|
|
total_skipped_running: int = Field(0, description="跳过:已在分析中的章节数")
|
|
|
|
|
total_already_completed: int = Field(0, description="跳过:已完成分析章节数")
|
|
|
|
|
started_tasks: Dict[str, AnalysisTaskStatusResponse] = Field(default_factory=dict, description="本次启动的分析任务状态映射")
|
|
|
|
|
|
|
|
|
|
|
2025-10-31 17:23:25 +08:00
|
|
|
class ChapterGenerateRequest(BaseModel):
|
|
|
|
|
"""AI生成章节内容的请求模型"""
|
2025-11-03 15:28:51 +08:00
|
|
|
style_id: Optional[int] = Field(None, description="写作风格ID,不提供则不使用任何风格")
|
|
|
|
|
target_word_count: Optional[int] = Field(
|
|
|
|
|
3000,
|
|
|
|
|
description="目标字数,默认3000字",
|
|
|
|
|
ge=500, # 最小500字
|
|
|
|
|
le=10000 # 最大10000字
|
2025-11-06 12:36:32 +08:00
|
|
|
)
|
2025-11-07 22:14:20 +08:00
|
|
|
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索参考资料)")
|
2025-12-01 21:16:35 +08:00
|
|
|
model: Optional[str] = Field(None, description="指定使用的AI模型,不提供则使用用户默认模型")
|
2025-12-06 14:08:20 +08:00
|
|
|
narrative_perspective: Optional[str] = Field(None, description="临时人称视角:first_person/third_person/omniscient,不提供则使用项目默认")
|
2025-11-06 12:36:32 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchGenerateRequest(BaseModel):
|
|
|
|
|
"""批量生成章节的请求模型"""
|
|
|
|
|
start_chapter_number: int = Field(..., description="起始章节序号")
|
|
|
|
|
count: int = Field(..., description="生成章节数量", ge=1, le=20)
|
|
|
|
|
style_id: Optional[int] = Field(None, description="写作风格ID")
|
|
|
|
|
target_word_count: Optional[int] = Field(
|
|
|
|
|
3000,
|
|
|
|
|
description="目标字数,默认3000字",
|
|
|
|
|
ge=500,
|
|
|
|
|
le=10000
|
|
|
|
|
)
|
|
|
|
|
enable_analysis: bool = Field(False, description="是否启用同步分析")
|
2025-11-07 22:14:20 +08:00
|
|
|
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索参考资料)")
|
2025-11-06 12:36:32 +08:00
|
|
|
max_retries: int = Field(3, description="每个章节的最大重试次数", ge=0, le=5)
|
2025-12-01 21:16:35 +08:00
|
|
|
model: Optional[str] = Field(None, description="指定使用的AI模型,不提供则使用用户默认模型")
|
2026-04-29 08:31:07 +08:00
|
|
|
narrative_perspective: Optional[str] = Field(None, description="临时指定叙事人称,不提供则使用项目默认")
|
2025-11-06 12:36:32 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchGenerateResponse(BaseModel):
|
|
|
|
|
"""批量生成响应模型"""
|
|
|
|
|
batch_id: str = Field(..., description="批次ID")
|
|
|
|
|
message: str = Field(..., description="响应消息")
|
|
|
|
|
chapters_to_generate: list[dict] = Field(..., description="待生成章节列表")
|
|
|
|
|
estimated_time_minutes: int = Field(..., description="预估耗时(分钟)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchGenerateStatusResponse(BaseModel):
|
|
|
|
|
"""批量生成状态响应模型"""
|
|
|
|
|
batch_id: str
|
|
|
|
|
status: str
|
|
|
|
|
total: int
|
|
|
|
|
completed: int
|
|
|
|
|
current_chapter_id: Optional[str] = None
|
|
|
|
|
current_chapter_number: Optional[int] = None
|
|
|
|
|
current_retry_count: Optional[int] = None
|
|
|
|
|
max_retries: Optional[int] = None
|
|
|
|
|
failed_chapters: list[dict] = []
|
|
|
|
|
created_at: Optional[str] = None
|
|
|
|
|
started_at: Optional[str] = None
|
|
|
|
|
completed_at: Optional[str] = None
|
2025-11-27 17:29:23 +08:00
|
|
|
error_message: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SceneData(BaseModel):
|
|
|
|
|
"""场景数据模型"""
|
|
|
|
|
location: str = Field(..., description="场景地点")
|
|
|
|
|
characters: List[str] = Field(..., description="参与角色列表")
|
|
|
|
|
purpose: str = Field(..., description="场景目的")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpansionPlanUpdate(BaseModel):
|
|
|
|
|
"""章节规划更新模型"""
|
2025-11-30 13:00:26 +08:00
|
|
|
summary: Optional[str] = Field(None, description="章节情节概要")
|
2025-11-27 17:29:23 +08:00
|
|
|
key_events: Optional[List[str]] = Field(None, description="关键事件列表")
|
|
|
|
|
character_focus: Optional[List[str]] = Field(None, description="涉及角色列表")
|
|
|
|
|
emotional_tone: Optional[str] = Field(None, description="情感基调")
|
|
|
|
|
narrative_goal: Optional[str] = Field(None, description="叙事目标")
|
|
|
|
|
conflict_type: Optional[str] = Field(None, description="冲突类型")
|
|
|
|
|
estimated_words: Optional[int] = Field(None, description="预估字数", ge=500, le=10000)
|
|
|
|
|
scenes: Optional[List[SceneData]] = Field(None, description="场景列表")
|
|
|
|
|
|
2026-01-09 17:13:19 +08:00
|
|
|
model_config = ConfigDict(json_schema_extra={
|
2025-11-27 17:29:23 +08:00
|
|
|
"example": {
|
|
|
|
|
"key_events": ["主角遇到挑战", "关键决策时刻"],
|
|
|
|
|
"character_focus": ["张三", "李四"],
|
|
|
|
|
"emotional_tone": "紧张激烈",
|
|
|
|
|
"narrative_goal": "推进主线剧情",
|
|
|
|
|
"conflict_type": "内心冲突",
|
|
|
|
|
"estimated_words": 3000,
|
|
|
|
|
"scenes": [
|
|
|
|
|
{
|
|
|
|
|
"location": "城市广场",
|
|
|
|
|
"characters": ["张三", "李四"],
|
|
|
|
|
"purpose": "初次相遇"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-01-09 17:13:19 +08:00
|
|
|
})
|
2025-11-27 17:29:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExpansionPlanResponse(BaseModel):
|
|
|
|
|
"""章节规划响应模型"""
|
|
|
|
|
id: str = Field(..., description="章节ID")
|
|
|
|
|
expansion_plan: Optional[Dict[str, Any]] = Field(None, description="规划数据")
|
2026-01-29 15:33:43 +08:00
|
|
|
message: str = Field(..., description="响应消息")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PartialRegenerateRequest(BaseModel):
|
|
|
|
|
"""局部重写请求参数"""
|
|
|
|
|
selected_text: str = Field(..., description="选中的原文内容")
|
|
|
|
|
start_position: int = Field(..., description="在章节内容中的起始位置(字符索引)", ge=0)
|
|
|
|
|
end_position: int = Field(..., description="在章节内容中的结束位置(字符索引)", ge=0)
|
|
|
|
|
user_instructions: str = Field(..., description="用户的修改要求", min_length=1, max_length=1000)
|
|
|
|
|
|
|
|
|
|
# 可选参数
|
|
|
|
|
context_chars: int = Field(
|
|
|
|
|
500,
|
|
|
|
|
description="上下文截取长度(前后各截取多少字符)",
|
|
|
|
|
ge=100,
|
|
|
|
|
le=2000
|
|
|
|
|
)
|
|
|
|
|
style_id: Optional[int] = Field(None, description="写作风格ID,不提供则使用项目默认风格")
|
|
|
|
|
length_mode: Optional[str] = Field(
|
|
|
|
|
"similar",
|
|
|
|
|
description="字数调整模式:similar(保持相近)/expand(适当扩展)/condense(精简压缩)/custom(自定义)"
|
|
|
|
|
)
|
|
|
|
|
target_word_count: Optional[int] = Field(
|
|
|
|
|
None,
|
|
|
|
|
description="指定目标字数(仅当length_mode为custom时有效)",
|
|
|
|
|
ge=10,
|
|
|
|
|
le=5000
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(json_schema_extra={
|
|
|
|
|
"example": {
|
|
|
|
|
"selected_text": "林霄挥剑斩向敌人,剑光凌厉,一招制敌。",
|
|
|
|
|
"start_position": 1234,
|
|
|
|
|
"end_position": 1260,
|
|
|
|
|
"user_instructions": "增加更细腻的打斗描写,加入主角的心理活动",
|
|
|
|
|
"context_chars": 500,
|
|
|
|
|
"length_mode": "expand"
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PartialRegenerateResponse(BaseModel):
|
|
|
|
|
"""局部重写响应模型"""
|
|
|
|
|
success: bool = Field(..., description="是否成功")
|
|
|
|
|
new_text: str = Field(..., description="重写后的新内容")
|
|
|
|
|
word_count: int = Field(..., description="新内容字数")
|
|
|
|
|
original_word_count: int = Field(..., description="原文字数")
|
|
|
|
|
message: str = Field("重写成功", description="响应消息")
|