feature:新增大纲续写-智能生成组织功能,自动添加组织成员
This commit is contained in:
+756
-83
@@ -25,7 +25,10 @@ from app.schemas.outline import (
|
|||||||
CreateChaptersFromPlansResponse,
|
CreateChaptersFromPlansResponse,
|
||||||
CharacterPredictionRequest,
|
CharacterPredictionRequest,
|
||||||
PredictedCharacter,
|
PredictedCharacter,
|
||||||
CharacterPredictionResponse
|
CharacterPredictionResponse,
|
||||||
|
OrganizationPredictionRequest,
|
||||||
|
PredictedOrganization,
|
||||||
|
OrganizationPredictionResponse
|
||||||
)
|
)
|
||||||
from app.services.ai_service import AIService
|
from app.services.ai_service import AIService
|
||||||
from app.services.prompt_service import prompt_service, PromptService
|
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)}")
|
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(
|
async def _generate_new_outline(
|
||||||
request: OutlineGenerateRequest,
|
request: OutlineGenerateRequest,
|
||||||
@@ -839,14 +972,10 @@ async def _continue_outline(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"⚠️ 【确认模式】创建确认角色失败: {e}", exc_info=True)
|
logger.error(f"⚠️ 【确认模式】创建确认角色失败: {e}", exc_info=True)
|
||||||
else:
|
else:
|
||||||
# 🔮 预测模式:仅预测角色,不自动创建,需要用户确认
|
# 根据 require_character_confirmation 决定处理方式
|
||||||
# 抛出特殊异常,在非SSE接口中会被捕获并返回449状态码
|
|
||||||
# 在SSE接口中会被特殊处理
|
|
||||||
try:
|
try:
|
||||||
from app.services.auto_character_service import get_auto_character_service
|
from app.services.auto_character_service import get_auto_character_service
|
||||||
|
|
||||||
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色")
|
|
||||||
|
|
||||||
# 构建已有章节概览
|
# 构建已有章节概览
|
||||||
all_chapters_brief_for_analysis = ""
|
all_chapters_brief_for_analysis = ""
|
||||||
if len(existing_outlines) > 20:
|
if len(existing_outlines) > 20:
|
||||||
@@ -861,43 +990,83 @@ async def _continue_outline(
|
|||||||
for o in existing_outlines
|
for o in existing_outlines
|
||||||
])
|
])
|
||||||
|
|
||||||
# 调用自动角色服务(✅ 设置 preview_only=True,仅预测不创建)
|
|
||||||
auto_char_service = get_auto_character_service(user_ai_service)
|
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 request.require_character_confirmation:
|
||||||
if auto_result.get("needs_new_characters") and auto_result.get("predicted_characters"):
|
# 🔮 预测模式:仅预测角色,不自动创建,需要用户确认
|
||||||
predicted_count = len(auto_result["predicted_characters"])
|
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色(需用户确认)")
|
||||||
logger.warning(
|
|
||||||
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新角色,需要用户确认!"
|
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(
|
if auto_result.get("needs_new_characters") and auto_result.get("predicted_characters"):
|
||||||
status_code=449, # 449 Retry With
|
predicted_count = len(auto_result["predicted_characters"])
|
||||||
detail={
|
logger.warning(
|
||||||
"code": "CHARACTER_CONFIRMATION_REQUIRED",
|
f"⚠️ 【预测模式】AI预测需要 {predicted_count} 个新角色,需要用户确认!"
|
||||||
"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}章"
|
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:
|
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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@@ -905,6 +1074,212 @@ async def _continue_outline(
|
|||||||
logger.error(f"⚠️ 【方案A】预测性角色引入失败: {e}", exc_info=True)
|
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 = []
|
all_new_outlines = []
|
||||||
current_start_chapter = last_chapter_number + 1
|
current_start_chapter = last_chapter_number + 1
|
||||||
@@ -1755,17 +2130,12 @@ async def continue_outline_generator(
|
|||||||
28
|
28
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 🔮 预测模式:仅预测角色,不自动创建,需要用户确认
|
# 根据 require_character_confirmation 决定处理方式
|
||||||
|
require_confirmation = data.get("require_character_confirmation", True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield await SSEResponse.send_progress(
|
|
||||||
"🔮 【预测模式】检测是否需要新角色(需用户确认)...",
|
|
||||||
27
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.services.auto_character_service import get_auto_character_service
|
from app.services.auto_character_service import get_auto_character_service
|
||||||
|
|
||||||
logger.info(f"🔮 【预测模式】在生成大纲前预测是否需要新角色")
|
|
||||||
|
|
||||||
# 构建已有章节概览
|
# 构建已有章节概览
|
||||||
all_chapters_brief_for_analysis = ""
|
all_chapters_brief_for_analysis = ""
|
||||||
if len(existing_outlines) > 20:
|
if len(existing_outlines) > 20:
|
||||||
@@ -1780,47 +2150,106 @@ async def continue_outline_generator(
|
|||||||
for o in existing_outlines
|
for o in existing_outlines
|
||||||
])
|
])
|
||||||
|
|
||||||
# 调用自动角色服务(✅ 设置 preview_only=True)
|
|
||||||
auto_char_service = get_auto_character_service(user_ai_service)
|
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 require_confirmation:
|
||||||
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(
|
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:
|
except Exception as e:
|
||||||
logger.error(f"⚠️ 【方案A】预测性角色引入失败: {e}", exc_info=True)
|
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 = []
|
all_new_outlines = []
|
||||||
current_start_chapter = last_chapter_number + 1
|
current_start_chapter = last_chapter_number + 1
|
||||||
|
|||||||
@@ -35,6 +35,40 @@ class CharacterPredictionResponse(BaseModel):
|
|||||||
predicted_characters: List[PredictedCharacter]
|
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):
|
class OutlineBase(BaseModel):
|
||||||
"""大纲基础模型"""
|
"""大纲基础模型"""
|
||||||
title: str = Field(..., description="章节标题")
|
title: str = Field(..., description="章节标题")
|
||||||
@@ -93,9 +127,17 @@ class OutlineGenerateRequest(BaseModel):
|
|||||||
plot_stage: str = Field("development", description="情节阶段: development(发展), climax(高潮), ending(结局)")
|
plot_stage: str = Field("development", description="情节阶段: development(发展), climax(高潮), ending(结局)")
|
||||||
keep_existing: bool = Field(False, description="是否保留现有大纲(续写时)")
|
keep_existing: bool = Field(False, description="是否保留现有大纲(续写时)")
|
||||||
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索情节设计参考)")
|
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索情节设计参考)")
|
||||||
|
|
||||||
|
# 自动角色引入相关参数
|
||||||
enable_auto_characters: bool = Field(True, description="是否启用自动角色引入(根据剧情推进自动创建新角色)")
|
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="用户确认的角色列表(跳过预测直接创建)")
|
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):
|
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
|
||||||
@@ -1679,6 +1679,248 @@ class PromptService:
|
|||||||
❌ 在描述中使用特殊符号
|
❌ 在描述中使用特殊符号
|
||||||
❌ 引用不存在的角色或组织
|
❌ 引用不存在的角色或组织
|
||||||
❌ 使用职业ID而非职业名称
|
❌ 使用职业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_level:0-100的整数,表示在世界中的影响力
|
||||||
|
- rank:0到10(职位等级)
|
||||||
|
- loyalty:0到100(成员忠诚度)
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<constraints>
|
||||||
|
【必须遵守】
|
||||||
|
✅ 符合剧情需求和世界观设定
|
||||||
|
✅ 组织要有独特的定位和价值
|
||||||
|
✅ character_name必须精确匹配【已有角色】
|
||||||
|
✅ target_organization_name必须精确匹配【已有组织】
|
||||||
|
✅ 组织能够推动剧情发展
|
||||||
|
|
||||||
|
【禁止事项】
|
||||||
|
❌ 输出markdown标记
|
||||||
|
❌ 在描述中使用特殊符号
|
||||||
|
❌ 引用不存在的角色或组织
|
||||||
|
❌ 创建功能与现有组织重复的组织
|
||||||
|
❌ 创建对剧情没有实际作用的组织
|
||||||
</constraints>"""
|
</constraints>"""
|
||||||
|
|
||||||
# 职业体系生成提示词 V2(RTCO框架)
|
# 职业体系生成提示词 V2(RTCO框架)
|
||||||
@@ -2166,6 +2408,20 @@ class PromptService:
|
|||||||
"parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere", "rules",
|
"parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere", "rules",
|
||||||
"existing_characters", "plot_context", "character_specification", "mcp_references"]
|
"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": {
|
"CAREER_SYSTEM_GENERATION": {
|
||||||
"name": "职业体系生成",
|
"name": "职业体系生成",
|
||||||
"category": "世界构建",
|
"category": "世界构建",
|
||||||
|
|||||||
@@ -33,6 +33,37 @@ interface CharacterConfirmationData {
|
|||||||
chapter_range: string;
|
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;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
export default function Outline() {
|
export default function Outline() {
|
||||||
@@ -56,6 +87,11 @@ export default function Outline() {
|
|||||||
const [pendingGenerateData, setPendingGenerateData] = useState<any>(null);
|
const [pendingGenerateData, setPendingGenerateData] = useState<any>(null);
|
||||||
const [selectedCharacterIndices, setSelectedCharacterIndices] = useState<number[]>([]);
|
const [selectedCharacterIndices, setSelectedCharacterIndices] = useState<number[]>([]);
|
||||||
|
|
||||||
|
// 组织确认相关状态
|
||||||
|
const [organizationConfirmData, setOrganizationConfirmData] = useState<OrganizationConfirmationData | null>(null);
|
||||||
|
const [organizationConfirmVisible, setOrganizationConfirmVisible] = useState(false);
|
||||||
|
const [selectedOrganizationIndices, setSelectedOrganizationIndices] = useState<number[]>([]);
|
||||||
|
|
||||||
// 缓存批量展开的规划数据,避免重复AI调用
|
// 缓存批量展开的规划数据,避免重复AI调用
|
||||||
const [cachedBatchExpansionResponse, setCachedBatchExpansionResponse] = useState<BatchOutlineExpansionResponse | null>(null);
|
const [cachedBatchExpansionResponse, setCachedBatchExpansionResponse] = useState<BatchOutlineExpansionResponse | null>(null);
|
||||||
|
|
||||||
@@ -124,6 +160,15 @@ export default function Outline() {
|
|||||||
}
|
}
|
||||||
}, [characterConfirmData]);
|
}, [characterConfirmData]);
|
||||||
|
|
||||||
|
// 当组织确认数据变化时,初始化选中状态(默认全选)
|
||||||
|
useEffect(() => {
|
||||||
|
if (organizationConfirmData) {
|
||||||
|
setSelectedOrganizationIndices(
|
||||||
|
organizationConfirmData.predicted_organizations.map((_, idx) => idx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [organizationConfirmData]);
|
||||||
|
|
||||||
// 移除事件监听,避免无限循环
|
// 移除事件监听,避免无限循环
|
||||||
// Hook 内部已经更新了 store,不需要再次刷新
|
// Hook 内部已经更新了 store,不需要再次刷新
|
||||||
|
|
||||||
@@ -205,6 +250,9 @@ export default function Outline() {
|
|||||||
plot_stage?: 'development' | 'climax' | 'ending';
|
plot_stage?: 'development' | 'climax' | 'ending';
|
||||||
keep_existing?: boolean;
|
keep_existing?: boolean;
|
||||||
enable_auto_characters?: boolean;
|
enable_auto_characters?: boolean;
|
||||||
|
require_character_confirmation?: boolean;
|
||||||
|
enable_auto_organizations?: boolean;
|
||||||
|
require_organization_confirmation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGenerate = async (values: GenerateFormValues) => {
|
const handleGenerate = async (values: GenerateFormValues) => {
|
||||||
@@ -237,7 +285,10 @@ export default function Outline() {
|
|||||||
mode: values.mode || 'auto',
|
mode: values.mode || 'auto',
|
||||||
story_direction: values.story_direction,
|
story_direction: values.story_direction,
|
||||||
plot_stage: values.plot_stage || 'development',
|
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参数
|
// 只有在用户选择了模型时才添加model参数
|
||||||
@@ -281,6 +332,20 @@ export default function Outline() {
|
|||||||
setCharacterConfirmData(data);
|
setCharacterConfirmData(data);
|
||||||
setCharacterConfirmVisible(true);
|
setCharacterConfirmVisible(true);
|
||||||
},
|
},
|
||||||
|
onOrganizationConfirmation: (data: any) => {
|
||||||
|
// ✨ 新增:处理组织确认事件
|
||||||
|
console.log('收到组织确认请求:', data);
|
||||||
|
// 关闭SSE进度Modal
|
||||||
|
setSSEModalVisible(false);
|
||||||
|
setIsGenerating(false);
|
||||||
|
|
||||||
|
// 保存待处理的生成数据
|
||||||
|
setPendingGenerateData(requestData);
|
||||||
|
|
||||||
|
// 显示组织确认对话框
|
||||||
|
setOrganizationConfirmData(data);
|
||||||
|
setOrganizationConfirmVisible(true);
|
||||||
|
},
|
||||||
onError: (error: string) => {
|
onError: (error: string) => {
|
||||||
// 现在只处理真正的错误
|
// 现在只处理真正的错误
|
||||||
message.error(`生成失败: ${error}`);
|
message.error(`生成失败: ${error}`);
|
||||||
@@ -359,6 +424,9 @@ export default function Outline() {
|
|||||||
theme: currentProject.theme || '',
|
theme: currentProject.theme || '',
|
||||||
model: defaultModel, // 添加默认模型
|
model: defaultModel, // 添加默认模型
|
||||||
enable_auto_characters: false, // 默认禁用自动角色引入
|
enable_auto_characters: false, // 默认禁用自动角色引入
|
||||||
|
require_character_confirmation: true, // 默认需要用户确认
|
||||||
|
enable_auto_organizations: false, // 默认禁用自动组织引入
|
||||||
|
require_organization_confirmation: true, // 默认需要用户确认
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasOutlines && (
|
{hasOutlines && (
|
||||||
@@ -467,19 +535,94 @@ export default function Outline() {
|
|||||||
<TextArea rows={2} placeholder="其他特殊要求(可选)" />
|
<TextArea rows={2} placeholder="其他特殊要求(可选)" />
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
label="智能角色引入"
|
label="智能角色引入"
|
||||||
name="enable_auto_characters"
|
name="enable_auto_characters"
|
||||||
tooltip="AI会根据剧情发展自动判断是否需要引入新角色,并自动创建角色卡片和建立关系"
|
tooltip="AI会根据剧情发展自动判断是否需要引入新角色,并自动创建角色卡片和建立关系"
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
>
|
>
|
||||||
<Radio.Group buttonStyle="solid">
|
<Radio.Group buttonStyle="solid">
|
||||||
<Radio.Button value={true}>启用</Radio.Button>
|
<Radio.Button value={true}>启用</Radio.Button>
|
||||||
<Radio.Button value={false}>禁用</Radio.Button>
|
<Radio.Button value={false}>禁用</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</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);
|
setCharacterConfirmData(null);
|
||||||
// 刷新大纲列表
|
// 刷新大纲列表
|
||||||
refreshOutlines();
|
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
|
// 显示进度Modal
|
||||||
setSSEProgress(0);
|
setSSEProgress(0);
|
||||||
setSSEMessage('跳过角色创建,开始续写大纲...');
|
setSSEMessage('跳过角色创建,继续生成...');
|
||||||
setSSEModalVisible(true);
|
setSSEModalVisible(true);
|
||||||
|
|
||||||
// 准备请求数据,禁用自动角色引入
|
// 准备请求数据,禁用自动角色引入
|
||||||
@@ -1687,6 +1839,15 @@ export default function Outline() {
|
|||||||
onResult: (data: any) => {
|
onResult: (data: any) => {
|
||||||
console.log('生成完成,结果:', data);
|
console.log('生成完成,结果:', data);
|
||||||
},
|
},
|
||||||
|
onOrganizationConfirmation: (data: any) => {
|
||||||
|
// 处理可能的后续组织确认
|
||||||
|
console.log('收到组织确认请求:', data);
|
||||||
|
setSSEModalVisible(false);
|
||||||
|
setIsGenerating(false);
|
||||||
|
setPendingGenerateData(requestData);
|
||||||
|
setOrganizationConfirmData(data);
|
||||||
|
setOrganizationConfirmVisible(true);
|
||||||
|
},
|
||||||
onError: (error: string) => {
|
onError: (error: string) => {
|
||||||
message.error(`生成失败: ${error}`);
|
message.error(`生成失败: ${error}`);
|
||||||
setSSEModalVisible(false);
|
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 = () => {
|
const renderCharacterConfirmModal = () => {
|
||||||
if (!characterConfirmData) return null;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 角色确认对话框 */}
|
{/* 角色确认对话框 */}
|
||||||
{renderCharacterConfirmModal()}
|
{renderCharacterConfirmModal()}
|
||||||
|
{/* 组织确认对话框 */}
|
||||||
|
{renderOrganizationConfirmModal()}
|
||||||
|
|
||||||
{/* 批量展开预览 Modal */}
|
{/* 批量展开预览 Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface SSEClientOptions {
|
|||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
onConnectionError?: (error: Event) => void;
|
onConnectionError?: (error: Event) => void;
|
||||||
onCharacterConfirmation?: (data: any) => void; // 新增:角色确认回调
|
onCharacterConfirmation?: (data: any) => void; // 新增:角色确认回调
|
||||||
|
onOrganizationConfirmation?: (data: any) => void; // 新增:组织确认回调
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SSEClient {
|
export class SSEClient {
|
||||||
@@ -200,6 +201,13 @@ export class SSEPostClient {
|
|||||||
}
|
}
|
||||||
currentEvent = ''; // 重置事件类型
|
currentEvent = ''; // 重置事件类型
|
||||||
return; // 暂停流程,等待用户确认
|
return; // 暂停流程,等待用户确认
|
||||||
|
} else if (currentEvent === 'organization_confirmation_required') {
|
||||||
|
// 处理组织确认事件
|
||||||
|
if (this.options.onOrganizationConfirmation) {
|
||||||
|
this.options.onOrganizationConfirmation(data);
|
||||||
|
}
|
||||||
|
currentEvent = ''; // 重置事件类型
|
||||||
|
return; // 暂停流程,等待用户确认
|
||||||
} else {
|
} else {
|
||||||
// 标准消息处理
|
// 标准消息处理
|
||||||
const message: SSEMessage = data;
|
const message: SSEMessage = data;
|
||||||
|
|||||||
Reference in New Issue
Block a user