From ba7ee591b61e88207a9975a618fd8ecc300c234b Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Mon, 5 Jan 2026 14:27:27 +0800 Subject: [PATCH] =?UTF-8?q?feature:=E6=96=B0=E5=A2=9E=E5=A4=A7=E7=BA=B2?= =?UTF-8?q?=E7=BB=AD=E5=86=99-=E6=99=BA=E8=83=BD=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E5=8A=9F=E8=83=BD=EF=BC=8C=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BB=84=E7=BB=87=E6=88=90=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/outlines.py | 831 ++++++++++++++++-- backend/app/schemas/outline.py | 42 + .../app/services/auto_organization_service.py | 504 +++++++++++ backend/app/services/prompt_service.py | 256 ++++++ frontend/src/pages/Outline.tsx | 429 ++++++++- frontend/src/utils/sseClient.ts | 8 + 6 files changed, 1986 insertions(+), 84 deletions(-) create mode 100644 backend/app/services/auto_organization_service.py diff --git a/backend/app/api/outlines.py b/backend/app/api/outlines.py index 799ddb1..b5da33a 100644 --- a/backend/app/api/outlines.py +++ b/backend/app/api/outlines.py @@ -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} 个新角色,需要用户确认!" + if require_confirmation: + # 🔮 预测模式:仅预测角色,不自动创建,需要用户确认 + yield await SSEResponse.send_progress( + "🔮 【预测模式】检测是否需要新角色(需用户确认)...", + 27 ) - # 🚨 使用专用事件类型通知前端需要角色确认 - 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}章" - } + 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 # ✅ 仅预测不创建 ) - return + + # 检查是否需要新角色 + 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( - "✅ 【预测模式】无需引入新角色,继续生成大纲", - 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=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 diff --git a/backend/app/schemas/outline.py b/backend/app/schemas/outline.py index d56b483..45cbb5c 100644 --- a/backend/app/schemas/outline.py +++ b/backend/app/schemas/outline.py @@ -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,8 +127,16 @@ 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): diff --git a/backend/app/services/auto_organization_service.py b/backend/app/services/auto_organization_service.py new file mode 100644 index 0000000..68eab85 --- /dev/null +++ b/backend/app/services/auto_organization_service.py @@ -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 \ No newline at end of file diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py index 6662716..47d6b0c 100644 --- a/backend/app/services/prompt_service.py +++ b/backend/app/services/prompt_service.py @@ -1679,6 +1679,248 @@ class PromptService: ❌ 在描述中使用特殊符号 ❌ 引用不存在的角色或组织 ❌ 使用职业ID而非职业名称 +""" + + # 自动组织引入 - 预测性分析提示词(RTCO框架) + AUTO_ORGANIZATION_ANALYSIS = """ +你是专业的小说世界构建顾问,擅长预测剧情发展对组织/势力的需求。 + + + +【分析任务】 +预测在接下来的{chapter_count}章续写中,根据剧情发展方向和阶段,是否需要引入新的组织或势力。 + +【重要说明】 +这是预测性分析,而非基于已生成内容的事后分析。 +组织包括:帮派、门派、公司、政府机构、神秘组织、家族等。 + + + +【项目信息】 +书名:{title} +类型:{genre} +主题:{theme} + +【世界观】 +时间背景:{time_period} +地理位置:{location} +氛围基调:{atmosphere} + + + +【已有组织】 +{existing_organizations} + +【已有角色】 +{existing_characters} + +【已有章节概览】 +{all_chapters_brief} + +【续写计划】 +- 起始章节:第{start_chapter}章 +- 续写数量:{chapter_count}章 +- 剧情阶段:{plot_stage} +- 发展方向:{story_direction} + + + +【预测分析维度】 + +**1. 世界观扩展需求** +根据发展方向,是否需要新的势力或组织来丰富世界观? + +**2. 冲突升级需求** +剧情是否需要新的对立势力、竞争组织或神秘集团? + +**3. 角色归属需求** +现有角色是否需要加入或对抗某个新组织? + +**4. 剧情推动需求** +新组织能否成为推动剧情的关键力量? + +**5. 引入时机** +新组织应该在哪个章节出现最合适? + +【预测依据】 +- 剧情阶段的典型组织需求(如:高潮阶段可能需要强大的敌对势力) +- 故事发展方向的逻辑需要(如:进入新地点需要当地势力) +- 世界观完整性需要(如:权力格局需要多方势力) +- 角色成长需要(如:主角需要加入或创建组织) + + + +【输出格式】 +返回纯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": "现有组织足以支撑即将的剧情发展,说明理由" +}} + + + +【必须遵守】 +✅ 这是预测性分析,面向未来剧情 +✅ 考虑世界观的丰富性和完整性 +✅ 确保引入必要性,不为引入而引入 +✅ 优先考虑组织的长期作用 +✅ 组织应该是推动剧情的关键力量 + +【禁止事项】 +❌ 输出markdown标记 +❌ 基于已生成内容做事后分析 +❌ 为了引入组织而强行引入 +❌ 设计一次性功能组织 +❌ 创建与现有组织功能重复的组织 +""" + + # 自动组织引入 - 生成提示词(RTCO框架) + AUTO_ORGANIZATION_GENERATION = """ +你是专业的世界构建师,擅长根据剧情需求创建完整的组织/势力设定。 + + + +【生成任务】 +为小说生成新组织的完整设定,包括基本信息、组织特性、背景历史和成员结构。 + + + +【项目信息】 +书名:{title} +类型:{genre} +主题:{theme} + +【世界观】 +时间背景:{time_period} +地理位置:{location} +氛围基调:{atmosphere} +世界规则:{rules} + + + +【已有组织】 +{existing_organizations} + +【已有角色】 +{existing_characters} + +【剧情上下文】 +{plot_context} + +【组织规格要求】 +{organization_specification} + + + +【MCP工具参考】 +{mcp_references} + + + +【核心要求】 +1. 组织必须符合剧情需求和世界观设定 +2. 组织要有明确的目的、结构和特色 +3. 组织特性、背景要有深度和独特性 +4. 外在表现要具体生动 +5. 考虑与已有组织的关系和互动 +6. 如果需要,可以建议将现有角色加入组织 + + + +【输出格式】 +返回纯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(成员忠诚度) + + + +【必须遵守】 +✅ 符合剧情需求和世界观设定 +✅ 组织要有独特的定位和价值 +✅ character_name必须精确匹配【已有角色】 +✅ target_organization_name必须精确匹配【已有组织】 +✅ 组织能够推动剧情发展 + +【禁止事项】 +❌ 输出markdown标记 +❌ 在描述中使用特殊符号 +❌ 引用不存在的角色或组织 +❌ 创建功能与现有组织重复的组织 +❌ 创建对剧情没有实际作用的组织 """ # 职业体系生成提示词 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": "世界构建", diff --git a/frontend/src/pages/Outline.tsx b/frontend/src/pages/Outline.tsx index 768f8b7..f286444 100644 --- a/frontend/src/pages/Outline.tsx +++ b/frontend/src/pages/Outline.tsx @@ -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(null); const [selectedCharacterIndices, setSelectedCharacterIndices] = useState([]); + // 组织确认相关状态 + const [organizationConfirmData, setOrganizationConfirmData] = useState(null); + const [organizationConfirmVisible, setOrganizationConfirmVisible] = useState(false); + const [selectedOrganizationIndices, setSelectedOrganizationIndices] = useState([]); + // 缓存批量展开的规划数据,避免重复AI调用 const [cachedBatchExpansionResponse, setCachedBatchExpansionResponse] = useState(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() {