update:1.更新大纲细化功能
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Query, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
@@ -125,7 +126,7 @@ async def get_project_chapters(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取指定项目的所有章节(路径参数版本)"""
|
||||
"""获取指定项目的所有章节(带大纲信息)"""
|
||||
# 验证用户权限
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(project_id, user_id, db)
|
||||
@@ -136,7 +137,7 @@ async def get_project_chapters(
|
||||
)
|
||||
total = count_result.scalar_one()
|
||||
|
||||
# 获取章节列表
|
||||
# 获取章节列表,同时加载关联的大纲信息
|
||||
result = await db.execute(
|
||||
select(Chapter)
|
||||
.where(Chapter.project_id == project_id)
|
||||
@@ -144,7 +145,46 @@ async def get_project_chapters(
|
||||
)
|
||||
chapters = result.scalars().all()
|
||||
|
||||
return ChapterListResponse(total=total, items=chapters)
|
||||
# 获取所有大纲信息(用于填充outline_title)
|
||||
outline_ids = [ch.outline_id for ch in chapters if ch.outline_id]
|
||||
outlines_map = {}
|
||||
if outline_ids:
|
||||
outlines_result = await db.execute(
|
||||
select(Outline).where(Outline.id.in_(outline_ids))
|
||||
)
|
||||
outlines_map = {o.id: o for o in outlines_result.scalars().all()}
|
||||
|
||||
# 为所有章节添加大纲信息(统一处理)
|
||||
chapters_with_outline = []
|
||||
for chapter in chapters:
|
||||
chapter_dict = {
|
||||
"id": chapter.id,
|
||||
"project_id": chapter.project_id,
|
||||
"chapter_number": chapter.chapter_number,
|
||||
"title": chapter.title,
|
||||
"content": chapter.content,
|
||||
"summary": chapter.summary,
|
||||
"word_count": chapter.word_count,
|
||||
"status": chapter.status,
|
||||
"outline_id": chapter.outline_id,
|
||||
"sub_index": chapter.sub_index,
|
||||
"expansion_plan": chapter.expansion_plan,
|
||||
"created_at": chapter.created_at,
|
||||
"updated_at": chapter.updated_at,
|
||||
}
|
||||
|
||||
# 添加大纲信息
|
||||
if chapter.outline_id and chapter.outline_id in outlines_map:
|
||||
outline = outlines_map[chapter.outline_id]
|
||||
chapter_dict["outline_title"] = outline.title
|
||||
chapter_dict["outline_order"] = outline.order_index
|
||||
else:
|
||||
chapter_dict["outline_title"] = None
|
||||
chapter_dict["outline_order"] = None
|
||||
|
||||
chapters_with_outline.append(chapter_dict)
|
||||
|
||||
return ChapterListResponse(total=total, items=chapters_with_outline)
|
||||
|
||||
|
||||
@router.get("/{chapter_id}", response_model=ChapterResponse, summary="获取章节详情")
|
||||
|
||||
+959
-164
File diff suppressed because it is too large
Load Diff
@@ -354,7 +354,9 @@ async def export_project_chapters(
|
||||
txt_content.append("\n" + "=" * 80 + "\n\n")
|
||||
|
||||
for chapter in chapters:
|
||||
txt_content.append(f"第 {chapter.chapter_number} 章 {chapter.title}")
|
||||
# 处理子章节序号显示
|
||||
chapter_display = f"{chapter.chapter_number}-{chapter.sub_index}" if chapter.sub_index and chapter.sub_index > 1 else str(chapter.chapter_number)
|
||||
txt_content.append(f"第 {chapter_display} 章 {chapter.title}")
|
||||
txt_content.append("-" * 80)
|
||||
txt_content.append("") # 空行
|
||||
|
||||
|
||||
@@ -17,8 +17,16 @@ class Chapter(Base):
|
||||
summary = Column(Text, comment="章节摘要")
|
||||
word_count = Column(Integer, default=0, comment="字数统计")
|
||||
status = Column(String(20), default="draft", comment="章节状态")
|
||||
|
||||
# 大纲关联字段(实现一对多关系)
|
||||
outline_id = Column(String(36), ForeignKey("outlines.id", ondelete="SET NULL"), nullable=True, comment="关联的大纲ID")
|
||||
sub_index = Column(Integer, default=1, comment="大纲下的子章节序号")
|
||||
|
||||
# 大纲展开规划数据(JSON格式)
|
||||
expansion_plan = Column(Text, comment="展开规划详情(JSON): 包含key_events, character_focus, emotional_tone等")
|
||||
|
||||
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Chapter(id={self.id}, chapter_number={self.chapter_number}, title={self.title})>"
|
||||
return f"<Chapter(id={self.id}, chapter_number={self.chapter_number}, title={self.title}, outline_id={self.outline_id})>"
|
||||
@@ -12,6 +12,9 @@ class ChapterBase(BaseModel):
|
||||
summary: Optional[str] = Field(None, description="章节摘要")
|
||||
word_count: Optional[int] = Field(0, description="字数")
|
||||
status: Optional[str] = Field("draft", description="章节状态")
|
||||
outline_id: Optional[str] = Field(None, description="关联的大纲ID")
|
||||
sub_index: Optional[int] = Field(1, description="大纲下的子章节序号")
|
||||
expansion_plan: Optional[str] = Field(None, description="展开规划详情(JSON)")
|
||||
|
||||
|
||||
class ChapterCreate(BaseModel):
|
||||
@@ -22,6 +25,9 @@ class ChapterCreate(BaseModel):
|
||||
content: Optional[str] = Field(None, description="章节内容")
|
||||
summary: Optional[str] = Field(None, description="章节摘要")
|
||||
status: Optional[str] = Field("draft", description="章节状态")
|
||||
outline_id: Optional[str] = Field(None, description="关联的大纲ID")
|
||||
sub_index: Optional[int] = Field(1, description="大纲下的子章节序号")
|
||||
expansion_plan: Optional[str] = Field(None, description="展开规划详情(JSON)")
|
||||
|
||||
|
||||
class ChapterUpdate(BaseModel):
|
||||
@@ -44,6 +50,11 @@ class ChapterResponse(BaseModel):
|
||||
summary: Optional[str] = None
|
||||
word_count: int = 0
|
||||
status: str
|
||||
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表联查)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@@ -78,12 +78,70 @@ class OutlineListResponse(BaseModel):
|
||||
items: list[OutlineResponse]
|
||||
|
||||
|
||||
class OutlineReorderItem(BaseModel):
|
||||
"""单个大纲重排序项"""
|
||||
id: str = Field(..., description="大纲ID")
|
||||
order_index: int = Field(..., description="新的序号", ge=1)
|
||||
class ChapterPlanItem(BaseModel):
|
||||
"""单个章节规划项"""
|
||||
sub_index: int = Field(..., description="子章节序号", ge=1)
|
||||
title: str = Field(..., description="章节标题")
|
||||
plot_summary: str = Field(..., description="剧情摘要(200-300字)")
|
||||
key_events: list[str] = Field(..., description="关键事件列表")
|
||||
character_focus: list[str] = Field(..., description="主要涉及的角色")
|
||||
emotional_tone: str = Field(..., description="情感基调")
|
||||
narrative_goal: str = Field(..., description="叙事目标")
|
||||
conflict_type: str = Field(..., description="冲突类型")
|
||||
estimated_words: int = Field(3000, description="预计字数", ge=1000)
|
||||
scenes: Optional[list[str]] = Field(None, description="场景列表(可选)")
|
||||
|
||||
|
||||
class OutlineReorderRequest(BaseModel):
|
||||
"""大纲批量重排序请求"""
|
||||
orders: list[OutlineReorderItem] = Field(..., description="排序列表")
|
||||
class OutlineExpansionRequest(BaseModel):
|
||||
"""大纲展开为多章节的请求模型(outline_id从路径参数获取)"""
|
||||
target_chapter_count: int = Field(3, description="目标章节数", ge=1, le=10)
|
||||
expansion_strategy: str = Field("balanced", description="展开策略: balanced(均衡), climax(高潮重点), detail(细节丰富)")
|
||||
enable_scene_analysis: bool = Field(False, description="是否包含场景规划")
|
||||
auto_create_chapters: bool = Field(True, description="是否自动创建章节记录")
|
||||
provider: Optional[str] = Field(None, description="AI提供商")
|
||||
model: Optional[str] = Field(None, description="AI模型")
|
||||
|
||||
|
||||
class OutlineExpansionResponse(BaseModel):
|
||||
"""大纲展开响应模型"""
|
||||
outline_id: str = Field(..., description="大纲ID")
|
||||
outline_title: str = Field(..., description="大纲标题")
|
||||
target_chapter_count: int = Field(..., description="目标章节数")
|
||||
actual_chapter_count: int = Field(..., description="实际生成的章节数")
|
||||
expansion_strategy: str = Field(..., description="使用的展开策略")
|
||||
chapter_plans: list[ChapterPlanItem] = Field(..., description="章节规划列表")
|
||||
created_chapters: Optional[list] = Field(None, description="已创建的章节列表")
|
||||
|
||||
|
||||
class BatchOutlineExpansionRequest(BaseModel):
|
||||
"""批量大纲展开请求模型"""
|
||||
project_id: str = Field(..., description="项目ID")
|
||||
outline_ids: Optional[list[str]] = Field(None, description="要展开的大纲ID列表(为空则展开所有)")
|
||||
chapters_per_outline: int = Field(3, description="每个大纲的目标章节数", ge=1, le=10)
|
||||
expansion_strategy: str = Field("balanced", description="展开策略")
|
||||
enable_scene_analysis: bool = Field(False, description="是否包含场景规划")
|
||||
auto_create_chapters: bool = Field(True, description="是否自动创建章节记录")
|
||||
provider: Optional[str] = Field(None, description="AI提供商")
|
||||
model: Optional[str] = Field(None, description="AI模型")
|
||||
|
||||
|
||||
class BatchOutlineExpansionResponse(BaseModel):
|
||||
"""批量大纲展开响应模型"""
|
||||
project_id: str = Field(..., description="项目ID")
|
||||
total_outlines_expanded: int = Field(..., description="总共展开的大纲数")
|
||||
total_chapters_created: int = Field(..., description="总共创建的章节数")
|
||||
expansion_results: list[OutlineExpansionResponse] = Field(..., description="展开结果列表")
|
||||
skipped_outlines: Optional[list[dict]] = Field(None, description="跳过的大纲列表(已展开)")
|
||||
|
||||
|
||||
class CreateChaptersFromPlansRequest(BaseModel):
|
||||
"""根据已有规划创建章节的请求模型"""
|
||||
chapter_plans: list[ChapterPlanItem] = Field(..., description="章节规划列表(来自之前的AI生成结果)")
|
||||
|
||||
|
||||
class CreateChaptersFromPlansResponse(BaseModel):
|
||||
"""根据已有规划创建章节的响应模型"""
|
||||
outline_id: str = Field(..., description="大纲ID")
|
||||
outline_title: str = Field(..., description="大纲标题")
|
||||
chapters_created: int = Field(..., description="创建的章节数")
|
||||
created_chapters: list = Field(..., description="创建的章节列表")
|
||||
@@ -0,0 +1,723 @@
|
||||
"""大纲剧情展开服务 - 将大纲节点展开为多个章节"""
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
import json
|
||||
|
||||
from app.models.outline import Outline
|
||||
from app.models.project import Project
|
||||
from app.models.character import Character
|
||||
from app.models.chapter import Chapter
|
||||
from app.services.ai_service import AIService
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PlotExpansionService:
|
||||
"""大纲剧情展开服务"""
|
||||
|
||||
def __init__(self, ai_service: AIService):
|
||||
self.ai_service = ai_service
|
||||
|
||||
async def analyze_outline_for_chapters(
|
||||
self,
|
||||
outline: Outline,
|
||||
project: Project,
|
||||
db: AsyncSession,
|
||||
target_chapter_count: int = 3,
|
||||
expansion_strategy: str = "balanced",
|
||||
enable_scene_analysis: bool = True,
|
||||
provider: Optional[str] = None,
|
||||
model: Optional[str] = None,
|
||||
batch_size: int = 5,
|
||||
progress_callback: Optional[callable] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
分析单个大纲,生成多章节规划(支持分批生成)
|
||||
|
||||
Args:
|
||||
outline: 大纲对象
|
||||
project: 项目对象
|
||||
db: 数据库会话
|
||||
target_chapter_count: 目标生成章节数
|
||||
expansion_strategy: 展开策略(balanced/climax/detail)
|
||||
enable_scene_analysis: 是否启用场景级分析
|
||||
provider: AI提供商
|
||||
model: AI模型
|
||||
batch_size: 每批生成的章节数(默认5章)
|
||||
progress_callback: 进度回调函数(可选)
|
||||
|
||||
Returns:
|
||||
章节规划列表
|
||||
"""
|
||||
logger.info(f"开始分析大纲 {outline.id},目标生成 {target_chapter_count} 章")
|
||||
|
||||
# 如果章节数较少,直接生成
|
||||
if target_chapter_count <= batch_size:
|
||||
return await self._generate_chapters_single_batch(
|
||||
outline=outline,
|
||||
project=project,
|
||||
db=db,
|
||||
target_chapter_count=target_chapter_count,
|
||||
expansion_strategy=expansion_strategy,
|
||||
enable_scene_analysis=enable_scene_analysis,
|
||||
provider=provider,
|
||||
model=model
|
||||
)
|
||||
|
||||
# 章节数较多,分批生成
|
||||
logger.info(f"章节数({target_chapter_count})超过批次大小({batch_size}),启用分批生成")
|
||||
return await self._generate_chapters_in_batches(
|
||||
outline=outline,
|
||||
project=project,
|
||||
db=db,
|
||||
target_chapter_count=target_chapter_count,
|
||||
expansion_strategy=expansion_strategy,
|
||||
enable_scene_analysis=enable_scene_analysis,
|
||||
provider=provider,
|
||||
model=model,
|
||||
batch_size=batch_size,
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
|
||||
async def _generate_chapters_single_batch(
|
||||
self,
|
||||
outline: Outline,
|
||||
project: Project,
|
||||
db: AsyncSession,
|
||||
target_chapter_count: int,
|
||||
expansion_strategy: str,
|
||||
enable_scene_analysis: bool,
|
||||
provider: Optional[str],
|
||||
model: Optional[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""单批次生成章节规划"""
|
||||
# 获取角色信息
|
||||
characters_result = await db.execute(
|
||||
select(Character).where(Character.project_id == project.id)
|
||||
)
|
||||
characters = characters_result.scalars().all()
|
||||
characters_info = "\n".join([
|
||||
f"- {char.name} ({'组织' if char.is_organization else '角色'}, {char.role_type}): "
|
||||
f"{char.personality[:100] if char.personality else '暂无描述'}"
|
||||
for char in characters
|
||||
])
|
||||
|
||||
# 获取大纲上下文(前后大纲)
|
||||
context_info = await self._get_outline_context(outline, project.id, db)
|
||||
|
||||
# 构建分析提示词
|
||||
prompt = self._build_expansion_prompt(
|
||||
outline=outline,
|
||||
project=project,
|
||||
characters_info=characters_info,
|
||||
context_info=context_info,
|
||||
target_chapter_count=target_chapter_count,
|
||||
expansion_strategy=expansion_strategy,
|
||||
enable_scene_analysis=enable_scene_analysis
|
||||
)
|
||||
|
||||
# 调用AI生成章节规划
|
||||
logger.info(f"调用AI生成章节规划...")
|
||||
ai_response = await self.ai_service.generate_text(
|
||||
prompt=prompt,
|
||||
provider=provider,
|
||||
model=model
|
||||
)
|
||||
|
||||
# 提取内容
|
||||
ai_content = ai_response.get("content", "") if isinstance(ai_response, dict) else ai_response
|
||||
|
||||
# 解析AI响应
|
||||
chapter_plans = self._parse_expansion_response(ai_content, outline.id)
|
||||
|
||||
logger.info(f"成功生成 {len(chapter_plans)} 个章节规划")
|
||||
return chapter_plans
|
||||
|
||||
async def _generate_chapters_in_batches(
|
||||
self,
|
||||
outline: Outline,
|
||||
project: Project,
|
||||
db: AsyncSession,
|
||||
target_chapter_count: int,
|
||||
expansion_strategy: str,
|
||||
enable_scene_analysis: bool,
|
||||
provider: Optional[str],
|
||||
model: Optional[str],
|
||||
batch_size: int,
|
||||
progress_callback: Optional[callable]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""分批生成章节规划"""
|
||||
# 计算批次数
|
||||
total_batches = (target_chapter_count + batch_size - 1) // batch_size
|
||||
logger.info(f"分批生成计划: 总共{target_chapter_count}章,分{total_batches}批,每批{batch_size}章")
|
||||
|
||||
# 获取角色信息(所有批次共用)
|
||||
characters_result = await db.execute(
|
||||
select(Character).where(Character.project_id == project.id)
|
||||
)
|
||||
characters = characters_result.scalars().all()
|
||||
characters_info = "\n".join([
|
||||
f"- {char.name} ({'组织' if char.is_organization else '角色'}, {char.role_type}): "
|
||||
f"{char.personality[:100] if char.personality else '暂无描述'}"
|
||||
for char in characters
|
||||
])
|
||||
|
||||
# 获取大纲上下文
|
||||
context_info = await self._get_outline_context(outline, project.id, db)
|
||||
|
||||
all_chapter_plans = []
|
||||
|
||||
for batch_num in range(total_batches):
|
||||
# 计算当前批次的章节数
|
||||
remaining_chapters = target_chapter_count - len(all_chapter_plans)
|
||||
current_batch_size = min(batch_size, remaining_chapters)
|
||||
current_start_index = len(all_chapter_plans) + 1
|
||||
|
||||
logger.info(f"开始生成第{batch_num + 1}/{total_batches}批,章节范围: {current_start_index}-{current_start_index + current_batch_size - 1}")
|
||||
|
||||
# 回调通知进度
|
||||
if progress_callback:
|
||||
await progress_callback(batch_num + 1, total_batches, current_start_index, current_batch_size)
|
||||
|
||||
# 构建当前批次的提示词(包含已生成章节的上下文)
|
||||
prompt = self._build_batch_expansion_prompt(
|
||||
outline=outline,
|
||||
project=project,
|
||||
characters_info=characters_info,
|
||||
context_info=context_info,
|
||||
target_chapter_count=current_batch_size,
|
||||
expansion_strategy=expansion_strategy,
|
||||
enable_scene_analysis=enable_scene_analysis,
|
||||
start_index=current_start_index,
|
||||
previous_chapters=all_chapter_plans,
|
||||
total_chapters=target_chapter_count
|
||||
)
|
||||
|
||||
# 调用AI生成当前批次
|
||||
logger.info(f"调用AI生成第{batch_num + 1}批...")
|
||||
ai_response = await self.ai_service.generate_text(
|
||||
prompt=prompt,
|
||||
provider=provider,
|
||||
model=model
|
||||
)
|
||||
|
||||
# 提取内容
|
||||
ai_content = ai_response.get("content", "") if isinstance(ai_response, dict) else ai_response
|
||||
|
||||
# 解析AI响应
|
||||
batch_plans = self._parse_expansion_response(ai_content, outline.id)
|
||||
|
||||
# 调整sub_index以保持连续性
|
||||
for i, plan in enumerate(batch_plans):
|
||||
plan["sub_index"] = current_start_index + i
|
||||
|
||||
all_chapter_plans.extend(batch_plans)
|
||||
|
||||
logger.info(f"第{batch_num + 1}批生成完成,本批生成{len(batch_plans)}章,累计{len(all_chapter_plans)}章")
|
||||
|
||||
logger.info(f"分批生成完成,共生成 {len(all_chapter_plans)} 个章节规划")
|
||||
return all_chapter_plans
|
||||
|
||||
async def batch_expand_outlines(
|
||||
self,
|
||||
project_id: str,
|
||||
db: AsyncSession,
|
||||
ai_service: AIService,
|
||||
target_chapters_per_outline: int = 3,
|
||||
expansion_strategy: str = "balanced",
|
||||
provider: Optional[str] = None,
|
||||
model: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量展开所有大纲为章节
|
||||
|
||||
Returns:
|
||||
{
|
||||
"total_outlines": 总大纲数,
|
||||
"total_chapters_planned": 规划的总章节数,
|
||||
"expansions": [每个大纲的展开结果]
|
||||
}
|
||||
"""
|
||||
logger.info(f"开始批量展开项目 {project_id} 的所有大纲")
|
||||
|
||||
# 获取项目
|
||||
project_result = await db.execute(
|
||||
select(Project).where(Project.id == project_id)
|
||||
)
|
||||
project = project_result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise ValueError(f"项目 {project_id} 不存在")
|
||||
|
||||
# 获取所有大纲
|
||||
outlines_result = await db.execute(
|
||||
select(Outline)
|
||||
.where(Outline.project_id == project_id)
|
||||
.order_by(Outline.order_index)
|
||||
)
|
||||
outlines = outlines_result.scalars().all()
|
||||
|
||||
if not outlines:
|
||||
logger.warning(f"项目 {project_id} 没有大纲")
|
||||
return {
|
||||
"total_outlines": 0,
|
||||
"total_chapters_planned": 0,
|
||||
"expansions": []
|
||||
}
|
||||
|
||||
# 逐个展开大纲
|
||||
expansions = []
|
||||
total_chapters = 0
|
||||
|
||||
for outline in outlines:
|
||||
try:
|
||||
chapter_plans = await self.analyze_outline_for_chapters(
|
||||
outline=outline,
|
||||
project=project,
|
||||
db=db,
|
||||
target_chapter_count=target_chapters_per_outline,
|
||||
expansion_strategy=expansion_strategy,
|
||||
provider=provider,
|
||||
model=model
|
||||
)
|
||||
|
||||
expansions.append({
|
||||
"outline_id": outline.id,
|
||||
"outline_title": outline.title,
|
||||
"chapter_plans": chapter_plans,
|
||||
"chapter_count": len(chapter_plans)
|
||||
})
|
||||
|
||||
total_chapters += len(chapter_plans)
|
||||
logger.info(f"大纲 {outline.title} 展开为 {len(chapter_plans)} 章")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"展开大纲 {outline.id} 失败: {str(e)}")
|
||||
expansions.append({
|
||||
"outline_id": outline.id,
|
||||
"outline_title": outline.title,
|
||||
"error": str(e),
|
||||
"chapter_count": 0
|
||||
})
|
||||
|
||||
result = {
|
||||
"total_outlines": len(outlines),
|
||||
"total_chapters_planned": total_chapters,
|
||||
"expansions": expansions
|
||||
}
|
||||
|
||||
logger.info(f"批量展开完成: {len(outlines)} 个大纲 → {total_chapters} 个章节规划")
|
||||
return result
|
||||
|
||||
async def create_chapters_from_plans(
|
||||
self,
|
||||
outline_id: str,
|
||||
chapter_plans: List[Dict[str, Any]],
|
||||
project_id: str,
|
||||
db: AsyncSession,
|
||||
start_chapter_number: int = None
|
||||
) -> List[Chapter]:
|
||||
"""
|
||||
根据章节规划创建实际的章节记录
|
||||
|
||||
Args:
|
||||
outline_id: 大纲ID
|
||||
chapter_plans: 章节规划列表
|
||||
project_id: 项目ID
|
||||
db: 数据库会话
|
||||
start_chapter_number: 起始章节号(如果为None,则自动计算)
|
||||
|
||||
Returns:
|
||||
创建的章节列表
|
||||
"""
|
||||
logger.info(f"根据规划创建 {len(chapter_plans)} 个章节记录")
|
||||
|
||||
# 如果没有指定起始章节号,自动计算
|
||||
if start_chapter_number is None:
|
||||
# 查询项目中已有章节的最大序号
|
||||
max_number_result = await db.execute(
|
||||
select(func.max(Chapter.chapter_number))
|
||||
.where(Chapter.project_id == project_id)
|
||||
)
|
||||
max_number = max_number_result.scalar()
|
||||
start_chapter_number = (max_number or 0) + 1
|
||||
logger.info(f"自动计算起始章节号: {start_chapter_number} (当前最大序号: {max_number})")
|
||||
|
||||
chapters = []
|
||||
for idx, plan in enumerate(chapter_plans):
|
||||
# 保存完整的展开规划数据(JSON格式)
|
||||
expansion_plan_json = json.dumps({
|
||||
"key_events": plan.get("key_events", []),
|
||||
"character_focus": plan.get("character_focus", []),
|
||||
"emotional_tone": plan.get("emotional_tone", ""),
|
||||
"narrative_goal": plan.get("narrative_goal", ""),
|
||||
"conflict_type": plan.get("conflict_type", ""),
|
||||
"estimated_words": plan.get("estimated_words", 3000),
|
||||
"scenes": plan.get("scenes", []) if plan.get("scenes") else None
|
||||
}, ensure_ascii=False)
|
||||
|
||||
chapter = Chapter(
|
||||
project_id=project_id,
|
||||
outline_id=outline_id,
|
||||
chapter_number=start_chapter_number + idx,
|
||||
sub_index=plan.get("sub_index", idx + 1),
|
||||
title=plan.get("title", f"第{start_chapter_number + idx}章"),
|
||||
summary=plan.get("plot_summary", ""),
|
||||
expansion_plan=expansion_plan_json,
|
||||
status="draft"
|
||||
)
|
||||
db.add(chapter)
|
||||
chapters.append(chapter)
|
||||
|
||||
await db.commit()
|
||||
|
||||
for chapter in chapters:
|
||||
await db.refresh(chapter)
|
||||
|
||||
logger.info(f"成功创建 {len(chapters)} 个章节记录(已保存展开规划数据)")
|
||||
return chapters
|
||||
|
||||
async def _get_outline_context(
|
||||
self,
|
||||
outline: Outline,
|
||||
project_id: str,
|
||||
db: AsyncSession
|
||||
) -> str:
|
||||
"""获取大纲的上下文(前后大纲)"""
|
||||
# 获取前一个大纲
|
||||
prev_result = await db.execute(
|
||||
select(Outline)
|
||||
.where(
|
||||
Outline.project_id == project_id,
|
||||
Outline.order_index < outline.order_index
|
||||
)
|
||||
.order_by(Outline.order_index.desc())
|
||||
.limit(1)
|
||||
)
|
||||
prev_outline = prev_result.scalar_one_or_none()
|
||||
|
||||
# 获取后一个大纲
|
||||
next_result = await db.execute(
|
||||
select(Outline)
|
||||
.where(
|
||||
Outline.project_id == project_id,
|
||||
Outline.order_index > outline.order_index
|
||||
)
|
||||
.order_by(Outline.order_index)
|
||||
.limit(1)
|
||||
)
|
||||
next_outline = next_result.scalar_one_or_none()
|
||||
|
||||
context = ""
|
||||
if prev_outline:
|
||||
context += f"【前一节】{prev_outline.title}: {prev_outline.content[:200]}...\n\n"
|
||||
if next_outline:
|
||||
context += f"【后一节】{next_outline.title}: {next_outline.content[:200]}...\n"
|
||||
|
||||
return context if context else "(无前后文)"
|
||||
|
||||
def _build_expansion_prompt(
|
||||
self,
|
||||
outline: Outline,
|
||||
project: Project,
|
||||
characters_info: str,
|
||||
context_info: str,
|
||||
target_chapter_count: int,
|
||||
expansion_strategy: str,
|
||||
enable_scene_analysis: bool
|
||||
) -> str:
|
||||
"""构建大纲展开提示词"""
|
||||
|
||||
strategy_desc = {
|
||||
"balanced": "均衡展开:每章剧情量相当,节奏平稳",
|
||||
"climax": "高潮重点:重点章节剧情丰富,其他章节简洁过渡",
|
||||
"detail": "细节丰富:每章都深入描写,场景和情感细腻"
|
||||
}
|
||||
|
||||
strategy_instruction = strategy_desc.get(expansion_strategy, strategy_desc["balanced"])
|
||||
|
||||
# 场景字段(避免f-string中的反斜杠)
|
||||
scene_field = ',\n "main_scenes": ["场景1", "场景2"]' if enable_scene_analysis else ''
|
||||
|
||||
scene_instruction = ""
|
||||
if enable_scene_analysis:
|
||||
scene_instruction = """
|
||||
5. 场景分析(每章需包含):
|
||||
- 主要场景地点
|
||||
- 场景氛围
|
||||
- 关键道具/环境元素
|
||||
"""
|
||||
|
||||
prompt = f"""你是专业的小说情节架构师。请分析以下大纲节点,将其展开为 {target_chapter_count} 个章节的详细规划。
|
||||
|
||||
【项目信息】
|
||||
小说名称:{project.title}
|
||||
类型:{project.genre or '通用'}
|
||||
主题:{project.theme or '未设定'}
|
||||
叙事视角:{project.narrative_perspective or '第三人称'}
|
||||
|
||||
【世界观背景】
|
||||
时间背景:{project.world_time_period or '未设定'}
|
||||
地理位置:{project.world_location or '未设定'}
|
||||
氛围基调:{project.world_atmosphere or '未设定'}
|
||||
|
||||
【角色信息】
|
||||
{characters_info or '暂无角色'}
|
||||
|
||||
【当前大纲节点 - 展开对象】
|
||||
序号:第 {outline.order_index} 节
|
||||
标题:{outline.title}
|
||||
内容:{outline.content}
|
||||
|
||||
【上下文参考】
|
||||
{context_info}
|
||||
|
||||
【展开策略】
|
||||
{strategy_instruction}
|
||||
|
||||
【⚠️ 重要约束 - 必须严格遵守】
|
||||
1. **内容边界约束**:
|
||||
- ✅ 只能展开【当前大纲节点】中明确描述的内容
|
||||
- ❌ 绝对不能推进到后续大纲的内容(如果有【后一节】信息)
|
||||
- ❌ 不要让剧情快速推进,要深化而非跨越
|
||||
|
||||
2. **展开原则**:
|
||||
- 将当前大纲的单一事件拆解为多个细节丰富的章节
|
||||
- 深入挖掘情感、心理、环境、对话等细节
|
||||
- 放慢叙事节奏,让读者充分体验当前阶段的剧情
|
||||
- 每个章节都应该是当前大纲内容的不同侧面或阶段
|
||||
|
||||
3. **如何避免剧情越界**:
|
||||
- 如果当前大纲描述"主角遇到困境",展开时应详写困境的发现、分析、情感冲击等
|
||||
- 不要直接写到"解决困境",除非原大纲明确包含解决过程
|
||||
- 如果看到【后一节】的内容,那些是禁区,绝不提前展开
|
||||
|
||||
【任务要求】
|
||||
1. 深度分析该大纲的剧情容量和叙事节奏
|
||||
2. 识别关键剧情点、冲突点和情感转折点(仅限当前大纲范围内)
|
||||
3. 将大纲拆解为 {target_chapter_count} 个章节,每章需包含:
|
||||
- sub_index: 子章节序号(1, 2, 3...)
|
||||
- title: 章节标题(体现该章核心冲突或情感)
|
||||
- plot_summary: 剧情摘要(200-300字,详细描述该章发生的事件,仅限当前大纲内容)
|
||||
- key_events: 关键事件列表(3-5个关键剧情点,必须在当前大纲范围内)
|
||||
- character_focus: 角色焦点(主要涉及的角色名称)
|
||||
- emotional_tone: 情感基调(如:紧张、温馨、悲伤、激动等)
|
||||
- narrative_goal: 叙事目标(该章要达成的叙事效果)
|
||||
- conflict_type: 冲突类型(如:内心挣扎、人际冲突、环境挑战等)
|
||||
- estimated_words: 预计字数(建议2000-5000字)
|
||||
{scene_instruction}
|
||||
4. 确保章节间:
|
||||
- 衔接自然流畅
|
||||
- 剧情递进合理(但不超出当前大纲边界)
|
||||
- 节奏张弛有度
|
||||
- 每章都有明确的叙事价值
|
||||
- 最后一章结束时,剧情发展程度应恰好完成当前大纲描述的内容,不多不少
|
||||
|
||||
【输出格式】
|
||||
请严格按照以下JSON数组格式输出,不要添加任何其他文字:
|
||||
[
|
||||
{{
|
||||
"sub_index": 1,
|
||||
"title": "章节标题",
|
||||
"plot_summary": "该章详细剧情摘要...",
|
||||
"key_events": ["关键事件1", "关键事件2", "关键事件3"],
|
||||
"character_focus": ["角色A", "角色B"],
|
||||
"emotional_tone": "情感基调",
|
||||
"narrative_goal": "叙事目标",
|
||||
"conflict_type": "冲突类型",
|
||||
"estimated_words": 3000{scene_field}
|
||||
}}
|
||||
]
|
||||
|
||||
请开始分析并生成章节规划:
|
||||
"""
|
||||
return prompt
|
||||
|
||||
def _build_batch_expansion_prompt(
|
||||
self,
|
||||
outline: Outline,
|
||||
project: Project,
|
||||
characters_info: str,
|
||||
context_info: str,
|
||||
target_chapter_count: int,
|
||||
expansion_strategy: str,
|
||||
enable_scene_analysis: bool,
|
||||
start_index: int,
|
||||
previous_chapters: List[Dict[str, Any]],
|
||||
total_chapters: int
|
||||
) -> str:
|
||||
"""构建分批展开提示词"""
|
||||
|
||||
strategy_desc = {
|
||||
"balanced": "均衡展开:每章剧情量相当,节奏平稳",
|
||||
"climax": "高潮重点:重点章节剧情丰富,其他章节简洁过渡",
|
||||
"detail": "细节丰富:每章都深入描写,场景和情感细腻"
|
||||
}
|
||||
|
||||
strategy_instruction = strategy_desc.get(expansion_strategy, strategy_desc["balanced"])
|
||||
|
||||
# 场景字段
|
||||
scene_field = ',\n "main_scenes": ["场景1", "场景2"]' if enable_scene_analysis else ''
|
||||
|
||||
scene_instruction = ""
|
||||
if enable_scene_analysis:
|
||||
scene_instruction = """
|
||||
5. 场景分析(每章需包含):
|
||||
- 主要场景地点
|
||||
- 场景氛围
|
||||
- 关键道具/环境元素
|
||||
"""
|
||||
|
||||
# 构建已生成章节的摘要
|
||||
previous_context = ""
|
||||
if previous_chapters:
|
||||
previous_summaries = []
|
||||
for ch in previous_chapters[-3:]: # 只显示最近3章
|
||||
previous_summaries.append(
|
||||
f"第{ch['sub_index']}节《{ch['title']}》: {ch['plot_summary'][:100]}..."
|
||||
)
|
||||
previous_context = f"""
|
||||
【已生成章节概要】(接续生成,注意衔接)
|
||||
{chr(10).join(previous_summaries)}
|
||||
|
||||
⚠️ 当前是第{start_index}-{start_index + target_chapter_count - 1}节(共{total_chapters}节中的一部分)
|
||||
"""
|
||||
|
||||
prompt = f"""你是专业的小说情节架构师。请继续分析以下大纲节点,将其展开为第{start_index}-{start_index + target_chapter_count - 1}节(共{target_chapter_count}个章节)的详细规划。
|
||||
|
||||
【项目信息】
|
||||
小说名称:{project.title}
|
||||
类型:{project.genre or '通用'}
|
||||
主题:{project.theme or '未设定'}
|
||||
叙事视角:{project.narrative_perspective or '第三人称'}
|
||||
|
||||
【世界观背景】
|
||||
时间背景:{project.world_time_period or '未设定'}
|
||||
地理位置:{project.world_location or '未设定'}
|
||||
氛围基调:{project.world_atmosphere or '未设定'}
|
||||
|
||||
【角色信息】
|
||||
{characters_info or '暂无角色'}
|
||||
|
||||
【当前大纲节点 - 展开对象】
|
||||
序号:第 {outline.order_index} 节
|
||||
标题:{outline.title}
|
||||
内容:{outline.content}
|
||||
|
||||
【上下文参考】
|
||||
{context_info}
|
||||
{previous_context}
|
||||
|
||||
【展开策略】
|
||||
{strategy_instruction}
|
||||
|
||||
【⚠️ 重要约束 - 必须严格遵守】
|
||||
1. **内容边界约束**:
|
||||
- ✅ 只能展开【当前大纲节点】中明确描述的内容
|
||||
- ❌ 绝对不能推进到后续大纲的内容(如果有【后一节】信息)
|
||||
- ❌ 不要让剧情快速推进,要深化而非跨越
|
||||
|
||||
2. **分批连续性约束**:
|
||||
- 这是第{start_index}-{start_index + target_chapter_count - 1}节,是整个展开的一部分
|
||||
- 必须与前面已生成的章节自然衔接
|
||||
- 从第{start_index}节开始编号(sub_index从{start_index}开始)
|
||||
- 继续深化当前大纲的内容,保持叙事连贯性
|
||||
|
||||
3. **展开原则**:
|
||||
- 将当前大纲的单一事件拆解为多个细节丰富的章节
|
||||
- 深入挖掘情感、心理、环境、对话等细节
|
||||
- 放慢叙事节奏,让读者充分体验当前阶段的剧情
|
||||
- 每个章节都应该是当前大纲内容的不同侧面或阶段
|
||||
|
||||
【任务要求】
|
||||
1. 深度分析该大纲的剧情容量和叙事节奏
|
||||
2. 识别关键剧情点、冲突点和情感转折点(仅限当前大纲范围内)
|
||||
3. 生成第{start_index}-{start_index + target_chapter_count - 1}节的章节规划,每章需包含:
|
||||
- sub_index: 子章节序号(从{start_index}开始)
|
||||
- title: 章节标题(体现该章核心冲突或情感)
|
||||
- plot_summary: 剧情摘要(200-300字,详细描述该章发生的事件)
|
||||
- key_events: 关键事件列表(3-5个关键剧情点)
|
||||
- character_focus: 角色焦点(主要涉及的角色名称)
|
||||
- emotional_tone: 情感基调(如:紧张、温馨、悲伤、激动等)
|
||||
- narrative_goal: 叙事目标(该章要达成的叙事效果)
|
||||
- conflict_type: 冲突类型(如:内心挣扎、人际冲突、环境挑战等)
|
||||
- estimated_words: 预计字数(建议2000-5000字)
|
||||
{scene_instruction}
|
||||
4. 确保章节间:
|
||||
- 与前面章节衔接自然流畅
|
||||
- 剧情递进合理(但不超出当前大纲边界)
|
||||
- 节奏张弛有度
|
||||
- 每章都有明确的叙事价值
|
||||
|
||||
【输出格式】
|
||||
请严格按照以下JSON数组格式输出,不要添加任何其他文字:
|
||||
[
|
||||
{{
|
||||
"sub_index": {start_index},
|
||||
"title": "章节标题",
|
||||
"plot_summary": "该章详细剧情摘要...",
|
||||
"key_events": ["关键事件1", "关键事件2", "关键事件3"],
|
||||
"character_focus": ["角色A", "角色B"],
|
||||
"emotional_tone": "情感基调",
|
||||
"narrative_goal": "叙事目标",
|
||||
"conflict_type": "冲突类型",
|
||||
"estimated_words": 3000{scene_field}
|
||||
}}
|
||||
]
|
||||
|
||||
请开始分析并生成第{start_index}-{start_index + target_chapter_count - 1}节的章节规划:
|
||||
"""
|
||||
return prompt
|
||||
|
||||
def _parse_expansion_response(
|
||||
self,
|
||||
ai_response: str,
|
||||
outline_id: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""解析AI的展开响应"""
|
||||
try:
|
||||
# 清理响应文本
|
||||
cleaned_text = ai_response.strip()
|
||||
if cleaned_text.startswith('```json'):
|
||||
cleaned_text = cleaned_text[7:]
|
||||
if cleaned_text.startswith('```'):
|
||||
cleaned_text = cleaned_text[3:]
|
||||
if cleaned_text.endswith('```'):
|
||||
cleaned_text = cleaned_text[:-3]
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
# 解析JSON
|
||||
chapter_plans = json.loads(cleaned_text)
|
||||
|
||||
# 确保是列表
|
||||
if not isinstance(chapter_plans, list):
|
||||
chapter_plans = [chapter_plans]
|
||||
|
||||
# 为每个章节规划添加outline_id
|
||||
for plan in chapter_plans:
|
||||
plan["outline_id"] = outline_id
|
||||
|
||||
return chapter_plans
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析AI响应失败: {e}, 响应内容: {ai_response[:500]}")
|
||||
# 返回一个基础规划
|
||||
return [{
|
||||
"outline_id": outline_id,
|
||||
"sub_index": 1,
|
||||
"title": "AI解析失败的默认章节",
|
||||
"plot_summary": ai_response[:500],
|
||||
"key_events": ["解析失败"],
|
||||
"character_focus": [],
|
||||
"emotional_tone": "未知",
|
||||
"narrative_goal": "需要重新生成",
|
||||
"conflict_type": "未知",
|
||||
"estimated_words": 3000
|
||||
}]
|
||||
|
||||
|
||||
# 工厂函数
|
||||
def create_plot_expansion_service(ai_service: AIService) -> PlotExpansionService:
|
||||
"""创建剧情展开服务实例"""
|
||||
return PlotExpansionService(ai_service)
|
||||
@@ -789,6 +789,81 @@ class PromptService:
|
||||
1. 只返回纯JSON对象,不要有```json```这样的标记
|
||||
2. 文本中不要使用中文引号(""),改用【】或《》
|
||||
3. 不要有任何额外的文字说明"""
|
||||
|
||||
# 大纲展开为多章节的提示词
|
||||
OUTLINE_EXPANSION = """你是专业的小说情节架构师。请分析以下大纲节点,将其展开为 {target_chapters} 个章节的详细规划。
|
||||
|
||||
【项目信息】
|
||||
小说名称:{title}
|
||||
类型:{genre}
|
||||
主题:{theme}
|
||||
叙事视角:{narrative_perspective}
|
||||
|
||||
【世界观背景】
|
||||
时间背景:{time_period}
|
||||
地理位置:{location}
|
||||
氛围基调:{atmosphere}
|
||||
世界规则:{rules}
|
||||
|
||||
【角色信息】
|
||||
{characters_info}
|
||||
|
||||
【大纲节点】
|
||||
序号:第 {outline_order} 节
|
||||
标题:{outline_title}
|
||||
内容:{outline_content}
|
||||
|
||||
【上下文】
|
||||
{context_info}
|
||||
|
||||
【展开策略】
|
||||
{strategy_instruction}
|
||||
|
||||
【任务要求】
|
||||
1. 深度分析该大纲的剧情容量和叙事节奏
|
||||
2. 识别关键剧情点、冲突点和情感转折点
|
||||
3. 将大纲拆解为 {target_chapters} 个章节,每章需包含:
|
||||
- sub_index: 子章节序号(1, 2, 3...)
|
||||
- title: 章节标题(体现该章核心冲突或情感)
|
||||
- plot_summary: 剧情摘要(200-300字,详细描述该章发生的事件)
|
||||
- key_events: 关键事件列表(3-5个关键剧情点)
|
||||
- character_focus: 角色焦点(主要涉及的角色名称)
|
||||
- emotional_tone: 情感基调(如:紧张、温馨、悲伤、激动等)
|
||||
- narrative_goal: 叙事目标(该章要达成的叙事效果)
|
||||
- conflict_type: 冲突类型(如:内心挣扎、人际冲突、环境挑战等)
|
||||
- estimated_words: 预计字数(建议2000-5000字)
|
||||
{scene_instruction}
|
||||
4. 确保章节间:
|
||||
- 衔接自然流畅
|
||||
- 剧情递进合理
|
||||
- 节奏张弛有度
|
||||
- 每章都有明确的叙事价值
|
||||
|
||||
**重要格式要求:**
|
||||
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
|
||||
2. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》
|
||||
3. 文本描述中的专有名词使用【】标记
|
||||
|
||||
请严格按照以下JSON数组格式输出:
|
||||
[
|
||||
{{
|
||||
"sub_index": 1,
|
||||
"title": "章节标题",
|
||||
"plot_summary": "该章详细剧情摘要(200-300字)...",
|
||||
"key_events": ["关键事件1", "关键事件2", "关键事件3"],
|
||||
"character_focus": ["角色A", "角色B"],
|
||||
"emotional_tone": "情感基调",
|
||||
"narrative_goal": "叙事目标",
|
||||
"conflict_type": "冲突类型",
|
||||
"estimated_words": 3000{scene_field}
|
||||
}}
|
||||
]
|
||||
|
||||
再次强调:
|
||||
1. 只返回纯JSON数组,不要有```json```这样的标记
|
||||
2. 数组中要包含{target_chapters}个章节对象
|
||||
3. 每个plot_summary必须是200-300字的详细描述
|
||||
4. 文本中不要使用中文引号(""),改用【】或《》"""
|
||||
|
||||
@staticmethod
|
||||
def format_prompt(template: str, **kwargs) -> str:
|
||||
@@ -1106,6 +1181,72 @@ class PromptService:
|
||||
project_context=project_context,
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_outline_expansion_prompt(cls, title: str, genre: str, theme: str,
|
||||
narrative_perspective: str, time_period: str,
|
||||
location: str, atmosphere: str, rules: str,
|
||||
characters_info: str, outline_order: int,
|
||||
outline_title: str, outline_content: str,
|
||||
context_info: str, strategy: str = "balanced",
|
||||
target_chapters: int = 3,
|
||||
include_scenes: bool = False) -> str:
|
||||
"""
|
||||
获取大纲展开为多章节的提示词
|
||||
|
||||
Args:
|
||||
title: 小说名称
|
||||
genre: 类型
|
||||
theme: 主题
|
||||
narrative_perspective: 叙事视角
|
||||
time_period: 时间背景
|
||||
location: 地理位置
|
||||
atmosphere: 氛围基调
|
||||
rules: 世界规则
|
||||
characters_info: 角色信息
|
||||
outline_order: 大纲序号
|
||||
outline_title: 大纲标题
|
||||
outline_content: 大纲内容
|
||||
context_info: 上下文信息
|
||||
strategy: 展开策略 (balanced/climax/detail)
|
||||
target_chapters: 目标章节数
|
||||
include_scenes: 是否包含场景字段
|
||||
"""
|
||||
# 根据策略生成指导说明
|
||||
strategy_instructions = {
|
||||
"balanced": "采用均衡策略:将大纲内容平均分配到各章节,保持节奏均匀,每章剧情密度相当。",
|
||||
"climax": "采用高潮重点策略:识别大纲中的高潮部分,为其分配更多章节进行细致展开,其他部分适当精简。",
|
||||
"detail": "采用细节丰富策略:深挖大纲中的每个细节,为每个关键事件、情感转折都安排足够的叙事空间。"
|
||||
}
|
||||
strategy_instruction = strategy_instructions.get(strategy, strategy_instructions["balanced"])
|
||||
|
||||
# 场景相关的指令和字段
|
||||
scene_instruction = ""
|
||||
scene_field = ""
|
||||
if include_scenes:
|
||||
scene_instruction = "\n - scenes: 场景列表(2-4个具体场景描述)"
|
||||
scene_field = ',\n "scenes": ["场景1", "场景2"]'
|
||||
|
||||
return cls.format_prompt(
|
||||
cls.OUTLINE_EXPANSION,
|
||||
title=title,
|
||||
genre=genre,
|
||||
theme=theme,
|
||||
narrative_perspective=narrative_perspective,
|
||||
time_period=time_period,
|
||||
location=location,
|
||||
atmosphere=atmosphere,
|
||||
rules=rules,
|
||||
characters_info=characters_info,
|
||||
outline_order=outline_order,
|
||||
outline_title=outline_title,
|
||||
outline_content=outline_content,
|
||||
context_info=context_info,
|
||||
strategy_instruction=strategy_instruction,
|
||||
target_chapters=target_chapters,
|
||||
scene_instruction=scene_instruction,
|
||||
scene_field=scene_field
|
||||
)
|
||||
|
||||
|
||||
# 创建全局提示词服务实例
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
-- 为Chapter表添加与Outline的关联关系
|
||||
-- 实现大纲到章节的一对多关系
|
||||
|
||||
-- 添加outline_id外键字段
|
||||
ALTER TABLE chapters
|
||||
ADD COLUMN outline_id VARCHAR(36) NULL;
|
||||
|
||||
-- 添加sub_index字段,表示在该大纲下的子章节序号
|
||||
ALTER TABLE chapters
|
||||
ADD COLUMN sub_index INTEGER DEFAULT 1;
|
||||
|
||||
-- 添加字段注释(PostgreSQL语法)
|
||||
COMMENT ON COLUMN chapters.outline_id IS '关联的大纲ID';
|
||||
COMMENT ON COLUMN chapters.sub_index IS '大纲下的子章节序号';
|
||||
|
||||
-- 添加外键约束
|
||||
ALTER TABLE chapters
|
||||
ADD CONSTRAINT fk_chapter_outline
|
||||
FOREIGN KEY (outline_id)
|
||||
REFERENCES outlines(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- 创建索引优化查询性能
|
||||
CREATE INDEX idx_chapters_outline_id ON chapters(outline_id);
|
||||
CREATE INDEX idx_chapters_outline_sub ON chapters(outline_id, sub_index);
|
||||
|
||||
-- 说明:
|
||||
-- outline_id为NULL表示旧数据或独立章节
|
||||
-- outline_id有值表示该章节由某个大纲展开生成
|
||||
-- sub_index表示在该大纲下的第几个子章节(从1开始)
|
||||
|
||||
-- 为 chapters 表添加 expansion_plan 字段
|
||||
-- 用于存储大纲展开规划的详细数据(JSON格式)
|
||||
|
||||
-- 添加字段
|
||||
ALTER TABLE chapters ADD COLUMN IF NOT EXISTS expansion_plan TEXT;
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON COLUMN chapters.expansion_plan IS '展开规划详情(JSON): 包含key_events, character_focus, emotional_tone等';
|
||||
|
||||
-- 查看修改结果
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'chapters'
|
||||
ORDER BY ordinal_position;
|
||||
+263
-90
@@ -1,11 +1,10 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Progress, Alert, Radio } from 'antd';
|
||||
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined } from '@ant-design/icons';
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Progress, Alert, Radio, Descriptions, Collapse } from 'antd';
|
||||
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useChapterSync } from '../store/hooks';
|
||||
import { projectApi, writingStyleApi } from '../services/api';
|
||||
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask } from '../types';
|
||||
import { cardStyles } from '../components/CardStyles';
|
||||
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types';
|
||||
import ChapterAnalysis from '../components/ChapterAnalysis';
|
||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||
|
||||
@@ -479,6 +478,34 @@ export default function Chapters() {
|
||||
|
||||
const sortedChapters = [...chapters].sort((a, b) => a.chapter_number - b.chapter_number);
|
||||
|
||||
// 按大纲分组章节
|
||||
const groupedChapters = useMemo(() => {
|
||||
const groups: Record<string, {
|
||||
outlineId: string | null;
|
||||
outlineTitle: string;
|
||||
outlineOrder: number;
|
||||
chapters: Chapter[];
|
||||
}> = {};
|
||||
|
||||
sortedChapters.forEach(chapter => {
|
||||
const key = chapter.outline_id || 'uncategorized';
|
||||
|
||||
if (!groups[key]) {
|
||||
groups[key] = {
|
||||
outlineId: chapter.outline_id || null,
|
||||
outlineTitle: chapter.outline_title || '未分类章节',
|
||||
outlineOrder: chapter.outline_order ?? 999,
|
||||
chapters: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[key].chapters.push(chapter);
|
||||
});
|
||||
|
||||
// 转换为数组并按大纲顺序排序
|
||||
return Object.values(groups).sort((a, b) => a.outlineOrder - b.outlineOrder);
|
||||
}, [sortedChapters]);
|
||||
|
||||
const handleExport = () => {
|
||||
if (chapters.length === 0) {
|
||||
message.warning('当前项目没有章节,无法导出');
|
||||
@@ -709,6 +736,97 @@ export default function Chapters() {
|
||||
}
|
||||
};
|
||||
|
||||
// 显示展开规划详情
|
||||
const showExpansionPlanModal = (chapter: Chapter) => {
|
||||
if (!chapter.expansion_plan) return;
|
||||
|
||||
try {
|
||||
const planData: ExpansionPlanData = JSON.parse(chapter.expansion_plan);
|
||||
|
||||
Modal.info({
|
||||
title: (
|
||||
<Space>
|
||||
<InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
<span>第{chapter.chapter_number}章展开规划</span>
|
||||
</Space>
|
||||
),
|
||||
width: 800,
|
||||
content: (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="章节标题">
|
||||
<strong>{chapter.title}</strong>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="情感基调">
|
||||
<Tag color="blue">{planData.emotional_tone}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="冲突类型">
|
||||
<Tag color="orange">{planData.conflict_type}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="预估字数">
|
||||
<Tag color="green">{planData.estimated_words}字</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="叙事目标">
|
||||
{planData.narrative_goal}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关键事件">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{planData.key_events.map((event, idx) => (
|
||||
<div key={idx} style={{ padding: '4px 0' }}>
|
||||
<Tag color="purple">{idx + 1}</Tag> {event}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="涉及角色">
|
||||
<Space wrap>
|
||||
{planData.character_focus.map((char, idx) => (
|
||||
<Tag key={idx} color="cyan">{char}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
{planData.scenes && planData.scenes.length > 0 && (
|
||||
<Descriptions.Item label="场景规划">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{planData.scenes.map((scene, idx) => (
|
||||
<Card key={idx} size="small" style={{ backgroundColor: '#fafafa' }}>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<strong>📍 地点:</strong>{scene.location}
|
||||
</div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<strong>👥 角色:</strong>
|
||||
<Space size="small" wrap style={{ marginLeft: 8 }}>
|
||||
{scene.characters.map((char, charIdx) => (
|
||||
<Tag key={charIdx}>{char}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
<div>
|
||||
<strong>🎯 目的:</strong>{scene.purpose}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
<Alert
|
||||
message="提示"
|
||||
description="这些是AI在大纲展开时生成的规划信息,可以作为创作章节内容时的参考。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
okText: '关闭',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('解析展开规划失败:', error);
|
||||
message.error('展开规划数据格式错误');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{
|
||||
@@ -754,21 +872,54 @@ export default function Chapters() {
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
{chapters.length === 0 ? (
|
||||
<Empty description="还没有章节,开始创作吧!" />
|
||||
) : (
|
||||
<Card style={cardStyles.base}>
|
||||
<List
|
||||
dataSource={sortedChapters}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
<Empty description="还没有章节,开始创作吧!" />
|
||||
) : (
|
||||
<Collapse
|
||||
bordered={false}
|
||||
defaultActiveKey={groupedChapters.map((_, idx) => idx.toString())}
|
||||
expandIcon={({ isActive }) => <CaretRightOutlined rotate={isActive ? 90 : 0} />}
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
{groupedChapters.map((group, groupIndex) => (
|
||||
<Collapse.Panel
|
||||
key={groupIndex.toString()}
|
||||
header={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Tag color={group.outlineId ? 'blue' : 'default'} style={{ margin: 0 }}>
|
||||
{group.outlineId ? `📖 大纲 ${group.outlineOrder}` : '📝 未分类'}
|
||||
</Tag>
|
||||
<span style={{ fontWeight: 600, fontSize: 16 }}>
|
||||
{group.outlineTitle}
|
||||
</span>
|
||||
<Badge
|
||||
count={`${group.chapters.length} 章`}
|
||||
style={{ backgroundColor: '#52c41a' }}
|
||||
/>
|
||||
<Badge
|
||||
count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)} 字`}
|
||||
style={{ backgroundColor: '#1890ff' }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
padding: '16px 0',
|
||||
marginBottom: 16,
|
||||
background: '#fff',
|
||||
borderRadius: 8,
|
||||
transition: 'background 0.3s ease',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: isMobile ? 'flex-start' : 'center'
|
||||
border: '1px solid #f0f0f0',
|
||||
}}
|
||||
actions={isMobile ? undefined : [
|
||||
>
|
||||
<List
|
||||
dataSource={group.chapters}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '16px 0',
|
||||
borderRadius: 8,
|
||||
transition: 'background 0.3s ease',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: isMobile ? 'flex-start' : 'center',
|
||||
}}
|
||||
actions={isMobile ? undefined : [
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEditor(item.id)}
|
||||
@@ -806,85 +957,107 @@ export default function Chapters() {
|
||||
>
|
||||
修改信息
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<List.Item.Meta
|
||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: '#1890ff' }} />}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 4 : 8, flexWrap: 'wrap', fontSize: isMobile ? 14 : 16 }}>
|
||||
<span>第{item.chapter_number}章:{item.title}</span>
|
||||
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: '#52c41a' }} />
|
||||
{renderAnalysisStatus(item.id)}
|
||||
{!canGenerateChapter(item) && (
|
||||
<Tooltip title={getGenerateDisabledReason(item)}>
|
||||
<Tag icon={<LockOutlined />} color="warning">
|
||||
需前置章节
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
item.content ? (
|
||||
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||
{item.content.substring(0, isMobile ? 80 : 150)}
|
||||
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{isMobile && (
|
||||
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEditor(item.id)}
|
||||
size="small"
|
||||
title="编辑内容"
|
||||
/>
|
||||
{(() => {
|
||||
const task = analysisTasksMap[item.id];
|
||||
const isAnalyzing = task && (task.status === 'pending' || task.status === 'running');
|
||||
const hasContent = item.content && item.content.trim() !== '';
|
||||
]}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<List.Item.Meta
|
||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: '#1890ff' }} />}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 4 : 8, flexWrap: 'wrap', fontSize: isMobile ? 14 : 16 }}>
|
||||
<span>
|
||||
第{item.chapter_number}章:{item.title}
|
||||
</span>
|
||||
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: '#52c41a' }} />
|
||||
{renderAnalysisStatus(item.id)}
|
||||
{item.expansion_plan && (
|
||||
<Tooltip title="已有展开规划,点击信息图标查看详情">
|
||||
<Tag icon={<CheckCircleOutlined />} color="blue">
|
||||
已展开
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!canGenerateChapter(item) && (
|
||||
<Tooltip title={getGenerateDisabledReason(item)}>
|
||||
<Tag icon={<LockOutlined />} color="warning">
|
||||
需前置章节
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{item.expansion_plan && (
|
||||
<Tooltip title="查看展开规划详情">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: '#1890ff', cursor: 'pointer', fontSize: 16 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showExpansionPlanModal(item);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
item.content ? (
|
||||
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
|
||||
{item.content.substring(0, isMobile ? 80 : 150)}
|
||||
{item.content.length > (isMobile ? 80 : 150) && '...'}
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}>暂无内容</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
!hasContent ? '请先生成章节内容' :
|
||||
isAnalyzing ? '分析中' :
|
||||
'查看分析'
|
||||
}
|
||||
>
|
||||
{isMobile && (
|
||||
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
|
||||
<Button
|
||||
type="text"
|
||||
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
||||
onClick={() => handleShowAnalysis(item.id)}
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEditor(item.id)}
|
||||
size="small"
|
||||
disabled={!hasContent || isAnalyzing}
|
||||
loading={isAnalyzing}
|
||||
title="编辑内容"
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => handleOpenModal(item.id)}
|
||||
size="small"
|
||||
title="修改信息"
|
||||
/>
|
||||
</Space>
|
||||
{(() => {
|
||||
const task = analysisTasksMap[item.id];
|
||||
const isAnalyzing = task && (task.status === 'pending' || task.status === 'running');
|
||||
const hasContent = item.content && item.content.trim() !== '';
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
!hasContent ? '请先生成章节内容' :
|
||||
isAnalyzing ? '分析中' :
|
||||
'查看分析'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
||||
onClick={() => handleShowAnalysis(item.id)}
|
||||
size="small"
|
||||
disabled={!hasContent || isAnalyzing}
|
||||
loading={isAnalyzing}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => handleOpenModal(item.id)}
|
||||
size="small"
|
||||
title="修改信息"
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
+874
-86
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,10 @@ import type {
|
||||
OutlineCreate,
|
||||
OutlineUpdate,
|
||||
OutlineReorderRequest,
|
||||
OutlineExpansionRequest,
|
||||
OutlineExpansionResponse,
|
||||
BatchOutlineExpansionRequest,
|
||||
BatchOutlineExpansionResponse,
|
||||
Character,
|
||||
CharacterUpdate,
|
||||
Chapter,
|
||||
@@ -295,6 +299,65 @@ export const outlineApi = {
|
||||
|
||||
generateOutline: (data: GenerateOutlineRequest) =>
|
||||
api.post<unknown, { total: number; items: Outline[] }>('/outlines/generate', data).then(res => res.items),
|
||||
|
||||
// 获取大纲关联的章节
|
||||
getOutlineChapters: (outlineId: string) =>
|
||||
api.get<unknown, {
|
||||
has_chapters: boolean;
|
||||
outline_id: string;
|
||||
outline_title: string;
|
||||
chapter_count: number;
|
||||
chapters: Array<{
|
||||
id: string;
|
||||
chapter_number: number;
|
||||
title: string;
|
||||
summary: string;
|
||||
sub_index: number;
|
||||
status: string;
|
||||
word_count: number;
|
||||
}>;
|
||||
expansion_plans: Array<{
|
||||
sub_index: number;
|
||||
title: string;
|
||||
plot_summary: string;
|
||||
key_events: string[];
|
||||
character_focus: string[];
|
||||
emotional_tone: string;
|
||||
narrative_goal: string;
|
||||
conflict_type: string;
|
||||
estimated_words: number;
|
||||
scenes?: Array<{
|
||||
location: string;
|
||||
characters: string[];
|
||||
purpose: string;
|
||||
}> | null;
|
||||
}> | null;
|
||||
}>(`/outlines/${outlineId}/chapters`),
|
||||
|
||||
// 单个大纲展开为多章
|
||||
expandOutline: (outlineId: string, data: OutlineExpansionRequest) =>
|
||||
api.post<unknown, OutlineExpansionResponse>(`/outlines/${outlineId}/expand`, data),
|
||||
|
||||
// 根据已有规划创建章节(避免重复AI调用)
|
||||
createChaptersFromPlans: (outlineId: string, chapterPlans: any[]) =>
|
||||
api.post<unknown, {
|
||||
outline_id: string;
|
||||
outline_title: string;
|
||||
chapters_created: number;
|
||||
created_chapters: Array<{
|
||||
id: string;
|
||||
chapter_number: number;
|
||||
title: string;
|
||||
summary: string;
|
||||
outline_id: string;
|
||||
sub_index: number;
|
||||
status: string;
|
||||
}>;
|
||||
}>(`/outlines/${outlineId}/create-chapters-from-plans`, { chapter_plans: chapterPlans }),
|
||||
|
||||
// 批量展开大纲
|
||||
batchExpandOutlines: (data: BatchOutlineExpansionRequest) =>
|
||||
api.post<unknown, BatchOutlineExpansionResponse>('/outlines/batch-expand', data),
|
||||
};
|
||||
|
||||
export const characterApi = {
|
||||
|
||||
@@ -200,23 +200,6 @@ export function useOutlineSync() {
|
||||
}
|
||||
}, [removeOutline]);
|
||||
|
||||
// 重排序大纲(带同步)
|
||||
const reorderOutlines = useCallback(async (orders: Array<{ id: string; order_index: number }>, projectId?: string) => {
|
||||
try {
|
||||
await outlineApi.reorderOutlines({ orders });
|
||||
// 重新获取完整列表以确保顺序正确
|
||||
const id = projectId || currentProject?.id;
|
||||
if (id) {
|
||||
const data = await outlineApi.getOutlines(id);
|
||||
const outlines = Array.isArray(data) ? data : (data as PaginationResponse<Outline>).items || [];
|
||||
setOutlines(outlines);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重排序大纲失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [currentProject?.id, setOutlines]); // 添加 currentProject?.id 到依赖数组
|
||||
|
||||
// AI生成大纲(带同步)
|
||||
const generateOutlines = useCallback(async (data: GenerateOutlineRequest) => {
|
||||
try {
|
||||
@@ -235,7 +218,6 @@ export function useOutlineSync() {
|
||||
createOutline,
|
||||
updateOutline: updateOutlineSync,
|
||||
deleteOutline,
|
||||
reorderOutlines,
|
||||
generateOutlines,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -202,6 +202,21 @@ export interface CharacterUpdate {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
// 展开规划数据结构
|
||||
export interface ExpansionPlanData {
|
||||
key_events: string[];
|
||||
character_focus: string[];
|
||||
emotional_tone: string;
|
||||
narrative_goal: string;
|
||||
conflict_type: string;
|
||||
estimated_words: number;
|
||||
scenes?: Array<{
|
||||
location: string;
|
||||
characters: string[];
|
||||
purpose: string;
|
||||
}> | null;
|
||||
}
|
||||
|
||||
// 章节类型定义
|
||||
export interface Chapter {
|
||||
id: string;
|
||||
@@ -212,6 +227,11 @@ export interface Chapter {
|
||||
chapter_number: number;
|
||||
word_count: number;
|
||||
status: 'draft' | 'writing' | 'completed';
|
||||
expansion_plan?: string; // JSON字符串,解析后为ExpansionPlanData
|
||||
outline_id?: string; // 关联的大纲ID
|
||||
sub_index?: number; // 大纲下的子章节序号
|
||||
outline_title?: string; // 大纲标题(从后端联表查询获得)
|
||||
outline_order?: number; // 大纲排序序号(从后端联表查询获得)
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -284,6 +304,72 @@ export interface OutlineReorderRequest {
|
||||
orders: OutlineReorderItem[];
|
||||
}
|
||||
|
||||
// 大纲展开相关类型定义
|
||||
export interface ChapterPlanItem {
|
||||
sub_index: number;
|
||||
title: string;
|
||||
plot_summary: string;
|
||||
key_events: string[];
|
||||
character_focus: string[];
|
||||
emotional_tone: string;
|
||||
narrative_goal: string;
|
||||
conflict_type: string;
|
||||
estimated_words: number;
|
||||
scenes?: Array<{
|
||||
location: string;
|
||||
characters: string[];
|
||||
purpose: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface OutlineExpansionRequest {
|
||||
target_chapter_count: number;
|
||||
expansion_strategy?: 'balanced' | 'climax' | 'detail';
|
||||
auto_create_chapters?: boolean;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface OutlineExpansionResponse {
|
||||
outline_id: string;
|
||||
outline_title: string;
|
||||
target_chapter_count: number;
|
||||
actual_chapter_count: number;
|
||||
expansion_strategy: string;
|
||||
chapter_plans: ChapterPlanItem[];
|
||||
created_chapters?: Array<{
|
||||
id: string;
|
||||
chapter_number: number;
|
||||
title: string;
|
||||
summary: string;
|
||||
outline_id: string;
|
||||
sub_index: number;
|
||||
status: string;
|
||||
}> | null;
|
||||
}
|
||||
|
||||
export interface BatchOutlineExpansionRequest {
|
||||
project_id: string;
|
||||
outline_ids?: string[];
|
||||
chapters_per_outline: number;
|
||||
expansion_strategy?: 'balanced' | 'climax' | 'detail';
|
||||
auto_create_chapters?: boolean;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface BatchOutlineExpansionResponse {
|
||||
project_id: string;
|
||||
total_outlines_expanded: number;
|
||||
total_chapters_created: number;
|
||||
expansion_results: OutlineExpansionResponse[];
|
||||
skipped_outlines?: Array<{
|
||||
outline_id: string;
|
||||
outline_title: string;
|
||||
reason: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GenerateCharacterRequest {
|
||||
project_id: string;
|
||||
name?: string;
|
||||
|
||||
Reference in New Issue
Block a user