feature:新增大纲生成后自动校验并补全缺失角色/组织功能,替代旧的预测确认机制

This commit is contained in:
xiamuceer-j
2026-02-12 12:39:11 +08:00
parent 58ff24c3d1
commit e31225c45f
3 changed files with 509 additions and 545 deletions
+48 -3
View File
@@ -898,7 +898,6 @@ async def characters_generator(
relationships=relationships_text, relationships=relationships_text,
organization_type=char_data.get("organization_type") if is_organization else None, organization_type=char_data.get("organization_type") if is_organization else None,
organization_purpose=char_data.get("organization_purpose") if is_organization else None, organization_purpose=char_data.get("organization_purpose") if is_organization else None,
organization_members=json.dumps(char_data.get("organization_members", []), ensure_ascii=False) if is_organization else None,
traits=json.dumps(char_data.get("traits", []), ensure_ascii=False) if char_data.get("traits") else None traits=json.dumps(char_data.get("traits", []), ensure_ascii=False) if char_data.get("traits") else None
) )
db.add(character) db.add(character)
@@ -1201,10 +1200,10 @@ async def characters_generator(
"personality": char.personality, "personality": char.personality,
"background": char.background, "background": char.background,
"appearance": char.appearance, "appearance": char.appearance,
"relationships": char.relationships, "relationships": "",
"organization_type": char.organization_type, "organization_type": char.organization_type,
"organization_purpose": char.organization_purpose, "organization_purpose": char.organization_purpose,
"organization_members": char.organization_members, "organization_members": "",
"traits": char.traits, "traits": char.traits,
"created_at": char.created_at.isoformat() if char.created_at else None, "created_at": char.created_at.isoformat() if char.created_at else None,
"updated_at": char.updated_at.isoformat() if char.updated_at else None "updated_at": char.updated_at.isoformat() if char.updated_at else None
@@ -1383,6 +1382,52 @@ async def outline_generator(
logger.info(f"✅ 成功创建{len(created_outlines)}个大纲节点") logger.info(f"✅ 成功创建{len(created_outlines)}个大纲节点")
# 🎭 角色校验:检查大纲structure中的characters是否存在对应角色
yield await tracker.saving("🎭 校验角色信息...", 0.5)
try:
from app.services.auto_character_service import get_auto_character_service
auto_char_service = get_auto_character_service(user_ai_service)
char_check_result = await auto_char_service.check_and_create_missing_characters(
project_id=project_id,
outline_data_list=outline_data[:outline_count],
db=db,
user_id=user_id,
enable_mcp=enable_mcp
)
if char_check_result["created_count"] > 0:
created_names = [c.name for c in char_check_result["created_characters"]]
logger.info(f"🎭 向导大纲:自动创建了 {char_check_result['created_count']} 个角色: {', '.join(created_names)}")
yield await tracker.saving(
f"🎭 自动创建了 {char_check_result['created_count']} 个角色: {', '.join(created_names)}",
0.6
)
except Exception as e:
logger.error(f"⚠️ 向导大纲角色校验失败(不影响主流程): {e}")
# 🏛️ 组织校验:检查大纲structure中的characterstype=organization)是否存在对应组织
yield await tracker.saving("🏛️ 校验组织信息...", 0.55)
try:
from app.services.auto_organization_service import get_auto_organization_service
auto_org_service = get_auto_organization_service(user_ai_service)
org_check_result = await auto_org_service.check_and_create_missing_organizations(
project_id=project_id,
outline_data_list=outline_data[:outline_count],
db=db,
user_id=user_id,
enable_mcp=enable_mcp
)
if org_check_result["created_count"] > 0:
created_names = [c.name for c in org_check_result["created_organizations"]]
logger.info(f"🏛️ 向导大纲:自动创建了 {org_check_result['created_count']} 个组织: {', '.join(created_names)}")
yield await tracker.saving(
f"🏛️ 自动创建了 {org_check_result['created_count']} 个组织: {', '.join(created_names)}",
0.65
)
except Exception as e:
logger.error(f"⚠️ 向导大纲组织校验失败(不影响主流程): {e}")
# 根据项目的大纲模式决定是否自动创建章节 # 根据项目的大纲模式决定是否自动创建章节
created_chapters = [] created_chapters = []
if project.outline_mode == 'one-to-one': if project.outline_mode == 'one-to-one':
+213 -262
View File
@@ -1,4 +1,4 @@
"""自动角色引入服务 - 在续写大纲时根据剧情推进自动引入新角色""" """自动角色服务 - 大纲生成后校验并自动补全缺失角色"""
from typing import List, Dict, Any, Optional, Callable, Awaitable from typing import List, Dict, Any, Optional, Callable, Awaitable
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
@@ -20,270 +20,24 @@ class AutoCharacterService:
def __init__(self, ai_service: AIService): def __init__(self, ai_service: AIService):
self.ai_service = ai_service self.ai_service = ai_service
async def analyze_and_create_characters(
self,
project_id: str,
outline_content: str,
existing_characters: List[Character],
db: AsyncSession,
user_id: str = None,
enable_mcp: bool = True,
all_chapters_brief: str = "",
start_chapter: int = 1,
chapter_count: int = 3,
plot_stage: str = "发展",
story_direction: str = "继续推进主线剧情",
preview_only: bool = False,
progress_callback: Optional[Callable[[str], Awaitable[None]]] = None
) -> Dict[str, Any]:
"""
预测性分析并创建需要的新角色(方案A:先角色后大纲)
Args:
project_id: 项目ID
outline_content: 当前批次大纲内容(用于向后兼容,实际不使用)
existing_characters: 现有角色列表
db: 数据库会话
user_id: 用户ID(用于MCP和自定义提示词)
enable_mcp: 是否启用MCP增强
all_chapters_brief: 已有章节概览
start_chapter: 起始章节号
chapter_count: 续写章节数
plot_stage: 剧情阶段
story_direction: 故事发展方向
preview_only: 仅预测不创建(用于角色确认机制)
Returns:
{
"new_characters": [角色对象列表], # preview_only=True时为空
"relationships_created": [关系对象列表], # preview_only=True时为空
"character_count": 新增角色数量,
"analysis_result": AI分析结果,
"predicted_characters": [预测的角色数据] # 仅preview_only=True时返回
"needs_new_characters": bool,
"reason": str
}
"""
logger.info(f"🎭 【方案A】预测性分析:检测是否需要引入新角色...")
logger.info(f" - 项目ID: {project_id}")
logger.info(f" - 续写计划: 第{start_chapter}章起,共{chapter_count}")
logger.info(f" - 剧情阶段: {plot_stage}")
logger.info(f" - 发展方向: {story_direction}")
logger.info(f" - 现有角色数: {len(existing_characters)}")
# 1. 获取项目信息
project_result = await db.execute(
select(Project).where(Project.id == project_id)
)
project = project_result.scalar_one_or_none()
if not project:
raise ValueError("项目不存在")
# 2. 构建现有角色信息摘要
existing_chars_summary = self._build_character_summary(existing_characters)
# 3. AI预测性分析是否需要新角色
analysis_result = await self._analyze_character_needs(
project=project,
outline_content=outline_content, # 保留参数向后兼容
existing_chars_summary=existing_chars_summary,
db=db,
user_id=user_id,
enable_mcp=enable_mcp,
all_chapters_brief=all_chapters_brief,
start_chapter=start_chapter,
chapter_count=chapter_count,
plot_stage=plot_stage,
story_direction=story_direction
)
# 4. 判断是否需要创建角色
if not analysis_result or not analysis_result.get("needs_new_characters"):
logger.info("✅ AI判断:当前剧情不需要引入新角色")
return {
"new_characters": [],
"relationships_created": [],
"character_count": 0,
"analysis_result": analysis_result,
"predicted_characters": [],
"needs_new_characters": False,
"reason": analysis_result.get("reason", "当前剧情不需要新角色")
}
# 5. 如果是预览模式,仅返回预测结果,不创建角色
if preview_only:
character_specs = analysis_result.get("character_specifications", [])
logger.info(f"🔮 预览模式:预测到 {len(character_specs)} 个角色,不创建数据库记录")
return {
"new_characters": [],
"relationships_created": [],
"character_count": 0,
"analysis_result": analysis_result,
"predicted_characters": character_specs,
"needs_new_characters": True,
"reason": analysis_result.get("reason", "预测需要新角色")
}
# 6. 批量生成新角色(非预览模式)
new_characters = []
relationships_created = []
character_specs = analysis_result.get("character_specifications", [])
logger.info(f"🎯 AI建议引入 {len(character_specs)} 个新角色")
for idx, spec in enumerate(character_specs):
try:
spec_name = spec.get('name', spec.get('role_description', '未命名'))
logger.info(f" [{idx+1}/{len(character_specs)}] 生成角色规格: {spec_name}")
logger.debug(f" 角色规格内容: {json.dumps(spec, ensure_ascii=False)}")
if progress_callback:
await progress_callback(f"🎨 [{idx+1}/{len(character_specs)}] 生成角色详情: {spec_name}")
# 生成角色详细信息
character_data = await self._generate_character_details(
spec=spec,
project=project,
existing_characters=existing_characters + new_characters, # 包含新创建的
db=db,
user_id=user_id,
enable_mcp=enable_mcp
)
logger.debug(f" AI生成的角色数据: {json.dumps(character_data, ensure_ascii=False)[:200]}")
if progress_callback:
await progress_callback(f"💾 [{idx+1}/{len(character_specs)}] 保存角色: {character_data.get('name', spec_name)}")
# 创建角色记录
character = await self._create_character_record(
project_id=project_id,
character_data=character_data,
db=db
)
new_characters.append(character)
logger.info(f" ✅ 创建新角色: {character.name} ({character.role_type}), ID: {character.id}")
if progress_callback:
await progress_callback(f"✅ [{idx+1}/{len(character_specs)}] 角色创建成功: {character.name}")
# 建立关系(兼容两种字段名)
relationships_data = character_data.get("relationships") or character_data.get("relationships_array", [])
logger.info(f" 🔍 检查关系数据:")
logger.info(f" - relationships字段: {character_data.get('relationships')}")
logger.info(f" - relationships_array字段: {character_data.get('relationships_array')}")
logger.info(f" - 最终使用的数据: {relationships_data}")
logger.info(f" - 关系数量: {len(relationships_data) if relationships_data else 0}")
if relationships_data:
logger.info(f" 🔗 开始创建 {len(relationships_data)} 条关系...")
for idx, rel in enumerate(relationships_data):
logger.info(f" [{idx+1}] {rel.get('target_character_name')} - {rel.get('relationship_type')}")
if progress_callback:
await progress_callback(f"🔗 [{idx+1}/{len(character_specs)}] 建立 {len(relationships_data)} 个关系")
else:
logger.warning(f" ⚠️ AI返回的角色数据中没有关系信息!")
logger.warning(f" 完整的character_data keys: {list(character_data.keys())}")
rels = await self._create_relationships(
new_character=character,
relationship_specs=relationships_data,
existing_characters=existing_characters + new_characters,
project_id=project_id,
db=db
)
relationships_created.extend(rels)
logger.info(f" ✅ 实际创建了 {len(rels)} 条关系记录")
except Exception as e:
logger.error(f" ❌ 创建角色失败: {e}", exc_info=True)
continue
# 7. 提交事务(注意:这里只flush,让调用方commit
await db.flush()
logger.info(f"🎉 自动角色引入完成: 新增{len(new_characters)}个角色, {len(relationships_created)}条关系")
return {
"new_characters": new_characters,
"relationships_created": relationships_created,
"character_count": len(new_characters),
"analysis_result": analysis_result
}
def _build_character_summary(self, characters: List[Character]) -> str: def _build_character_summary(self, characters: List[Character]) -> str:
"""构建现有角色摘要""" """构建现有角色摘要信息"""
if not characters: if not characters:
return "暂无角色" return "暂无已有角色"
summary = [] lines = []
for char in characters: for char in characters:
char_type = "组织" if char.is_organization else "角色" parts = [f"- {char.name}"]
role_desc = char.role_type or "未知" if char.role_type:
personality = (char.personality or "")[:50] role_map = {"protagonist": "主角", "supporting": "配角", "antagonist": "反派"}
summary.append(f"- {char.name} ({char_type}, {role_desc}): {personality}") parts.append(f"({role_map.get(char.role_type, char.role_type)})")
if char.personality:
parts.append(f"性格: {char.personality[:50]}")
if char.background:
parts.append(f"背景: {char.background[:50]}")
lines.append(" ".join(parts))
return "\n".join(summary[:20]) # 最多显示20个 return "\n".join(lines)
async def _analyze_character_needs(
self,
project: Project,
outline_content: str,
existing_chars_summary: str,
db: AsyncSession,
user_id: str,
enable_mcp: bool,
all_chapters_brief: str = "",
start_chapter: int = 1,
chapter_count: int = 3,
plot_stage: str = "发展",
story_direction: str = "继续推进主线剧情"
) -> Dict[str, Any]:
"""AI预测性分析是否需要新角色(方案A"""
# 构建分析提示词
template = await PromptService.get_template(
"AUTO_CHARACTER_ANALYSIS",
user_id,
db
)
# 使用新的预测性分析参数
prompt = PromptService.format_prompt(
template,
title=project.title,
theme=project.theme or "未设定",
genre=project.genre or "未设定",
time_period=project.world_time_period or "未设定",
location=project.world_location or "未设定",
atmosphere=project.world_atmosphere or "未设定",
existing_characters=existing_chars_summary,
all_chapters_brief=all_chapters_brief,
start_chapter=start_chapter,
chapter_count=chapter_count,
plot_stage=plot_stage,
story_direction=story_direction
)
try:
# 使用统一的JSON调用方法(支持自动MCP工具加载)
analysis = await self.ai_service.call_with_json_retry(
prompt=prompt,
max_retries=3,
)
logger.info(f" ✅ AI分析完成: needs_new_characters={analysis.get('needs_new_characters')}")
return analysis
except json.JSONDecodeError as e:
logger.error(f" ❌ 角色需求分析JSON解析失败: {e}")
return {"needs_new_characters": False}
except Exception as e:
logger.error(f" ❌ 角色需求分析失败: {e}")
return {"needs_new_characters": False}
async def _generate_character_details( async def _generate_character_details(
self, self,
@@ -447,7 +201,7 @@ class AutoCharacterService:
else: else:
logger.warning(f" ⚠️ AI返回的副职业名称未找到: {career_name}") logger.warning(f" ⚠️ AI返回的副职业名称未找到: {career_name}")
# 创建角色 # 创建角色(不再写入 relationships 文本字段,关系统一由 character_relationships 表管理)
character = Character( character = Character(
project_id=project_id, project_id=project_id,
name=character_data.get("name", "未命名角色"), name=character_data.get("name", "未命名角色"),
@@ -458,7 +212,6 @@ class AutoCharacterService:
personality=character_data.get("personality", ""), personality=character_data.get("personality", ""),
background=character_data.get("background", ""), background=character_data.get("background", ""),
appearance=character_data.get("appearance", ""), appearance=character_data.get("appearance", ""),
relationships=character_data.get("relationships_text", ""),
organization_type=character_data.get("organization_type") if is_organization else None, 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_purpose=character_data.get("organization_purpose") if is_organization else None,
traits=json.dumps(character_data.get("traits", []), ensure_ascii=False) if character_data.get("traits") else None, traits=json.dumps(character_data.get("traits", []), ensure_ascii=False) if character_data.get("traits") else None,
@@ -594,6 +347,204 @@ class AutoCharacterService:
return relationships return relationships
async def check_and_create_missing_characters(
self,
project_id: str,
outline_data_list: list,
db: AsyncSession,
user_id: str = None,
enable_mcp: bool = True,
progress_callback: Optional[Callable[[str], Awaitable[None]]] = None
) -> Dict[str, Any]:
"""
根据大纲structure中的characters字段校验项目是否存在对应角色,
如果不存在则根据大纲摘要自动生成角色信息。
Args:
project_id: 项目ID
outline_data_list: 大纲数据列表(每个元素包含 characters、summary 等字段)
db: 数据库会话
user_id: 用户ID
enable_mcp: 是否启用MCP
progress_callback: 进度回调
Returns:
{
"created_characters": [角色对象列表],
"missing_names": [缺失的角色名称列表],
"created_count": 创建的角色数量
}
"""
logger.info(f"🔍 【角色校验】开始校验大纲中提到的角色是否存在...")
# 1. 从所有大纲的structure中提取角色名称(兼容新旧格式)
all_character_names = set()
character_context = {} # 记录角色出现的上下文(大纲摘要)
for outline_item in outline_data_list:
if isinstance(outline_item, dict):
characters = outline_item.get("characters", [])
summary = outline_item.get("summary", "") or outline_item.get("content", "")
title = outline_item.get("title", "")
if isinstance(characters, list):
for char_entry in characters:
# 新格式:{"name": "xxx", "type": "character"/"organization"}
if isinstance(char_entry, dict):
entry_type = char_entry.get("type", "character")
entry_name = char_entry.get("name", "")
# 只处理 character 类型,跳过 organization
if entry_type == "organization" or not entry_name.strip():
continue
name = entry_name.strip()
# 旧格式:纯字符串
elif isinstance(char_entry, str) and char_entry.strip():
name = char_entry.strip()
else:
continue
all_character_names.add(name)
# 收集角色出现的上下文
if name not in character_context:
character_context[name] = []
character_context[name].append(f"{title}》: {summary[:200]}")
if not all_character_names:
logger.info("🔍 【角色校验】大纲中未提到任何角色,跳过校验")
return {
"created_characters": [],
"missing_names": [],
"created_count": 0
}
logger.info(f"🔍 【角色校验】大纲中提到的角色: {', '.join(all_character_names)}")
# 2. 获取项目现有角色
existing_result = await db.execute(
select(Character).where(Character.project_id == project_id)
)
existing_characters = existing_result.scalars().all()
existing_names = {char.name for char in existing_characters}
# 3. 找出缺失的角色
missing_names = all_character_names - existing_names
if not missing_names:
logger.info("✅ 【角色校验】所有角色已存在,无需创建")
return {
"created_characters": [],
"missing_names": [],
"created_count": 0
}
logger.info(f"⚠️ 【角色校验】发现 {len(missing_names)} 个缺失角色: {', '.join(missing_names)}")
# 4. 获取项目信息
project_result = await db.execute(
select(Project).where(Project.id == project_id)
)
project = project_result.scalar_one_or_none()
if not project:
logger.error("❌ 【角色校验】项目不存在")
return {
"created_characters": [],
"missing_names": list(missing_names),
"created_count": 0
}
# 5. 为每个缺失的角色生成并创建角色信息
created_characters = []
for idx, char_name in enumerate(missing_names):
try:
if progress_callback:
await progress_callback(
f"🎭 [{idx+1}/{len(missing_names)}] 自动创建角色:{char_name}..."
)
# 构建角色规格(基于大纲上下文)
context_summaries = character_context.get(char_name, [])
context_text = "\n".join(context_summaries[:3]) # 最多3个上下文
spec = {
"name": char_name,
"role_description": f"在大纲中出现的角色,出现场景:\n{context_text}",
"suggested_role_type": "supporting",
"importance": "medium"
}
logger.info(f" 🤖 [{idx+1}/{len(missing_names)}] 生成角色详情: {char_name}")
# 生成角色详细信息
character_data = await self._generate_character_details(
spec=spec,
project=project,
existing_characters=list(existing_characters) + created_characters,
db=db,
user_id=user_id,
enable_mcp=enable_mcp
)
# 确保使用大纲中的角色名称
character_data['name'] = char_name
if progress_callback:
await progress_callback(
f"💾 [{idx+1}/{len(missing_names)}] 保存角色:{char_name}..."
)
# 创建角色记录
character = await self._create_character_record(
project_id=project_id,
character_data=character_data,
db=db
)
created_characters.append(character)
logger.info(f" ✅ [{idx+1}/{len(missing_names)}] 角色创建成功: {character.name}")
# 建立关系
relationships_data = character_data.get("relationships") or character_data.get("relationships_array", [])
if relationships_data:
if progress_callback:
await progress_callback(
f"🔗 [{idx+1}/{len(missing_names)}] 建立 {len(relationships_data)} 个关系:{char_name}..."
)
await self._create_relationships(
new_character=character,
relationship_specs=relationships_data,
existing_characters=list(existing_characters) + created_characters,
project_id=project_id,
db=db
)
if progress_callback:
await progress_callback(
f"✅ [{idx+1}/{len(missing_names)}] 角色创建完成:{char_name}"
)
except Exception as e:
logger.error(f" ❌ 创建角色 {char_name} 失败: {e}", exc_info=True)
if progress_callback:
await progress_callback(
f"⚠️ [{idx+1}/{len(missing_names)}] 角色 {char_name} 创建失败"
)
continue
# 6. flush 到数据库(让调用方 commit)
if created_characters:
await db.flush()
logger.info(f"🎉 【角色校验】完成: 发现 {len(missing_names)} 个缺失角色,成功创建 {len(created_characters)}")
return {
"created_characters": created_characters,
"missing_names": list(missing_names),
"created_count": len(created_characters)
}
# 全局实例缓存 # 全局实例缓存
_auto_character_service_instance: Optional[AutoCharacterService] = None _auto_character_service_instance: Optional[AutoCharacterService] = None
+248 -280
View File
@@ -1,4 +1,4 @@
"""自动组织引入服务 - 在续写大纲时根据剧情推进自动引入新组织""" """自动组织服务 - 大纲生成后校验并自动补全缺失组织"""
from typing import List, Dict, Any, Optional, Callable, Awaitable from typing import List, Dict, Any, Optional, Callable, Awaitable
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
@@ -20,293 +20,34 @@ class AutoOrganizationService:
def __init__(self, ai_service: AIService): def __init__(self, ai_service: AIService):
self.ai_service = ai_service self.ai_service = ai_service
async def analyze_and_create_organizations( def _build_character_summary(self, characters: List[Character]) -> str:
self, """构建现有角色摘要信息"""
project_id: str, if not characters:
outline_content: str, return "暂无已有角色"
existing_characters: List[Character],
existing_organizations: List[Dict[str, Any]],
db: AsyncSession,
user_id: str = None,
enable_mcp: bool = True,
all_chapters_brief: str = "",
start_chapter: int = 1,
chapter_count: int = 3,
plot_stage: str = "发展",
story_direction: str = "继续推进主线剧情",
preview_only: bool = False,
progress_callback: Optional[Callable[[str], Awaitable[None]]] = None
) -> Dict[str, Any]:
"""
预测性分析并创建需要的新组织
Args: lines = []
project_id: 项目ID for char in characters:
outline_content: 当前批次大纲内容(用于向后兼容,实际不使用) parts = [f"- {char.name}"]
existing_characters: 现有角色列表 if char.role_type:
existing_organizations: 现有组织列表 role_map = {"protagonist": "主角", "supporting": "配角", "antagonist": "反派"}
db: 数据库会话 parts.append(f"({role_map.get(char.role_type, char.role_type)})")
user_id: 用户ID(用于MCP和自定义提示词) if char.personality:
enable_mcp: 是否启用MCP增强 parts.append(f"性格: {char.personality[:50]}")
all_chapters_brief: 已有章节概览 lines.append(" ".join(parts))
start_chapter: 起始章节号
chapter_count: 续写章节数
plot_stage: 剧情阶段
story_direction: 故事发展方向
preview_only: 仅预测不创建(用于组织确认机制)
Returns: return "\n".join(lines)
{
"new_organizations": [组织对象列表], # preview_only=True时为空
"members_created": [成员关系列表], # preview_only=True时为空
"organization_count": 新增组织数量,
"analysis_result": AI分析结果,
"predicted_organizations": [预测的组织数据] # 仅preview_only=True时返回
"needs_new_organizations": bool,
"reason": str
}
"""
logger.info(f"🏛️ 【组织引入】预测性分析:检测是否需要引入新组织...")
logger.info(f" - 项目ID: {project_id}")
logger.info(f" - 续写计划: 第{start_chapter}章起,共{chapter_count}")
logger.info(f" - 剧情阶段: {plot_stage}")
logger.info(f" - 发展方向: {story_direction}")
logger.info(f" - 现有角色数: {len(existing_characters)}")
logger.info(f" - 现有组织数: {len(existing_organizations)}")
# 1. 获取项目信息
project_result = await db.execute(
select(Project).where(Project.id == project_id)
)
project = project_result.scalar_one_or_none()
if not project:
raise ValueError("项目不存在")
# 2. 构建现有组织信息摘要
existing_orgs_summary = self._build_organization_summary(existing_organizations)
existing_chars_summary = self._build_character_summary(existing_characters)
# 3. AI预测性分析是否需要新组织
if progress_callback:
await progress_callback("🤖 AI分析组织需求...")
analysis_result = await self._analyze_organization_needs(
project=project,
outline_content=outline_content,
existing_orgs_summary=existing_orgs_summary,
existing_chars_summary=existing_chars_summary,
db=db,
user_id=user_id,
enable_mcp=enable_mcp,
all_chapters_brief=all_chapters_brief,
start_chapter=start_chapter,
chapter_count=chapter_count,
plot_stage=plot_stage,
story_direction=story_direction
)
if progress_callback:
await progress_callback("✅ 组织需求分析完成")
# 4. 判断是否需要创建组织
if not analysis_result or not analysis_result.get("needs_new_organizations"):
logger.info("✅ AI判断:当前剧情不需要引入新组织")
return {
"new_organizations": [],
"members_created": [],
"organization_count": 0,
"analysis_result": analysis_result,
"predicted_organizations": [],
"needs_new_organizations": False,
"reason": analysis_result.get("reason", "当前剧情不需要新组织")
}
# 5. 如果是预览模式,仅返回预测结果,不创建组织
if preview_only:
organization_specs = analysis_result.get("organization_specifications", [])
logger.info(f"🔮 预览模式:预测到 {len(organization_specs)} 个组织,不创建数据库记录")
return {
"new_organizations": [],
"members_created": [],
"organization_count": 0,
"analysis_result": analysis_result,
"predicted_organizations": organization_specs,
"needs_new_organizations": True,
"reason": analysis_result.get("reason", "预测需要新组织")
}
# 6. 批量生成新组织(非预览模式)
new_organizations = []
members_created = []
organization_specs = analysis_result.get("organization_specifications", [])
logger.info(f"🎯 AI建议引入 {len(organization_specs)} 个新组织")
for idx, spec in enumerate(organization_specs):
try:
spec_name = spec.get('name', spec.get('organization_description', '未命名'))
logger.info(f" [{idx+1}/{len(organization_specs)}] 生成组织规格: {spec_name}")
logger.debug(f" 组织规格内容: {json.dumps(spec, ensure_ascii=False)}")
if progress_callback:
await progress_callback(f"🏛️ [{idx+1}/{len(organization_specs)}] 生成组织详情: {spec_name}")
# 生成组织详细信息
organization_data = await self._generate_organization_details(
spec=spec,
project=project,
existing_characters=existing_characters,
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=enable_mcp
)
logger.debug(f" AI生成的组织数据: {json.dumps(organization_data, ensure_ascii=False)[:200]}")
if progress_callback:
await progress_callback(f"💾 [{idx+1}/{len(organization_specs)}] 保存组织: {organization_data.get('name', spec_name)}")
# 创建组织记录(先创建Character记录,再创建Organization记录)
character, organization = await self._create_organization_record(
project_id=project_id,
organization_data=organization_data,
db=db
)
new_organizations.append({
"character": character,
"organization": organization
})
logger.info(f" ✅ 创建新组织: {character.name}, ID: {organization.id}")
if progress_callback:
await progress_callback(f"✅ [{idx+1}/{len(organization_specs)}] 组织创建成功: {character.name}")
# 建立成员关系
members_data = organization_data.get("initial_members", [])
if members_data:
logger.info(f" 🔗 开始创建 {len(members_data)} 个成员关系...")
if progress_callback:
await progress_callback(f"🔗 [{idx+1}/{len(organization_specs)}] 建立 {len(members_data)} 个成员关系")
members = await self._create_member_relationships(
organization=organization,
member_specs=members_data,
existing_characters=existing_characters,
project_id=project_id,
db=db
)
members_created.extend(members)
logger.info(f" ✅ 实际创建了 {len(members)} 个成员关系记录")
except Exception as e:
logger.error(f" ❌ 创建组织失败: {e}", exc_info=True)
continue
# 7. 提交事务(注意:这里只flush,让调用方commit
await db.flush()
logger.info(f"🎉 自动组织引入完成: 新增{len(new_organizations)}个组织, {len(members_created)}个成员关系")
return {
"new_organizations": new_organizations,
"members_created": members_created,
"organization_count": len(new_organizations),
"analysis_result": analysis_result,
"predicted_organizations": [],
"needs_new_organizations": True,
"reason": analysis_result.get("reason", "")
}
def _build_organization_summary(self, organizations: List[Dict[str, Any]]) -> str: def _build_organization_summary(self, organizations: List[Dict[str, Any]]) -> str:
"""构建现有组织摘要""" """构建现有组织摘要信息"""
if not organizations: if not organizations:
return "暂无组织" return "暂无已有组织"
summary = [] lines = []
for org in organizations: for org in organizations:
org_name = org.get("name", "未知") name = org.get("name", "未知") if isinstance(org, dict) else getattr(org, "name", "未知")
org_type = org.get("organization_type", "未知类型") lines.append(f"- {name}")
power_level = org.get("power_level", 50)
purpose = (org.get("organization_purpose") or "")[:50]
summary.append(f"- {org_name} ({org_type}, 势力等级:{power_level}): {purpose}")
return "\n".join(summary[:15]) # 最多显示15个 return "\n".join(lines)
def _build_character_summary(self, characters: List[Character]) -> str:
"""构建现有角色摘要"""
if not characters:
return "暂无角色"
summary = []
for char in characters:
if not char.is_organization: # 只统计非组织角色
char_role = char.role_type or "未知"
personality = (char.personality or "")[:30]
summary.append(f"- {char.name} ({char_role}): {personality}")
return "\n".join(summary[:20]) # 最多显示20个
async def _analyze_organization_needs(
self,
project: Project,
outline_content: str,
existing_orgs_summary: str,
existing_chars_summary: str,
db: AsyncSession,
user_id: str,
enable_mcp: bool,
all_chapters_brief: str = "",
start_chapter: int = 1,
chapter_count: int = 3,
plot_stage: str = "发展",
story_direction: str = "继续推进主线剧情"
) -> Dict[str, Any]:
"""AI预测性分析是否需要新组织"""
# 构建分析提示词
template = await PromptService.get_template(
"AUTO_ORGANIZATION_ANALYSIS",
user_id,
db
)
# 使用新的预测性分析参数
prompt = PromptService.format_prompt(
template,
title=project.title,
theme=project.theme or "未设定",
genre=project.genre or "未设定",
time_period=project.world_time_period or "未设定",
location=project.world_location or "未设定",
atmosphere=project.world_atmosphere or "未设定",
existing_organizations=existing_orgs_summary,
existing_characters=existing_chars_summary,
all_chapters_brief=all_chapters_brief,
start_chapter=start_chapter,
chapter_count=chapter_count,
plot_stage=plot_stage,
story_direction=story_direction
)
try:
# 使用统一的JSON调用方法(支持自动MCP工具加载)
analysis = await self.ai_service.call_with_json_retry(
prompt=prompt,
max_retries=3,
)
logger.info(f" ✅ AI分析完成: needs_new_organizations={analysis.get('needs_new_organizations')}")
return analysis
except json.JSONDecodeError as e:
logger.error(f" ❌ 组织需求分析JSON解析失败: {e}")
return {"needs_new_organizations": False}
except Exception as e:
logger.error(f" ❌ 组织需求分析失败: {e}")
return {"needs_new_organizations": False}
async def _generate_organization_details( async def _generate_organization_details(
self, self,
@@ -485,6 +226,233 @@ class AutoOrganizationService:
return members return members
async def check_and_create_missing_organizations(
self,
project_id: str,
outline_data_list: list,
db: AsyncSession,
user_id: str = None,
enable_mcp: bool = True,
progress_callback: Optional[Callable[[str], Awaitable[None]]] = None
) -> Dict[str, Any]:
"""
根据大纲structure中的characters字段(type=organization)校验项目是否存在对应组织,
如果不存在则根据大纲摘要自动生成组织信息。
Args:
project_id: 项目ID
outline_data_list: 大纲数据列表(每个元素包含 characters、summary 等字段)
db: 数据库会话
user_id: 用户ID
enable_mcp: 是否启用MCP
progress_callback: 进度回调
Returns:
{
"created_organizations": [组织对象列表],
"missing_names": [缺失的组织名称列表],
"created_count": 创建的组织数量
}
"""
logger.info(f"🔍 【组织校验】开始校验大纲中提到的组织是否存在...")
# 1. 从所有大纲的structure中提取组织名称(兼容新旧格式)
all_organization_names = set()
organization_context = {} # 记录组织出现的上下文(大纲摘要)
for outline_item in outline_data_list:
if isinstance(outline_item, dict):
characters = outline_item.get("characters", [])
summary = outline_item.get("summary", "") or outline_item.get("content", "")
title = outline_item.get("title", "")
if isinstance(characters, list):
for char_entry in characters:
# 新格式:{"name": "xxx", "type": "character"/"organization"}
if isinstance(char_entry, dict):
entry_type = char_entry.get("type", "character")
entry_name = char_entry.get("name", "")
# 只处理 organization 类型
if entry_type != "organization" or not entry_name.strip():
continue
name = entry_name.strip()
all_organization_names.add(name)
if name not in organization_context:
organization_context[name] = []
organization_context[name].append(f"{title}》: {summary[:200]}")
# 旧格式:纯字符串,无法区分类型,跳过
if not all_organization_names:
logger.info("🔍 【组织校验】大纲中未提到任何组织,跳过校验")
return {
"created_organizations": [],
"missing_names": [],
"created_count": 0
}
logger.info(f"🔍 【组织校验】大纲中提到的组织: {', '.join(all_organization_names)}")
# 2. 获取项目现有组织(通过Character表的is_organization字段)
existing_result = await db.execute(
select(Character).where(
Character.project_id == project_id,
Character.is_organization == True
)
)
existing_org_characters = existing_result.scalars().all()
existing_org_names = {char.name for char in existing_org_characters}
# 3. 找出缺失的组织
missing_names = all_organization_names - existing_org_names
if not missing_names:
logger.info("✅ 【组织校验】所有组织已存在,无需创建")
return {
"created_organizations": [],
"missing_names": [],
"created_count": 0
}
logger.info(f"⚠️ 【组织校验】发现 {len(missing_names)} 个缺失组织: {', '.join(missing_names)}")
# 4. 获取项目信息
project_result = await db.execute(
select(Project).where(Project.id == project_id)
)
project = project_result.scalar_one_or_none()
if not project:
logger.error("❌ 【组织校验】项目不存在")
return {
"created_organizations": [],
"missing_names": list(missing_names),
"created_count": 0
}
# 5. 获取现有角色和组织信息
all_chars_result = await db.execute(
select(Character).where(Character.project_id == project_id)
)
existing_characters = list(all_chars_result.scalars().all())
existing_organizations = []
for char in existing_org_characters:
org_result = await db.execute(
select(Organization).where(Organization.character_id == char.id)
)
org = org_result.scalar_one_or_none()
if org:
existing_organizations.append({
"name": char.name,
"organization_type": char.organization_type,
"organization_purpose": char.organization_purpose,
"power_level": org.power_level,
"location": org.location,
"motto": org.motto
})
# 6. 为每个缺失的组织生成并创建组织信息
created_organizations = []
for idx, org_name in enumerate(missing_names):
try:
if progress_callback:
await progress_callback(
f"🏛️ [{idx+1}/{len(missing_names)}] 自动创建组织:{org_name}..."
)
# 构建组织规格(基于大纲上下文)
context_summaries = organization_context.get(org_name, [])
context_text = "\n".join(context_summaries[:3])
spec = {
"name": org_name,
"organization_description": f"在大纲中出现的组织/势力,出现场景:\n{context_text}",
"organization_type": "未知",
"importance": "medium"
}
logger.info(f" 🤖 [{idx+1}/{len(missing_names)}] 生成组织详情: {org_name}")
# 生成组织详细信息
organization_data = await self._generate_organization_details(
spec=spec,
project=project,
existing_characters=existing_characters,
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=enable_mcp
)
# 确保使用大纲中的组织名称
organization_data['name'] = org_name
if progress_callback:
await progress_callback(
f"💾 [{idx+1}/{len(missing_names)}] 保存组织:{org_name}..."
)
# 创建组织记录
org_character, organization = await self._create_organization_record(
project_id=project_id,
organization_data=organization_data,
db=db
)
created_organizations.append(org_character)
existing_characters.append(org_character)
existing_organizations.append({
"name": org_character.name,
"organization_type": org_character.organization_type,
"organization_purpose": org_character.organization_purpose,
"power_level": organization.power_level,
"location": organization.location,
"motto": organization.motto
})
logger.info(f" ✅ [{idx+1}/{len(missing_names)}] 组织创建成功: {org_character.name}")
# 建立成员关系
members_data = organization_data.get("initial_members", [])
if members_data:
if progress_callback:
await progress_callback(
f"🔗 [{idx+1}/{len(missing_names)}] 建立 {len(members_data)} 个成员关系:{org_name}..."
)
await self._create_member_relationships(
organization=organization,
member_specs=members_data,
existing_characters=existing_characters,
project_id=project_id,
db=db
)
if progress_callback:
await progress_callback(
f"✅ [{idx+1}/{len(missing_names)}] 组织创建完成:{org_name}"
)
except Exception as e:
logger.error(f" ❌ 创建组织 {org_name} 失败: {e}", exc_info=True)
if progress_callback:
await progress_callback(
f"⚠️ [{idx+1}/{len(missing_names)}] 组织 {org_name} 创建失败"
)
continue
# 7. flush 到数据库(让调用方 commit)
if created_organizations:
await db.flush()
logger.info(f"🎉 【组织校验】完成: 发现 {len(missing_names)} 个缺失组织,成功创建 {len(created_organizations)}")
return {
"created_organizations": created_organizations,
"missing_names": list(missing_names),
"created_count": len(created_organizations)
}
# 全局实例缓存 # 全局实例缓存
_auto_organization_service_instance: Optional[AutoOrganizationService] = None _auto_organization_service_instance: Optional[AutoOrganizationService] = None