update:1.新增统一的JSON清洗和重试方法,避免AI响应json格式错误 2.重构提示词模板命名,优化大纲章节初始化提示词 3.移除布冯冗余代码,提高代码复用性 4.优化系统默认写作风格预设提示词和规则
This commit is contained in:
@@ -352,412 +352,6 @@ async def create_character(
|
||||
raise HTTPException(status_code=500, detail=f"创建角色失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate", response_model=CharacterResponse, summary="AI生成角色")
|
||||
async def generate_character(
|
||||
request: CharacterGenerateRequest,
|
||||
http_request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||
):
|
||||
"""
|
||||
使用AI生成角色卡
|
||||
|
||||
根据用户输入的信息,结合项目的世界观、主题等背景,
|
||||
AI会生成一个完整、详细的角色设定卡片。
|
||||
|
||||
生成内容包括:姓名、年龄、性别、性格、外貌、背景故事、人际关系等
|
||||
"""
|
||||
# 验证用户权限和项目是否存在
|
||||
user_id = getattr(http_request.state, 'user_id', None)
|
||||
project = await verify_project_access(request.project_id, user_id, db)
|
||||
|
||||
try:
|
||||
# 获取已存在的角色列表,用于关系网络
|
||||
existing_chars_result = await db.execute(
|
||||
select(Character)
|
||||
.where(Character.project_id == request.project_id)
|
||||
.order_by(Character.created_at.desc())
|
||||
)
|
||||
existing_characters = existing_chars_result.scalars().all()
|
||||
|
||||
# 构建现有角色信息摘要(包含组织)
|
||||
existing_chars_info = ""
|
||||
character_list = []
|
||||
organization_list = []
|
||||
|
||||
if existing_characters:
|
||||
for c in existing_characters[:10]: # 最多显示10个
|
||||
if c.is_organization:
|
||||
organization_list.append(f"- {c.name} [{c.organization_type or '组织'}]")
|
||||
else:
|
||||
character_list.append(f"- {c.name}({c.role_type or '未知'})")
|
||||
|
||||
if character_list:
|
||||
existing_chars_info += "\n已有角色:\n" + "\n".join(character_list)
|
||||
if organization_list:
|
||||
existing_chars_info += "\n\n已有组织:\n" + "\n".join(organization_list)
|
||||
|
||||
# 构建项目上下文信息
|
||||
project_context = f"""
|
||||
项目信息:
|
||||
- 书名:{project.title}
|
||||
- 主题:{project.theme or '未设定'}
|
||||
- 类型:{project.genre or '未设定'}
|
||||
- 时间背景:{project.world_time_period or '未设定'}
|
||||
- 地理位置:{project.world_location or '未设定'}
|
||||
- 氛围基调:{project.world_atmosphere or '未设定'}
|
||||
- 世界规则:{project.world_rules or '未设定'}
|
||||
{existing_chars_info}
|
||||
"""
|
||||
|
||||
# 构建用户输入信息
|
||||
user_input = f"""
|
||||
用户要求:
|
||||
- 角色名称:{request.name or '请AI生成'}
|
||||
- 角色定位:{request.role_type or 'supporting'}(protagonist=主角, supporting=配角, antagonist=反派)
|
||||
- 背景设定:{request.background or '无特殊要求'}
|
||||
- 其他要求:{request.requirements or '无'}
|
||||
"""
|
||||
|
||||
# 获取自定义提示词模板
|
||||
template = await PromptService.get_template("SINGLE_CHARACTER_GENERATION", user_id, db)
|
||||
# 格式化提示词
|
||||
prompt = PromptService.format_prompt(
|
||||
template,
|
||||
project_context=project_context,
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
# 调用AI生成角色(支持MCP工具)
|
||||
logger.info(f"🎯 开始为项目 {request.project_id} 生成角色(启用MCP)")
|
||||
logger.info(f" - 角色名:{request.name or 'AI生成'}")
|
||||
logger.info(f" - 角色定位:{request.role_type}")
|
||||
logger.info(f" - 背景设定:{request.background or '无'}")
|
||||
logger.info(f" - AI提供商:{user_ai_service.api_provider}")
|
||||
logger.info(f" - AI模型:{user_ai_service.default_model}")
|
||||
logger.info(f" - Prompt长度:{len(prompt)} 字符")
|
||||
logger.info(f" - 用户ID:{user_id}")
|
||||
|
||||
try:
|
||||
# 🔧 MCP工具增强:静默检查并收集参考资料
|
||||
mcp_enhanced_prompt = prompt
|
||||
if user_id:
|
||||
try:
|
||||
from app.services.mcp_tool_service import mcp_tool_service
|
||||
available_tools = await mcp_tool_service.get_user_enabled_tools(
|
||||
user_id=user_id,
|
||||
db_session=db
|
||||
)
|
||||
|
||||
# 只在有工具时才调用
|
||||
if available_tools:
|
||||
logger.info(f"🔍 检测到可用MCP工具,尝试收集参考资料...")
|
||||
result = await user_ai_service.generate_text_with_mcp(
|
||||
prompt=prompt,
|
||||
user_id=user_id,
|
||||
db_session=db,
|
||||
enable_mcp=True,
|
||||
max_tool_rounds=1, # 减少为1轮,避免超时
|
||||
tool_choice="auto",
|
||||
provider=None,
|
||||
model=None
|
||||
)
|
||||
|
||||
# 提取内容
|
||||
if isinstance(result, dict):
|
||||
ai_response = result.get('content', '')
|
||||
logger.info(f"✅ AI响应接收完成(MCP增强),长度:{len(ai_response)} 字符")
|
||||
if result.get('tool_calls_made', 0) > 0:
|
||||
logger.info(f" - MCP工具调用:{result['tool_calls_made']} 次")
|
||||
else:
|
||||
ai_response = result
|
||||
logger.info(f"✅ AI响应接收完成,长度:{len(ai_response) if ai_response else 0} 字符")
|
||||
else:
|
||||
logger.debug(f"用户 {user_id} 未启用MCP工具,使用基础模式")
|
||||
# 不使用MCP,直接生成
|
||||
result = await user_ai_service.generate_text(prompt=prompt)
|
||||
ai_response = result.get('content', '') if isinstance(result, dict) else result
|
||||
|
||||
except Exception as mcp_error:
|
||||
logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(mcp_error)}")
|
||||
# 降级:不使用MCP
|
||||
result = await user_ai_service.generate_text(prompt=prompt)
|
||||
ai_response = result.get('content', '') if isinstance(result, dict) else result
|
||||
else:
|
||||
# 无用户ID,直接使用基础模式
|
||||
result = await user_ai_service.generate_text(prompt=prompt)
|
||||
ai_response = result.get('content', '') if isinstance(result, dict) else result
|
||||
|
||||
except Exception as ai_error:
|
||||
logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"AI服务调用失败:{str(ai_error)}"
|
||||
)
|
||||
|
||||
# 检查AI响应
|
||||
if not ai_response or not ai_response.strip():
|
||||
logger.error("❌ AI返回了空响应")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="AI服务返回空响应。可能原因:1) API配置错误 2) 模型不支持 3) 网络问题。请检查后端日志。"
|
||||
)
|
||||
|
||||
logger.info(f"📝 开始清理AI响应")
|
||||
# 清理AI响应,移除可能的markdown标记
|
||||
cleaned_response = ai_response.strip()
|
||||
original_length = len(cleaned_response)
|
||||
|
||||
if cleaned_response.startswith("```json"):
|
||||
cleaned_response = cleaned_response[7:]
|
||||
logger.info(" - 移除了 ```json 标记")
|
||||
if cleaned_response.startswith("```"):
|
||||
cleaned_response = cleaned_response[3:]
|
||||
logger.info(" - 移除了 ``` 标记")
|
||||
if cleaned_response.endswith("```"):
|
||||
cleaned_response = cleaned_response[:-3]
|
||||
logger.info(" - 移除了末尾 ``` 标记")
|
||||
cleaned_response = cleaned_response.strip()
|
||||
|
||||
logger.info(f" - 清理前长度:{original_length},清理后长度:{len(cleaned_response)}")
|
||||
logger.info(f" - 清理后内容预览(前300字符):{cleaned_response[:300]}")
|
||||
|
||||
# 解析AI响应
|
||||
logger.info(f"🔍 开始解析JSON")
|
||||
try:
|
||||
character_data = json.loads(cleaned_response)
|
||||
logger.info(f"✅ JSON解析成功")
|
||||
logger.info(f" - 解析后的字段:{list(character_data.keys())}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"❌ JSON解析失败")
|
||||
logger.error(f" - 错误位置:line {e.lineno}, column {e.colno}")
|
||||
logger.error(f" - 错误信息:{str(e)}")
|
||||
logger.error(f" - 完整响应内容(前1000字符):{cleaned_response[:1000]}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"AI返回的内容无法解析为JSON。错误:{str(e)}。响应内容已记录到日志,请查看后端日志排查。"
|
||||
)
|
||||
|
||||
# 转换traits为JSON字符串
|
||||
traits_json = json.dumps(character_data.get("traits", []), ensure_ascii=False) if character_data.get("traits") else None
|
||||
|
||||
# 判断是否为组织
|
||||
is_organization = character_data.get("is_organization", False)
|
||||
|
||||
# 创建角色
|
||||
character = Character(
|
||||
project_id=request.project_id,
|
||||
name=character_data.get("name", request.name or "未命名角色"),
|
||||
age=str(character_data.get("age", "")),
|
||||
gender=character_data.get("gender"),
|
||||
is_organization=is_organization,
|
||||
role_type=request.role_type or "supporting",
|
||||
personality=character_data.get("personality", ""),
|
||||
background=character_data.get("background", ""),
|
||||
appearance=character_data.get("appearance", ""),
|
||||
relationships=character_data.get("relationships_text", character_data.get("relationships", "")), # 优先使用文本描述
|
||||
organization_type=character_data.get("organization_type") if is_organization else None,
|
||||
organization_purpose=character_data.get("organization_purpose") if is_organization else None,
|
||||
organization_members=json.dumps(character_data.get("organization_members", []), ensure_ascii=False) if is_organization else None,
|
||||
traits=traits_json
|
||||
)
|
||||
db.add(character)
|
||||
await db.flush() # 获取character.id
|
||||
|
||||
logger.info(f"✅ 角色创建成功:{character.name} (ID: {character.id}, 是否组织: {is_organization})")
|
||||
|
||||
# 如果是组织,自动创建Organization详情记录
|
||||
if is_organization:
|
||||
org_check = await db.execute(
|
||||
select(Organization).where(Organization.character_id == character.id)
|
||||
)
|
||||
existing_org = org_check.scalar_one_or_none()
|
||||
|
||||
if not existing_org:
|
||||
organization = Organization(
|
||||
character_id=character.id,
|
||||
project_id=request.project_id,
|
||||
member_count=0,
|
||||
power_level=character_data.get("power_level", 50),
|
||||
location=character_data.get("location"),
|
||||
motto=character_data.get("motto"),
|
||||
color=character_data.get("color")
|
||||
)
|
||||
db.add(organization)
|
||||
await db.flush()
|
||||
logger.info(f"✅ 自动创建组织详情:{character.name} (Org ID: {organization.id})")
|
||||
else:
|
||||
logger.info(f"ℹ️ 组织详情已存在:{character.name}")
|
||||
|
||||
# 处理结构化关系数据(仅针对非组织角色)
|
||||
if not is_organization:
|
||||
relationships_data = character_data.get("relationships", [])
|
||||
if relationships_data and isinstance(relationships_data, list):
|
||||
logger.info(f"📊 开始处理 {len(relationships_data)} 条关系数据")
|
||||
created_rels = 0
|
||||
|
||||
for rel in relationships_data:
|
||||
try:
|
||||
target_name = rel.get("target_character_name")
|
||||
if not target_name:
|
||||
logger.debug(f" ⚠️ 关系缺少target_character_name,跳过")
|
||||
continue
|
||||
|
||||
target_result = await db.execute(
|
||||
select(Character).where(
|
||||
Character.project_id == request.project_id,
|
||||
Character.name == target_name
|
||||
)
|
||||
)
|
||||
target_char = target_result.scalar_one_or_none()
|
||||
|
||||
if target_char:
|
||||
# 检查是否已存在相同关系
|
||||
existing_rel = await db.execute(
|
||||
select(CharacterRelationship).where(
|
||||
CharacterRelationship.project_id == request.project_id,
|
||||
CharacterRelationship.character_from_id == character.id,
|
||||
CharacterRelationship.character_to_id == target_char.id
|
||||
)
|
||||
)
|
||||
if existing_rel.scalar_one_or_none():
|
||||
logger.debug(f" ℹ️ 关系已存在:{character.name} -> {target_name}")
|
||||
continue
|
||||
|
||||
relationship = CharacterRelationship(
|
||||
project_id=request.project_id,
|
||||
character_from_id=character.id,
|
||||
character_to_id=target_char.id,
|
||||
relationship_name=rel.get("relationship_type", "未知关系"),
|
||||
intimacy_level=rel.get("intimacy_level", 50),
|
||||
description=rel.get("description", ""),
|
||||
started_at=rel.get("started_at"),
|
||||
source="ai"
|
||||
)
|
||||
|
||||
# 匹配预定义关系类型
|
||||
rel_type_result = await db.execute(
|
||||
select(RelationshipType).where(
|
||||
RelationshipType.name == rel.get("relationship_type")
|
||||
)
|
||||
)
|
||||
rel_type = rel_type_result.scalar_one_or_none()
|
||||
if rel_type:
|
||||
relationship.relationship_type_id = rel_type.id
|
||||
|
||||
db.add(relationship)
|
||||
created_rels += 1
|
||||
logger.info(f" ✅ 创建关系:{character.name} -> {target_name} ({rel.get('relationship_type')})")
|
||||
else:
|
||||
logger.warning(f" ⚠️ 目标角色不存在:{target_name}")
|
||||
|
||||
except Exception as rel_error:
|
||||
logger.warning(f" ❌ 创建关系失败:{str(rel_error)}")
|
||||
continue
|
||||
|
||||
logger.info(f"✅ 成功创建 {created_rels} 条关系记录")
|
||||
|
||||
# 处理组织成员关系(仅针对非组织角色)
|
||||
if not is_organization:
|
||||
org_memberships = character_data.get("organization_memberships", [])
|
||||
if org_memberships and isinstance(org_memberships, list):
|
||||
logger.info(f"🏢 开始处理 {len(org_memberships)} 条组织成员关系")
|
||||
created_members = 0
|
||||
|
||||
for membership in org_memberships:
|
||||
try:
|
||||
org_name = membership.get("organization_name")
|
||||
if not org_name:
|
||||
logger.debug(f" ⚠️ 组织成员关系缺少organization_name,跳过")
|
||||
continue
|
||||
|
||||
org_char_result = await db.execute(
|
||||
select(Character).where(
|
||||
Character.project_id == request.project_id,
|
||||
Character.name == org_name,
|
||||
Character.is_organization == True
|
||||
)
|
||||
)
|
||||
org_char = org_char_result.scalar_one_or_none()
|
||||
|
||||
if org_char:
|
||||
# 获取或创建Organization记录
|
||||
org_result = await db.execute(
|
||||
select(Organization).where(Organization.character_id == org_char.id)
|
||||
)
|
||||
org = org_result.scalar_one_or_none()
|
||||
|
||||
if not org:
|
||||
# 如果组织Character存在但Organization不存在,自动创建
|
||||
org = Organization(
|
||||
character_id=org_char.id,
|
||||
project_id=request.project_id,
|
||||
member_count=0
|
||||
)
|
||||
db.add(org)
|
||||
await db.flush()
|
||||
logger.info(f" ℹ️ 自动创建缺失的组织详情:{org_name}")
|
||||
|
||||
# 检查是否已存在成员关系
|
||||
existing_member = await db.execute(
|
||||
select(OrganizationMember).where(
|
||||
OrganizationMember.organization_id == org.id,
|
||||
OrganizationMember.character_id == character.id
|
||||
)
|
||||
)
|
||||
if existing_member.scalar_one_or_none():
|
||||
logger.debug(f" ℹ️ 成员关系已存在:{character.name} -> {org_name}")
|
||||
continue
|
||||
|
||||
# 创建成员关系
|
||||
member = OrganizationMember(
|
||||
organization_id=org.id,
|
||||
character_id=character.id,
|
||||
position=membership.get("position", "成员"),
|
||||
rank=membership.get("rank", 0),
|
||||
loyalty=membership.get("loyalty", 50),
|
||||
joined_at=membership.get("joined_at"),
|
||||
status=membership.get("status", "active"),
|
||||
source="ai"
|
||||
)
|
||||
db.add(member)
|
||||
|
||||
# 更新组织成员计数
|
||||
org.member_count += 1
|
||||
|
||||
created_members += 1
|
||||
logger.info(f" ✅ 添加成员:{character.name} -> {org_name} ({membership.get('position')})")
|
||||
else:
|
||||
logger.warning(f" ⚠️ 组织不存在:{org_name}")
|
||||
|
||||
except Exception as org_error:
|
||||
logger.warning(f" ❌ 添加组织成员失败:{str(org_error)}")
|
||||
continue
|
||||
|
||||
logger.info(f"✅ 成功创建 {created_members} 条组织成员记录")
|
||||
|
||||
# 记录生成历史
|
||||
history = GenerationHistory(
|
||||
project_id=request.project_id,
|
||||
prompt=prompt,
|
||||
generated_content=json.dumps(result, ensure_ascii=False) if isinstance(result, dict) else ai_response,
|
||||
model=user_ai_service.default_model
|
||||
)
|
||||
db.add(history)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(character)
|
||||
|
||||
logger.info(f"🎉 成功为项目 {request.project_id} 生成角色: {character.name}")
|
||||
|
||||
return character
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成角色失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成角色失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate-stream", summary="AI生成角色(流式)")
|
||||
async def generate_character_stream(
|
||||
request: CharacterGenerateRequest,
|
||||
@@ -894,19 +488,14 @@ async def generate_character_stream(
|
||||
|
||||
yield await SSEResponse.send_progress("解析AI响应...", 60)
|
||||
|
||||
# 清理AI响应
|
||||
cleaned_response = ai_response.strip()
|
||||
if cleaned_response.startswith("```json"):
|
||||
cleaned_response = cleaned_response[7:]
|
||||
if cleaned_response.startswith("```"):
|
||||
cleaned_response = cleaned_response[3:]
|
||||
if cleaned_response.endswith("```"):
|
||||
cleaned_response = cleaned_response[:-3]
|
||||
cleaned_response = cleaned_response.strip()
|
||||
|
||||
# ✅ 使用统一的 JSON 清洗方法
|
||||
try:
|
||||
cleaned_response = user_ai_service._clean_json_response(ai_response)
|
||||
character_data = json.loads(cleaned_response)
|
||||
logger.info(f"✅ 角色JSON解析成功")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"❌ 角色JSON解析失败: {e}")
|
||||
logger.error(f" 原始响应预览: {ai_response[:200]}")
|
||||
yield await SSEResponse.send_error(f"AI返回的内容无法解析为JSON:{str(e)}")
|
||||
return
|
||||
|
||||
|
||||
@@ -162,26 +162,10 @@ async def generate_options(
|
||||
content = response.get("content", "")
|
||||
logger.info(f"AI返回内容长度: {len(content)}")
|
||||
|
||||
# 解析JSON
|
||||
# 解析JSON(使用统一的JSON清洗方法)
|
||||
try:
|
||||
# 清理可能的markdown标记
|
||||
cleaned_content = content.strip()
|
||||
if cleaned_content.startswith('```json'):
|
||||
cleaned_content = cleaned_content[7:].lstrip('\n\r')
|
||||
elif cleaned_content.startswith('```'):
|
||||
cleaned_content = cleaned_content[3:].lstrip('\n\r')
|
||||
if cleaned_content.endswith('```'):
|
||||
cleaned_content = cleaned_content[:-3].rstrip('\n\r')
|
||||
cleaned_content = cleaned_content.strip()
|
||||
|
||||
# 检查JSON是否完整
|
||||
if not cleaned_content.endswith('}'):
|
||||
logger.warning(f"⚠️ JSON可能被截断,尝试补全...")
|
||||
if '"options"' in cleaned_content:
|
||||
if cleaned_content.count('[') > cleaned_content.count(']'):
|
||||
cleaned_content += '"]}'
|
||||
elif cleaned_content.count('{') > cleaned_content.count('}'):
|
||||
cleaned_content += '}'
|
||||
# 使用统一的JSON清洗方法
|
||||
cleaned_content = ai_service._clean_json_response(content)
|
||||
|
||||
result = json.loads(cleaned_content)
|
||||
|
||||
@@ -305,16 +289,10 @@ async def quick_generate(
|
||||
|
||||
content = response.get("content", "")
|
||||
|
||||
# 解析JSON
|
||||
# 解析JSON(使用统一的JSON清洗方法)
|
||||
try:
|
||||
cleaned_content = content.strip()
|
||||
if cleaned_content.startswith('```json'):
|
||||
cleaned_content = cleaned_content[7:].lstrip('\n\r')
|
||||
elif cleaned_content.startswith('```'):
|
||||
cleaned_content = cleaned_content[3:].lstrip('\n\r')
|
||||
if cleaned_content.endswith('```'):
|
||||
cleaned_content = cleaned_content[:-3].rstrip('\n\r')
|
||||
cleaned_content = cleaned_content.strip()
|
||||
# 使用统一的JSON清洗方法
|
||||
cleaned_content = ai_service._clean_json_response(content)
|
||||
|
||||
result = json.loads(cleaned_content)
|
||||
|
||||
|
||||
@@ -429,199 +429,6 @@ async def remove_organization_member(
|
||||
logger.info(f"移除成员成功:{member_id}")
|
||||
return {"message": "成员移除成功", "id": member_id}
|
||||
|
||||
@router.post("/generate", response_model=CharacterResponse, summary="AI生成组织")
|
||||
async def generate_organization(
|
||||
gen_request: OrganizationGenerateRequest,
|
||||
http_request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||
):
|
||||
"""
|
||||
使用AI生成组织设定
|
||||
|
||||
根据用户输入的信息,结合项目的世界观、主题等背景,
|
||||
AI会生成一个完整、详细的组织设定。
|
||||
|
||||
生成内容包括:组织名称、类型、特性、背景、目的、势力等级等
|
||||
"""
|
||||
# 验证用户权限
|
||||
user_id = getattr(http_request.state, 'user_id', None)
|
||||
project = await verify_project_access(gen_request.project_id, user_id, db)
|
||||
|
||||
try:
|
||||
# 获取已存在的角色和组织列表
|
||||
existing_chars_result = await db.execute(
|
||||
select(Character)
|
||||
.where(Character.project_id == gen_request.project_id)
|
||||
.order_by(Character.created_at.desc())
|
||||
)
|
||||
existing_characters = existing_chars_result.scalars().all()
|
||||
|
||||
# 构建现有角色和组织信息摘要
|
||||
existing_info = ""
|
||||
character_list = []
|
||||
organization_list = []
|
||||
|
||||
if existing_characters:
|
||||
for c in existing_characters[:10]: # 最多显示10个
|
||||
if c.is_organization:
|
||||
organization_list.append(f"- {c.name} [{c.organization_type or '组织'}]")
|
||||
else:
|
||||
character_list.append(f"- {c.name}({c.role_type or '未知'})")
|
||||
|
||||
if character_list:
|
||||
existing_info += "\n已有角色:\n" + "\n".join(character_list)
|
||||
if organization_list:
|
||||
existing_info += "\n\n已有组织:\n" + "\n".join(organization_list)
|
||||
|
||||
# 构建项目上下文信息
|
||||
project_context = f"""
|
||||
项目信息:
|
||||
- 书名:{project.title}
|
||||
- 主题:{project.theme or '未设定'}
|
||||
- 类型:{project.genre or '未设定'}
|
||||
- 时间背景:{project.world_time_period or '未设定'}
|
||||
- 地理位置:{project.world_location or '未设定'}
|
||||
- 氛围基调:{project.world_atmosphere or '未设定'}
|
||||
- 世界规则:{project.world_rules or '未设定'}
|
||||
{existing_info}
|
||||
"""
|
||||
|
||||
# 构建用户输入信息
|
||||
user_input = f"""
|
||||
用户要求:
|
||||
- 组织名称:{gen_request.name or '请AI生成'}
|
||||
- 组织类型:{gen_request.organization_type or '请AI根据世界观决定'}
|
||||
- 背景设定:{gen_request.background or '无特殊要求'}
|
||||
- 其他要求:{gen_request.requirements or '无'}
|
||||
"""
|
||||
|
||||
# 获取自定义提示词模板
|
||||
template = await PromptService.get_template("SINGLE_ORGANIZATION_GENERATION", user_id, db)
|
||||
# 格式化提示词
|
||||
prompt = PromptService.format_prompt(
|
||||
template,
|
||||
project_context=project_context,
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
# 调用AI生成组织
|
||||
logger.info(f"🎯 开始为项目 {gen_request.project_id} 生成组织")
|
||||
logger.info(f" - 组织名:{gen_request.name or 'AI生成'}")
|
||||
logger.info(f" - 组织类型:{gen_request.organization_type or 'AI决定'}")
|
||||
logger.info(f" - 背景设定:{gen_request.background or '无'}")
|
||||
logger.info(f" - AI提供商:{user_ai_service.api_provider}")
|
||||
logger.info(f" - AI模型:{user_ai_service.default_model}")
|
||||
logger.info(f" - Prompt长度:{len(prompt)} 字符")
|
||||
|
||||
try:
|
||||
ai_response = await user_ai_service.generate_text(prompt=prompt)
|
||||
logger.info(f"✅ AI响应接收完成")
|
||||
except Exception as ai_error:
|
||||
logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"AI服务调用失败:{str(ai_error)}"
|
||||
)
|
||||
|
||||
# generate_text返回的是字典,需要提取content字段
|
||||
ai_content = ai_response.get("content", "") if isinstance(ai_response, dict) else str(ai_response)
|
||||
|
||||
# 检查AI响应
|
||||
if not ai_content or not ai_content.strip():
|
||||
logger.error("❌ AI返回了空响应")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="AI服务返回空响应。请检查AI配置和网络连接。"
|
||||
)
|
||||
|
||||
logger.info(f"📝 开始清理AI响应,长度:{len(ai_content)} 字符")
|
||||
# 清理AI响应
|
||||
cleaned_response = ai_content.strip()
|
||||
if cleaned_response.startswith("```json"):
|
||||
cleaned_response = cleaned_response[7:]
|
||||
if cleaned_response.startswith("```"):
|
||||
cleaned_response = cleaned_response[3:]
|
||||
if cleaned_response.endswith("```"):
|
||||
cleaned_response = cleaned_response[:-3]
|
||||
cleaned_response = cleaned_response.strip()
|
||||
|
||||
logger.info(f" - 清理后长度:{len(cleaned_response)}")
|
||||
|
||||
# 解析AI响应
|
||||
logger.info(f"🔍 开始解析JSON")
|
||||
try:
|
||||
organization_data = json.loads(cleaned_response)
|
||||
logger.info(f"✅ JSON解析成功")
|
||||
logger.info(f" - 解析后的字段:{list(organization_data.keys())}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"❌ JSON解析失败:{str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"AI返回的内容无法解析为JSON。错误:{str(e)}"
|
||||
)
|
||||
|
||||
# 创建角色记录(组织也是角色的一种)
|
||||
character = Character(
|
||||
project_id=gen_request.project_id,
|
||||
name=organization_data.get("name", gen_request.name or "未命名组织"),
|
||||
is_organization=True,
|
||||
role_type="supporting", # 组织通常作为配角
|
||||
personality=organization_data.get("personality", ""),
|
||||
background=organization_data.get("background", ""),
|
||||
appearance=organization_data.get("appearance", ""),
|
||||
organization_type=organization_data.get("organization_type"),
|
||||
organization_purpose=organization_data.get("organization_purpose"),
|
||||
organization_members=json.dumps(
|
||||
organization_data.get("organization_members", []),
|
||||
ensure_ascii=False
|
||||
),
|
||||
traits=json.dumps(
|
||||
organization_data.get("traits", []),
|
||||
ensure_ascii=False
|
||||
)
|
||||
)
|
||||
db.add(character)
|
||||
await db.flush()
|
||||
|
||||
logger.info(f"✅ 组织角色创建成功:{character.name} (ID: {character.id})")
|
||||
|
||||
# 自动创建Organization详情记录
|
||||
organization = Organization(
|
||||
character_id=character.id,
|
||||
project_id=gen_request.project_id,
|
||||
member_count=0,
|
||||
power_level=organization_data.get("power_level", 50),
|
||||
location=organization_data.get("location"),
|
||||
motto=organization_data.get("motto"),
|
||||
color=organization_data.get("color")
|
||||
)
|
||||
db.add(organization)
|
||||
await db.flush()
|
||||
|
||||
logger.info(f"✅ 组织详情创建成功:{character.name} (Org ID: {organization.id})")
|
||||
|
||||
# 记录生成历史
|
||||
history = GenerationHistory(
|
||||
project_id=gen_request.project_id,
|
||||
prompt=prompt,
|
||||
generated_content=ai_content,
|
||||
model=user_ai_service.default_model
|
||||
)
|
||||
db.add(history)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(character)
|
||||
|
||||
logger.info(f"🎉 成功为项目 {gen_request.project_id} 生成组织: {character.name}")
|
||||
|
||||
return character
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成组织失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成组织失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate-stream", summary="AI生成组织(流式)")
|
||||
async def generate_organization_stream(
|
||||
gen_request: OrganizationGenerateRequest,
|
||||
@@ -718,19 +525,14 @@ async def generate_organization_stream(
|
||||
|
||||
yield await SSEResponse.send_progress("解析AI响应...", 60)
|
||||
|
||||
# 清理AI响应
|
||||
cleaned_response = ai_content.strip()
|
||||
if cleaned_response.startswith("```json"):
|
||||
cleaned_response = cleaned_response[7:]
|
||||
if cleaned_response.startswith("```"):
|
||||
cleaned_response = cleaned_response[3:]
|
||||
if cleaned_response.endswith("```"):
|
||||
cleaned_response = cleaned_response[:-3]
|
||||
cleaned_response = cleaned_response.strip()
|
||||
|
||||
# ✅ 使用统一的 JSON 清洗方法
|
||||
try:
|
||||
cleaned_response = user_ai_service._clean_json_response(ai_content)
|
||||
organization_data = json.loads(cleaned_response)
|
||||
logger.info(f"✅ 组织JSON解析成功")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"❌ 组织JSON解析失败: {e}")
|
||||
logger.error(f" 原始响应预览: {ai_content[:200]}")
|
||||
yield await SSEResponse.send_error(f"AI返回的内容无法解析为JSON:{str(e)}")
|
||||
return
|
||||
|
||||
|
||||
+18
-394
@@ -463,72 +463,6 @@ async def predict_characters(
|
||||
logger.error(f"角色预测失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"角色预测失败: {str(e)}")
|
||||
|
||||
@router.post("/generate", response_model=OutlineListResponse, summary="AI生成/续写大纲")
|
||||
async def generate_outline(
|
||||
request: OutlineGenerateRequest,
|
||||
http_request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||
):
|
||||
"""
|
||||
使用AI生成或续写小说大纲 - 智能模式
|
||||
|
||||
支持三种模式:
|
||||
- auto: 自动判断(无大纲→新建,有大纲→续写)
|
||||
- new: 强制全新生成
|
||||
- continue: 强制续写模式
|
||||
"""
|
||||
# 验证用户权限
|
||||
user_id = getattr(http_request.state, 'user_id', None)
|
||||
project = await verify_project_access(request.project_id, user_id, db)
|
||||
|
||||
try:
|
||||
# 获取现有大纲(强制从数据库获取最新数据,包括用户手动修改的内容)
|
||||
existing_result = await db.execute(
|
||||
select(Outline)
|
||||
.where(Outline.project_id == request.project_id)
|
||||
.order_by(Outline.order_index)
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
existing_outlines = existing_result.scalars().all()
|
||||
|
||||
# 判断实际执行模式
|
||||
actual_mode = request.mode
|
||||
if actual_mode == "auto":
|
||||
actual_mode = "continue" if existing_outlines else "new"
|
||||
logger.info(f"自动判断模式:{'续写' if existing_outlines else '新建'}")
|
||||
|
||||
# 模式:全新生成
|
||||
if actual_mode == "new":
|
||||
return await _generate_new_outline(
|
||||
request, project, db, user_ai_service, user_id
|
||||
)
|
||||
|
||||
# 模式:续写
|
||||
elif actual_mode == "continue":
|
||||
if not existing_outlines:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="续写模式需要已有大纲,当前项目没有大纲"
|
||||
)
|
||||
|
||||
# 获取用户ID用于记忆检索
|
||||
user_id = getattr(http_request.state, "user_id", "system")
|
||||
return await _continue_outline(
|
||||
request, project, existing_outlines, db, user_ai_service, user_id
|
||||
)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"不支持的模式: {request.mode}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"生成大纲失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成大纲失败: {str(e)}")
|
||||
|
||||
|
||||
async def _generate_new_outline(
|
||||
@@ -621,7 +555,7 @@ async def _generate_new_outline(
|
||||
mcp_reference_materials = ""
|
||||
|
||||
# 使用完整提示词(插入MCP参考资料,支持自定义)
|
||||
template = await PromptService.get_template("COMPLETE_OUTLINE_GENERATION", user_id, db)
|
||||
template = await PromptService.get_template("OUTLINE_CREATE", user_id, db)
|
||||
prompt = PromptService.format_prompt(
|
||||
template,
|
||||
title=project.title,
|
||||
@@ -1085,7 +1019,7 @@ async def _continue_outline(
|
||||
mcp_reference_materials = ""
|
||||
|
||||
# 使用标准续写提示词模板(支持记忆+MCP增强+自定义)
|
||||
template = await PromptService.get_template("OUTLINE_CONTINUE_GENERATION", user_id, db)
|
||||
template = await PromptService.get_template("OUTLINE_CONTINUE", user_id, db)
|
||||
prompt = PromptService.format_prompt(
|
||||
template,
|
||||
title=project.title,
|
||||
@@ -1163,17 +1097,12 @@ async def _continue_outline(
|
||||
|
||||
|
||||
def _parse_ai_response(ai_response: str) -> list:
|
||||
"""解析AI响应为章节数据列表"""
|
||||
"""解析AI响应为章节数据列表(使用统一的JSON清洗方法)"""
|
||||
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清洗方法(从AIService导入)
|
||||
from app.services.ai_service import AIService
|
||||
ai_service_temp = AIService()
|
||||
cleaned_text = ai_service_temp._clean_json_response(ai_response)
|
||||
|
||||
outline_data = json.loads(cleaned_text)
|
||||
|
||||
@@ -1185,16 +1114,24 @@ def _parse_ai_response(ai_response: str) -> list:
|
||||
else:
|
||||
outline_data = [outline_data]
|
||||
|
||||
logger.info(f"✅ 成功解析 {len(outline_data)} 个章节数据")
|
||||
return outline_data
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"AI响应解析失败: {e}")
|
||||
logger.error(f"❌ AI响应解析失败: {e}")
|
||||
# 返回一个包含原始内容的章节
|
||||
return [{
|
||||
"title": "AI生成的大纲",
|
||||
"content": ai_response[:1000],
|
||||
"summary": ai_response[:1000]
|
||||
}]
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 解析异常: {str(e)}")
|
||||
return [{
|
||||
"title": "解析异常的大纲",
|
||||
"content": "系统错误",
|
||||
"summary": "系统错误"
|
||||
}]
|
||||
|
||||
|
||||
async def _save_outlines(
|
||||
@@ -1377,7 +1314,7 @@ async def new_outline_generator(
|
||||
|
||||
# 使用完整提示词(插入MCP参考资料,支持自定义)
|
||||
yield await SSEResponse.send_progress("准备AI提示词...", 20)
|
||||
template = await PromptService.get_template("COMPLETE_OUTLINE_GENERATION", user_id_for_mcp, db)
|
||||
template = await PromptService.get_template("OUTLINE_CREATE", user_id_for_mcp, db)
|
||||
prompt = PromptService.format_prompt(
|
||||
template,
|
||||
title=project.title,
|
||||
@@ -1877,7 +1814,7 @@ async def continue_outline_generator(
|
||||
)
|
||||
|
||||
# 使用标准续写提示词模板(支持记忆+MCP增强+自定义)
|
||||
template = await PromptService.get_template("OUTLINE_CONTINUE_GENERATION", user_id, db)
|
||||
template = await PromptService.get_template("OUTLINE_CONTINUE", user_id, db)
|
||||
prompt = PromptService.format_prompt(
|
||||
template,
|
||||
title=project.title,
|
||||
@@ -2322,142 +2259,6 @@ async def create_single_chapter_from_outline(
|
||||
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,
|
||||
expansion_request: OutlineExpansionRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||
):
|
||||
"""
|
||||
根据单个大纲摘要,通过AI分析生成多个章节规划
|
||||
|
||||
流程:
|
||||
1. 获取大纲信息和上下文(前后大纲)
|
||||
2. 调用AI分析大纲,生成多章节规划
|
||||
3. 根据规划创建章节记录(outline_id关联到原大纲)
|
||||
|
||||
参数:
|
||||
- outline_id: 要展开的大纲ID
|
||||
- expansion_request: 展开配置(章节数量、展开策略等)
|
||||
|
||||
返回:
|
||||
- 展开后的章节列表和规划详情
|
||||
"""
|
||||
# 验证用户权限
|
||||
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-many':
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"当前项目为{project.outline_mode}模式,不支持展开功能。请使用一对一创建。"
|
||||
)
|
||||
|
||||
try:
|
||||
# 创建展开服务实例
|
||||
expansion_service = PlotExpansionService(user_ai_service)
|
||||
|
||||
# 获取项目信息
|
||||
project_result = await db.execute(
|
||||
select(Project).where(Project.id == outline.project_id)
|
||||
)
|
||||
project = project_result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 分析大纲并生成章节规划
|
||||
logger.info(f"开始展开大纲 {outline_id},目标章节数: {expansion_request.target_chapter_count}")
|
||||
|
||||
chapter_plans = await expansion_service.analyze_outline_for_chapters(
|
||||
outline=outline,
|
||||
project=project,
|
||||
db=db,
|
||||
target_chapter_count=expansion_request.target_chapter_count,
|
||||
expansion_strategy=expansion_request.expansion_strategy,
|
||||
enable_scene_analysis=expansion_request.enable_scene_analysis,
|
||||
provider=expansion_request.provider,
|
||||
model=expansion_request.model
|
||||
)
|
||||
|
||||
if not chapter_plans:
|
||||
raise HTTPException(status_code=500, detail="AI分析失败,未能生成章节规划")
|
||||
|
||||
logger.info(f"AI分析完成,生成了 {len(chapter_plans)} 个章节规划")
|
||||
|
||||
# 根据规划创建章节记录
|
||||
if expansion_request.auto_create_chapters:
|
||||
created_chapters = await expansion_service.create_chapters_from_plans(
|
||||
outline_id=outline_id,
|
||||
chapter_plans=chapter_plans,
|
||||
project_id=outline.project_id,
|
||||
db=db,
|
||||
start_chapter_number=None # 自动计算章节序号
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 刷新章节数据
|
||||
for chapter in created_chapters:
|
||||
await db.refresh(chapter)
|
||||
|
||||
logger.info(f"成功创建 {len(created_chapters)} 个章节记录")
|
||||
|
||||
# 构建响应
|
||||
return OutlineExpansionResponse(
|
||||
outline_id=outline_id,
|
||||
outline_title=outline.title,
|
||||
target_chapter_count=expansion_request.target_chapter_count,
|
||||
actual_chapter_count=len(chapter_plans),
|
||||
expansion_strategy=expansion_request.expansion_strategy,
|
||||
chapter_plans=chapter_plans,
|
||||
created_chapters=[
|
||||
{
|
||||
"id": ch.id,
|
||||
"chapter_number": ch.chapter_number,
|
||||
"title": ch.title,
|
||||
"summary": ch.summary,
|
||||
"outline_id": ch.outline_id,
|
||||
"sub_index": ch.sub_index,
|
||||
"status": ch.status
|
||||
}
|
||||
for ch in created_chapters
|
||||
]
|
||||
)
|
||||
else:
|
||||
# 仅返回章节规划,不创建记录
|
||||
logger.info(f"仅生成规划,未创建章节记录")
|
||||
return OutlineExpansionResponse(
|
||||
outline_id=outline_id,
|
||||
outline_title=outline.title,
|
||||
target_chapter_count=expansion_request.target_chapter_count,
|
||||
actual_chapter_count=len(chapter_plans),
|
||||
expansion_strategy=expansion_request.expansion_strategy,
|
||||
chapter_plans=chapter_plans,
|
||||
created_chapters=None
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
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-stream", summary="展开单个大纲为多章(SSE流式)")
|
||||
async def expand_outline_to_chapters_stream(
|
||||
outline_id: str,
|
||||
@@ -2585,183 +2386,6 @@ async def get_outline_chapters(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/batch-expand", response_model=BatchOutlineExpansionResponse, summary="批量展开大纲为多章")
|
||||
async def batch_expand_outlines(
|
||||
batch_request: BatchOutlineExpansionRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_ai_service: AIService = Depends(get_user_ai_service)
|
||||
):
|
||||
"""
|
||||
批量展开项目中的所有大纲或指定大纲列表
|
||||
|
||||
流程:
|
||||
1. 获取项目中的所有大纲(或指定大纲列表)
|
||||
2. 逐个分析大纲,生成多章节规划
|
||||
3. 根据规划批量创建章节记录
|
||||
|
||||
参数:
|
||||
- batch_request: 批量展开配置
|
||||
|
||||
返回:
|
||||
- 所有展开的大纲和章节信息
|
||||
"""
|
||||
# 验证用户权限
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(batch_request.project_id, user_id, db)
|
||||
|
||||
try:
|
||||
# 创建展开服务实例
|
||||
expansion_service = PlotExpansionService(user_ai_service)
|
||||
|
||||
# 获取项目信息
|
||||
project_result = await db.execute(
|
||||
select(Project).where(Project.id == batch_request.project_id)
|
||||
)
|
||||
project = project_result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 获取要展开的大纲列表
|
||||
if batch_request.outline_ids:
|
||||
# 展开指定的大纲
|
||||
outlines_result = await db.execute(
|
||||
select(Outline)
|
||||
.where(
|
||||
Outline.project_id == batch_request.project_id,
|
||||
Outline.id.in_(batch_request.outline_ids)
|
||||
)
|
||||
.order_by(Outline.order_index)
|
||||
)
|
||||
else:
|
||||
# 展开所有大纲
|
||||
outlines_result = await db.execute(
|
||||
select(Outline)
|
||||
.where(Outline.project_id == batch_request.project_id)
|
||||
.order_by(Outline.order_index)
|
||||
)
|
||||
|
||||
outlines = outlines_result.scalars().all()
|
||||
|
||||
if not outlines:
|
||||
raise HTTPException(status_code=404, detail="没有找到要展开的大纲")
|
||||
|
||||
# 批量展开大纲
|
||||
logger.info(f"开始批量展开 {len(outlines)} 个大纲")
|
||||
|
||||
expansion_results = []
|
||||
total_chapters_created = 0
|
||||
skipped_outlines = []
|
||||
|
||||
for outline in outlines:
|
||||
try:
|
||||
# 检查大纲是否已经展开过
|
||||
existing_chapters_result = await db.execute(
|
||||
select(Chapter)
|
||||
.where(Chapter.outline_id == outline.id)
|
||||
.limit(1)
|
||||
)
|
||||
existing_chapter = existing_chapters_result.scalar_one_or_none()
|
||||
|
||||
if existing_chapter:
|
||||
logger.info(f"大纲 {outline.title} (ID: {outline.id}) 已经展开过,跳过")
|
||||
skipped_outlines.append({
|
||||
"outline_id": outline.id,
|
||||
"outline_title": outline.title,
|
||||
"reason": "已展开"
|
||||
})
|
||||
continue
|
||||
|
||||
# 分析大纲生成章节规划
|
||||
chapter_plans = await expansion_service.analyze_outline_for_chapters(
|
||||
outline=outline,
|
||||
project=project,
|
||||
db=db,
|
||||
target_chapter_count=batch_request.chapters_per_outline,
|
||||
expansion_strategy=batch_request.expansion_strategy,
|
||||
enable_scene_analysis=batch_request.enable_scene_analysis,
|
||||
provider=batch_request.provider,
|
||||
model=batch_request.model
|
||||
)
|
||||
|
||||
created_chapters = None
|
||||
if batch_request.auto_create_chapters:
|
||||
# 创建章节记录
|
||||
chapters = await expansion_service.create_chapters_from_plans(
|
||||
outline_id=outline.id,
|
||||
chapter_plans=chapter_plans,
|
||||
project_id=outline.project_id,
|
||||
db=db,
|
||||
start_chapter_number=None # 自动计算章节序号
|
||||
)
|
||||
created_chapters = [
|
||||
{
|
||||
"id": ch.id,
|
||||
"chapter_number": ch.chapter_number,
|
||||
"title": ch.title,
|
||||
"summary": ch.summary,
|
||||
"outline_id": ch.outline_id,
|
||||
"sub_index": ch.sub_index,
|
||||
"status": ch.status
|
||||
}
|
||||
for ch in chapters
|
||||
]
|
||||
total_chapters_created += len(chapters)
|
||||
|
||||
expansion_results.append({
|
||||
"outline_id": outline.id,
|
||||
"outline_title": outline.title,
|
||||
"target_chapter_count": batch_request.chapters_per_outline,
|
||||
"actual_chapter_count": len(chapter_plans),
|
||||
"expansion_strategy": batch_request.expansion_strategy,
|
||||
"chapter_plans": chapter_plans,
|
||||
"created_chapters": created_chapters
|
||||
})
|
||||
|
||||
logger.info(f"大纲 {outline.title} 展开完成,生成 {len(chapter_plans)} 个章节规划")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"展开大纲 {outline.id} 失败: {str(e)}", exc_info=True)
|
||||
expansion_results.append({
|
||||
"outline_id": outline.id,
|
||||
"outline_title": outline.title,
|
||||
"target_chapter_count": batch_request.chapters_per_outline,
|
||||
"actual_chapter_count": 0,
|
||||
"expansion_strategy": batch_request.expansion_strategy,
|
||||
"chapter_plans": [],
|
||||
"created_chapters": None,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
logger.info(f"批量展开完成: {len(expansion_results)} 个大纲,共生成 {total_chapters_created} 个章节")
|
||||
|
||||
# 构建响应
|
||||
return BatchOutlineExpansionResponse(
|
||||
project_id=batch_request.project_id,
|
||||
total_outlines_expanded=len(expansion_results),
|
||||
total_chapters_created=total_chapters_created,
|
||||
expansion_results=[
|
||||
OutlineExpansionResponse(
|
||||
outline_id=result["outline_id"],
|
||||
outline_title=result["outline_title"],
|
||||
target_chapter_count=result["target_chapter_count"],
|
||||
actual_chapter_count=result["actual_chapter_count"],
|
||||
expansion_strategy=result["expansion_strategy"],
|
||||
chapter_plans=result["chapter_plans"],
|
||||
created_chapters=result.get("created_chapters")
|
||||
)
|
||||
for result in expansion_results
|
||||
]
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"批量大纲展开失败: {str(e)}", exc_info=True)
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"批量大纲展开失败: {str(e)}")
|
||||
|
||||
|
||||
async def batch_expand_outlines_generator(
|
||||
data: Dict[str, Any],
|
||||
db: AsyncSession,
|
||||
|
||||
@@ -162,26 +162,19 @@ async def world_building_generator(
|
||||
if chunk_count % 20 == 0:
|
||||
yield await SSEResponse.send_heartbeat()
|
||||
|
||||
# 解析结果
|
||||
# 解析结果 - 使用统一的JSON清洗方法
|
||||
yield await SSEResponse.send_progress("解析AI返回结果...", 80)
|
||||
|
||||
world_data = {}
|
||||
try:
|
||||
cleaned_text = accumulated_text.strip()
|
||||
|
||||
# 移除markdown代码块标记
|
||||
if cleaned_text.startswith('```json'):
|
||||
cleaned_text = cleaned_text[7:].lstrip('\n\r')
|
||||
elif cleaned_text.startswith('```'):
|
||||
cleaned_text = cleaned_text[3:].lstrip('\n\r')
|
||||
if cleaned_text.endswith('```'):
|
||||
cleaned_text = cleaned_text[:-3].rstrip('\n\r')
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
# ✅ 使用 AIService 的统一清洗方法
|
||||
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
|
||||
world_data = json.loads(cleaned_text)
|
||||
logger.info(f"✅ 世界观JSON解析成功")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"世界构建JSON解析失败: {e}")
|
||||
logger.error(f"❌ 世界构建JSON解析失败: {e}")
|
||||
logger.error(f" 原始内容预览: {accumulated_text[:200]}")
|
||||
world_data = {
|
||||
"time_period": "AI返回格式错误,请重试",
|
||||
"location": "AI返回格式错误,请重试",
|
||||
@@ -478,17 +471,8 @@ async def characters_generator(
|
||||
accumulated_text += chunk
|
||||
yield await SSEResponse.send_chunk(chunk)
|
||||
|
||||
# 解析批次结果
|
||||
cleaned_text = accumulated_text.strip()
|
||||
# 移除markdown代码块标记
|
||||
if cleaned_text.startswith('```json'):
|
||||
cleaned_text = cleaned_text[7:].lstrip('\n\r')
|
||||
elif cleaned_text.startswith('```'):
|
||||
cleaned_text = cleaned_text[3:].lstrip('\n\r')
|
||||
if cleaned_text.endswith('```'):
|
||||
cleaned_text = cleaned_text[:-3].rstrip('\n\r')
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
# 解析批次结果 - 使用统一的JSON清洗方法
|
||||
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
|
||||
characters_data = json.loads(cleaned_text)
|
||||
if not isinstance(characters_data, list):
|
||||
characters_data = [characters_data]
|
||||
@@ -944,7 +928,7 @@ async def outline_generator(
|
||||
outline_requirements += "5. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》标记\n"
|
||||
|
||||
# 获取自定义提示词模板
|
||||
template = await PromptService.get_template("COMPLETE_OUTLINE_GENERATION", user_id, db)
|
||||
template = await PromptService.get_template("OUTLINE_CREATE", user_id, db)
|
||||
outline_prompt = PromptService.format_prompt(
|
||||
template,
|
||||
title=project.title,
|
||||
@@ -972,18 +956,11 @@ async def outline_generator(
|
||||
accumulated_text += chunk
|
||||
yield await SSEResponse.send_chunk(chunk)
|
||||
|
||||
# 解析大纲结果
|
||||
# 解析大纲结果 - 使用统一的JSON清洗方法
|
||||
yield await SSEResponse.send_progress("解析大纲...", 40)
|
||||
cleaned_text = accumulated_text.strip()
|
||||
if cleaned_text.startswith('```json'):
|
||||
cleaned_text = cleaned_text[7:].lstrip('\n\r')
|
||||
elif cleaned_text.startswith('```'):
|
||||
cleaned_text = cleaned_text[3:].lstrip('\n\r')
|
||||
if cleaned_text.endswith('```'):
|
||||
cleaned_text = cleaned_text[:-3].rstrip('\n\r')
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
try:
|
||||
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
|
||||
outline_data = json.loads(cleaned_text)
|
||||
if not isinstance(outline_data, list):
|
||||
outline_data = [outline_data]
|
||||
@@ -1239,22 +1216,14 @@ async def world_building_regenerate_generator(
|
||||
if chunk_count % 20 == 0:
|
||||
yield await SSEResponse.send_heartbeat()
|
||||
|
||||
# 解析结果
|
||||
# 解析结果 - 使用统一的JSON清洗方法
|
||||
yield await SSEResponse.send_progress("解析AI返回结果...", 80)
|
||||
|
||||
world_data = {}
|
||||
try:
|
||||
cleaned_text = accumulated_text.strip()
|
||||
|
||||
if cleaned_text.startswith('```json'):
|
||||
cleaned_text = cleaned_text[7:].lstrip('\n\r')
|
||||
elif cleaned_text.startswith('```'):
|
||||
cleaned_text = cleaned_text[3:].lstrip('\n\r')
|
||||
if cleaned_text.endswith('```'):
|
||||
cleaned_text = cleaned_text[:-3].rstrip('\n\r')
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
cleaned_text = user_ai_service._clean_json_response(accumulated_text)
|
||||
world_data = json.loads(cleaned_text)
|
||||
logger.info(f"✅ 世界观重新生成JSON解析成功")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"世界构建JSON解析失败: {e}")
|
||||
|
||||
Reference in New Issue
Block a user