"""角色状态更新服务 - 根据章节分析结果自动更新角色心理状态、关系和组织成员""" from typing import Dict, Any, List, Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, or_, and_ from app.models.character import Character from app.models.relationship import CharacterRelationship, Organization, OrganizationMember from app.logger import get_logger import uuid logger = get_logger(__name__) # 亲密度调整关键词映射 INTIMACY_ADJUSTMENTS = { # 正向变化 "改善": +10, "加深": +15, "信任": +10, "亲近": +15, "友好": +10, "认可": +10, "合作": +5, "和解": +20, "喜欢": +15, "爱": +20, "尊敬": +10, "感激": +10, "好转": +10, "增进": +10, "亲密": +15, "忠诚": +10, # 负向变化 "恶化": -10, "疏远": -15, "背叛": -30, "敌对": -25, "矛盾": -10, "冲突": -15, "怀疑": -10, "不信任": -15, "厌恶": -20, "仇恨": -25, "决裂": -30, "猜忌": -10, "紧张": -5, "破裂": -25, "反目": -25, "嫉妒": -10, # 特殊变化 "初识": 0, "相遇": 0, "结盟": +10, "分离": -5, } class CharacterStateUpdateService: """角色状态更新服务 - 根据章节分析结果自动更新角色心理状态和关系""" @staticmethod async def update_from_analysis( db: AsyncSession, project_id: str, character_states: List[Dict[str, Any]], chapter_id: str, chapter_number: int ) -> Dict[str, Any]: """ 根据章节分析结果更新角色状态和关系 Args: db: 数据库会话 project_id: 项目ID character_states: 角色状态变化列表(来自PlotAnalysis) chapter_id: 章节ID chapter_number: 章节编号 Returns: 更新结果字典 """ if not character_states: logger.info("📋 角色状态列表为空,跳过状态和关系更新") return { "state_updated_count": 0, "relationship_created_count": 0, "relationship_updated_count": 0, "org_updated_count": 0, "changes": [] } result = { "state_updated_count": 0, "relationship_created_count": 0, "relationship_updated_count": 0, "org_updated_count": 0, "changes": [] } logger.info(f"🔍 开始分析第{chapter_number}章的角色状态、关系和组织变化...") # 预加载项目所有角色(含组织,按名称索引,减少重复查询) all_characters_result = await db.execute( select(Character).where(Character.project_id == project_id) ) all_characters = all_characters_result.scalars().all() # 非组织角色按名称索引 characters_by_name: Dict[str, Character] = { c.name: c for c in all_characters if not c.is_organization } # 预加载组织信息(按组织角色名称索引) orgs_result = await db.execute( select(Organization).where(Organization.project_id == project_id) ) all_orgs = orgs_result.scalars().all() # 构建 character_id -> name 的反向映射 char_id_to_name: Dict[str, str] = {c.id: c.name for c in all_characters} # 组织名称 -> Organization 映射 org_by_name: Dict[str, Organization] = {} for org in all_orgs: org_char_name = char_id_to_name.get(org.character_id) if org_char_name: org_by_name[org_char_name] = org for char_state in character_states: char_name = char_state.get('character_name') if not char_name: continue character = characters_by_name.get(char_name) if not character: logger.warning(f" ⚠️ 角色不存在: {char_name},跳过状态更新") continue # 0. 检查角色存活状态变化 survival_status = char_state.get('survival_status') if survival_status and survival_status in ('deceased', 'missing', 'retired'): await CharacterStateUpdateService._update_survival_status( db=db, project_id=project_id, character=character, new_status=survival_status, chapter_number=chapter_number, key_event=char_state.get('key_event', ''), changes=result["changes"] ) result["state_updated_count"] += 1 # 死亡/失踪后不再更新心理状态等,直接跳到下一个角色 continue # 1. 更新心理状态 state_updated = await CharacterStateUpdateService._update_psychological_state( character=character, char_state=char_state, chapter_number=chapter_number, changes=result["changes"] ) if state_updated: result["state_updated_count"] += 1 # 2. 更新关系 relationship_changes = char_state.get('relationship_changes', {}) if relationship_changes and isinstance(relationship_changes, dict): created, updated = await CharacterStateUpdateService._update_relationships( db=db, project_id=project_id, character=character, relationship_changes=relationship_changes, chapter_number=chapter_number, chapter_id=chapter_id, characters_by_name=characters_by_name, changes=result["changes"] ) result["relationship_created_count"] += created result["relationship_updated_count"] += updated # 3. 更新组织成员关系 organization_changes = char_state.get('organization_changes', []) if organization_changes and isinstance(organization_changes, list): org_updated = await CharacterStateUpdateService._update_organization_memberships( db=db, project_id=project_id, character=character, organization_changes=organization_changes, chapter_number=chapter_number, org_by_name=org_by_name, changes=result["changes"] ) result["org_updated_count"] += org_updated # 提交所有更改 total_changes = ( result["state_updated_count"] + result["relationship_created_count"] + result["relationship_updated_count"] + result["org_updated_count"] ) if total_changes > 0: await db.commit() logger.info( f"✅ 角色状态更新完成: " f"心理状态{result['state_updated_count']}个, " f"新建关系{result['relationship_created_count']}个, " f"更新关系{result['relationship_updated_count']}个, " f"组织变动{result['org_updated_count']}个" ) else: logger.info("📋 本章没有角色状态或关系变化") return result @staticmethod async def _update_survival_status( db: AsyncSession, project_id: str, character: Character, new_status: str, chapter_number: int, key_event: str, changes: List[str] ) -> None: """ 更新角色存活状态及级联影响 死亡/失踪时: - 更新 Character.status 和 status_changed_chapter - 更新所有活跃关系状态为 past - 更新所有组织成员身份为 deceased/retired """ STATUS_DESC = { 'deceased': '死亡', 'missing': '失踪', 'retired': '退场' } status_desc = STATUS_DESC.get(new_status, new_status) # 防止低章节覆盖 if (character.status_changed_chapter is not None and chapter_number < character.status_changed_chapter): logger.info(f" ⏭️ {character.name} 状态已在第{character.status_changed_chapter}章变更,跳过") return old_status = character.status or 'active' character.status = new_status character.status_changed_chapter = chapter_number character.current_state = f"{status_desc}(第{chapter_number}章)" character.state_updated_chapter = chapter_number event_desc = f":{key_event[:50]}" if key_event else "" changes.append(f"💀 {character.name} {status_desc}{event_desc}") logger.info(f" 💀 {character.name} 状态: {old_status} → {new_status}") # 级联更新:所有活跃关系变为 past rels_result = await db.execute( select(CharacterRelationship).where( and_( CharacterRelationship.project_id == project_id, CharacterRelationship.status == 'active', or_( CharacterRelationship.character_from_id == character.id, CharacterRelationship.character_to_id == character.id ) ) ) ) active_rels = rels_result.scalars().all() for rel in active_rels: rel.status = 'past' rel.ended_at = f"第{chapter_number}章" if active_rels: logger.info(f" 📋 {character.name} {status_desc},{len(active_rels)}条关系标记为past") # 级联更新:所有组织成员身份 member_status = 'deceased' if new_status == 'deceased' else 'retired' members_result = await db.execute( select(OrganizationMember).where( and_( OrganizationMember.character_id == character.id, OrganizationMember.status == 'active' ) ) ) active_members = members_result.scalars().all() for member in active_members: member.status = member_status member.left_at = f"第{chapter_number}章" member.notes = ( f"{member.notes or ''}\n[第{chapter_number}章] 角色{status_desc}" ).strip() if active_members: logger.info(f" 📋 {character.name} {status_desc},{len(active_members)}个组织身份标记为{member_status}") @staticmethod async def _update_psychological_state( character: Character, char_state: Dict[str, Any], chapter_number: int, changes: List[str] ) -> bool: """ 更新角色心理状态 Args: character: 角色对象 char_state: 角色状态数据 chapter_number: 章节号 changes: 变更日志列表 Returns: 是否有实际更新 """ state_after = char_state.get('state_after') if not state_after: return False # 章节号校验:防止低章节分析覆盖高章节状态 if (character.state_updated_chapter is not None and chapter_number < character.state_updated_chapter): logger.info( f" ⏭️ {character.name} 的心理状态已被第{character.state_updated_chapter}章更新," f"跳过第{chapter_number}章的更新" ) return False old_state = character.current_state character.current_state = state_after character.state_updated_chapter = chapter_number state_before = char_state.get('state_before', '未知') psychological_change = char_state.get('psychological_change', '') change_desc = f"👤 {character.name} 心理状态: {state_before} → {state_after}" if psychological_change: change_desc += f" ({psychological_change[:50]})" changes.append(change_desc) logger.info(f" ✅ {character.name} 心理状态更新: {state_before} → {state_after}") return True @staticmethod async def _update_relationships( db: AsyncSession, project_id: str, character: Character, relationship_changes: Dict[str, Any], chapter_number: int, chapter_id: str, characters_by_name: Dict[str, Character], changes: List[str] ) -> tuple[int, int]: """ 更新角色关系 关系名称直接使用AI分析返回的变化描述,不强制映射到预定义类型。 relationship_type_id 仅在能明确匹配时作为辅助设置。 Args: db: 数据库会话 project_id: 项目ID character: 角色A relationship_changes: 关系变化字典 {"角色名": "变化描述" 或 {"change": ..., ...}} chapter_number: 章节号 chapter_id: 章节ID characters_by_name: 角色名到角色对象的映射 changes: 变更日志列表 Returns: (新建数量, 更新数量) """ created_count = 0 updated_count = 0 for target_name, change_info in relationship_changes.items(): try: # 解析变化信息(支持两种格式) if isinstance(change_info, str): change_desc = change_info elif isinstance(change_info, dict): change_desc = change_info.get('change', str(change_info)) else: change_desc = str(change_info) if not change_desc: continue # 查找目标角色 target_character = characters_by_name.get(target_name) if not target_character: logger.warning(f" ⚠️ 关系目标角色不存在: {target_name},跳过") continue # 避免自身关系 if character.id == target_character.id: continue # 查询是否已存在关系(A→B 或 B→A) existing_rel_result = await db.execute( select(CharacterRelationship).where( and_( CharacterRelationship.project_id == project_id, or_( and_( CharacterRelationship.character_from_id == character.id, CharacterRelationship.character_to_id == target_character.id ), and_( CharacterRelationship.character_from_id == target_character.id, CharacterRelationship.character_to_id == character.id ) ) ) ) ) existing_rel = existing_rel_result.scalar_one_or_none() # 计算亲密度调整 intimacy_delta = CharacterStateUpdateService._calculate_intimacy_delta(change_desc) if existing_rel: # 更新已有关系 # 更新关系名称为最新的变化描述(以AI分析结果为准) existing_rel.relationship_name = change_desc # 追加变更记录到描述 chapter_note = f"[第{chapter_number}章] {change_desc}" if existing_rel.description: existing_rel.description = f"{existing_rel.description}\n{chapter_note}" else: existing_rel.description = chapter_note # 调整亲密度 if intimacy_delta != 0: old_intimacy = existing_rel.intimacy_level or 0 new_intimacy = max(-100, min(100, old_intimacy + intimacy_delta)) existing_rel.intimacy_level = new_intimacy logger.info( f" 📊 {character.name}↔{target_name} 亲密度: " f"{old_intimacy} → {new_intimacy} ({'+' if intimacy_delta > 0 else ''}{intimacy_delta})" ) updated_count += 1 changes.append( f"🔄 {character.name}↔{target_name} 关系更新: {change_desc}" ) logger.info(f" ✅ 更新关系: {character.name}↔{target_name} - {change_desc}") else: # 创建新关系 — 关系名称直接使用AI的变化描述 # 设定初始亲密度 initial_intimacy = max(-100, min(100, 50 + intimacy_delta)) new_relationship = CharacterRelationship( id=str(uuid.uuid4()), project_id=project_id, character_from_id=character.id, character_to_id=target_character.id, relationship_type_id=None, # 不强制关联预定义类型 relationship_name=change_desc, # 直接使用AI分析返回的描述 intimacy_level=initial_intimacy, status="active", description=f"[第{chapter_number}章] {change_desc}", source="analysis" ) db.add(new_relationship) created_count += 1 changes.append( f"✨ {character.name}→{target_name} 新关系: {change_desc}" ) logger.info( f" ✅ 创建关系: {character.name}→{target_name} " f"({change_desc}, 亲密度:{initial_intimacy})" ) except Exception as item_error: logger.error( f" ❌ 更新 {character.name}→{target_name} 关系失败: {str(item_error)}" ) return created_count, updated_count @staticmethod async def _update_organization_memberships( db: AsyncSession, project_id: str, character: Character, organization_changes: List[Dict[str, Any]], chapter_number: int, org_by_name: Dict[str, Organization], changes: List[str] ) -> int: """ 更新角色的组织成员关系 Args: db: 数据库会话 project_id: 项目ID character: 角色对象 organization_changes: 组织变动列表 chapter_number: 章节号 org_by_name: 组织名称到Organization对象的映射 changes: 变更日志列表 Returns: 更新数量 """ updated_count = 0 # 忠诚度变化关键词映射 LOYALTY_ADJUSTMENTS = { "提升": +10, "增强": +10, "坚定": +15, "忠心": +15, "动摇": -15, "怀疑": -10, "不满": -10, "降低": -10, "背叛": -50, "叛变": -50, "反感": -20, "失望": -15, } for org_change in organization_changes: try: org_name = org_change.get('organization_name') change_type = org_change.get('change_type', '') new_position = org_change.get('new_position') loyalty_change_desc = org_change.get('loyalty_change', '') description = org_change.get('description', '') if not org_name: continue # 查找组织 organization = org_by_name.get(org_name) if not organization: logger.warning(f" ⚠️ 组织不存在: {org_name},跳过组织变动更新") continue # 查找已有成员关系 existing_member_result = await db.execute( select(OrganizationMember).where( and_( OrganizationMember.organization_id == organization.id, OrganizationMember.character_id == character.id ) ) ) existing_member = existing_member_result.scalar_one_or_none() # 计算忠诚度变化 loyalty_delta = 0 if loyalty_change_desc: for keyword, adjustment in LOYALTY_ADJUSTMENTS.items(): if keyword in loyalty_change_desc: loyalty_delta += adjustment loyalty_delta = max(-50, min(50, loyalty_delta)) if change_type == 'joined': # 加入组织 if existing_member: # 已存在,可能是重新加入 if existing_member.status != 'active': existing_member.status = 'active' existing_member.left_at = None if new_position: existing_member.position = new_position existing_member.notes = ( f"{existing_member.notes or ''}\n[第{chapter_number}章] 重新加入: {description}" ).strip() updated_count += 1 changes.append(f"🏛️ {character.name} 重新加入 {org_name}") logger.info(f" ✅ {character.name} 重新加入 {org_name}") else: # 创建新成员关系 new_member = OrganizationMember( id=str(uuid.uuid4()), organization_id=organization.id, character_id=character.id, position=new_position or '成员', rank=0, loyalty=max(0, min(100, 50 + loyalty_delta)), status='active', joined_at=f"第{chapter_number}章", source='analysis', notes=f"[第{chapter_number}章] {description}" if description else None ) db.add(new_member) organization.member_count = (organization.member_count or 0) + 1 updated_count += 1 changes.append(f"🏛️ {character.name} 加入 {org_name}({new_position or '成员'})") logger.info(f" ✅ {character.name} 加入 {org_name} 为 {new_position or '成员'}") elif change_type in ('left', 'expelled', 'betrayed'): # 离开/被开除/叛变 if existing_member and existing_member.status == 'active': status_map = { 'left': 'retired', 'expelled': 'expelled', 'betrayed': 'expelled' } existing_member.status = status_map.get(change_type, 'retired') existing_member.left_at = f"第{chapter_number}章" if loyalty_delta != 0: existing_member.loyalty = max(0, min(100, (existing_member.loyalty or 50) + loyalty_delta)) existing_member.notes = ( f"{existing_member.notes or ''}\n[第{chapter_number}章] {change_type}: {description}" ).strip() updated_count += 1 type_desc = {'left': '离开', 'expelled': '被开除', 'betrayed': '叛变'} changes.append(f"🏛️ {character.name} {type_desc.get(change_type, change_type)} {org_name}") logger.info(f" ✅ {character.name} {type_desc.get(change_type, change_type)} {org_name}") elif change_type == 'promoted': # 晋升 if existing_member: old_position = existing_member.position if new_position: existing_member.position = new_position existing_member.rank = (existing_member.rank or 0) + 1 if loyalty_delta != 0: existing_member.loyalty = max(0, min(100, (existing_member.loyalty or 50) + loyalty_delta)) elif loyalty_delta == 0: # 晋升默认提升忠诚度 existing_member.loyalty = max(0, min(100, (existing_member.loyalty or 50) + 5)) existing_member.notes = ( f"{existing_member.notes or ''}\n[第{chapter_number}章] 晋升: {old_position} → {new_position or '更高职位'}: {description}" ).strip() updated_count += 1 changes.append(f"🏛️ {character.name} 在 {org_name} 晋升: {old_position} → {new_position or '更高职位'}") logger.info(f" ✅ {character.name} 在 {org_name} 晋升为 {new_position or '更高职位'}") else: logger.warning(f" ⚠️ {character.name} 不是 {org_name} 的成员,无法晋升") elif change_type == 'demoted': # 降级 if existing_member: old_position = existing_member.position if new_position: existing_member.position = new_position existing_member.rank = max(0, (existing_member.rank or 0) - 1) if loyalty_delta != 0: existing_member.loyalty = max(0, min(100, (existing_member.loyalty or 50) + loyalty_delta)) elif loyalty_delta == 0: # 降级默认降低忠诚度 existing_member.loyalty = max(0, min(100, (existing_member.loyalty or 50) - 5)) existing_member.notes = ( f"{existing_member.notes or ''}\n[第{chapter_number}章] 降级: {old_position} → {new_position or '更低职位'}: {description}" ).strip() updated_count += 1 changes.append(f"🏛️ {character.name} 在 {org_name} 降级: {old_position} → {new_position or '更低职位'}") logger.info(f" ✅ {character.name} 在 {org_name} 降级为 {new_position or '更低职位'}") else: logger.warning(f" ⚠️ {character.name} 不是 {org_name} 的成员,无法降级") else: # 其他类型的变化(如忠诚度变化等) if existing_member and loyalty_delta != 0: old_loyalty = existing_member.loyalty or 50 existing_member.loyalty = max(0, min(100, old_loyalty + loyalty_delta)) existing_member.notes = ( f"{existing_member.notes or ''}\n[第{chapter_number}章] {change_type}: {description}" ).strip() updated_count += 1 changes.append( f"🏛️ {character.name} 在 {org_name} 忠诚度变化: " f"{old_loyalty} → {existing_member.loyalty}" ) logger.info( f" ✅ {character.name} 在 {org_name} 忠诚度: " f"{old_loyalty} → {existing_member.loyalty}" ) except Exception as item_error: logger.error( f" ❌ 更新 {character.name} 的组织 {org_change.get('organization_name', '未知')} 变动失败: {str(item_error)}" ) return updated_count @staticmethod async def update_organization_states( db: AsyncSession, project_id: str, organization_states: List[Dict[str, Any]], chapter_number: int ) -> Dict[str, Any]: """ 根据章节分析结果更新组织自身状态(势力等级、据点、宗旨等) Args: db: 数据库会话 project_id: 项目ID organization_states: 组织状态变化列表(来自分析结果顶级字段) chapter_number: 章节编号 Returns: 更新结果字典 """ if not organization_states: return {"updated_count": 0, "changes": []} result = {"updated_count": 0, "changes": []} logger.info(f"🏛️ 开始更新第{chapter_number}章的组织自身状态...") # 预加载项目所有组织角色 all_chars_result = await db.execute( select(Character).where( Character.project_id == project_id, Character.is_organization == True ) ) org_chars = all_chars_result.scalars().all() org_char_by_name: Dict[str, Character] = {c.name: c for c in org_chars} # 预加载组织详情 char_ids = [c.id for c in org_chars] if not char_ids: logger.info("🏛️ 项目中无组织,跳过组织状态更新") return result orgs_result = await db.execute( select(Organization).where(Organization.character_id.in_(char_ids)) ) all_orgs = orgs_result.scalars().all() org_by_char_id: Dict[str, Organization] = {org.character_id: org for org in all_orgs} for org_state in organization_states: try: org_name = org_state.get('organization_name') if not org_name: continue org_char = org_char_by_name.get(org_name) if not org_char: logger.warning(f" ⚠️ 组织不存在: {org_name},跳过状态更新") continue organization = org_by_char_id.get(org_char.id) if not organization: logger.warning(f" ⚠️ 组织 {org_name} 无详情记录,跳过状态更新") continue updated = False change_parts = [] # 检查组织是否被覆灭 is_destroyed = org_state.get('is_destroyed', False) if is_destroyed: # 组织覆灭:级联处理 org_char.status = 'destroyed' org_char.status_changed_chapter = chapter_number org_char.current_state = f"覆灭(第{chapter_number}章)" org_char.state_updated_chapter = chapter_number organization.power_level = 0 # 所有活跃成员标记为retired members_result = await db.execute( select(OrganizationMember).where( and_( OrganizationMember.organization_id == organization.id, OrganizationMember.status == 'active' ) ) ) active_members = members_result.scalars().all() for member in active_members: member.status = 'retired' member.left_at = f"第{chapter_number}章" member.notes = ( f"{member.notes or ''}\n[第{chapter_number}章] 组织覆灭" ).strip() key_event = org_state.get('key_event', '') event_desc = f":{key_event[:40]}" if key_event else "" result["updated_count"] += 1 change_summary = f"💀 {org_name} 覆灭{event_desc},{len(active_members)}名成员受影响" result["changes"].append(change_summary) logger.info(f" 💀 {change_summary}") continue # 覆灭后不再更新其他属性 # 势力等级变化 power_change = org_state.get('power_change', 0) if power_change and isinstance(power_change, (int, float)): old_power = organization.power_level or 50 new_power = max(0, min(100, old_power + int(power_change))) if new_power != old_power: organization.power_level = new_power change_parts.append(f"势力:{old_power}→{new_power}") updated = True # 据点变化 new_location = org_state.get('new_location') if new_location and isinstance(new_location, str): old_location = organization.location or '未设定' organization.location = new_location change_parts.append(f"据点:{old_location}→{new_location}") updated = True # 宗旨/目标变化 new_purpose = org_state.get('new_purpose') if new_purpose and isinstance(new_purpose, str): old_purpose = (org_char.organization_purpose or '未设定')[:30] org_char.organization_purpose = new_purpose change_parts.append(f"宗旨变更") updated = True # 状态描述 -> 更新到 Character 的 current_state status_desc = org_state.get('status_description') if status_desc and isinstance(status_desc, str): org_char.current_state = status_desc org_char.state_updated_chapter = chapter_number if not change_parts: # 如果只有状态描述没有其他变化 change_parts.append(f"状态:{status_desc[:30]}") updated = True if updated: result["updated_count"] += 1 key_event = org_state.get('key_event', '') change_summary = f"🏛️ {org_name} 状态变化: {', '.join(change_parts)}" if key_event: change_summary += f" (因:{key_event[:40]})" result["changes"].append(change_summary) logger.info(f" ✅ {change_summary}") except Exception as item_error: logger.error( f" ❌ 更新组织 {org_state.get('organization_name', '未知')} 状态失败: {str(item_error)}" ) if result["updated_count"] > 0: await db.commit() logger.info(f"✅ 组织状态更新完成: {result['updated_count']}个组织") return result @staticmethod def _calculate_intimacy_delta(change_desc: str) -> int: """ 根据变化描述计算亲密度调整值 Args: change_desc: 关系变化描述文本 Returns: 亲密度调整值 """ delta = 0 matched = False for keyword, adjustment in INTIMACY_ADJUSTMENTS.items(): if keyword in change_desc: delta += adjustment matched = True # 限制单次调整幅度 if matched: delta = max(-30, min(30, delta)) return delta