update:1.小说项目创建支持双模式生成,大纲-章节(一对一&一对多) 2.新增章节管理-编辑章节规划功能 3.修复灵感模式可重复点击选项问题,刷新对话内容丢失问题

This commit is contained in:
xiamuceer
2025-11-27 17:29:23 +08:00
parent 8121c04af9
commit deb6cc37a4
27 changed files with 1797 additions and 216 deletions
+198 -9
View File
@@ -28,7 +28,8 @@ from app.schemas.chapter import (
ChapterGenerateRequest,
BatchGenerateRequest,
BatchGenerateResponse,
BatchGenerateStatusResponse
BatchGenerateStatusResponse,
ExpansionPlanUpdate
)
from app.schemas.regeneration import (
ChapterRegenerateRequest,
@@ -1008,6 +1009,10 @@ async def generate_chapter_content_stream(
yield f"data: {json.dumps({'type': 'error', 'error': '项目不存在'}, ensure_ascii=False)}\n\n"
return
# 获取项目的大纲模式
outline_mode = project.outline_mode if project else 'one-to-many'
logger.info(f"📋 项目大纲模式: {outline_mode}")
# 获取对应的大纲
outline_result = await db_session.execute(
select(Outline)
@@ -1188,6 +1193,48 @@ async def generate_chapter_content_stream(
logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}")
yield f"data: {json.dumps({'type': 'progress', 'message': '⚠️ MCP工具暂时不可用,使用基础模式', 'progress': 32}, ensure_ascii=False)}\n\n"
# 📋 根据大纲模式构建差异化的章节大纲上下文
chapter_outline_content = ""
if outline_mode == 'one-to-one':
# 一对一模式:使用大纲的 content
chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲'
logger.info(f"✏️ 一对一模式:使用大纲内容作为章节指导")
else:
# 一对多模式:优先使用 expansion_plan 的详细规划
if current_chapter.expansion_plan:
try:
plan = json.loads(current_chapter.expansion_plan)
chapter_outline_content = f"""【本章详细规划】
剧情摘要:{plan.get('plot_summary', '')}
关键事件:
{chr(10).join(f'- {event}' for event in plan.get('key_events', []))}
角色焦点:{', '.join(plan.get('character_focus', []))}
情感基调:{plan.get('emotional_tone', '未设定')}
叙事目标:{plan.get('narrative_goal', '未设定')}
冲突类型:{plan.get('conflict_type', '未设定')}"""
# 可选:附加章节 summary
if current_chapter.summary and current_chapter.summary.strip():
chapter_outline_content += f"\n\n【章节补充说明】\n{current_chapter.summary}"
# 可选:附加大纲的背景信息
if outline:
chapter_outline_content += f"\n\n【大纲节点背景】\n{outline.content}"
logger.info(f"✏️ 一对多模式:使用expansion_plan详细规划({len(chapter_outline_content)}字符)")
except json.JSONDecodeError as e:
logger.warning(f"⚠️ expansion_plan解析失败: {e},回退到大纲内容")
chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲'
else:
# 没有expansion_plan,使用大纲内容
chapter_outline_content = outline.content if outline else current_chapter.summary or '暂无大纲'
logger.warning(f"⚠️ 一对多模式但无expansion_plan,使用大纲内容")
# 根据是否有前置内容选择不同的提示词,并应用写作风格、记忆增强和MCP参考资料
if previous_content:
prompt = prompt_service.get_chapter_generation_with_context_prompt(
@@ -1204,11 +1251,12 @@ async def generate_chapter_content_stream(
previous_content=previous_content,
chapter_number=current_chapter.chapter_number,
chapter_title=current_chapter.title,
chapter_outline=outline.content if outline else current_chapter.summary or '暂无大纲',
chapter_outline=chapter_outline_content,
style_content=style_content,
target_word_count=target_word_count,
memory_context=memory_context,
mcp_references=mcp_reference_materials
mcp_references=mcp_reference_materials,
outline_mode=outline_mode
)
else:
prompt = prompt_service.get_chapter_generation_prompt(
@@ -1224,11 +1272,12 @@ async def generate_chapter_content_stream(
outlines_context=outlines_context,
chapter_number=current_chapter.chapter_number,
chapter_title=current_chapter.title,
chapter_outline=outline.content if outline else current_chapter.summary or '暂无大纲',
chapter_outline=chapter_outline_content,
style_content=style_content,
target_word_count=target_word_count,
memory_context=memory_context,
mcp_references=mcp_reference_materials
mcp_references=mcp_reference_materials,
outline_mode=outline_mode
)
if mcp_reference_materials:
@@ -1238,11 +1287,39 @@ async def generate_chapter_content_stream(
# 流式生成内容
full_content = ""
chunk_count = 0
last_progress = 0
async for chunk in user_ai_service.generate_text_stream(prompt=prompt):
full_content += chunk
chunk_count += 1
# 发送内容块
yield f"data: {json.dumps({'type': 'content', 'content': chunk}, ensure_ascii=False)}\n\n"
# 每50个chunk发送一次进度更新(估算)
if chunk_count % 50 == 0:
current_word_count = len(full_content)
# 根据目标字数估算进度(35%起步,最高95%,为后续保存留5%)
estimated_progress = min(95, 35 + int((current_word_count / target_word_count) * 60))
# 只在进度变化时发送
if estimated_progress > last_progress:
progress_data = {
'type': 'progress',
'progress': estimated_progress,
'message': f'正在创作中... 已生成 {current_word_count}',
'word_count': current_word_count,
'status': 'processing'
}
yield f"data: {json.dumps(progress_data, ensure_ascii=False)}\n\n"
last_progress = estimated_progress
await asyncio.sleep(0) # 让出控制权
# 发送保存进度
yield f"data: {json.dumps({'type': 'progress', 'progress': 98, 'message': '正在保存章节...', 'status': 'processing'}, ensure_ascii=False)}\n\n"
# 更新章节内容到数据库
old_word_count = current_chapter.word_count or 0
current_chapter.content = full_content
@@ -1297,6 +1374,9 @@ async def generate_chapter_content_stream(
ai_service=user_ai_service
)
# 发送最终进度100%
yield f"data: {json.dumps({'type': 'progress', 'progress': 100, 'message': '创作完成!', 'word_count': new_word_count, 'status': 'success'}, ensure_ascii=False)}\n\n"
# 发送完成事件(包含分析任务ID
completion_data = {
'type': 'done',
@@ -2216,6 +2296,10 @@ async def generate_single_chapter_for_batch(
if not project:
raise Exception("项目不存在")
# 获取项目的大纲模式
outline_mode = project.outline_mode if project else 'one-to-many'
logger.info(f"📋 批量生成 - 项目大纲模式: {outline_mode}")
# 获取对应的大纲
outline_result = await db_session.execute(
select(Outline)
@@ -2285,6 +2369,48 @@ async def generate_single_chapter_for_batch(
character_names=[c.name for c in characters] if characters else None
)
# 📋 根据大纲模式构建差异化的章节大纲上下文
chapter_outline_content = ""
if outline_mode == 'one-to-one':
# 一对一模式:使用大纲的 content
chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲'
logger.info(f"✏️ 批量生成 - 一对一模式:使用大纲内容")
else:
# 一对多模式:优先使用 expansion_plan 的详细规划
if chapter.expansion_plan:
try:
plan = json.loads(chapter.expansion_plan)
chapter_outline_content = f"""【本章详细规划】
剧情摘要:{plan.get('plot_summary', '')}
关键事件:
{chr(10).join(f'- {event}' for event in plan.get('key_events', []))}
角色焦点:{', '.join(plan.get('character_focus', []))}
情感基调:{plan.get('emotional_tone', '未设定')}
叙事目标:{plan.get('narrative_goal', '未设定')}
冲突类型:{plan.get('conflict_type', '未设定')}"""
# 可选:附加章节 summary
if chapter.summary and chapter.summary.strip():
chapter_outline_content += f"\n\n【章节补充说明】\n{chapter.summary}"
# 可选:附加大纲的背景信息
if outline:
chapter_outline_content += f"\n\n【大纲节点背景】\n{outline.content}"
logger.info(f"✏️ 批量生成 - 一对多模式:使用expansion_plan详细规划")
except json.JSONDecodeError as e:
logger.warning(f"⚠️ expansion_plan解析失败: {e},回退到大纲内容")
chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲'
else:
# 没有expansion_plan,使用大纲内容
chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲'
logger.warning(f"⚠️ 批量生成 - 一对多模式但无expansion_plan,使用大纲内容")
# 生成提示词
if previous_content:
prompt = prompt_service.get_chapter_generation_with_context_prompt(
@@ -2301,10 +2427,11 @@ async def generate_single_chapter_for_batch(
previous_content=previous_content,
chapter_number=chapter.chapter_number,
chapter_title=chapter.title,
chapter_outline=outline.content if outline else chapter.summary or '暂无大纲',
chapter_outline=chapter_outline_content,
style_content=style_content,
target_word_count=target_word_count,
memory_context=memory_context
memory_context=memory_context,
outline_mode=outline_mode
)
else:
prompt = prompt_service.get_chapter_generation_prompt(
@@ -2320,10 +2447,11 @@ async def generate_single_chapter_for_batch(
outlines_context=outlines_context,
chapter_number=chapter.chapter_number,
chapter_title=chapter.title,
chapter_outline=outline.content if outline else chapter.summary or '暂无大纲',
chapter_outline=chapter_outline_content,
style_content=style_content,
target_word_count=target_word_count,
memory_context=memory_context
memory_context=memory_context,
outline_mode=outline_mode
)
# 非流式生成内容
@@ -2643,3 +2771,64 @@ async def get_regeneration_tasks(
]
}
@router.put("/{chapter_id}/expansion-plan", response_model=dict, summary="更新章节规划信息")
async def update_chapter_expansion_plan(
chapter_id: str,
expansion_plan: ExpansionPlanUpdate,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
更新章节的展开规划信息
Args:
chapter_id: 章节ID
expansion_plan: 规划信息更新数据
Returns:
更新后的章节规划信息
"""
# 获取章节
result = await db.execute(
select(Chapter).where(Chapter.id == chapter_id)
)
chapter = result.scalar_one_or_none()
if not chapter:
raise HTTPException(status_code=404, detail="章节不存在")
# 验证用户权限
user_id = getattr(request.state, 'user_id', None)
await verify_project_access(chapter.project_id, user_id, db)
# 准备更新数据(排除None值)
plan_data = expansion_plan.model_dump(exclude_unset=True, exclude_none=True)
# 如果已有规划,合并更新;否则创建新规划
if chapter.expansion_plan:
try:
existing_plan = json.loads(chapter.expansion_plan)
# 合并更新
existing_plan.update(plan_data)
chapter.expansion_plan = json.dumps(existing_plan, ensure_ascii=False)
except json.JSONDecodeError:
logger.warning(f"章节 {chapter_id} 的expansion_plan格式错误,将覆盖")
chapter.expansion_plan = json.dumps(plan_data, ensure_ascii=False)
else:
chapter.expansion_plan = json.dumps(plan_data, ensure_ascii=False)
await db.commit()
await db.refresh(chapter)
logger.info(f"章节规划更新成功: {chapter_id}")
# 返回更新后的规划数据
updated_plan = json.loads(chapter.expansion_plan) if chapter.expansion_plan else None
return {
"id": chapter.id,
"expansion_plan": updated_plan,
"message": "规划信息更新成功"
}
+174 -11
View File
@@ -236,18 +236,29 @@ async def delete_outline(
# 验证用户权限
user_id = getattr(request.state, 'user_id', None)
await verify_project_access(outline.project_id, user_id, db)
project = await verify_project_access(outline.project_id, user_id, db)
project_id = outline.project_id
deleted_order = outline.order_index
# 删除该大纲对应的所有章节(通过outline_id关联)
delete_result = await db.execute(
delete(Chapter).where(Chapter.outline_id == outline_id)
)
deleted_chapters_count = delete_result.rowcount
logger.info(f"删除大纲 {outline_id},同时删除了 {deleted_chapters_count} 个关联章节")
# 根据项目模式删除对应的章节
if project.outline_mode == 'one-to-one':
# one-to-one模式:通过chapter_number删除对应章节
delete_result = await db.execute(
delete(Chapter).where(
Chapter.project_id == project_id,
Chapter.chapter_number == outline.order_index
)
)
deleted_chapters_count = delete_result.rowcount
logger.info(f"一对一模式:删除大纲 {outline_id}(序号{outline.order_index}),同时删除了第{outline.order_index}章({deleted_chapters_count}个章节)")
else:
# one-to-many模式:通过outline_id删除关联章节
delete_result = await db.execute(
delete(Chapter).where(Chapter.outline_id == outline_id)
)
deleted_chapters_count = delete_result.rowcount
logger.info(f"一对多模式:删除大纲 {outline_id},同时删除了 {deleted_chapters_count} 个关联章节")
# 删除大纲
await db.delete(outline)
@@ -264,6 +275,21 @@ async def delete_outline(
for o in subsequent_outlines:
o.order_index -= 1
# 如果是one-to-one模式,还需要重新排序后续章节的chapter_number
if project.outline_mode == 'one-to-one':
chapters_result = await db.execute(
select(Chapter).where(
Chapter.project_id == project_id,
Chapter.chapter_number > deleted_order
).order_by(Chapter.chapter_number)
)
subsequent_chapters = chapters_result.scalars().all()
for ch in subsequent_chapters:
ch.chapter_number -= 1
logger.info(f"一对一模式:重新排序了 {len(subsequent_chapters)} 个后续章节")
await db.commit()
return {
@@ -852,7 +878,17 @@ async def _save_outlines(
db: AsyncSession,
start_index: int = 1
) -> List[Outline]:
"""保存大纲到数据库(不自动创建章节)"""
"""
保存大纲到数据库
如果项目为one-to-one模式,同时自动创建对应的章节
"""
# 获取项目信息以确定outline_mode
project_result = await db.execute(
select(Project).where(Project.id == project_id)
)
project = project_result.scalar_one_or_none()
outlines = []
for idx, chapter_data in enumerate(outline_data):
@@ -879,6 +915,28 @@ async def _save_outlines(
db.add(outline)
outlines.append(outline)
# 如果是one-to-one模式,自动创建章节
if project and project.outline_mode == 'one-to-one':
await db.flush() # 确保大纲有ID
for outline in outlines:
await db.refresh(outline)
# 为每个大纲创建对应的章节
chapter = Chapter(
project_id=project_id,
title=outline.title,
summary=outline.content,
chapter_number=outline.order_index,
sub_index=1,
outline_id=None, # one-to-one模式不关联outline_id
status='pending',
content=""
)
db.add(chapter)
logger.info(f"一对一模式:为{len(outlines)}个大纲自动创建了对应的章节")
return outlines
@@ -1646,6 +1704,104 @@ async def expand_outline_generator(
yield await SSEResponse.send_error(f"展开失败: {str(e)}")
@router.post("/{outline_id}/create-single-chapter", summary="一对一创建章节(传统模式)")
async def create_single_chapter_from_outline(
outline_id: str,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
传统模式:一个大纲对应创建一个章节
适用场景:
- 项目的outline_mode为'one-to-one'
- 直接将大纲内容作为章节摘要
- 不调用AI,不展开
流程:
1. 验证项目模式为one-to-one
2. 检查该大纲是否已创建章节
3. 创建章节记录(outline_id=NULLchapter_number=outline.order_index
返回:创建的章节信息
"""
# 验证用户权限
user_id = getattr(request.state, 'user_id', None)
# 获取大纲
result = await db.execute(
select(Outline).where(Outline.id == outline_id)
)
outline = result.scalar_one_or_none()
if not outline:
raise HTTPException(status_code=404, detail="大纲不存在")
# 验证项目权限并获取项目信息
project = await verify_project_access(outline.project_id, user_id, db)
# 验证项目模式
if project.outline_mode != 'one-to-one':
raise HTTPException(
status_code=400,
detail=f"当前项目为{project.outline_mode}模式,不支持一对一创建。请使用展开功能。"
)
# 检查该大纲对应的章节是否已存在
existing_chapter_result = await db.execute(
select(Chapter).where(
Chapter.project_id == outline.project_id,
Chapter.chapter_number == outline.order_index,
Chapter.sub_index == 1
)
)
existing_chapter = existing_chapter_result.scalar_one_or_none()
if existing_chapter:
raise HTTPException(
status_code=400,
detail=f"{outline.order_index}章已存在,不能重复创建"
)
try:
# 创建章节(outline_id=NULL表示一对一模式)
new_chapter = Chapter(
project_id=outline.project_id,
title=outline.title,
summary=outline.content, # 使用大纲内容作为摘要
chapter_number=outline.order_index,
sub_index=1, # 一对一模式固定为1
outline_id=None, # 传统模式不关联outline_id
status='pending'
)
db.add(new_chapter)
await db.commit()
await db.refresh(new_chapter)
logger.info(f"一对一模式:为大纲 {outline.title} 创建章节 {new_chapter.chapter_number}")
return {
"message": "章节创建成功",
"chapter": {
"id": new_chapter.id,
"project_id": new_chapter.project_id,
"title": new_chapter.title,
"summary": new_chapter.summary,
"chapter_number": new_chapter.chapter_number,
"sub_index": new_chapter.sub_index,
"outline_id": new_chapter.outline_id,
"status": new_chapter.status,
"created_at": new_chapter.created_at.isoformat() if new_chapter.created_at else None
}
}
except Exception as e:
logger.error(f"一对一创建章节失败: {str(e)}", exc_info=True)
await db.rollback()
raise HTTPException(status_code=500, detail=f"创建章节失败: {str(e)}")
@router.post("/{outline_id}/expand", response_model=OutlineExpansionResponse, summary="展开单个大纲为多章")
async def expand_outline_to_chapters(
outline_id: str,
@@ -1681,8 +1837,15 @@ async def expand_outline_to_chapters(
if not outline:
raise HTTPException(status_code=404, detail="大纲不存在")
# 验证项目权限
await verify_project_access(outline.project_id, user_id, db)
# 验证项目权限并获取项目信息
project = await verify_project_access(outline.project_id, user_id, db)
# 验证项目模式
if project.outline_mode != 'one-to-many':
raise HTTPException(
status_code=400,
detail=f"当前项目为{project.outline_mode}模式,不支持展开功能。请使用一对一创建。"
)
try:
# 创建展开服务实例
+2 -3
View File
@@ -354,9 +354,8 @@ async def export_project_chapters(
txt_content.append("\n" + "=" * 80 + "\n\n")
for chapter in chapters:
# 处理子章节序号显示
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(f"{chapter.chapter_number}{chapter.title}")
txt_content.append("-" * 80)
txt_content.append("") # 空行
+55 -10
View File
@@ -47,6 +47,7 @@ async def world_building_generator(
target_words = data.get("target_words")
chapter_count = data.get("chapter_count")
character_count = data.get("character_count")
outline_mode = data.get("outline_mode", "one-to-many") # 大纲模式,默认一对多
provider = data.get("provider")
model = data.get("model")
enable_mcp = data.get("enable_mcp", True) # 默认启用MCP
@@ -215,6 +216,7 @@ async def world_building_generator(
target_words=target_words,
chapter_count=chapter_count,
character_count=character_count,
outline_mode=outline_mode, # 设置大纲模式
wizard_status="incomplete",
wizard_step=1,
status="planning"
@@ -1017,39 +1019,82 @@ async def outline_generator(
logger.info(f"✅ 成功创建{len(created_outlines)}个大纲节点")
# 向导流程中不展开大纲,避免等待时间过长
# 用户可以在大纲页面手动展开需要的大纲节点
yield await SSEResponse.send_progress("跳过大纲展开,加快创建速度...", 85)
# 根据项目的大纲模式决定是否自动创建章节
created_chapters = []
if project.outline_mode == 'one-to-one':
# 一对一模式:自动为每个大纲创建对应的章节
yield await SSEResponse.send_progress("一对一模式:自动创建章节...", 50)
for outline in created_outlines:
chapter = Chapter(
project_id=project_id,
title=outline.title,
content="", # 空内容,等待用户生成
outline_id=None, # 一对一模式下不关联outline_id
chapter_number=outline.order_index, # 使用chapter_number而不是order_index
status="pending"
)
db.add(chapter)
created_chapters.append(chapter)
await db.flush()
for chapter in created_chapters:
await db.refresh(chapter)
logger.info(f"✅ 一对一模式:自动创建了{len(created_chapters)}个章节")
yield await SSEResponse.send_progress(f"已自动创建{len(created_chapters)}个章节", 85)
else:
# 一对多模式:跳过自动创建,用户可手动展开
yield await SSEResponse.send_progress("细化模式:跳过自动创建章节", 85)
logger.info(f"📝 细化模式:跳过章节创建,用户可在大纲页面手动展开")
# 更新项目信息
project.chapter_count = 0 # 向导阶段不创建章节
project.chapter_count = len(created_chapters) # 记录实际创建章节
project.narrative_perspective = narrative_perspective
project.target_words = target_words
project.status = "writing"
project.wizard_status = "completed"
project.wizard_step = 3
project.wizard_step = 3
await db.commit()
db_committed = True
logger.info(f"📊 向导大纲生成完成:")
logger.info(f" - 创建大纲节点:{len(created_outlines)}")
logger.info(f" - 提示:可在大纲页面手动展开为章节")
logger.info(f" - 创建章节:{len(created_chapters)}")
logger.info(f" - 大纲模式:{project.outline_mode}")
# 构建结果消息
if project.outline_mode == 'one-to-one':
result_message = f"成功生成{len(created_outlines)}个大纲节点并自动创建{len(created_chapters)}个章节(传统模式)"
result_note = "已自动创建章节,可直接生成内容"
else:
result_message = f"成功生成{len(created_outlines)}个大纲节点(细化模式,可在大纲页面手动展开)"
result_note = "可在大纲页面展开为多个章节"
# 发送结果
yield await SSEResponse.send_result({
"message": f"成功生成{len(created_outlines)}个大纲节点(未展开章节,可在大纲页面手动展开)",
"message": result_message,
"outline_count": len(created_outlines),
"chapter_count": 0,
"chapter_count": len(created_chapters),
"outline_mode": project.outline_mode,
"outlines": [
{
"id": outline.id,
"order_index": outline.order_index,
"title": outline.title,
"content": outline.content[:100] + "..." if len(outline.content) > 100 else outline.content,
"note": "可在大纲页面展开为章节"
"note": result_note
} for outline in created_outlines
]
],
"chapters": [
{
"id": chapter.id,
"chapter_number": chapter.chapter_number,
"title": chapter.title,
"status": chapter.status
} for chapter in created_chapters
] if created_chapters else []
})
yield await SSEResponse.send_progress("完成!", 100, "success")
+9 -1
View File
@@ -1,5 +1,5 @@
"""项目数据模型"""
from sqlalchemy import Column, String, Text, DateTime, Integer
from sqlalchemy import Column, String, Text, DateTime, Integer, CheckConstraint
from sqlalchemy.sql import func
from app.database import Base
import uuid
@@ -20,6 +20,7 @@ class Project(Base):
status = Column(String(20), default="planning", comment="创作状态")
wizard_status = Column(String(20), default="incomplete", comment="向导完成状态: incomplete/completed")
wizard_step = Column(Integer, default=0, comment="向导当前步骤: 0-4")
outline_mode = Column(String(20), nullable=False, default="one-to-many", comment="大纲章节模式: one-to-one(传统模式) 或 one-to-many(细化模式)")
# 世界构建字段
world_time_period = Column(Text, comment="时间背景")
@@ -35,5 +36,12 @@ class Project(Base):
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
__table_args__ = (
CheckConstraint(
"outline_mode IN ('one-to-one', 'one-to-many')",
name='check_outline_mode'
),
)
def __repr__(self):
return f"<Project(id={self.id}, title={self.title})>"
+46 -2
View File
@@ -1,6 +1,6 @@
"""章节相关的Pydantic模型"""
from pydantic import BaseModel, Field
from typing import Optional
from typing import Optional, List, Dict, Any
from datetime import datetime
@@ -118,4 +118,48 @@ class BatchGenerateStatusResponse(BaseModel):
created_at: Optional[str] = None
started_at: Optional[str] = None
completed_at: Optional[str] = None
error_message: Optional[str] = None
error_message: Optional[str] = None
class SceneData(BaseModel):
"""场景数据模型"""
location: str = Field(..., description="场景地点")
characters: List[str] = Field(..., description="参与角色列表")
purpose: str = Field(..., description="场景目的")
class ExpansionPlanUpdate(BaseModel):
"""章节规划更新模型"""
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="场景列表")
class Config:
json_schema_extra = {
"example": {
"key_events": ["主角遇到挑战", "关键决策时刻"],
"character_focus": ["张三", "李四"],
"emotional_tone": "紧张激烈",
"narrative_goal": "推进主线剧情",
"conflict_type": "内心冲突",
"estimated_words": 3000,
"scenes": [
{
"location": "城市广场",
"characters": ["张三", "李四"],
"purpose": "初次相遇"
}
]
}
}
class ExpansionPlanResponse(BaseModel):
"""章节规划响应模型"""
id: str = Field(..., description="章节ID")
expansion_plan: Optional[Dict[str, Any]] = Field(None, description="规划数据")
message: str = Field(..., description="响应消息")
+10 -1
View File
@@ -1,6 +1,6 @@
"""项目相关的Pydantic模型"""
from pydantic import BaseModel, Field
from typing import Optional
from typing import Optional, Literal
from datetime import datetime
@@ -11,6 +11,10 @@ class ProjectBase(BaseModel):
theme: Optional[str] = Field(None, description="主题")
genre: Optional[str] = Field(None, description="小说类型")
target_words: Optional[int] = Field(None, description="目标字数")
outline_mode: Literal["one-to-one", "one-to-many"] = Field(
default="one-to-many",
description="大纲章节模式: one-to-one(传统模式,1大纲→1章节) 或 one-to-many(细化模式,1大纲→N章节)"
)
class ProjectCreate(ProjectBase):
@@ -51,6 +55,7 @@ class ProjectResponse(ProjectBase):
chapter_count: Optional[int] = None
narrative_perspective: Optional[str] = None
character_count: Optional[int] = None
outline_mode: str # 显式声明以确保响应中包含
created_at: datetime
updated_at: datetime
@@ -73,6 +78,10 @@ class ProjectWizardRequest(BaseModel):
narrative_perspective: str = Field(..., description="叙事视角")
character_count: int = Field(5, ge=5, description="角色数量(至少5个)")
target_words: Optional[int] = Field(None, description="目标字数")
outline_mode: Literal["one-to-one", "one-to-many"] = Field(
default="one-to-many",
description="大纲章节模式"
)
class WorldBuildingResponse(BaseModel):
+41 -9
View File
@@ -207,14 +207,16 @@ class PromptService:
4. **无特殊符号**文本中不使用引号方括号等特殊符号包裹内容
5. **丰富细节**每个字段提供充实的原创内容避免模板化表达
# 反面示例(避免这样的设定)
不好的设定故事设定在大崩解后的XX纪元新世界秩序文明重启...
好的设定故事设定在2024年的深圳互联网创业浪潮下的年轻人...
请根据输入的类型和主题生成**规模适当风格匹配**的世界观设定
不好的设定升华纪元共鸣指数灵光纯度...现代都市题材不要用这些
好的设定通过高考分数学历背景家庭条件来衡量个人价值...符合现实
# JSON格式示例
请根据输入的类型和主题生成**规模适当风格匹配**的世界观设定"""
{{
"time_period": "时间背景与社会状态的详细描述(300-500字)",
"location": "空间环境与地理特征的详细描述(300-500字)",
"atmosphere": "感官体验与情感基调的详细描述(300-500字)",
"rules": "世界规则与社会结构的详细描述(300-500字)"
}}"""
# 批量角色生成提示词
CHARACTERS_BATCH_GENERATION = """你是一位专业的角色设定师。请根据以下世界观和要求,生成{count}个立体丰满的角色和组织:
@@ -1019,7 +1021,8 @@ class PromptService:
chapter_outline: str, style_content: str = "",
target_word_count: int = 3000,
memory_context: dict = None,
mcp_references: str = "") -> str:
mcp_references: str = "",
outline_mode: str = "one-to-many") -> str:
"""
获取章节完整创作提示词
@@ -1028,6 +1031,7 @@ class PromptService:
target_word_count: 目标字数默认3000字
memory_context: 记忆上下文可选
mcp_references: MCP工具搜索的参考资料可选
outline_mode: 大纲模式 (one-to-one/one-to-many)
"""
# 计算最大字数(目标字数+1000
max_word_count = target_word_count + 1000
@@ -1050,6 +1054,13 @@ class PromptService:
mcp_text += mcp_references
mcp_text += "\n"
# 根据大纲模式添加创作指导
mode_instruction = ""
if outline_mode == 'one-to-one':
mode_instruction = "\n\n【创作模式说明】\n本章采用一对一模式:一个大纲节点对应一个章节。请充分展开大纲中的情节,注重叙事的完整性和丰满度。\n"
else:
mode_instruction = "\n\n【创作模式说明】\n本章采用细纲模式:本章是大纲节点的细化展开之一。请严格遵循上述详细规划中的剧情点、角色焦点和情感基调,确保与整体规划保持一致。\n"
base_prompt = cls.format_prompt(
cls.CHAPTER_GENERATION,
title=title,
@@ -1079,7 +1090,13 @@ class PromptService:
if insert_text:
base_prompt = base_prompt.replace(
"本章信息:",
insert_text + "\n\n本章信息:"
insert_text + mode_instruction + "\n\n本章信息:"
)
else:
# 没有记忆和MCP时也要插入模式说明
base_prompt = base_prompt.replace(
"本章信息:",
mode_instruction + "\n\n本章信息:"
)
# 如果有风格要求,应用到提示词中
@@ -1098,7 +1115,8 @@ class PromptService:
style_content: str = "",
target_word_count: int = 3000,
memory_context: dict = None,
mcp_references: str = "") -> str:
mcp_references: str = "",
outline_mode: str = "one-to-many") -> str:
"""
获取章节完整创作提示词带前置章节上下文和记忆增强
@@ -1107,6 +1125,7 @@ class PromptService:
target_word_count: 目标字数默认3000字
memory_context: 记忆上下文可选
mcp_references: MCP工具搜索的参考资料可选
outline_mode: 大纲模式 (one-to-one/one-to-many)
"""
# 计算最大字数(目标字数+1000
max_word_count = target_word_count + 1000
@@ -1128,6 +1147,13 @@ class PromptService:
memory_text += "以下是通过MCP工具搜索到的相关参考资料,可用于丰富情节和细节:\n\n"
memory_text += mcp_references
# 根据大纲模式添加创作指导
mode_instruction = ""
if outline_mode == 'one-to-one':
mode_instruction = "\n\n【创作模式说明】\n本章采用一对一模式:一个大纲节点对应一个章节。请在承接前文的基础上,充分展开大纲中的情节,保持叙事的完整性。\n"
else:
mode_instruction = "\n\n【创作模式说明】\n本章采用细纲模式:本章是大纲节点的细化展开之一。请严格遵循上述详细规划(expansion_plan)中的剧情点、角色焦点、情感基调和叙事目标,确保与整体规划保持一致,同时自然衔接前文内容。\n"
base_prompt = cls.format_prompt(
cls.CHAPTER_GENERATION_WITH_CONTEXT,
title=title,
@@ -1149,6 +1175,12 @@ class PromptService:
memory_context=memory_text
)
# 插入模式说明
base_prompt = base_prompt.replace(
"本章信息:",
mode_instruction + "\n本章信息:"
)
# 如果有风格要求,应用到提示词中
if style_content:
return WritingStyleManager.apply_style_to_prompt(base_prompt, style_content)
@@ -0,0 +1,25 @@
-- Migration: Add outline_mode to projects table
-- Description: 为项目表添加大纲模式字段,支持一对一和一对多两种模式
-- Date: 2025-11-27
-- 1. 添加 outline_mode 字段
ALTER TABLE projects
ADD COLUMN outline_mode VARCHAR(20) NOT NULL DEFAULT 'one-to-many';
-- 2. 添加检查约束,确保只能是两个有效值之一
ALTER TABLE projects
ADD CONSTRAINT check_outline_mode
CHECK (outline_mode IN ('one-to-one', 'one-to-many'));
-- 3. 创建索引以提高查询性能
CREATE INDEX idx_projects_outline_mode ON projects(outline_mode);
-- 4. 为现有项目设置默认模式为一对多(细化模式)
-- 这是因为现有项目大多使用展开功能
UPDATE projects SET outline_mode = 'one-to-many' WHERE outline_mode IS NULL;
-- 5. 添加注释
COMMENT ON COLUMN projects.outline_mode IS '大纲章节模式: one-to-one(传统模式,1大纲→1章节) 或 one-to-many(细化模式,1大纲→N章节)';
-- 验证迁移结果
-- SELECT id, title, outline_mode FROM projects LIMIT 10;