feature:新增大纲续写-智能生成组织功能,自动添加组织成员

This commit is contained in:
xiamuceer
2026-01-05 14:27:27 +08:00
parent 6e603ee1a9
commit ba7ee591b6
6 changed files with 1986 additions and 84 deletions
+756 -83
View File
@@ -25,7 +25,10 @@ from app.schemas.outline import (
CreateChaptersFromPlansResponse,
CharacterPredictionRequest,
PredictedCharacter,
CharacterPredictionResponse
CharacterPredictionResponse,
OrganizationPredictionRequest,
PredictedOrganization,
OrganizationPredictionResponse
)
from app.services.ai_service import AIService
from app.services.prompt_service import prompt_service, PromptService
@@ -464,6 +467,136 @@ async def predict_characters(
raise HTTPException(status_code=500, detail=f"角色预测失败: {str(e)}")
@router.post("/predict-organizations", summary="预测续写所需组织")
async def predict_organizations(
request_data: OrganizationPredictionRequest,
http_request: Request,
db: AsyncSession = Depends(get_db),
user_ai_service: AIService = Depends(get_user_ai_service)
):
"""
预测续写大纲时可能需要的新组织
用于组织确认机制的第一步:在生成大纲前预测组织需求
"""
from app.schemas.outline import OrganizationPredictionResponse, PredictedOrganization
from app.models.relationship import Organization
# 验证用户权限
user_id = getattr(http_request.state, 'user_id', None)
project = await verify_project_access(request_data.project_id, user_id, db)
try:
# 获取现有大纲
existing_result = await db.execute(
select(Outline)
.where(Outline.project_id == request_data.project_id)
.order_by(Outline.order_index)
)
existing_outlines = existing_result.scalars().all()
if not existing_outlines:
return OrganizationPredictionResponse(
needs_new_organizations=False,
reason="项目尚无大纲,无法预测组织需求",
organization_count=0,
predicted_organizations=[]
)
# 获取现有角色
characters_result = await db.execute(
select(Character).where(Character.project_id == request_data.project_id)
)
characters = characters_result.scalars().all()
# 获取现有组织
organizations_result = await db.execute(
select(Character, Organization)
.join(Organization, Character.id == Organization.character_id)
.where(
Character.project_id == request_data.project_id,
Character.is_organization == True
)
)
organizations_raw = organizations_result.all()
existing_organizations = []
for char, org in organizations_raw:
existing_organizations.append({
"id": org.id,
"name": char.name,
"organization_type": char.organization_type,
"organization_purpose": char.organization_purpose,
"power_level": org.power_level,
"location": org.location,
"motto": org.motto
})
# 构建已有章节概览
all_chapters_brief = ""
if len(existing_outlines) > 20:
recent_20 = existing_outlines[-20:]
all_chapters_brief = "\n".join([
f"{o.order_index}章《{o.title}"
for o in recent_20
])
else:
all_chapters_brief = "\n".join([
f"{o.order_index}章《{o.title}"
for o in existing_outlines
])
# 调用自动组织服务进行预测
from app.services.auto_organization_service import get_auto_organization_service
auto_org_service = get_auto_organization_service(user_ai_service)
# 使用预测模式(不创建组织,仅分析)
last_chapter_number = existing_outlines[-1].order_index
auto_result = await auto_org_service.analyze_and_create_organizations(
project_id=request_data.project_id,
outline_content="", # 预测模式不需要大纲内容
existing_characters=list(characters),
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=request_data.enable_mcp,
all_chapters_brief=all_chapters_brief,
start_chapter=last_chapter_number + 1,
chapter_count=request_data.chapter_count,
plot_stage=request_data.plot_stage,
story_direction=request_data.story_direction,
preview_only=True # 仅预测不创建
)
# 构建预测响应
predicted_organizations = []
for org_data in auto_result.get("predicted_organizations", []):
predicted_organizations.append(PredictedOrganization(
name=org_data.get("name"),
organization_description=org_data.get("organization_description", ""),
organization_type=org_data.get("organization_type", "未知"),
importance=org_data.get("importance", "medium"),
appearance_chapter=org_data.get("appearance_chapter", last_chapter_number + 1),
power_level=org_data.get("power_level", 50),
plot_function=org_data.get("plot_function", ""),
location=org_data.get("location"),
motto=org_data.get("motto"),
initial_members=org_data.get("initial_members", []),
relationship_suggestions=org_data.get("relationship_suggestions", [])
))
return OrganizationPredictionResponse(
needs_new_organizations=auto_result.get("needs_new_organizations", False),
reason=auto_result.get("reason", ""),
organization_count=len(predicted_organizations),
predicted_organizations=predicted_organizations
)
except Exception as e:
logger.error(f"组织预测失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"组织预测失败: {str(e)}")
async def _generate_new_outline(
request: OutlineGenerateRequest,
@@ -839,14 +972,10 @@ async def _continue_outline(
except Exception as e:
logger.error(f"⚠️ 【确认模式】创建确认角色失败: {e}", exc_info=True)
else:
# 🔮 预测模式:仅预测角色,不自动创建,需要用户确认
# 抛出特殊异常,在非SSE接口中会被捕获并返回449状态码
# 在SSE接口中会被特殊处理
# 根据 require_character_confirmation 决定处理方式
try:
from app.services.auto_character_service import get_auto_character_service
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色")
# 构建已有章节概览
all_chapters_brief_for_analysis = ""
if len(existing_outlines) > 20:
@@ -861,43 +990,83 @@ async def _continue_outline(
for o in existing_outlines
])
# 调用自动角色服务(✅ 设置 preview_only=True,仅预测不创建)
auto_char_service = get_auto_character_service(user_ai_service)
auto_result = await auto_char_service.analyze_and_create_characters(
project_id=project.id,
outline_content="", # 预测模式不需要大纲内容
existing_characters=list(characters),
db=db,
user_id=user_id,
enable_mcp=request.enable_mcp,
all_chapters_brief=all_chapters_brief_for_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=request.plot_stage,
story_direction=request.story_direction or "自然延续",
preview_only=True # ✅ 关键修复:设置为True,仅预测不创建
)
# 检查是否需要新角色
if auto_result.get("needs_new_characters") and auto_result.get("predicted_characters"):
predicted_count = len(auto_result["predicted_characters"])
logger.warning(
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新角色,需要用户确认!"
if request.require_character_confirmation:
# 🔮 预测模式:仅预测角色,不自动创建,需要用户确认
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色(需用户确认)")
auto_result = await auto_char_service.analyze_and_create_characters(
project_id=project.id,
outline_content="", # 预测模式不需要大纲内容
existing_characters=list(characters),
db=db,
user_id=user_id,
enable_mcp=request.enable_mcp,
all_chapters_brief=all_chapters_brief_for_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=request.plot_stage,
story_direction=request.story_direction or "自然延续",
preview_only=True # ✅ 仅预测不创建
)
# 🚨 抛出特殊异常,包含预测的角色信息
raise HTTPException(
status_code=449, # 449 Retry With
detail={
"code": "CHARACTER_CONFIRMATION_REQUIRED",
"message": "续写需要引入新角色,请先确认角色信息",
"predicted_characters": auto_result["predicted_characters"],
"reason": auto_result.get("reason", "剧情发展需要新角色"),
"chapter_range": f"{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}"
}
)
# 检查是否需要新角色
if auto_result.get("needs_new_characters") and auto_result.get("predicted_characters"):
predicted_count = len(auto_result["predicted_characters"])
logger.warning(
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新角色,需要用户确认!"
)
# 🚨 抛出特殊异常,包含预测的角色信息
raise HTTPException(
status_code=449, # 449 Retry With
detail={
"code": "CHARACTER_CONFIRMATION_REQUIRED",
"message": "续写需要引入新角色,请先确认角色信息",
"predicted_characters": auto_result["predicted_characters"],
"reason": auto_result.get("reason", "剧情发展需要新角色"),
"chapter_range": f"{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}"
}
)
else:
logger.info(f"✅ 【预测模式】AI判断无需引入新角色,继续生成大纲")
else:
logger.info(f"✅ 【预测模式】AI判断无需引入新角色,继续生成大纲")
# 🚀 直接创建模式:预测后自动创建,无需用户确认
logger.info(f"🚀 【直接创建模式】在生成大纲前预测并直接创建新角色(无需确认)")
auto_result = await auto_char_service.analyze_and_create_characters(
project_id=project.id,
outline_content="",
existing_characters=list(characters),
db=db,
user_id=user_id,
enable_mcp=request.enable_mcp,
all_chapters_brief=all_chapters_brief_for_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=request.plot_stage,
story_direction=request.story_direction or "自然延续",
preview_only=False # ✅ 直接创建角色
)
# 如果创建了新角色,更新角色列表
if auto_result.get("new_characters"):
new_count = len(auto_result["new_characters"])
logger.info(f"✅ 【直接创建模式】自动创建了 {new_count} 个新角色")
# 提交角色到数据库
await db.commit()
# 更新角色信息(供后续大纲生成使用)
characters.extend(auto_result["new_characters"])
characters_info = "\n".join([
f"- {char.name} ({'组织' if char.is_organization else '角色'}, {char.role_type}): "
f"{char.personality[:100] if char.personality else '暂无描述'}"
for char in characters
])
else:
logger.info(f"✅ 【直接创建模式】AI判断无需引入新角色,继续生成大纲")
except HTTPException:
raise
@@ -905,6 +1074,212 @@ async def _continue_outline(
logger.error(f"⚠️ 【方案A】预测性角色引入失败: {e}", exc_info=True)
# 不阻断大纲生成流程
# 🏛️ 【组织引入】在生成大纲前预测并创建组织
if request.enable_auto_organizations:
from app.models.relationship import Organization
# 获取现有组织
organizations_result = await db.execute(
select(Character, Organization)
.join(Organization, Character.id == Organization.character_id)
.where(
Character.project_id == project.id,
Character.is_organization == True
)
)
organizations_raw = organizations_result.all()
existing_organizations = []
for char, org in organizations_raw:
existing_organizations.append({
"id": org.id,
"name": char.name,
"organization_type": char.organization_type,
"organization_purpose": char.organization_purpose,
"power_level": org.power_level,
"location": org.location,
"motto": org.motto
})
# 检查是否有用户确认的组织列表
if request.confirmed_organizations:
# 直接使用用户确认的组织列表创建组织
try:
from app.services.auto_organization_service import get_auto_organization_service
logger.info(f"🏛️ 【确认模式】用户提供了 {len(request.confirmed_organizations)} 个确认的组织,直接创建")
auto_org_service = get_auto_organization_service(user_ai_service)
for org_data in request.confirmed_organizations:
try:
# 生成组织详细信息
organization_data = await auto_org_service._generate_organization_details(
spec=org_data,
project=project,
existing_characters=list(characters),
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=request.enable_mcp
)
# 创建组织记录
org_character, organization = await auto_org_service._create_organization_record(
project_id=project.id,
organization_data=organization_data,
db=db
)
# 建立成员关系
members_data = organization_data.get("initial_members", [])
if members_data:
await auto_org_service._create_member_relationships(
organization=organization,
member_specs=members_data,
existing_characters=list(characters),
project_id=project.id,
db=db
)
# 更新角色列表(组织也是Character)
characters.append(org_character)
existing_organizations.append({
"id": organization.id,
"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"✅ 创建确认的组织: {org_character.name}")
except Exception as e:
logger.error(f"创建确认的组织失败: {e}", exc_info=True)
continue
# 提交组织到数据库
await db.commit()
# 更新角色信息(供后续大纲生成使用)
characters_info = "\n".join([
f"- {char.name} ({'组织' if char.is_organization else '角色'}, {char.role_type}): "
f"{char.personality[:100] if char.personality else '暂无描述'}"
for char in characters
])
logger.info(f"✅ 【确认模式】成功创建 {len(request.confirmed_organizations)} 个用户确认的组织")
except Exception as e:
logger.error(f"⚠️ 【确认模式】创建确认组织失败: {e}", exc_info=True)
else:
# 根据 require_organization_confirmation 决定处理方式
try:
from app.services.auto_organization_service import get_auto_organization_service
# 构建已有章节概览
all_chapters_brief_for_org_analysis = ""
if len(existing_outlines) > 20:
recent_20 = existing_outlines[-20:]
all_chapters_brief_for_org_analysis = "\n".join([
f"{o.order_index}章《{o.title}"
for o in recent_20
])
else:
all_chapters_brief_for_org_analysis = "\n".join([
f"{o.order_index}章《{o.title}"
for o in existing_outlines
])
auto_org_service = get_auto_organization_service(user_ai_service)
if request.require_organization_confirmation:
# 🔮 预测模式:仅预测组织,不自动创建,需要用户确认
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新组织(需用户确认)")
auto_result = await auto_org_service.analyze_and_create_organizations(
project_id=project.id,
outline_content="", # 预测模式不需要大纲内容
existing_characters=list(characters),
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=request.enable_mcp,
all_chapters_brief=all_chapters_brief_for_org_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=request.plot_stage,
story_direction=request.story_direction or "自然延续",
preview_only=True # ✅ 仅预测不创建
)
# 检查是否需要新组织
if auto_result.get("needs_new_organizations") and auto_result.get("predicted_organizations"):
predicted_count = len(auto_result["predicted_organizations"])
logger.warning(
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新组织,需要用户确认!"
)
# 🚨 抛出特殊异常,包含预测的组织信息
raise HTTPException(
status_code=449, # 449 Retry With
detail={
"code": "ORGANIZATION_CONFIRMATION_REQUIRED",
"message": "续写需要引入新组织,请先确认组织信息",
"predicted_organizations": auto_result["predicted_organizations"],
"reason": auto_result.get("reason", "剧情发展需要新组织"),
"chapter_range": f"{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}"
}
)
else:
logger.info(f"✅ 【预测模式】AI判断无需引入新组织,继续生成大纲")
else:
# 🚀 直接创建模式:预测后自动创建,无需用户确认
logger.info(f"🚀 【直接创建模式】在生成大纲前预测并直接创建新组织(无需确认)")
auto_result = await auto_org_service.analyze_and_create_organizations(
project_id=project.id,
outline_content="",
existing_characters=list(characters),
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=request.enable_mcp,
all_chapters_brief=all_chapters_brief_for_org_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=request.plot_stage,
story_direction=request.story_direction or "自然延续",
preview_only=False # ✅ 直接创建组织
)
# 如果创建了新组织,更新角色列表
if auto_result.get("new_organizations"):
new_count = len(auto_result["new_organizations"])
logger.info(f"✅ 【直接创建模式】自动创建了 {new_count} 个新组织")
# 提交组织到数据库
await db.commit()
# 更新角色信息(供后续大纲生成使用)
for org_item in auto_result["new_organizations"]:
org_char = org_item.get("character")
if org_char:
characters.append(org_char)
characters_info = "\n".join([
f"- {char.name} ({'组织' if char.is_organization else '角色'}, {char.role_type}): "
f"{char.personality[:100] if char.personality else '暂无描述'}"
for char in characters
])
else:
logger.info(f"✅ 【直接创建模式】AI判断无需引入新组织,继续生成大纲")
except HTTPException:
raise
except Exception as e:
logger.error(f"⚠️ 【组织引入】预测性组织引入失败: {e}", exc_info=True)
# 不阻断大纲生成流程
# 批量生成
all_new_outlines = []
current_start_chapter = last_chapter_number + 1
@@ -1755,17 +2130,12 @@ async def continue_outline_generator(
28
)
else:
# 🔮 预测模式:仅预测角色,不自动创建,需要用户确认
# 根据 require_character_confirmation 决定处理方式
require_confirmation = data.get("require_character_confirmation", True)
try:
yield await SSEResponse.send_progress(
"🔮 【预测模式】检测是否需要新角色(需用户确认)...",
27
)
from app.services.auto_character_service import get_auto_character_service
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色")
# 构建已有章节概览
all_chapters_brief_for_analysis = ""
if len(existing_outlines) > 20:
@@ -1780,47 +2150,106 @@ async def continue_outline_generator(
for o in existing_outlines
])
# 调用自动角色服务(✅ 设置 preview_only=True
auto_char_service = get_auto_character_service(user_ai_service)
auto_result = await auto_char_service.analyze_and_create_characters(
project_id=project_id,
outline_content="", # 预测模式不需要大纲内容
existing_characters=list(characters),
db=db,
user_id=user_id,
enable_mcp=data.get("enable_mcp", True),
all_chapters_brief=all_chapters_brief_for_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=data.get("plot_stage", "development"),
story_direction=data.get("story_direction", "自然延续"),
preview_only=True # ✅ 关键修复:仅预测不创建
)
# 检查是否需要新角色
if auto_result.get("needs_new_characters") and auto_result.get("predicted_characters"):
predicted_count = len(auto_result["predicted_characters"])
logger.warning(
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新角色,需要用户确认!"
)
# 🚨 使用专用事件类型通知前端需要角色确认
yield await SSEResponse.send_event(
event="character_confirmation_required",
data={
"message": "续写需要引入新角色,请先确认角色信息",
"predicted_characters": auto_result["predicted_characters"],
"reason": auto_result.get("reason", "剧情发展需要新角色"),
"chapter_range": f"{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}"
}
)
return
else:
if require_confirmation:
# 🔮 预测模式:仅预测角色,不自动创建,需要用户确认
yield await SSEResponse.send_progress(
" 【预测模式】无需引入新角色,继续生成大纲",
28
"🔮 【预测模式】检测是否需要新角色(需用户确认)...",
27
)
logger.info(f"✅ 【预测模式】AI判断无需引入新角色")
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色")
auto_result = await auto_char_service.analyze_and_create_characters(
project_id=project_id,
outline_content="", # 预测模式不需要大纲内容
existing_characters=list(characters),
db=db,
user_id=user_id,
enable_mcp=data.get("enable_mcp", True),
all_chapters_brief=all_chapters_brief_for_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=data.get("plot_stage", "development"),
story_direction=data.get("story_direction", "自然延续"),
preview_only=True # ✅ 仅预测不创建
)
# 检查是否需要新角色
if auto_result.get("needs_new_characters") and auto_result.get("predicted_characters"):
predicted_count = len(auto_result["predicted_characters"])
logger.warning(
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新角色,需要用户确认!"
)
# 🚨 使用专用事件类型通知前端需要角色确认
yield await SSEResponse.send_event(
event="character_confirmation_required",
data={
"message": "续写需要引入新角色,请先确认角色信息",
"predicted_characters": auto_result["predicted_characters"],
"reason": auto_result.get("reason", "剧情发展需要新角色"),
"chapter_range": f"{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}"
}
)
return
else:
yield await SSEResponse.send_progress(
"✅ 【预测模式】无需引入新角色,继续生成大纲",
28
)
logger.info(f"✅ 【预测模式】AI判断无需引入新角色")
else:
# 🚀 直接创建模式:预测后自动创建,无需用户确认
yield await SSEResponse.send_progress(
"🚀 【直接创建模式】检测并自动创建新角色(无需确认)...",
27
)
logger.info(f"🚀 【直接创建模式】在生成大纲前预测并直接创建新角色")
auto_result = await auto_char_service.analyze_and_create_characters(
project_id=project_id,
outline_content="",
existing_characters=list(characters),
db=db,
user_id=user_id,
enable_mcp=data.get("enable_mcp", True),
all_chapters_brief=all_chapters_brief_for_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=data.get("plot_stage", "development"),
story_direction=data.get("story_direction", "自然延续"),
preview_only=False # ✅ 直接创建角色
)
# 如果创建了新角色,更新角色列表
if auto_result.get("new_characters"):
new_count = len(auto_result["new_characters"])
logger.info(f"✅ 【直接创建模式】自动创建了 {new_count} 个新角色")
yield await SSEResponse.send_progress(
f"✅ 【直接创建模式】自动创建了 {new_count} 个新角色",
28
)
# 提交角色到数据库
await db.commit()
# 更新角色信息(供后续大纲生成使用)
characters.extend(auto_result["new_characters"])
characters_info = "\n".join([
f"- {char.name} ({'组织' if char.is_organization else '角色'}, {char.role_type}): "
f"{char.personality[:100] if char.personality else '暂无描述'}"
for char in characters
])
else:
yield await SSEResponse.send_progress(
"✅ 【直接创建模式】无需引入新角色,继续生成大纲",
28
)
logger.info(f"✅ 【直接创建模式】AI判断无需引入新角色")
except Exception as e:
logger.error(f"⚠️ 【方案A】预测性角色引入失败: {e}", exc_info=True)
@@ -1830,6 +2259,250 @@ async def continue_outline_generator(
)
# 不阻断大纲生成流程
# 🏛️ 【组织引入】在生成大纲前预测并创建组织
enable_auto_organizations = data.get("enable_auto_organizations", True)
confirmed_organizations = data.get("confirmed_organizations")
if enable_auto_organizations:
from app.models.relationship import Organization
# 获取现有组织
organizations_result = await db.execute(
select(Character, Organization)
.join(Organization, Character.id == Organization.character_id)
.where(
Character.project_id == project_id,
Character.is_organization == True
)
)
organizations_raw = organizations_result.all()
existing_organizations = []
for char, org in organizations_raw:
existing_organizations.append({
"id": org.id,
"name": char.name,
"organization_type": char.organization_type,
"organization_purpose": char.organization_purpose,
"power_level": org.power_level,
"location": org.location,
"motto": org.motto
})
# 检查是否有用户确认的组织列表
if confirmed_organizations:
# 直接使用用户确认的组织列表创建组织
try:
yield await SSEResponse.send_progress(
f"🏛️ 【确认模式】创建 {len(confirmed_organizations)} 个用户确认的组织...",
29
)
from app.services.auto_organization_service import get_auto_organization_service
logger.info(f"🏛️ 【确认模式】用户提供了 {len(confirmed_organizations)} 个确认的组织,直接创建")
auto_org_service = get_auto_organization_service(user_ai_service)
created_org_count = 0
for org_data in confirmed_organizations:
try:
# 生成组织详细信息
organization_data = await auto_org_service._generate_organization_details(
spec=org_data,
project=project,
existing_characters=list(characters),
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=data.get("enable_mcp", True)
)
# 创建组织记录
org_character, organization = await auto_org_service._create_organization_record(
project_id=project_id,
organization_data=organization_data,
db=db
)
# 建立成员关系
members_data = organization_data.get("initial_members", [])
if members_data:
await auto_org_service._create_member_relationships(
organization=organization,
member_specs=members_data,
existing_characters=list(characters),
project_id=project_id,
db=db
)
# 更新角色列表(组织也是Character)
characters.append(org_character)
existing_organizations.append({
"id": organization.id,
"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
})
created_org_count += 1
logger.info(f"✅ 创建确认的组织: {org_character.name}")
except Exception as e:
logger.error(f"创建确认的组织失败: {e}", exc_info=True)
continue
# 提交组织到数据库
await db.commit()
yield await SSEResponse.send_progress(
f"✅ 【确认模式】成功创建 {created_org_count} 个组织",
30
)
logger.info(f"✅ 【确认模式】成功创建 {created_org_count} 个用户确认的组织")
except Exception as e:
logger.error(f"⚠️ 【确认模式】创建确认组织失败: {e}", exc_info=True)
yield await SSEResponse.send_progress(
f"⚠️ 组织创建失败,继续生成大纲",
30
)
else:
# 根据 require_organization_confirmation 决定处理方式
require_org_confirmation = data.get("require_organization_confirmation", True)
try:
from app.services.auto_organization_service import get_auto_organization_service
# 构建已有章节概览
all_chapters_brief_for_org_analysis = ""
if len(existing_outlines) > 20:
recent_20 = existing_outlines[-20:]
all_chapters_brief_for_org_analysis = "\n".join([
f"{o.order_index}章《{o.title}"
for o in recent_20
])
else:
all_chapters_brief_for_org_analysis = "\n".join([
f"{o.order_index}章《{o.title}"
for o in existing_outlines
])
auto_org_service = get_auto_organization_service(user_ai_service)
if require_org_confirmation:
# 🔮 预测模式:仅预测组织,不自动创建,需要用户确认
yield await SSEResponse.send_progress(
"🔮 【预测模式】检测是否需要新组织(需用户确认)...",
29
)
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新组织")
auto_result = await auto_org_service.analyze_and_create_organizations(
project_id=project_id,
outline_content="", # 预测模式不需要大纲内容
existing_characters=list(characters),
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=data.get("enable_mcp", True),
all_chapters_brief=all_chapters_brief_for_org_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=data.get("plot_stage", "development"),
story_direction=data.get("story_direction", "自然延续"),
preview_only=True # ✅ 仅预测不创建
)
# 检查是否需要新组织
if auto_result.get("needs_new_organizations") and auto_result.get("predicted_organizations"):
predicted_count = len(auto_result["predicted_organizations"])
logger.warning(
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新组织,需要用户确认!"
)
# 🚨 使用专用事件类型通知前端需要组织确认
yield await SSEResponse.send_event(
event="organization_confirmation_required",
data={
"message": "续写需要引入新组织,请先确认组织信息",
"predicted_organizations": auto_result["predicted_organizations"],
"reason": auto_result.get("reason", "剧情发展需要新组织"),
"chapter_range": f"{last_chapter_number + 1}-{last_chapter_number + total_chapters_to_generate}"
}
)
return
else:
yield await SSEResponse.send_progress(
"✅ 【预测模式】无需引入新组织,继续生成大纲",
30
)
logger.info(f"✅ 【预测模式】AI判断无需引入新组织")
else:
# 🚀 直接创建模式:预测后自动创建,无需用户确认
yield await SSEResponse.send_progress(
"🚀 【直接创建模式】检测并自动创建新组织(无需确认)...",
29
)
logger.info(f"🚀 【直接创建模式】在生成大纲前预测并直接创建新组织")
auto_result = await auto_org_service.analyze_and_create_organizations(
project_id=project_id,
outline_content="",
existing_characters=list(characters),
existing_organizations=existing_organizations,
db=db,
user_id=user_id,
enable_mcp=data.get("enable_mcp", True),
all_chapters_brief=all_chapters_brief_for_org_analysis,
start_chapter=last_chapter_number + 1,
chapter_count=total_chapters_to_generate,
plot_stage=data.get("plot_stage", "development"),
story_direction=data.get("story_direction", "自然延续"),
preview_only=False # ✅ 直接创建组织
)
# 如果创建了新组织,更新角色列表
if auto_result.get("new_organizations"):
new_count = len(auto_result["new_organizations"])
logger.info(f"✅ 【直接创建模式】自动创建了 {new_count} 个新组织")
yield await SSEResponse.send_progress(
f"✅ 【直接创建模式】自动创建了 {new_count} 个新组织",
30
)
# 提交组织到数据库
await db.commit()
# 更新角色信息(供后续大纲生成使用)
for org_item in auto_result["new_organizations"]:
org_char = org_item.get("character")
if org_char:
characters.append(org_char)
characters_info = "\n".join([
f"- {char.name} ({'组织' if char.is_organization else '角色'}, {char.role_type}): "
f"{char.personality[:100] if char.personality else '暂无描述'}"
for char in characters
])
else:
yield await SSEResponse.send_progress(
"✅ 【直接创建模式】无需引入新组织,继续生成大纲",
30
)
logger.info(f"✅ 【直接创建模式】AI判断无需引入新组织")
except Exception as e:
logger.error(f"⚠️ 【组织引入】预测性组织引入失败: {e}", exc_info=True)
yield await SSEResponse.send_progress(
f"⚠️ 组织预测失败,继续生成大纲",
30
)
# 不阻断大纲生成流程
# 批量生成
all_new_outlines = []
current_start_chapter = last_chapter_number + 1
+42
View File
@@ -35,6 +35,40 @@ class CharacterPredictionResponse(BaseModel):
predicted_characters: List[PredictedCharacter]
# 组织预测相关Schema
class OrganizationPredictionRequest(BaseModel):
"""组织预测请求"""
project_id: str
start_chapter: int
chapter_count: int = 3
plot_stage: str = "development"
story_direction: Optional[str] = "自然延续"
enable_mcp: bool = True
class PredictedOrganization(BaseModel):
"""预测的组织信息"""
name: Optional[str] = None
organization_description: str
organization_type: str
importance: str
appearance_chapter: int
power_level: int = 50
plot_function: str
location: Optional[str] = None
motto: Optional[str] = None
initial_members: List[Dict[str, Any]] = []
relationship_suggestions: List[Dict[str, str]] = []
class OrganizationPredictionResponse(BaseModel):
"""组织预测响应"""
needs_new_organizations: bool
reason: str
organization_count: int
predicted_organizations: List[PredictedOrganization]
class OutlineBase(BaseModel):
"""大纲基础模型"""
title: str = Field(..., description="章节标题")
@@ -93,9 +127,17 @@ class OutlineGenerateRequest(BaseModel):
plot_stage: str = Field("development", description="情节阶段: development(发展), climax(高潮), ending(结局)")
keep_existing: bool = Field(False, description="是否保留现有大纲(续写时)")
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索情节设计参考)")
# 自动角色引入相关参数
enable_auto_characters: bool = Field(True, description="是否启用自动角色引入(根据剧情推进自动创建新角色)")
require_character_confirmation: bool = Field(True, description="是否需要用户确认新角色(False则AI预测的角色直接创建)")
confirmed_characters: Optional[List[Dict[str, Any]]] = Field(None, description="用户确认的角色列表(跳过预测直接创建)")
# 自动组织引入相关参数
enable_auto_organizations: bool = Field(True, description="是否启用自动组织引入(根据剧情推进自动创建新组织)")
require_organization_confirmation: bool = Field(True, description="是否需要用户确认新组织(False则AI预测的组织直接创建)")
confirmed_organizations: Optional[List[Dict[str, Any]]] = Field(None, description="用户确认的组织列表(跳过预测直接创建)")
class ChapterOutlineGenerateRequest(BaseModel):
"""为单个章节生成大纲的请求模型"""
@@ -0,0 +1,504 @@
"""自动组织引入服务 - 在续写大纲时根据剧情推进自动引入新组织"""
from typing import List, Dict, Any, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
import json
from app.models.character import Character
from app.models.relationship import Organization, OrganizationMember
from app.models.project import Project
from app.services.ai_service import AIService
from app.services.prompt_service import PromptService
from app.logger import get_logger
logger = get_logger(__name__)
class AutoOrganizationService:
"""自动组织引入服务"""
def __init__(self, ai_service: AIService):
self.ai_service = ai_service
async def analyze_and_create_organizations(
self,
project_id: str,
outline_content: str,
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
) -> Dict[str, Any]:
"""
预测性分析并创建需要的新组织
Args:
project_id: 项目ID
outline_content: 当前批次大纲内容(用于向后兼容,实际不使用)
existing_characters: 现有角色列表
existing_organizations: 现有组织列表
db: 数据库会话
user_id: 用户ID(用于MCP和自定义提示词)
enable_mcp: 是否启用MCP增强
all_chapters_brief: 已有章节概览
start_chapter: 起始章节号
chapter_count: 续写章节数
plot_stage: 剧情阶段
story_direction: 故事发展方向
preview_only: 仅预测不创建(用于组织确认机制)
Returns:
{
"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预测性分析是否需要新组织
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
)
# 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)}")
# 生成组织详细信息
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]}")
# 创建组织记录(先创建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}")
# 建立成员关系
members_data = organization_data.get("initial_members", [])
if members_data:
logger.info(f" 🔗 开始创建 {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:
"""构建现有组织摘要"""
if not organizations:
return "暂无组织"
summary = []
for org in organizations:
org_name = org.get("name", "未知")
org_type = org.get("organization_type", "未知类型")
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个
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:
# 调用AI分析(使用统一的JSON调用方法)
if enable_mcp and user_id:
result = await self.ai_service.generate_text_with_mcp(
prompt=prompt,
user_id=user_id,
db_session=db,
enable_mcp=True,
max_tool_rounds=2
)
content = result.get("content", "")
# 使用统一的JSON清洗方法
cleaned = self.ai_service._clean_json_response(content)
analysis = json.loads(cleaned)
else:
# 非MCP调用:使用带自动重试的JSON调用
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(
self,
spec: Dict[str, Any],
project: Project,
existing_characters: List[Character],
existing_organizations: List[Dict[str, Any]],
db: AsyncSession,
user_id: str,
enable_mcp: bool
) -> Dict[str, Any]:
"""生成组织详细信息"""
# 构建组织生成提示词
template = await PromptService.get_template(
"AUTO_ORGANIZATION_GENERATION",
user_id,
db
)
existing_orgs_summary = self._build_organization_summary(existing_organizations)
existing_chars_summary = self._build_character_summary(existing_characters)
prompt = PromptService.format_prompt(
template,
title=project.title,
genre=project.genre or "未设定",
theme=project.theme or "未设定",
time_period=project.world_time_period or "未设定",
location=project.world_location or "未设定",
atmosphere=project.world_atmosphere or "未设定",
rules=project.world_rules or "未设定",
existing_organizations=existing_orgs_summary,
existing_characters=existing_chars_summary,
plot_context="根据剧情需要引入的新组织",
organization_specification=json.dumps(spec, ensure_ascii=False, indent=2),
mcp_references="" # 暂时不使用MCP增强
)
# 调用AI生成(使用统一的JSON调用方法)
try:
if enable_mcp and user_id:
result = await self.ai_service.generate_text_with_mcp(
prompt=prompt,
user_id=user_id,
db_session=db,
enable_mcp=True,
max_tool_rounds=2
)
content = result.get("content", "")
# 使用统一的JSON清洗方法
cleaned = self.ai_service._clean_json_response(content)
organization_data = json.loads(cleaned)
else:
# 非MCP调用:使用带自动重试的JSON调用
organization_data = await self.ai_service.call_with_json_retry(
prompt=prompt,
max_retries=3
)
org_name = organization_data.get('name', '未知')
logger.info(f" ✅ 组织详情生成成功: {org_name}")
logger.debug(f" 组织数据字段: {list(organization_data.keys())}")
# 确保关键字段存在
if 'name' not in organization_data or not organization_data['name']:
logger.warning(f" ⚠️ AI返回的组织数据缺少name字段,使用规格中的信息")
organization_data['name'] = spec.get('name', f"新组织{spec.get('organization_description', '')[:10]}")
return organization_data
except Exception as e:
logger.error(f" ❌ 生成组织详情失败: {e}")
raise
async def _create_organization_record(
self,
project_id: str,
organization_data: Dict[str, Any],
db: AsyncSession
) -> tuple:
"""创建组织数据库记录(包括Character和Organization"""
# 首先创建Character记录(is_organization=True
character = Character(
project_id=project_id,
name=organization_data.get("name", "未命名组织"),
is_organization=True,
role_type=organization_data.get("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"),
traits=json.dumps(organization_data.get("traits", []), ensure_ascii=False) if organization_data.get("traits") else None
)
db.add(character)
await db.flush()
# 然后创建Organization记录
organization = Organization(
character_id=character.id,
project_id=project_id,
power_level=organization_data.get("power_level", 50),
member_count=0,
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}, Organization ID: {organization.id}")
return character, organization
async def _create_member_relationships(
self,
organization: Organization,
member_specs: List[Dict[str, Any]],
existing_characters: List[Character],
project_id: str,
db: AsyncSession
) -> List[OrganizationMember]:
"""创建组织成员关系"""
if not member_specs:
return []
members = []
for member_spec in member_specs:
try:
character_name = member_spec.get("character_name")
if not character_name:
continue
# 查找目标角色
target_char = next(
(c for c in existing_characters if c.name == character_name and not c.is_organization),
None
)
if not target_char:
logger.warning(f" ⚠️ 目标角色不存在: {character_name}")
continue
# 检查成员关系是否已存在
existing_member = await db.execute(
select(OrganizationMember).where(
OrganizationMember.organization_id == organization.id,
OrganizationMember.character_id == target_char.id
)
)
if existing_member.scalar_one_or_none():
logger.debug(f" ️ 成员关系已存在: {character_name} -> {organization.id}")
continue
# 创建成员关系
member = OrganizationMember(
organization_id=organization.id,
character_id=target_char.id,
position=member_spec.get("position", "成员"),
rank=member_spec.get("rank", 0),
loyalty=member_spec.get("loyalty", 50),
status=member_spec.get("status", "active"),
joined_at=member_spec.get("joined_at"),
source="auto" # 标记为自动生成
)
db.add(member)
members.append(member)
logger.info(
f" ✅ 创建成员关系: {character_name} -> {organization.id} "
f"({member_spec.get('position', '成员')})"
)
except Exception as e:
logger.warning(f" ❌ 创建成员关系失败: {e}")
continue
# 更新组织成员数量
if members:
organization.member_count = (organization.member_count or 0) + len(members)
return members
# 全局实例缓存
_auto_organization_service_instance: Optional[AutoOrganizationService] = None
def get_auto_organization_service(ai_service: AIService) -> AutoOrganizationService:
"""获取自动组织服务实例(单例模式)"""
global _auto_organization_service_instance
if _auto_organization_service_instance is None:
_auto_organization_service_instance = AutoOrganizationService(ai_service)
return _auto_organization_service_instance
+256
View File
@@ -1679,6 +1679,248 @@ class PromptService:
❌ 在描述中使用特殊符号
❌ 引用不存在的角色或组织
❌ 使用职业ID而非职业名称
</constraints>"""
# 自动组织引入 - 预测性分析提示词(RTCO框架)
AUTO_ORGANIZATION_ANALYSIS = """<system>
你是专业的小说世界构建顾问,擅长预测剧情发展对组织/势力的需求。
</system>
<task>
【分析任务】
预测在接下来的{chapter_count}章续写中,根据剧情发展方向和阶段,是否需要引入新的组织或势力。
【重要说明】
这是预测性分析,而非基于已生成内容的事后分析。
组织包括:帮派、门派、公司、政府机构、神秘组织、家族等。
</task>
<project priority="P1">
【项目信息】
书名:{title}
类型:{genre}
主题:{theme}
【世界观】
时间背景:{time_period}
地理位置:{location}
氛围基调:{atmosphere}
</project>
<context priority="P0">
【已有组织】
{existing_organizations}
【已有角色】
{existing_characters}
【已有章节概览】
{all_chapters_brief}
【续写计划】
- 起始章节:第{start_chapter}
- 续写数量:{chapter_count}
- 剧情阶段:{plot_stage}
- 发展方向:{story_direction}
</context>
<analysis_framework priority="P0">
【预测分析维度】
**1. 世界观扩展需求**
根据发展方向,是否需要新的势力或组织来丰富世界观?
**2. 冲突升级需求**
剧情是否需要新的对立势力、竞争组织或神秘集团?
**3. 角色归属需求**
现有角色是否需要加入或对抗某个新组织?
**4. 剧情推动需求**
新组织能否成为推动剧情的关键力量?
**5. 引入时机**
新组织应该在哪个章节出现最合适?
【预测依据】
- 剧情阶段的典型组织需求(如:高潮阶段可能需要强大的敌对势力)
- 故事发展方向的逻辑需要(如:进入新地点需要当地势力)
- 世界观完整性需要(如:权力格局需要多方势力)
- 角色成长需要(如:主角需要加入或创建组织)
</analysis_framework>
<output priority="P0">
【输出格式】
返回纯JSON对象(两种情况之一):
**情况A:需要新组织**
{{
"needs_new_organizations": true,
"reason": "预测分析原因(150-200字),说明为什么即将的剧情需要新组织",
"organization_count": 1,
"organization_specifications": [
{{
"name": "建议的组织名字(可选)",
"organization_description": "组织在剧情中的定位和作用(100-150字)",
"organization_type": "帮派/门派/公司/政府/家族/神秘组织等",
"importance": "high/medium/low",
"appearance_chapter": {start_chapter},
"power_level": 70,
"plot_function": "在剧情中的具体功能",
"location": "组织所在地或活动区域",
"motto": "组织口号或宗旨(可选)",
"initial_members": [
{{
"character_name": "现有角色名(如需加入)",
"position": "职位",
"reason": "为什么加入"
}}
],
"relationship_suggestions": [
{{
"target_organization": "已有组织名",
"relationship_type": "建议的关系类型(盟友/敌对/竞争/合作等)",
"reason": "为什么建立这种关系"
}}
]
}}
]
}}
**情况B:不需要新组织**
{{
"needs_new_organizations": false,
"reason": "现有组织足以支撑即将的剧情发展,说明理由"
}}
</output>
<constraints>
【必须遵守】
✅ 这是预测性分析,面向未来剧情
✅ 考虑世界观的丰富性和完整性
✅ 确保引入必要性,不为引入而引入
✅ 优先考虑组织的长期作用
✅ 组织应该是推动剧情的关键力量
【禁止事项】
❌ 输出markdown标记
❌ 基于已生成内容做事后分析
❌ 为了引入组织而强行引入
❌ 设计一次性功能组织
❌ 创建与现有组织功能重复的组织
</constraints>"""
# 自动组织引入 - 生成提示词(RTCO框架)
AUTO_ORGANIZATION_GENERATION = """<system>
你是专业的世界构建师,擅长根据剧情需求创建完整的组织/势力设定。
</system>
<task>
【生成任务】
为小说生成新组织的完整设定,包括基本信息、组织特性、背景历史和成员结构。
</task>
<project priority="P1">
【项目信息】
书名:{title}
类型:{genre}
主题:{theme}
【世界观】
时间背景:{time_period}
地理位置:{location}
氛围基调:{atmosphere}
世界规则:{rules}
</project>
<context priority="P0">
【已有组织】
{existing_organizations}
【已有角色】
{existing_characters}
【剧情上下文】
{plot_context}
【组织规格要求】
{organization_specification}
</context>
<mcp_context priority="P2">
【MCP工具参考】
{mcp_references}
</mcp_context>
<requirements priority="P0">
【核心要求】
1. 组织必须符合剧情需求和世界观设定
2. 组织要有明确的目的、结构和特色
3. 组织特性、背景要有深度和独特性
4. 外在表现要具体生动
5. 考虑与已有组织的关系和互动
6. 如果需要,可以建议将现有角色加入组织
</requirements>
<output priority="P0">
【输出格式】
返回纯JSON对象:
{{
"name": "组织名称",
"is_organization": true,
"role_type": "supporting",
"organization_type": "组织类型(帮派/门派/公司/政府/家族/神秘组织等)",
"personality": "组织特性的详细描述(150-200字):运作方式、核心理念、行事风格、文化价值观",
"background": "组织背景故事(200-300字):建立历史、发展历程、重要事件、当前地位",
"appearance": "外在表现(100-150字):总部位置、标志性建筑、组织标志、成员着装",
"organization_purpose": "组织目的和宗旨:明确目标、长期愿景、行动准则",
"power_level": 75,
"location": "所在地点:主要活动区域、势力范围",
"motto": "组织格言或口号",
"color": "组织代表颜色",
"traits": ["特征1", "特征2", "特征3"],
"initial_members": [
{{
"character_name": "已存在的角色名称",
"position": "职位名称",
"rank": 8,
"loyalty": 80,
"joined_at": "加入时间(可选)",
"status": "active"
}}
],
"organization_relationships": [
{{
"target_organization_name": "已存在的组织名称",
"relationship_type": "盟友/敌对/竞争/合作/从属等",
"description": "关系的具体描述"
}}
]
}}
【数值范围】
- power_level0-100的整数,表示在世界中的影响力
- rank0到10(职位等级)
- loyalty0到100(成员忠诚度)
</output>
<constraints>
【必须遵守】
✅ 符合剧情需求和世界观设定
✅ 组织要有独特的定位和价值
✅ character_name必须精确匹配【已有角色】
✅ target_organization_name必须精确匹配【已有组织】
✅ 组织能够推动剧情发展
【禁止事项】
❌ 输出markdown标记
❌ 在描述中使用特殊符号
❌ 引用不存在的角色或组织
❌ 创建功能与现有组织重复的组织
❌ 创建对剧情没有实际作用的组织
</constraints>"""
# 职业体系生成提示词 V2(RTCO框架)
@@ -2166,6 +2408,20 @@ class PromptService:
"parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere", "rules",
"existing_characters", "plot_context", "character_specification", "mcp_references"]
},
"AUTO_ORGANIZATION_ANALYSIS": {
"name": "自动组织分析",
"category": "自动组织引入",
"description": "分析新生成的大纲,判断是否需要引入新组织",
"parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere",
"existing_organizations", "existing_characters", "all_chapters_brief", "start_chapter", "chapter_count", "plot_stage", "story_direction"]
},
"AUTO_ORGANIZATION_GENERATION": {
"name": "自动组织生成",
"category": "自动组织引入",
"description": "根据剧情需求自动生成新组织的完整设定",
"parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere", "rules",
"existing_organizations", "existing_characters", "plot_context", "organization_specification", "mcp_references"]
},
"CAREER_SYSTEM_GENERATION": {
"name": "职业体系生成",
"category": "世界构建",
+424 -5
View File
@@ -33,6 +33,37 @@ interface CharacterConfirmationData {
chapter_range: string;
}
// 组织预测数据类型
interface PredictedOrganization {
name?: string;
organization_description: string;
organization_type: string;
importance: string;
appearance_chapter: number;
power_level: number;
plot_function: string;
location?: string;
motto?: string;
initial_members: Array<{
character_name: string;
position: string;
reason?: string;
}>;
relationship_suggestions: Array<{
target_organization: string;
relationship_type: string;
reason?: string;
}>;
}
interface OrganizationConfirmationData {
code: string;
message: string;
predicted_organizations: PredictedOrganization[];
reason: string;
chapter_range: string;
}
const { TextArea } = Input;
export default function Outline() {
@@ -56,6 +87,11 @@ export default function Outline() {
const [pendingGenerateData, setPendingGenerateData] = useState<any>(null);
const [selectedCharacterIndices, setSelectedCharacterIndices] = useState<number[]>([]);
// 组织确认相关状态
const [organizationConfirmData, setOrganizationConfirmData] = useState<OrganizationConfirmationData | null>(null);
const [organizationConfirmVisible, setOrganizationConfirmVisible] = useState(false);
const [selectedOrganizationIndices, setSelectedOrganizationIndices] = useState<number[]>([]);
// 缓存批量展开的规划数据,避免重复AI调用
const [cachedBatchExpansionResponse, setCachedBatchExpansionResponse] = useState<BatchOutlineExpansionResponse | null>(null);
@@ -124,6 +160,15 @@ export default function Outline() {
}
}, [characterConfirmData]);
// 当组织确认数据变化时,初始化选中状态(默认全选)
useEffect(() => {
if (organizationConfirmData) {
setSelectedOrganizationIndices(
organizationConfirmData.predicted_organizations.map((_, idx) => idx)
);
}
}, [organizationConfirmData]);
// 移除事件监听,避免无限循环
// Hook 内部已经更新了 store,不需要再次刷新
@@ -205,6 +250,9 @@ export default function Outline() {
plot_stage?: 'development' | 'climax' | 'ending';
keep_existing?: boolean;
enable_auto_characters?: boolean;
require_character_confirmation?: boolean;
enable_auto_organizations?: boolean;
require_organization_confirmation?: boolean;
}
const handleGenerate = async (values: GenerateFormValues) => {
@@ -237,7 +285,10 @@ export default function Outline() {
mode: values.mode || 'auto',
story_direction: values.story_direction,
plot_stage: values.plot_stage || 'development',
enable_auto_characters: values.enable_auto_characters !== undefined ? values.enable_auto_characters : true
enable_auto_characters: values.enable_auto_characters !== undefined ? values.enable_auto_characters : true,
require_character_confirmation: values.require_character_confirmation !== undefined ? values.require_character_confirmation : true,
enable_auto_organizations: values.enable_auto_organizations !== undefined ? values.enable_auto_organizations : true,
require_organization_confirmation: values.require_organization_confirmation !== undefined ? values.require_organization_confirmation : true
};
// 只有在用户选择了模型时才添加model参数
@@ -281,6 +332,20 @@ export default function Outline() {
setCharacterConfirmData(data);
setCharacterConfirmVisible(true);
},
onOrganizationConfirmation: (data: any) => {
// ✨ 新增:处理组织确认事件
console.log('收到组织确认请求:', data);
// 关闭SSE进度Modal
setSSEModalVisible(false);
setIsGenerating(false);
// 保存待处理的生成数据
setPendingGenerateData(requestData);
// 显示组织确认对话框
setOrganizationConfirmData(data);
setOrganizationConfirmVisible(true);
},
onError: (error: string) => {
// 现在只处理真正的错误
message.error(`生成失败: ${error}`);
@@ -359,6 +424,9 @@ export default function Outline() {
theme: currentProject.theme || '',
model: defaultModel, // 添加默认模型
enable_auto_characters: false, // 默认禁用自动角色引入
require_character_confirmation: true, // 默认需要用户确认
enable_auto_organizations: false, // 默认禁用自动组织引入
require_organization_confirmation: true, // 默认需要用户确认
}}
>
{hasOutlines && (
@@ -467,19 +535,94 @@ export default function Outline() {
<TextArea rows={2} placeholder="其他特殊要求(可选)" />
</Form.Item>
{/* 自动角色引入开关 - 仅在续写模式显示 */}
{isContinue && (
{/* 自动角色和组织引入开关 - 仅在续写模式显示 */}
{isContinue && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 角色引入部分 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24, alignItems: 'flex-start' }}>
<Form.Item
label="智能角色引入"
name="enable_auto_characters"
tooltip="AI会根据剧情发展自动判断是否需要引入新角色,并自动创建角色卡片和建立关系"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
)}
{/* 角色确认选项 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.enable_auto_characters !== currentValues.enable_auto_characters
}
>
{({ getFieldValue }) => {
const enableAutoChars = getFieldValue('enable_auto_characters');
if (!enableAutoChars) return null;
return (
<Form.Item
label="新角色确认"
name="require_character_confirmation"
tooltip="启用后,AI预测到需要新角色时会先让您确认;禁用后,AI预测的角色将直接创建"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
</div>
{/* 组织引入部分 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24, alignItems: 'flex-start' }}>
<Form.Item
label="智能组织引入"
name="enable_auto_organizations"
tooltip="AI会根据剧情发展自动判断是否需要引入新组织/势力,并自动创建设定和建立关系"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
{/* 组织确认选项 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.enable_auto_organizations !== currentValues.enable_auto_organizations
}
>
{({ getFieldValue }) => {
const enableAutoOrgs = getFieldValue('enable_auto_organizations');
if (!enableAutoOrgs) return null;
return (
<Form.Item
label="新组织确认"
name="require_organization_confirmation"
tooltip="启用后,AI预测到需要新组织时会先让您确认;禁用后,AI预测的组织将直接创建"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
</div>
</div>
)}
</>
);
}}
@@ -1640,6 +1783,15 @@ export default function Outline() {
setCharacterConfirmData(null);
// 刷新大纲列表
refreshOutlines();
},
onOrganizationConfirmation: (data: any) => {
// 处理可能的后续组织确认
console.log('收到组织确认请求:', data);
setSSEModalVisible(false);
setIsGenerating(false);
setPendingGenerateData(requestData);
setOrganizationConfirmData(data);
setOrganizationConfirmVisible(true);
}
});
@@ -1666,7 +1818,7 @@ export default function Outline() {
// 显示进度Modal
setSSEProgress(0);
setSSEMessage('跳过角色创建,开始续写大纲...');
setSSEMessage('跳过角色创建,继续生成...');
setSSEModalVisible(true);
// 准备请求数据,禁用自动角色引入
@@ -1687,6 +1839,15 @@ export default function Outline() {
onResult: (data: any) => {
console.log('生成完成,结果:', data);
},
onOrganizationConfirmation: (data: any) => {
// 处理可能的后续组织确认
console.log('收到组织确认请求:', data);
setSSEModalVisible(false);
setIsGenerating(false);
setPendingGenerateData(requestData);
setOrganizationConfirmData(data);
setOrganizationConfirmVisible(true);
},
onError: (error: string) => {
message.error(`生成失败: ${error}`);
setSSEModalVisible(false);
@@ -1714,6 +1875,128 @@ export default function Outline() {
}
};
// 处理组织确认 - 用户同意创建组织
const handleConfirmOrganizations = async (selectedOrganizations: PredictedOrganization[]) => {
if (!pendingGenerateData) {
message.error('生成数据丢失,请重新操作');
return;
}
try {
setOrganizationConfirmVisible(false);
setIsGenerating(true);
// 显示进度Modal
setSSEProgress(0);
setSSEMessage('正在创建确认的组织...');
setSSEModalVisible(true);
// 准备请求数据,添加确认的组织
const requestData = {
...pendingGenerateData,
confirmed_organizations: selectedOrganizations
};
console.log('携带确认组织重新请求:', requestData);
// 重新发起SSE请求
const apiUrl = `/api/outlines/generate-stream`;
const client = new SSEPostClient(apiUrl, requestData, {
onProgress: (msg: string, progress: number) => {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
console.log('生成完成,结果:', data);
},
onError: (error: string) => {
message.error(`生成失败: ${error}`);
setSSEModalVisible(false);
setIsGenerating(false);
},
onComplete: () => {
message.success('大纲生成完成!');
setSSEModalVisible(false);
setIsGenerating(false);
// 清理状态
setPendingGenerateData(null);
setOrganizationConfirmData(null);
// 刷新大纲列表
refreshOutlines();
}
});
client.connect();
} catch (error) {
console.error('确认组织失败:', error);
message.error('操作失败');
setSSEModalVisible(false);
setIsGenerating(false);
}
};
// 处理组织确认 - 用户拒绝创建组织
const handleRejectOrganizations = async () => {
if (!pendingGenerateData) {
message.error('生成数据丢失,请重新操作');
return;
}
try {
setOrganizationConfirmVisible(false);
setIsGenerating(true);
// 显示进度Modal
setSSEProgress(0);
setSSEMessage('跳过组织创建,继续生成...');
setSSEModalVisible(true);
// 准备请求数据,禁用自动组织引入
const requestData = {
...pendingGenerateData,
enable_auto_organizations: false // 禁用自动组织引入
};
console.log('跳过组织创建,重新请求:', requestData);
// 重新发起SSE请求
const apiUrl = `/api/outlines/generate-stream`;
const client = new SSEPostClient(apiUrl, requestData, {
onProgress: (msg: string, progress: number) => {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
console.log('生成完成,结果:', data);
},
onError: (error: string) => {
message.error(`生成失败: ${error}`);
setSSEModalVisible(false);
setIsGenerating(false);
},
onComplete: () => {
message.success('大纲生成完成!');
setSSEModalVisible(false);
setIsGenerating(false);
// 清理状态
setPendingGenerateData(null);
setOrganizationConfirmData(null);
// 刷新大纲列表
refreshOutlines();
}
});
client.connect();
} catch (error) {
console.error('跳过组织创建失败:', error);
message.error('操作失败');
setSSEModalVisible(false);
setIsGenerating(false);
}
};
// 渲染角色确认对话框
const renderCharacterConfirmModal = () => {
if (!characterConfirmData) return null;
@@ -1853,10 +2136,146 @@ export default function Outline() {
);
};
// 渲染组织确认对话框
const renderOrganizationConfirmModal = () => {
if (!organizationConfirmData) return null;
return (
<Modal
title={
<Space>
<ExclamationCircleOutlined style={{ color: 'var(--color-warning)' }} />
<span></span>
</Space>
}
open={organizationConfirmVisible}
onOk={() => {
const selectedOrganizations = organizationConfirmData.predicted_organizations.filter(
(_, idx) => selectedOrganizationIndices.includes(idx)
);
handleConfirmOrganizations(selectedOrganizations);
}}
onCancel={() => {
modalApi.confirm({
title: '确认操作',
content: '是否跳过组织创建,直接续写大纲?',
okText: '跳过组织,继续续写',
cancelText: '返回选择',
onOk: handleRejectOrganizations
});
}}
width={800}
centered
okText={`确认创建选中的 ${selectedOrganizationIndices.length} 个组织`}
cancelText="跳过组织创建"
>
<div>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-warning-bg)', borderRadius: 4, border: '1px solid var(--color-warning-border)' }}>
<div style={{ fontWeight: 500, marginBottom: 8, color: '#d48806' }}>
AI
</div>
<div style={{ color: '#666', marginBottom: 8 }}>
{organizationConfirmData.reason}
</div>
<Tag color="blue">{organizationConfirmData.chapter_range}</Tag>
<Tag color="green">{organizationConfirmData.predicted_organizations.length} </Tag>
</div>
<div style={{ marginBottom: 12 }}>
<Space>
<Button
size="small"
onClick={() => setSelectedOrganizationIndices(
organizationConfirmData.predicted_organizations.map((_, idx) => idx)
)}
>
</Button>
<Button
size="small"
onClick={() => setSelectedOrganizationIndices([])}
>
</Button>
</Space>
</div>
<List
dataSource={organizationConfirmData.predicted_organizations}
renderItem={(org, index) => (
<List.Item
key={index}
style={{
background: selectedOrganizationIndices.includes(index) ? '#f0f5ff' : 'transparent',
padding: 12,
borderRadius: 4,
marginBottom: 8,
border: selectedOrganizationIndices.includes(index) ? '1px solid var(--color-primary)' : '1px solid var(--color-border-secondary)',
cursor: 'pointer'
}}
onClick={() => {
if (selectedOrganizationIndices.includes(index)) {
setSelectedOrganizationIndices(selectedOrganizationIndices.filter(i => i !== index));
} else {
setSelectedOrganizationIndices([...selectedOrganizationIndices, index]);
}
}}
>
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<Space>
<input
type="checkbox"
checked={selectedOrganizationIndices.includes(index)}
onChange={() => { }}
style={{ cursor: 'pointer' }}
/>
<span style={{ fontWeight: 500, fontSize: 16 }}>
{org.name || org.organization_description}
</span>
<Tag color="blue">{org.organization_type}</Tag>
<Tag color="orange">: {org.power_level}</Tag>
</Space>
<Tag>{org.appearance_chapter}</Tag>
</div>
<div style={{ marginBottom: 8, color: '#666' }}>
<strong></strong>{org.plot_function}
</div>
{org.location && (
<div style={{ marginBottom: 8 }}>
<strong></strong>{org.location}
</div>
)}
{org.initial_members && org.initial_members.length > 0 && (
<div style={{ marginBottom: 8 }}>
<strong></strong>
<Space wrap style={{ marginLeft: 8 }}>
{org.initial_members.map((member, idx) => (
<Tag key={idx} color="purple">
{member.character_name} - {member.position}
</Tag>
))}
</Space>
</div>
)}
</div>
</List.Item>
)}
/>
</div>
</Modal>
);
};
return (
<>
{/* 角色确认对话框 */}
{renderCharacterConfirmModal()}
{/* 组织确认对话框 */}
{renderOrganizationConfirmModal()}
{/* 批量展开预览 Modal */}
<Modal
+8
View File
@@ -18,6 +18,7 @@ export interface SSEClientOptions {
onComplete?: () => void;
onConnectionError?: (error: Event) => void;
onCharacterConfirmation?: (data: any) => void; // 新增:角色确认回调
onOrganizationConfirmation?: (data: any) => void; // 新增:组织确认回调
}
export class SSEClient {
@@ -200,6 +201,13 @@ export class SSEPostClient {
}
currentEvent = ''; // 重置事件类型
return; // 暂停流程,等待用户确认
} else if (currentEvent === 'organization_confirmation_required') {
// 处理组织确认事件
if (this.options.onOrganizationConfirmation) {
this.options.onOrganizationConfirmation(data);
}
currentEvent = ''; // 重置事件类型
return; // 暂停流程,等待用户确认
} else {
// 标准消息处理
const message: SSEMessage = data;