update:优化章节上下文构建系统和大纲查找逻辑,优先使用outline_id直接关联

This commit is contained in:
xiamuceer-j
2026-02-12 12:40:12 +08:00
parent 26be04c32a
commit 76cf695c85
2 changed files with 858 additions and 470 deletions
+361 -318
View File
@@ -20,6 +20,7 @@ from app.models.project import Project
from app.models.outline import Outline from app.models.outline import Outline
from app.models.character import Character from app.models.character import Character
from app.models.career import Career, CharacterCareer from app.models.career import Career, CharacterCareer
from app.models.relationship import CharacterRelationship, Organization, OrganizationMember
from app.models.generation_history import GenerationHistory from app.models.generation_history import GenerationHistory
from app.models.writing_style import WritingStyle from app.models.writing_style import WritingStyle
from app.models.analysis_task import AnalysisTask from app.models.analysis_task import AnalysisTask
@@ -476,199 +477,6 @@ async def check_prerequisites(db: AsyncSession, chapter: Chapter) -> tuple[bool,
return True, "", previous_chapters return True, "", previous_chapters
async def build_smart_chapter_context(
db: AsyncSession,
project_id: str,
current_chapter_number: int,
user_id: str
) -> dict:
"""
智能构建章节生成上下文(支持海量章节场景)
策略:
1. 故事骨架:每50章采样1章(标题+摘要)
2. 相关历史:通过chapter_summary记忆语义检索15个最相关章节
3. 近期概要:最近30章的简要摘要(200字/章)
4. 最近完整:最近3章的完整内容
Args:
db: 数据库会话
project_id: 项目ID
current_chapter_number: 当前章节序号
user_id: 用户ID
Returns:
包含各部分上下文的字典
"""
context_parts = {
'story_skeleton': '', # 故事骨架
'relevant_history': '', # 相关历史章节
'recent_summary': '', # 近期概要
'recent_full': '', # 最近完整内容
'stats': {} # 统计信息
}
try:
# 1. 获取所有已完成的前置章节(只取ID和序号)
all_chapters_result = await db.execute(
select(Chapter.id, Chapter.chapter_number, Chapter.title)
.where(Chapter.project_id == project_id)
.where(Chapter.chapter_number < current_chapter_number)
.where(Chapter.content != None)
.where(Chapter.content != "")
.order_by(Chapter.chapter_number)
)
all_chapters_info = all_chapters_result.all()
total_previous = len(all_chapters_info)
if total_previous == 0:
logger.info("📚 这是第一章,无需构建前置上下文")
return context_parts
logger.info(f"📚 开始构建智能上下文:共{total_previous}章前置内容")
# 2. 构建故事骨架(每50章采样)
skeleton_chapters = []
if total_previous > 50:
sample_interval = 50
skeleton_indices = list(range(0, total_previous, sample_interval))
for idx in skeleton_indices:
chapter_info = all_chapters_info[idx]
# 获取章节摘要(优先从chapter_summary记忆获取)
summary_result = await db.execute(
select(StoryMemory.content)
.where(StoryMemory.project_id == project_id)
.where(StoryMemory.chapter_id == chapter_info.id)
.where(StoryMemory.memory_type == 'chapter_summary')
.limit(1)
)
summary_row = summary_result.scalar_one_or_none()
summary = summary_row if summary_row else "(无摘要)"
skeleton_chapters.append({
'number': chapter_info.chapter_number,
'title': chapter_info.title,
'summary': summary
})
context_parts['story_skeleton'] = "【故事骨架】\n" + "\n".join([
f"{ch['number']}章《{ch['title']}》:{ch['summary']}"
for ch in skeleton_chapters
])
logger.info(f" ✅ 故事骨架:采样{len(skeleton_chapters)}章(每50章1个)")
# 3. 语义检索相关历史章节(使用chapter_summary记忆)
# 获取当前章节的大纲作为查询
current_outline_result = await db.execute(
select(Outline.content)
.where(Outline.project_id == project_id)
.where(Outline.order_index == current_chapter_number)
)
current_outline = current_outline_result.scalar_one_or_none()
if current_outline and total_previous > 3:
# 使用记忆服务进行语义检索
relevant_memories = await memory_service.search_memories(
user_id=user_id,
project_id=project_id,
query=current_outline,
memory_types=['chapter_summary'],
limit=15, # 检索15个最相关的章节
min_importance=0.0 # 不过滤重要性,依赖语义相关度
)
if relevant_memories:
relevant_chapters_text = []
for mem in relevant_memories:
# 获取章节信息
chapter_result = await db.execute(
select(Chapter.chapter_number, Chapter.title)
.where(Chapter.id == mem['metadata'].get('chapter_id'))
)
chapter_info = chapter_result.first()
if chapter_info:
relevant_chapters_text.append(
f"{chapter_info.chapter_number}章《{chapter_info.title}》:{mem['content']} "
f"(相关度:{mem['similarity']:.2f})"
)
context_parts['relevant_history'] = "【相关历史章节】\n" + "\n".join(relevant_chapters_text)
logger.info(f" ✅ 相关历史:语义检索到{len(relevant_chapters_text)}")
# 4. 近期概要(最近30章,每章200字摘要)
recent_summary_count = min(30, total_previous)
recent_for_summary = all_chapters_info[-recent_summary_count:] if total_previous > 3 else []
if recent_for_summary and len(recent_for_summary) > 3: # 至少要有3章才做摘要
recent_summaries = []
for chapter_info in recent_for_summary[:-3]: # 排除最后3章(它们会完整展示)
# 优先获取chapter_summary记忆
summary_result = await db.execute(
select(StoryMemory.content)
.where(StoryMemory.project_id == project_id)
.where(StoryMemory.chapter_id == chapter_info.id)
.where(StoryMemory.memory_type == 'chapter_summary')
.limit(1)
)
summary = summary_result.scalar_one_or_none()
if summary:
recent_summaries.append(
f"{chapter_info.chapter_number}章《{chapter_info.title}》:{summary}"
)
if recent_summaries:
context_parts['recent_summary'] = "【近期章节概要】\n" + "\n".join(recent_summaries)
logger.info(f" ✅ 近期概要:{len(recent_summaries)}章摘要")
# 5. 最近完整内容(最近3章)
recent_full_count = min(3, total_previous)
recent_full_chapters = all_chapters_info[-recent_full_count:]
# 获取完整内容
recent_full_texts = []
for chapter_info in recent_full_chapters:
chapter_result = await db.execute(
select(Chapter.content)
.where(Chapter.id == chapter_info.id)
)
content = chapter_result.scalar_one_or_none()
if content:
recent_full_texts.append(
f"=== 第{chapter_info.chapter_number}章:{chapter_info.title} ===\n{content}"
)
context_parts['recent_full'] = "【最近章节完整内容】\n" + "\n\n".join(recent_full_texts)
logger.info(f" ✅ 最近完整:{len(recent_full_texts)}章全文")
# 6. 统计信息
context_parts['stats'] = {
'total_previous': total_previous,
'skeleton_samples': len(skeleton_chapters),
'relevant_history': len(relevant_memories) if current_outline and total_previous > 3 else 0,
'recent_summaries': len(recent_summaries) if recent_for_summary and len(recent_for_summary) > 3 else 0,
'recent_full': len(recent_full_texts)
}
# 计算总长度
total_length = sum([
len(context_parts['story_skeleton']),
len(context_parts['relevant_history']),
len(context_parts['recent_summary']),
len(context_parts['recent_full'])
])
context_parts['stats']['total_length'] = total_length
logger.info(f"📊 智能上下文构建完成:总长度 {total_length} 字符")
except Exception as e:
logger.error(f"❌ 构建智能上下文失败: {str(e)}", exc_info=True)
return context_parts
async def build_characters_info_with_careers( async def build_characters_info_with_careers(
db: AsyncSession, db: AsyncSession,
project_id: str, project_id: str,
@@ -710,12 +518,76 @@ async def build_characters_info_with_careers(
character_ids = [c.id for c in characters] character_ids = [c.id for c in characters]
if not character_ids: if not character_ids:
return '暂无角色信息' return '暂无角色信息'
# 构建全局角色名称映射(用于关系显示)
all_chars_result = await db.execute(
select(Character.id, Character.name).where(Character.project_id == project_id)
)
all_char_name_map = {row.id: row.name for row in all_chars_result.all()}
character_careers_result = await db.execute( character_careers_result = await db.execute(
select(CharacterCareer).where(CharacterCareer.character_id.in_(character_ids)) select(CharacterCareer).where(CharacterCareer.character_id.in_(character_ids))
) )
character_careers = character_careers_result.scalars().all() character_careers = character_careers_result.scalars().all()
# 获取所有角色的关系(一次性查询)
from sqlalchemy import or_
rels_result = await db.execute(
select(CharacterRelationship).where(
CharacterRelationship.project_id == project_id,
or_(
CharacterRelationship.character_from_id.in_(character_ids),
CharacterRelationship.character_to_id.in_(character_ids)
)
)
)
all_relationships = rels_result.scalars().all()
# 按角色ID分组关系
char_rels_map: dict[str, list] = {cid: [] for cid in character_ids}
for r in all_relationships:
if r.character_from_id in char_rels_map:
char_rels_map[r.character_from_id].append(r)
if r.character_to_id in char_rels_map:
char_rels_map[r.character_to_id].append(r)
# 获取所有组织及其成员关系(一次性查询)
orgs_result = await db.execute(
select(Organization).where(Organization.project_id == project_id)
)
all_orgs = orgs_result.scalars().all()
# 构建组织ID到组织名称的映射(通过关联的Character记录)
org_name_map = {} # org_id -> org_name
char_id_to_org = {} # character_id -> Organization(用于组织实体补充详情)
for org in all_orgs:
org_name_map[org.id] = all_char_name_map.get(org.character_id, '未知组织')
char_id_to_org[org.character_id] = org
# 获取所有组织的成员关系(一次性查询)
org_ids = [org.id for org in all_orgs]
all_org_members = []
if org_ids:
all_org_members_result = await db.execute(
select(OrganizationMember).where(
OrganizationMember.organization_id.in_(org_ids)
)
)
all_org_members = all_org_members_result.scalars().all()
# 按组织ID分组成员(用于组织实体显示成员列表)
org_members_map: dict[str, list] = {oid: [] for oid in org_ids}
for m in all_org_members:
if m.organization_id in org_members_map:
org_members_map[m.organization_id].append(m)
# 获取涉及当前非组织角色的成员关系
non_org_char_ids = [c.id for c in characters if not c.is_organization]
char_org_map: dict[str, list] = {cid: [] for cid in non_org_char_ids}
for m in all_org_members:
if m.character_id in char_org_map:
char_org_map[m.character_id].append(m)
# 构建角色ID到职业信息的映射 # 构建角色ID到职业信息的映射
char_career_map = {} char_career_map = {}
for cc in character_careers: for cc in character_careers:
@@ -741,9 +613,52 @@ async def build_characters_info_with_careers(
# 构建角色信息字符串 # 构建角色信息字符串
characters_info_parts = [] characters_info_parts = []
for c in characters: for c in characters:
# 基本信息 # 基本信息(含存活状态标记)
entity_type = '组织' if c.is_organization else '角色' entity_type = '组织' if c.is_organization else '角色'
base_info = f"- {c.name}({entity_type}, {c.role_type})" status_marker = ""
char_status = getattr(c, 'status', None) or 'active'
if char_status != 'active':
STATUS_MARKERS = {
'deceased': '💀已死亡',
'missing': '❓已失踪',
'retired': '📤已退场',
'destroyed': '💀已覆灭'
}
status_marker = f" [{STATUS_MARKERS.get(char_status, char_status)}]"
base_info = f"- {c.name}({entity_type}, {c.role_type}){status_marker}"
# 组织实体:补充组织详情
org_detail_str = ""
if c.is_organization and c.id in char_id_to_org:
org = char_id_to_org[c.id]
org_detail_parts = []
if c.organization_type:
org_detail_parts.append(f"类型:{c.organization_type}")
if c.organization_purpose:
purpose_preview = c.organization_purpose[:60] if len(c.organization_purpose) > 60 else c.organization_purpose
org_detail_parts.append(f"宗旨:{purpose_preview}")
if org.power_level is not None:
org_detail_parts.append(f"势力等级:{org.power_level}")
if org.location:
org_detail_parts.append(f"据点:{org.location}")
if org.motto:
org_detail_parts.append(f"口号:{org.motto}")
if org.member_count:
org_detail_parts.append(f"成员数:{org.member_count}")
if org_detail_parts:
org_detail_str = f" | {', '.join(org_detail_parts)}"
# 显示组织的核心成员列表(最多5个)
if org.id in org_members_map and org_members_map[org.id]:
member_parts = []
for m in sorted(org_members_map[org.id], key=lambda x: -(x.rank or 0))[:5]:
m_name = all_char_name_map.get(m.character_id, '未知')
m_desc = f"{m_name}({m.position})"
if m.status and m.status != 'active':
m_desc += f"[{m.status}]"
member_parts.append(m_desc)
if member_parts:
org_detail_str += f" | 成员: {', '.join(member_parts)}"
# 职业信息 # 职业信息
career_info_str = "" career_info_str = ""
@@ -764,6 +679,55 @@ async def build_characters_info_with_careers(
sub_list.append(f"{sub['name']}({stage_desc})") sub_list.append(f"{sub['name']}({stage_desc})")
career_info_str += f" | 副职业: {', '.join(sub_list)}" career_info_str += f" | 副职业: {', '.join(sub_list)}"
# 心理状态(由章节分析自动更新)
state_str = ""
if c.current_state:
state_preview = c.current_state[:50] if len(c.current_state) > 50 else c.current_state
state_str = f" | 当前状态: {state_preview}"
if c.state_updated_chapter:
state_str += f"(第{c.state_updated_chapter}章)"
# 组织成员信息(非组织角色才显示所属组织)
org_str = ""
if not c.is_organization and c.id in char_org_map and char_org_map[c.id]:
org_parts = []
for m in char_org_map[c.id][:3]: # 最多显示3个组织
o_name = org_name_map.get(m.organization_id, '未知组织')
o_desc = f"{o_name}({m.position})"
if m.loyalty is not None and m.loyalty != 50:
o_desc += f"[忠诚度:{m.loyalty}]"
if m.status and m.status != 'active':
o_desc += f"[{m.status}]"
org_parts.append(o_desc)
if org_parts:
org_str = f" | 所属组织: {', '.join(org_parts)}"
# 关系信息
rel_str = ""
if c.id in char_rels_map and char_rels_map[c.id]:
rel_parts = []
seen_pairs = set() # 避免重复显示同一对关系
for r in char_rels_map[c.id][:5]: # 最多显示5个关系
# 确定对方角色名
if r.character_from_id == c.id:
other_name = all_char_name_map.get(r.character_to_id, '未知')
else:
other_name = all_char_name_map.get(r.character_from_id, '未知')
pair_key = tuple(sorted([c.id, r.character_from_id if r.character_from_id != c.id else r.character_to_id]))
if pair_key in seen_pairs:
continue
seen_pairs.add(pair_key)
rel_name = r.relationship_name or '关联'
rel_desc = f"{other_name}({rel_name})"
if r.intimacy_level is not None and r.intimacy_level != 50:
rel_desc += f"[亲密度:{r.intimacy_level}]"
rel_parts.append(rel_desc)
if rel_parts:
rel_str = f" | 关系: {', '.join(rel_parts)}"
# 性格描述 # 性格描述
personality_str = "" personality_str = ""
if c.personality: if c.personality:
@@ -771,7 +735,7 @@ async def build_characters_info_with_careers(
personality_str = f": {personality_preview}" personality_str = f": {personality_preview}"
# 组合完整信息 # 组合完整信息
full_info = base_info + career_info_str + personality_str full_info = base_info + org_detail_str + career_info_str + state_str + org_str + rel_str + personality_str
characters_info_parts.append(full_info) characters_info_parts.append(full_info)
return "\n".join(characters_info_parts) return "\n".join(characters_info_parts)
@@ -903,6 +867,63 @@ async def analyze_chapter_background(
) )
logger.info(f"📋 后台分析 - 已获取{len(existing_foreshadows)}个已埋入伏笔用于匹配(含智能回收标记)") logger.info(f"📋 后台分析 - 已获取{len(existing_foreshadows)}个已埋入伏笔用于匹配(含智能回收标记)")
# 获取项目角色信息(根据大纲/展开规划筛选本章相关角色)
filter_character_names = None
# 1-N模式:从expansion_plan中提取character_focus
if chapter.expansion_plan:
try:
plan = json.loads(chapter.expansion_plan)
focus_names = plan.get('character_focus', [])
if focus_names:
filter_character_names = focus_names
logger.info(f"📋 从expansion_plan提取角色焦点: {filter_character_names}")
except (json.JSONDecodeError, Exception):
pass
# 1-1模式:从outline.structure中提取characters
if not filter_character_names and chapter.outline_id:
try:
outline_result = await db_session.execute(
select(Outline).where(Outline.id == chapter.outline_id)
)
chapter_outline = outline_result.scalar_one_or_none()
if chapter_outline and chapter_outline.structure:
structure = json.loads(chapter_outline.structure)
raw_characters = structure.get('characters', [])
if raw_characters:
filter_character_names = [
c['name'] if isinstance(c, dict) else c
for c in raw_characters
]
logger.info(f"📋 从outline.structure提取角色: {filter_character_names}")
except (json.JSONDecodeError, Exception):
pass
# 查询角色(根据筛选名单或全部)
characters_query = select(Character).where(Character.project_id == project_id)
if filter_character_names:
characters_query = characters_query.where(Character.name.in_(filter_character_names))
characters_result = await db_session.execute(characters_query)
project_characters = characters_result.scalars().all()
# 如果筛选后无角色,降级为全部角色
if not project_characters and filter_character_names:
logger.warning(f"⚠️ 筛选后无匹配角色,降级为全部角色")
characters_result = await db_session.execute(
select(Character).where(Character.project_id == project_id)
)
project_characters = characters_result.scalars().all()
filter_character_names = None
characters_info = await build_characters_info_with_careers(
db=db_session,
project_id=project_id,
characters=project_characters,
filter_character_names=filter_character_names
)
logger.info(f"📋 后台分析 - 已获取{len(project_characters)}个角色信息用于分析")
# 定义重试回调函数,用于在重试时更新任务状态 # 定义重试回调函数,用于在重试时更新任务状态
async def on_retry_callback(attempt: int, max_retries: int, wait_time: int, error_reason: str): async def on_retry_callback(attempt: int, max_retries: int, wait_time: int, error_reason: str):
"""重试时更新任务状态,让前端能感知到重试进度""" """重试时更新任务状态,让前端能感知到重试进度"""
@@ -924,7 +945,7 @@ async def analyze_chapter_background(
except Exception as callback_error: except Exception as callback_error:
logger.warning(f"⚠️ 更新重试状态失败: {callback_error}") logger.warning(f"⚠️ 更新重试状态失败: {callback_error}")
# 3. 使用PlotAnalyzer分析章节(传入已有伏笔列表和重试回调) # 3. 使用PlotAnalyzer分析章节(传入已有伏笔列表、角色信息和重试回调)
analyzer = PlotAnalyzer(ai_service) analyzer = PlotAnalyzer(ai_service)
analysis_result = await analyzer.analyze_chapter( analysis_result = await analyzer.analyze_chapter(
chapter_number=chapter.chapter_number, chapter_number=chapter.chapter_number,
@@ -932,7 +953,8 @@ async def analyze_chapter_background(
content=chapter.content, content=chapter.content,
word_count=chapter.word_count or len(chapter.content), word_count=chapter.word_count or len(chapter.content),
existing_foreshadows=existing_foreshadows, existing_foreshadows=existing_foreshadows,
on_retry=on_retry_callback on_retry=on_retry_callback,
characters_info=characters_info
) )
if not analysis_result: if not analysis_result:
@@ -1131,6 +1153,72 @@ async def analyze_chapter_background(
else: else:
logger.debug("📋 分析结果中无角色状态信息,跳过职业更新") logger.debug("📋 分析结果中无角色状态信息,跳过职业更新")
# 👤 更新角色心理状态和关系(根据分析结果)
if analysis_result.get('character_states'):
try:
from app.services.character_state_update_service import CharacterStateUpdateService
logger.info(f"👤 开始根据分析结果更新角色状态、关系和组织成员...")
async with write_lock:
state_update_result = await CharacterStateUpdateService.update_from_analysis(
db=db_session,
project_id=project_id,
character_states=analysis_result.get('character_states', []),
chapter_id=chapter_id,
chapter_number=chapter.chapter_number
)
total_state_changes = (
state_update_result['state_updated_count'] +
state_update_result['relationship_created_count'] +
state_update_result['relationship_updated_count'] +
state_update_result.get('org_updated_count', 0)
)
if total_state_changes > 0:
logger.info(
f"✅ 角色状态更新: 心理状态{state_update_result['state_updated_count']}个, "
f"新建关系{state_update_result['relationship_created_count']}个, "
f"更新关系{state_update_result['relationship_updated_count']}个, "
f"组织变动{state_update_result.get('org_updated_count', 0)}"
)
if state_update_result['changes']:
for change in state_update_result['changes'][:8]:
logger.info(f" - {change}")
else:
logger.info("️ 本章节无角色状态、关系或组织变化")
except Exception as state_error:
# 角色状态更新失败不应影响整个分析流程
logger.error(f"⚠️ 更新角色状态、关系和组织失败: {str(state_error)}", exc_info=True)
# 🏛️ 更新组织自身状态(根据分析结果)
if analysis_result.get('organization_states'):
try:
from app.services.character_state_update_service import CharacterStateUpdateService
logger.info(f"🏛️ 开始根据分析结果更新组织自身状态...")
async with write_lock:
org_state_result = await CharacterStateUpdateService.update_organization_states(
db=db_session,
project_id=project_id,
organization_states=analysis_result.get('organization_states', []),
chapter_number=chapter.chapter_number
)
if org_state_result['updated_count'] > 0:
logger.info(
f"✅ 组织状态更新: {org_state_result['updated_count']}个组织"
)
if org_state_result['changes']:
for change in org_state_result['changes'][:5]:
logger.info(f" - {change}")
else:
logger.info("️ 本章节无组织自身状态变化")
except Exception as org_state_error:
# 组织状态更新失败不应影响整个分析流程
logger.error(f"⚠️ 更新组织自身状态失败: {str(org_state_error)}", exc_info=True)
# 🔮 自动更新伏笔状态(根据分析结果) # 🔮 自动更新伏笔状态(根据分析结果)
if analysis_result.get('foreshadows'): if analysis_result.get('foreshadows'):
try: try:
@@ -1319,13 +1407,21 @@ async def generate_chapter_content_stream(
outline_mode = project.outline_mode if project else 'one-to-many' outline_mode = project.outline_mode if project else 'one-to-many'
logger.info(f"📋 项目大纲模式: {outline_mode}") logger.info(f"📋 项目大纲模式: {outline_mode}")
# 获取对应的大纲 # 获取对应的大纲(优先使用 chapter.outline_id 直接关联)
outline_result = await db_session.execute( if current_chapter.outline_id:
select(Outline) outline_result = await db_session.execute(
.where(Outline.project_id == current_chapter.project_id) select(Outline)
.where(Outline.order_index == current_chapter.chapter_number) .where(Outline.id == current_chapter.outline_id)
.execution_options(populate_existing=True) .execution_options(populate_existing=True)
) )
else:
# 回退到按序号查找
outline_result = await db_session.execute(
select(Outline)
.where(Outline.project_id == current_chapter.project_id)
.where(Outline.order_index == current_chapter.chapter_number)
.execution_options(populate_existing=True)
)
outline = outline_result.scalar_one_or_none() outline = outline_result.scalar_one_or_none()
# 获取写作风格 # 获取写作风格
@@ -1429,6 +1525,7 @@ async def generate_chapter_content_stream(
genre=project.genre or '未设定', genre=project.genre or '未设定',
narrative_perspective=chapter_perspective, narrative_perspective=chapter_perspective,
previous_chapter_content=chapter_context.continuation_point, previous_chapter_content=chapter_context.continuation_point,
previous_chapter_summary=chapter_context.previous_chapter_summary or '(无上一章摘要)',
characters_info=chapter_context.chapter_characters or '暂无角色信息', characters_info=chapter_context.chapter_characters or '暂无角色信息',
chapter_careers=chapter_context.chapter_careers or '暂无职业信息', chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
@@ -1476,9 +1573,10 @@ async def generate_chapter_content_stream(
genre=project.genre or '未设定', genre=project.genre or '未设定',
narrative_perspective=chapter_perspective, narrative_perspective=chapter_perspective,
characters_info=chapter_context.chapter_characters or '暂无角色信息', characters_info=chapter_context.chapter_characters or '暂无角色信息',
chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
previous_chapter_summary=previous_summary, previous_chapter_summary=previous_summary,
story_skeleton=chapter_context.story_skeleton or '', recent_chapters_context=chapter_context.recent_chapters_context or '',
relevant_memories=chapter_context.relevant_memories or '' relevant_memories=chapter_context.relevant_memories or ''
) )
logger.debug(f"创建第{current_chapter.chapter_number}章提示词: {base_prompt}") logger.debug(f"创建第{current_chapter.chapter_number}章提示词: {base_prompt}")
@@ -1495,7 +1593,10 @@ async def generate_chapter_content_stream(
target_word_count=target_word_count, target_word_count=target_word_count,
genre=project.genre or '未设定', genre=project.genre or '未设定',
narrative_perspective=chapter_perspective, narrative_perspective=chapter_perspective,
characters_info=chapter_context.chapter_characters or '暂无角色信息' characters_info=chapter_context.chapter_characters or '暂无角色信息',
chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
relevant_memories=chapter_context.relevant_memories or '暂无相关记忆'
) )
logger.debug(f"创建第一章提示词: {base_prompt}") logger.debug(f"创建第一章提示词: {base_prompt}")
@@ -2658,49 +2759,19 @@ async def generate_single_chapter_for_batch(
outline_mode = project.outline_mode if project else 'one-to-many' outline_mode = project.outline_mode if project else 'one-to-many'
logger.info(f"📋 批量生成 - 项目大纲模式: {outline_mode}") logger.info(f"📋 批量生成 - 项目大纲模式: {outline_mode}")
# 获取对应的大纲 # 获取对应的大纲(优先使用 chapter.outline_id 直接关联)
outline_result = await db_session.execute( if chapter.outline_id:
select(Outline) outline_result = await db_session.execute(
.where(Outline.project_id == chapter.project_id) select(Outline).where(Outline.id == chapter.outline_id)
.where(Outline.order_index == chapter.chapter_number) )
)
outline = outline_result.scalar_one_or_none()
# 获取角色信息(包含职业信息)
characters_result = await db_session.execute(
select(Character).where(Character.project_id == chapter.project_id)
)
characters = characters_result.scalars().all()
# 📝 根据大纲模式智能筛选相关角色(批量生成)
filter_character_names = None
if outline_mode == 'one-to-one':
# 1-1模式:从outline.structure中提取characters字段
if outline and outline.structure:
try:
structure = json.loads(outline.structure)
filter_character_names = structure.get('characters', [])
if filter_character_names:
logger.info(f"📋 批量生成 - 1-1模式:从structure提取角色列表 {filter_character_names}")
except json.JSONDecodeError:
logger.warning(f"⚠️ 批量生成 - outline.structure解析失败,使用全部角色")
else: else:
# 1-n模式:从chapter.expansion_plan中提取character_focus字段 # 回退到按序号查找
if chapter.expansion_plan: outline_result = await db_session.execute(
try: select(Outline)
plan = json.loads(chapter.expansion_plan) .where(Outline.project_id == chapter.project_id)
filter_character_names = plan.get('character_focus', []) .where(Outline.order_index == chapter.chapter_number)
if filter_character_names: )
logger.info(f"📋 批量生成 - 1-n模式:从expansion_plan提取角色焦点 {filter_character_names}") outline = outline_result.scalar_one_or_none()
except json.JSONDecodeError:
logger.warning(f"⚠️ 批量生成 - expansion_plan解析失败,使用全部角色")
characters_info = await build_characters_info_with_careers(
db=db_session,
project_id=chapter.project_id,
characters=characters,
filter_character_names=filter_character_names
)
# 获取写作风格 # 获取写作风格
style_content = "" style_content = ""
@@ -2753,52 +2824,8 @@ async def generate_single_chapter_for_batch(
logger.info(f" - 相关记忆: {chapter_context.context_stats.get('memory_count', 0)}") logger.info(f" - 相关记忆: {chapter_context.context_stats.get('memory_count', 0)}")
logger.info(f" - 总上下文长度: {chapter_context.context_stats.get('total_length', 0)} 字符") logger.info(f" - 总上下文长度: {chapter_context.context_stats.get('total_length', 0)} 字符")
# 📋 根据大纲模式构建差异化的章节大纲上下文
chapter_outline_content = ""
if outline_mode == 'one-to-one':
# 一对一模式:使用大纲的 content
chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲'
logger.info(f"✏️ 批量生成 - 一对一模式:使用大纲内容")
else:
# 一对多模式:优先使用 expansion_plan 的详细规划
if chapter.expansion_plan:
try:
plan = json.loads(chapter.expansion_plan)
chapter_outline_content = f"""【本章详细规划】
剧情摘要:{plan.get('plot_summary', '')}
关键事件:
{chr(10).join(f'- {event}' for event in plan.get('key_events', []))}
角色焦点:{', '.join(plan.get('character_focus', []))}
情感基调:{plan.get('emotional_tone', '未设定')}
叙事目标:{plan.get('narrative_goal', '未设定')}
冲突类型:{plan.get('conflict_type', '未设定')}"""
# 可选:附加章节 summary
if chapter.summary and chapter.summary.strip():
chapter_outline_content += f"\n\n【章节补充说明】\n{chapter.summary}"
# 可选:附加大纲的背景信息(限制长度)
if outline:
outline_bg = outline.content
if len(outline_bg) > 200:
outline_bg = outline_bg[:200] + "..."
chapter_outline_content += f"\n\n【大纲节点背景】\n{outline_bg}"
logger.info(f"✏️ 批量生成 - 一对多模式:使用expansion_plan详细规划")
except json.JSONDecodeError as e:
logger.warning(f"⚠️ expansion_plan解析失败: {e},回退到大纲内容")
chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲'
else:
# 没有expansion_plan,使用大纲内容
chapter_outline_content = outline.content if outline else chapter.summary or '暂无大纲'
logger.warning(f"⚠️ 批量生成 - 一对多模式但无expansion_plan,使用大纲内容")
# 🚀 根据大纲模式选择提示词模板(批量生成) # 🚀 根据大纲模式选择提示词模板(批量生成)
# 统一使用 context_builder 构建的 chapter_context 结果,与单章生成保持一致
if outline_mode == 'one-to-one': if outline_mode == 'one-to-one':
# 1-1模式 # 1-1模式
if chapter_context.continuation_point: if chapter_context.continuation_point:
@@ -2817,7 +2844,8 @@ async def generate_single_chapter_for_batch(
characters_info=chapter_context.chapter_characters or '暂无角色信息', characters_info=chapter_context.chapter_characters or '暂无角色信息',
chapter_careers=chapter_context.chapter_careers or '暂无职业信息', chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
relevant_memories=chapter_context.relevant_memories or '暂无相关记忆' relevant_memories=chapter_context.relevant_memories or '暂无相关记忆',
previous_chapter_summary=chapter_context.previous_chapter_summary or ''
) )
else: else:
# 第一章 # 第一章
@@ -2837,17 +2865,16 @@ async def generate_single_chapter_for_batch(
relevant_memories=chapter_context.relevant_memories or '暂无相关记忆' relevant_memories=chapter_context.relevant_memories or '暂无相关记忆'
) )
else: else:
# 1-n模式:使用原有的完整模板 # 1-n模式:使用 context_builder 构建的结果,与单章生成保持一致
if chapter_context.continuation_point: if chapter_context.continuation_point:
# 有前置内容,使用 WITH_CONTEXT 模板 # 有前置内容,使用 WITH_CONTEXT 模板
# 优先使用 context_builder 的摘要,其次使用传入的 previous_summary_context
final_prev_summary = "(无上一章摘要,请根据锚点续写)" final_prev_summary = "(无上一章摘要,请根据锚点续写)"
if previous_summary_context: if chapter_context.previous_chapter_summary:
final_prev_summary = chapter_context.previous_chapter_summary
elif previous_summary_context:
final_prev_summary = previous_summary_context final_prev_summary = previous_summary_context
elif hasattr(chapter_context, 'recent_summary') and chapter_context.recent_summary:
lines = chapter_context.recent_summary.strip().split('\n')
if lines:
final_prev_summary = lines[-1]
template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_MANY_NEXT", user_id, db_session) template = await PromptService.get_template("CHAPTER_GENERATION_ONE_TO_MANY_NEXT", user_id, db_session)
base_prompt = PromptService.format_prompt( base_prompt = PromptService.format_prompt(
@@ -2855,15 +2882,16 @@ async def generate_single_chapter_for_batch(
project_title=project.title, project_title=project.title,
chapter_number=chapter.chapter_number, chapter_number=chapter.chapter_number,
chapter_title=chapter.title, chapter_title=chapter.title,
chapter_outline=chapter_outline_content, chapter_outline=chapter_context.chapter_outline,
target_word_count=target_word_count, target_word_count=target_word_count,
continuation_point=chapter_context.continuation_point, continuation_point=chapter_context.continuation_point,
genre=project.genre or '未设定', genre=project.genre or '未设定',
narrative_perspective=project.narrative_perspective or '第三人称', narrative_perspective=project.narrative_perspective or '第三人称',
characters_info=characters_info or '暂无角色信息', characters_info=chapter_context.chapter_characters or '暂无角色信息',
chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔', foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
previous_chapter_summary=final_prev_summary, previous_chapter_summary=final_prev_summary,
story_skeleton=chapter_context.story_skeleton or '', recent_chapters_context=chapter_context.recent_chapters_context or '',
relevant_memories=chapter_context.relevant_memories or '' relevant_memories=chapter_context.relevant_memories or ''
) )
else: else:
@@ -2874,11 +2902,14 @@ async def generate_single_chapter_for_batch(
project_title=project.title, project_title=project.title,
chapter_number=chapter.chapter_number, chapter_number=chapter.chapter_number,
chapter_title=chapter.title, chapter_title=chapter.title,
chapter_outline=chapter_outline_content, chapter_outline=chapter_context.chapter_outline,
target_word_count=target_word_count, target_word_count=target_word_count,
genre=project.genre or '未设定', genre=project.genre or '未设定',
narrative_perspective=project.narrative_perspective or '第三人称', narrative_perspective=project.narrative_perspective or '第三人称',
characters_info=characters_info or '暂无角色信息' characters_info=chapter_context.chapter_characters or '暂无角色信息',
chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
relevant_memories=chapter_context.relevant_memories or '暂无相关记忆'
) )
# 应用写作风格 # 应用写作风格
@@ -3050,12 +3081,18 @@ async def regenerate_chapter_stream(
filter_character_names = None filter_character_names = None
if outline_mode == 'one-to-one': if outline_mode == 'one-to-one':
# 1-1模式:从outline.structure中提取characters字段 # 1-1模式:从outline.structure中提取characters字段(优先使用 outline_id
outline_result_temp = await temp_db.execute( if chapter.outline_id:
select(Outline.structure) outline_result_temp = await temp_db.execute(
.where(Outline.project_id == chapter.project_id) select(Outline.structure)
.where(Outline.order_index == chapter.chapter_number) .where(Outline.id == chapter.outline_id)
) )
else:
outline_result_temp = await temp_db.execute(
select(Outline.structure)
.where(Outline.project_id == chapter.project_id)
.where(Outline.order_index == chapter.chapter_number)
)
outline_structure = outline_result_temp.scalar_one_or_none() outline_structure = outline_result_temp.scalar_one_or_none()
if outline_structure: if outline_structure:
try: try:
@@ -3083,12 +3120,18 @@ async def regenerate_chapter_stream(
filter_character_names=filter_character_names filter_character_names=filter_character_names
) )
# 获取章节大纲 # 获取章节大纲(优先使用 chapter.outline_id 直接关联)
outline_result = await temp_db.execute( if chapter.outline_id:
select(Outline) outline_result = await temp_db.execute(
.where(Outline.project_id == chapter.project_id) select(Outline).where(Outline.id == chapter.outline_id)
.where(Outline.order_index == chapter.chapter_number) )
) else:
# 回退到按序号查找
outline_result = await temp_db.execute(
select(Outline)
.where(Outline.project_id == chapter.project_id)
.where(Outline.order_index == chapter.chapter_number)
)
outline = outline_result.scalar_one_or_none() outline = outline_result.scalar_one_or_none()
# 获取写作风格 # 获取写作风格
+497 -152
View File
@@ -13,6 +13,7 @@ from app.models.character import Character
from app.models.career import Career, CharacterCareer from app.models.career import Career, CharacterCareer
from app.models.memory import StoryMemory from app.models.memory import StoryMemory
from app.models.foreshadow import Foreshadow from app.models.foreshadow import Foreshadow
from app.models.relationship import CharacterRelationship, Organization, OrganizationMember
from app.logger import get_logger from app.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -24,14 +25,15 @@ class OneToManyContext:
1-N模式章节上下文数据结构 1-N模式章节上下文数据结构
采用RTCO框架的分层设计: 采用RTCO框架的分层设计:
- P0-核心:大纲、衔接锚点、字数要求 - P0-核心:大纲(含最近10章规划)、衔接锚点(500字+摘要)、字数要求
- P1-重要:角色、情感基调、风格 - P1-重要:角色(完整版含关系/组织/职业)、职业详情、情感基调
- P2-参考:记忆、故事骨架、伏笔提醒 - P2-参考:记忆(始终启用,相关度>0.6、伏笔提醒
""" """
# === P0-核心信息 === # === P0-核心信息 ===
chapter_outline: str = "" # 本章大纲(从expansion_plan构建) chapter_outline: str = "" # 本章大纲(从expansion_plan构建)
continuation_point: Optional[str] = None # 衔接锚点 recent_chapters_context: Optional[str] = None # 最近10章expansion_plan摘要
continuation_point: Optional[str] = None # 衔接锚点(统一500字)
previous_chapter_summary: Optional[str] = None # 上一章剧情摘要 previous_chapter_summary: Optional[str] = None # 上一章剧情摘要
previous_chapter_events: Optional[List[str]] = None # 上一章关键事件 previous_chapter_events: Optional[List[str]] = None # 上一章关键事件
target_word_count: int = 3000 target_word_count: int = 3000
@@ -49,13 +51,12 @@ class OneToManyContext:
theme: str = "" theme: str = ""
# === P1-重要信息 === # === P1-重要信息 ===
chapter_characters: str = "" # 从character_focus筛选的角色 chapter_characters: str = "" # 完整版角色信息(含年龄、外貌、背景、关系、组织)
chapter_careers: Optional[str] = None # 独立的职业详情(含完整阶段体系)
emotional_tone: str = "" emotional_tone: str = ""
style_instruction: str = ""
# === P2-参考信息 === # === P2-参考信息 ===
relevant_memories: Optional[str] = None relevant_memories: Optional[str] = None # 始终启用(相关度>0.6
story_skeleton: Optional[str] = None # 50章+启用
foreshadow_reminders: Optional[str] = None foreshadow_reminders: Optional[str] = None
# === 元信息 === # === 元信息 ===
@@ -64,9 +65,10 @@ class OneToManyContext:
def get_total_context_length(self) -> int: def get_total_context_length(self) -> int:
"""计算总上下文长度""" """计算总上下文长度"""
total = 0 total = 0
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters', for field_name in ['chapter_outline', 'recent_chapters_context', 'continuation_point',
'relevant_memories', 'story_skeleton', 'style_instruction', 'chapter_characters', 'chapter_careers',
'foreshadow_reminders', 'previous_chapter_summary']: 'relevant_memories', 'foreshadow_reminders',
'previous_chapter_summary']:
value = getattr(self, field_name, None) value = getattr(self, field_name, None)
if value: if value:
total += len(value) total += len(value)
@@ -102,6 +104,7 @@ class OneToOneContext:
# === P1-重要信息 === # === P1-重要信息 ===
continuation_point: Optional[str] = None # 上一章最后500字 continuation_point: Optional[str] = None # 上一章最后500字
previous_chapter_summary: Optional[str] = None # 上一章剧情摘要
chapter_characters: str = "" # 从structure.characters获取 chapter_characters: str = "" # 从structure.characters获取
chapter_careers: Optional[str] = None # 本章涉及的职业完整信息 chapter_careers: Optional[str] = None # 本章涉及的职业完整信息
@@ -115,8 +118,9 @@ class OneToOneContext:
def get_total_context_length(self) -> int: def get_total_context_length(self) -> int:
"""计算总上下文长度""" """计算总上下文长度"""
total = 0 total = 0
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters', for field_name in ['chapter_outline', 'continuation_point', 'previous_chapter_summary',
'chapter_careers', 'foreshadow_reminders', 'relevant_memories']: 'chapter_characters', 'chapter_careers', 'foreshadow_reminders',
'relevant_memories']:
value = getattr(self, field_name, None) value = getattr(self, field_name, None)
if value: if value:
total += len(value) total += len(value)
@@ -129,23 +133,20 @@ class OneToManyContextBuilder:
""" """
1-N模式上下文构建器 1-N模式上下文构建器
实现动态裁剪逻辑,根据章节序号自动调整上下文复杂度 上下文构建策略
- 第1章:无前置上下文,仅提供大纲和角色 - 章节大纲:本章expansion_plan + 最近10章expansion_plan摘要
- 第2-10章:上一章结尾300字 + 涉及角色 - 衔接锚点:统一上一章末尾500字 + 摘要
- 第11-50章:上一章结尾500字 + 相关记忆3条 - 角色信息:完整版(含年龄、外貌、背景、关系、组织、职业)
- 第51章+:上一章结尾500字 + 故事骨架 + 智能记忆5条 - 职业详情:独立的chapter_careers字段,含完整阶段体系
- 相关记忆:始终启用(相关度>0.6)
- 伏笔提醒:始终启用
""" """
# 配置常量 # 配置常量
ENDING_LENGTH_SHORT = 300 # 1-10章:短衔接 ENDING_LENGTH = 500 # 统一衔接长度500字
ENDING_LENGTH_NORMAL = 500 # 11章+:标准衔接 MEMORY_COUNT = 10 # 记忆条数
MEMORY_COUNT_LIGHT = 3 # 11-50章:轻量记忆 MEMORY_SIMILARITY_THRESHOLD = 0.6 # 记忆相关度阈值
MEMORY_COUNT_FULL = 5 # 51章+:完整记忆 RECENT_CHAPTERS_COUNT = 10 # 最近章节规划数量
SKELETON_THRESHOLD = 50 # 启用故事骨架的章节阈值
SKELETON_SAMPLE_INTERVAL = 10 # 故事骨架采样间隔
MEMORY_IMPORTANCE_THRESHOLD = 0.7 # 记忆重要性阈值
STYLE_MAX_LENGTH = 200 # 风格描述最大长度
MAX_CONTEXT_LENGTH = 3000 # 总上下文最大字符数
def __init__(self, memory_service=None, foreshadow_service=None): def __init__(self, memory_service=None, foreshadow_service=None):
""" """
@@ -178,7 +179,7 @@ class OneToManyContextBuilder:
outline: 大纲对象(可选) outline: 大纲对象(可选)
user_id: 用户ID user_id: 用户ID
db: 数据库会话 db: 数据库会话
style_content: 写作风格内容(可选) style_content: 写作风格内容(可选,不再使用,保留参数兼容性
target_word_count: 目标字数 target_word_count: 目标字数
temp_narrative_perspective: 临时叙事视角(可选,覆盖项目默认) temp_narrative_perspective: 临时叙事视角(可选,覆盖项目默认)
@@ -211,59 +212,47 @@ class OneToManyContextBuilder:
# === P0-核心信息(始终构建)=== # === P0-核心信息(始终构建)===
context.chapter_outline = self._build_chapter_outline_1n(chapter, outline) context.chapter_outline = self._build_chapter_outline_1n(chapter, outline)
# === 衔接锚点(根据章节调整长度,增强版含摘要和事件)=== # === 最近10章expansion_plan摘要 ===
if chapter_number > 1:
context.recent_chapters_context = await self._build_recent_chapters_context(
chapter, project.id, db
)
logger.info(f" ✅ 最近章节规划: {len(context.recent_chapters_context or '')}字符")
# === 衔接锚点(统一500字 + 摘要)===
if chapter_number == 1: if chapter_number == 1:
context.continuation_point = None context.continuation_point = None
context.previous_chapter_summary = None context.previous_chapter_summary = None
context.previous_chapter_events = None context.previous_chapter_events = None
logger.info(" ✅ 第1章无需衔接锚点") logger.info(" ✅ 第1章无需衔接锚点")
elif chapter_number <= 10:
ending_info = await self._get_last_ending_enhanced(
chapter, db, self.ENDING_LENGTH_SHORT
)
context.continuation_point = ending_info.get('ending_text')
context.previous_chapter_summary = ending_info.get('summary')
context.previous_chapter_events = ending_info.get('key_events')
logger.info(f" ✅ 衔接锚点(短): {len(context.continuation_point or '')}字符")
else: else:
ending_info = await self._get_last_ending_enhanced( ending_info = await self._get_last_ending_enhanced(
chapter, db, self.ENDING_LENGTH_NORMAL chapter, db, self.ENDING_LENGTH
) )
context.continuation_point = ending_info.get('ending_text') context.continuation_point = ending_info.get('ending_text')
context.previous_chapter_summary = ending_info.get('summary') context.previous_chapter_summary = ending_info.get('summary')
context.previous_chapter_events = ending_info.get('key_events') context.previous_chapter_events = ending_info.get('key_events')
logger.info(f" ✅ 衔接锚点(标准): {len(context.continuation_point or '')}字符") logger.info(f" ✅ 衔接锚点: {len(context.continuation_point or '')}字符")
# === P1-重要信息 === # === P1-重要信息 ===
context.chapter_characters = await self._build_chapter_characters_1n( # 角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情
characters_info, careers_info = await self._build_chapter_characters_1n(
chapter, project, outline, db chapter, project, outline, db
) )
context.chapter_characters = characters_info
context.chapter_careers = careers_info
context.emotional_tone = self._extract_emotional_tone(chapter, outline) context.emotional_tone = self._extract_emotional_tone(chapter, outline)
logger.info(f" ✅ 角色信息: {len(context.chapter_characters)}字符")
logger.info(f" ✅ 职业信息: {len(context.chapter_careers or '')}字符")
# 写作风格(摘要化) # === P2-参考信息(始终启用)===
if style_content: if self.memory_service:
context.style_instruction = self._summarize_style(style_content) context.relevant_memories = await self._get_relevant_memories_enhanced(
user_id, project.id, chapter_number,
# === P2-参考信息(条件触发)=== context.chapter_outline, db
if chapter_number > 10 and self.memory_service:
memory_limit = (
self.MEMORY_COUNT_LIGHT if chapter_number <= 50
else self.MEMORY_COUNT_FULL
)
context.relevant_memories = await self._get_relevant_memories(
user_id, project.id, chapter_number,
context.chapter_outline,
limit=memory_limit
) )
logger.info(f" ✅ 相关记忆: {len(context.relevant_memories or '')}字符") logger.info(f" ✅ 相关记忆: {len(context.relevant_memories or '')}字符")
# 故事骨架(50章+
if chapter_number > self.SKELETON_THRESHOLD:
context.story_skeleton = await self._build_story_skeleton(
project.id, chapter_number, db
)
logger.info(f" ✅ 故事骨架: {len(context.story_skeleton or '')}字符")
# === P2-伏笔提醒=== # === P2-伏笔提醒===
if self.foreshadow_service: if self.foreshadow_service:
context.foreshadow_reminders = await self._get_foreshadow_reminders( context.foreshadow_reminders = await self._get_foreshadow_reminders(
@@ -279,8 +268,9 @@ class OneToManyContextBuilder:
"has_continuation": context.continuation_point is not None, "has_continuation": context.continuation_point is not None,
"continuation_length": len(context.continuation_point or ""), "continuation_length": len(context.continuation_point or ""),
"characters_length": len(context.chapter_characters), "characters_length": len(context.chapter_characters),
"careers_length": len(context.chapter_careers or ""),
"recent_context_length": len(context.recent_chapters_context or ""),
"memories_length": len(context.relevant_memories or ""), "memories_length": len(context.relevant_memories or ""),
"skeleton_length": len(context.story_skeleton or ""),
"foreshadow_length": len(context.foreshadow_reminders or ""), "foreshadow_length": len(context.foreshadow_reminders or ""),
"total_length": context.get_total_context_length() "total_length": context.get_total_context_length()
} }
@@ -321,16 +311,21 @@ class OneToManyContextBuilder:
project: Project, project: Project,
outline: Optional[Outline], outline: Optional[Outline],
db: AsyncSession db: AsyncSession
) -> str: ) -> tuple[str, Optional[str]]:
"""构建1-N模式的角色信息(从expansion_plan提取character_focus""" """构建1-N模式的角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情"""
from sqlalchemy import or_
# 获取所有角色 # 获取所有角色
characters_result = await db.execute( characters_result = await db.execute(
select(Character).where(Character.project_id == project.id) select(Character).where(Character.project_id == project.id)
) )
characters = characters_result.scalars().all() all_characters = characters_result.scalars().all()
if not characters: if not all_characters:
return "暂无角色信息" return "暂无角色信息", None
# 构建全局角色名称映射(用于关系查询)
all_char_map = {c.id: c.name for c in all_characters}
# 从expansion_plan中提取角色焦点 # 从expansion_plan中提取角色焦点
filter_character_names = None filter_character_names = None
@@ -342,26 +337,338 @@ class OneToManyContextBuilder:
pass pass
# 筛选角色 # 筛选角色
characters = all_characters
if filter_character_names: if filter_character_names:
characters = [c for c in characters if c.name in filter_character_names] characters = [c for c in all_characters if c.name in filter_character_names]
if not characters: if not characters:
return "暂无相关角色" return "暂无相关角色", None
# 构建精简的角色信息 # 限制最多10个角色
char_lines = [] characters = characters[:10]
for c in characters[:10]: character_ids = [c.id for c in characters]
role_type = "主角" if c.role_type == "protagonist" else (
"反派" if c.role_type == "antagonist" else "配角" # === 批量查询关系数据 ===
rels_result = await db.execute(
select(CharacterRelationship).where(
CharacterRelationship.project_id == project.id,
or_(
CharacterRelationship.character_from_id.in_(character_ids),
CharacterRelationship.character_to_id.in_(character_ids)
)
) )
personality_brief = "" )
if c.personality: all_rels = rels_result.scalars().all()
personality_brief = c.personality[:50]
if len(c.personality) > 50:
personality_brief += "..."
char_lines.append(f"- {c.name}({role_type}): {personality_brief}")
return "\n".join(char_lines) # 按角色ID分组关系
char_rels_map: Dict[str, List] = {cid: [] for cid in character_ids}
for r in all_rels:
if r.character_from_id in char_rels_map:
char_rels_map[r.character_from_id].append(r)
if r.character_to_id in char_rels_map:
char_rels_map[r.character_to_id].append(r)
# === 批量查询组织成员数据 ===
non_org_ids = [c.id for c in characters if not c.is_organization]
org_memberships_map: Dict[str, List] = {cid: [] for cid in non_org_ids}
if non_org_ids:
member_result = await db.execute(
select(OrganizationMember, Character.name).join(
Organization, OrganizationMember.organization_id == Organization.id
).join(
Character, Organization.character_id == Character.id
).where(OrganizationMember.character_id.in_(non_org_ids))
)
for m, org_name in member_result.all():
if m.character_id in org_memberships_map:
org_memberships_map[m.character_id].append((m, org_name))
# === 批量查询职业关联数据(CharacterCareer===
char_career_result = await db.execute(
select(CharacterCareer).where(CharacterCareer.character_id.in_(character_ids))
)
all_char_careers = char_career_result.scalars().all()
# 收集所有职业ID
career_ids = set()
for cc in all_char_careers:
career_ids.add(cc.career_id)
# 也加入 main_career_id
for c in characters:
if not c.is_organization and c.main_career_id:
career_ids.add(c.main_career_id)
careers_map: Dict[str, Career] = {}
if career_ids:
careers_result = await db.execute(
select(Career).where(Career.id.in_(list(career_ids)))
)
careers_map = {c.id: c for c in careers_result.scalars().all()}
# 构建角色ID到职业关联的映射
char_career_relations: Dict[str, Dict[str, List]] = {}
for cc in all_char_careers:
if cc.character_id not in char_career_relations:
char_career_relations[cc.character_id] = {'main': [], 'sub': []}
if cc.career_type == 'main':
char_career_relations[cc.character_id]['main'].append(cc)
else:
char_career_relations[cc.character_id]['sub'].append(cc)
# === 查询组织角色的成员列表 ===
org_chars = [c for c in characters if c.is_organization]
org_members_map: Dict[str, List] = {}
if org_chars:
org_char_ids = [c.id for c in org_chars]
orgs_result = await db.execute(
select(Organization).where(Organization.character_id.in_(org_char_ids))
)
orgs = orgs_result.scalars().all()
if orgs:
org_id_to_char_id = {o.id: o.character_id for o in orgs}
org_ids = [o.id for o in orgs]
members_result = await db.execute(
select(OrganizationMember, Character.name).join(
Character, OrganizationMember.character_id == Character.id
).where(OrganizationMember.organization_id.in_(org_ids))
)
for m, member_name in members_result.all():
char_id = org_id_to_char_id.get(m.organization_id)
if char_id:
if char_id not in org_members_map:
org_members_map[char_id] = []
org_members_map[char_id].append((m, member_name))
# === 构建完整版角色信息 ===
characters_info_parts = []
for c in characters:
entity_type = '组织' if c.is_organization else '角色'
role_type_map = {
'protagonist': '主角',
'antagonist': '反派',
'supporting': '配角'
}
role_type = role_type_map.get(c.role_type, c.role_type or '配角')
info_lines = [f"{c.name}】({entity_type}, {role_type})"]
# 详细属性
if c.age:
info_lines.append(f" 年龄: {c.age}")
if c.gender:
info_lines.append(f" 性别: {c.gender}")
if c.appearance:
appearance_preview = c.appearance[:100] if len(c.appearance) > 100 else c.appearance
info_lines.append(f" 外貌: {appearance_preview}")
if c.personality:
personality_preview = c.personality[:100] if len(c.personality) > 100 else c.personality
info_lines.append(f" 性格: {personality_preview}")
if c.background:
background_preview = c.background[:150] if len(c.background) > 150 else c.background
info_lines.append(f" 背景: {background_preview}")
# 职业信息
if c.id in char_career_relations:
career_rel = char_career_relations[c.id]
if career_rel['main']:
for cc in career_rel['main']:
career = careers_map.get(cc.career_id)
if career:
try:
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
stage_name = f'{cc.current_stage}'
for stage in (stages or []):
if stage.get('level') == cc.current_stage:
stage_name = stage.get('name', stage_name)
break
except (json.JSONDecodeError, AttributeError, TypeError):
stage_name = f'{cc.current_stage}'
info_lines.append(f" 主职业: {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})")
if career_rel['sub']:
for cc in career_rel['sub']:
career = careers_map.get(cc.career_id)
if career:
try:
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
stage_name = f'{cc.current_stage}'
for stage in (stages or []):
if stage.get('level') == cc.current_stage:
stage_name = stage.get('name', stage_name)
break
except (json.JSONDecodeError, AttributeError, TypeError):
stage_name = f'{cc.current_stage}'
info_lines.append(f" 副职业: {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})")
elif not c.is_organization and c.main_career_id:
career = careers_map.get(c.main_career_id)
if career:
stage = c.main_career_stage or 1
info_lines.append(f" 主职业: {career.name}(第{stage}阶段)")
# 角色关系
if not c.is_organization and c.id in char_rels_map:
rels = char_rels_map[c.id]
if rels:
rel_parts = []
for r in rels:
if r.character_from_id == c.id:
target_name = all_char_map.get(r.character_to_id, "未知")
else:
target_name = all_char_map.get(r.character_from_id, "未知")
rel_name = r.relationship_name or "相关"
rel_parts.append(f"{target_name}{rel_name}")
info_lines.append(f" 关系网络: {''.join(rel_parts)}")
# 组织归属
if not c.is_organization and c.id in org_memberships_map:
memberships = org_memberships_map[c.id]
if memberships:
org_parts = [f"{org_name}{m.position}" for m, org_name in memberships[:2]]
info_lines.append(f" 组织归属: {''.join(org_parts)}")
# 组织特有信息
if c.is_organization:
if c.organization_type:
info_lines.append(f" 组织类型: {c.organization_type}")
if c.organization_purpose:
info_lines.append(f" 组织目的: {c.organization_purpose[:100]}")
if c.id in org_members_map:
members = org_members_map[c.id]
if members:
member_parts = [f"{name}{m.position}" for m, name in members[:5]]
info_lines.append(f" 组织成员: {''.join(member_parts)}")
characters_info_parts.append("\n".join(info_lines))
characters_result_str = "\n\n".join(characters_info_parts)
logger.info(f" ✅ [1-N完整版] 构建了 {len(characters_info_parts)} 个角色信息,总长度: {len(characters_result_str)} 字符")
# === 构建独立职业详情 ===
careers_info_parts = []
if careers_map:
for career_id, career in careers_map.items():
career_lines = [f"{career.name} ({career.type}职业)"]
if career.description:
career_lines.append(f" 描述: {career.description}")
if career.category:
career_lines.append(f" 分类: {career.category}")
try:
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
if stages:
career_lines.append(f" 阶段体系: (共{career.max_stage}阶)")
for stage in stages:
level = stage.get('level', '?')
name = stage.get('name', '未命名')
desc = stage.get('description', '')
career_lines.append(f" {level}阶-{name}: {desc}")
except (json.JSONDecodeError, AttributeError, TypeError):
career_lines.append(f" 阶段体系: 共{career.max_stage}")
if career.special_abilities:
career_lines.append(f" 特殊能力: {career.special_abilities}")
careers_info_parts.append("\n".join(career_lines))
careers_result_str = None
if careers_info_parts:
careers_result_str = "\n\n".join(careers_info_parts)
logger.info(f" ✅ [1-N完整版] 构建了 {len(careers_map)} 个职业详情,总长度: {len(careers_result_str)} 字符")
return characters_result_str, careers_result_str
async def _build_recent_chapters_context(
self,
chapter: Chapter,
project_id: str,
db: AsyncSession
) -> Optional[str]:
"""构建最近10章的expansion_plan摘要"""
try:
result = await db.execute(
select(Chapter.chapter_number, Chapter.title, Chapter.expansion_plan, Chapter.summary)
.where(Chapter.project_id == project_id)
.where(Chapter.chapter_number < chapter.chapter_number)
.order_by(Chapter.chapter_number.desc())
.limit(self.RECENT_CHAPTERS_COUNT)
)
recent_chapters = result.all()
if not recent_chapters:
return None
# 按章节号正序排列
recent_chapters = sorted(recent_chapters, key=lambda x: x[0])
lines = ["【最近章节规划】"]
for ch_num, ch_title, expansion_plan, summary in recent_chapters:
if expansion_plan:
try:
plan = json.loads(expansion_plan)
plot_summary = plan.get('plot_summary', '')
key_events = plan.get('key_events', [])
events_str = ''.join(key_events[:3]) if key_events else ''
line = f"{ch_num}章《{ch_title}》:{plot_summary}"
if events_str:
line += f"(关键事件:{events_str}"
lines.append(line)
except json.JSONDecodeError:
if summary:
lines.append(f"{ch_num}章《{ch_title}》:{summary[:100]}")
elif summary:
lines.append(f"{ch_num}章《{ch_title}》:{summary[:100]}")
if len(lines) <= 1:
return None
return "\n".join(lines)
except Exception as e:
logger.error(f"❌ 构建最近章节上下文失败: {str(e)}")
return None
async def _get_relevant_memories_enhanced(
self,
user_id: str,
project_id: str,
chapter_number: int,
chapter_outline: str,
db: AsyncSession
) -> Optional[str]:
"""获取相关记忆(始终启用,相关度>0.6)"""
if not self.memory_service:
return None
try:
query_text = chapter_outline[:500].replace('\n', ' ')
relevant_memories = await self.memory_service.search_memories(
user_id=user_id,
project_id=project_id,
query=query_text,
limit=15,
min_importance=0.0
)
# 过滤相关度>0.6
filtered_memories = [
mem for mem in relevant_memories
if mem.get('similarity', 0) > self.MEMORY_SIMILARITY_THRESHOLD
]
if not filtered_memories:
return None
memory_lines = ["【相关记忆】"]
for mem in filtered_memories[:self.MEMORY_COUNT]:
similarity = mem.get('similarity', 0)
content = mem.get('content', '')[:100]
memory_lines.append(f"- (相关度:{similarity:.2f}) {content}")
return "\n".join(memory_lines) if len(memory_lines) > 1 else None
except Exception as e:
logger.error(f"❌ 获取相关记忆失败: {str(e)}")
return None
async def _get_last_ending_enhanced( async def _get_last_ending_enhanced(
self, self,
@@ -379,11 +686,13 @@ class OneToManyContextBuilder:
if chapter.chapter_number <= 1: if chapter.chapter_number <= 1:
return result_info return result_info
# 查询上一章 # 查询上一章:不假设序号连续,取 chapter_number < 当前章 中最大的
result = await db.execute( result = await db.execute(
select(Chapter) select(Chapter)
.where(Chapter.project_id == chapter.project_id) .where(Chapter.project_id == chapter.project_id)
.where(Chapter.chapter_number == chapter.chapter_number - 1) .where(Chapter.chapter_number < chapter.chapter_number)
.order_by(Chapter.chapter_number.desc())
.limit(1)
) )
prev_chapter = result.scalar_one_or_none() prev_chapter = result.scalar_one_or_none()
@@ -475,7 +784,12 @@ class OneToManyContextBuilder:
chapter_outline: str, chapter_outline: str,
limit: int = 3 limit: int = 3
) -> Optional[str]: ) -> Optional[str]:
"""获取与本章最相关的记忆""" """
获取与本章最相关的记忆
注意:伏笔相关信息统一由 _get_foreshadow_reminders() 通过 foreshadow_service 提供,
此方法只负责获取故事记忆,不再从旧的 memory_service 获取伏笔信息。
"""
if not self.memory_service: if not self.memory_service:
return None return None
@@ -488,80 +802,33 @@ class OneToManyContextBuilder:
min_importance=self.MEMORY_IMPORTANCE_THRESHOLD min_importance=self.MEMORY_IMPORTANCE_THRESHOLD
) )
foreshadows = await self._get_due_foreshadows( return self._format_memories(relevant, max_length=500)
user_id, project_id, chapter_number,
lookahead=5
)
return self._format_memories(relevant, foreshadows, max_length=500)
except Exception as e: except Exception as e:
logger.error(f"❌ 获取相关记忆失败: {str(e)}") logger.error(f"❌ 获取相关记忆失败: {str(e)}")
return None return None
async def _get_due_foreshadows(
self,
user_id: str,
project_id: str,
chapter_number: int,
lookahead: int = 5
) -> List[Dict[str, Any]]:
"""获取即将需要回收的伏笔"""
if not self.memory_service:
return []
try:
foreshadows = await self.memory_service.find_unresolved_foreshadows(
user_id, project_id, chapter_number
)
due_foreshadows = []
for fs in foreshadows:
meta = fs.get('metadata', {})
fs_chapter = meta.get('chapter_number', 0)
if chapter_number - fs_chapter >= lookahead:
due_foreshadows.append({
'chapter': fs_chapter,
'content': fs.get('content', '')[:60],
'importance': meta.get('importance', 0.5)
})
return due_foreshadows[:2]
except Exception as e:
logger.error(f"❌ 获取待回收伏笔失败: {str(e)}")
return []
def _format_memories( def _format_memories(
self, self,
relevant: List[Dict[str, Any]], relevant: List[Dict[str, Any]],
foreshadows: List[Dict[str, Any]],
max_length: int = 500 max_length: int = 500
) -> str: ) -> str:
"""格式化记忆为简洁文本""" """格式化记忆为简洁文本(纯记忆,不含伏笔)"""
lines = [] if not relevant:
return None
lines = ["【相关记忆】"]
current_length = 0 current_length = 0
if foreshadows: for mem in relevant:
lines.append("【待回收伏笔】") content = mem.get('content', '')[:80]
for fs in foreshadows[:2]: text = f"- {content}"
text = f"- 第{fs['chapter']}章埋下:{fs['content']}" if current_length + len(text) > max_length:
if current_length + len(text) > max_length: break
break lines.append(text)
lines.append(text) current_length += len(text)
current_length += len(text)
if relevant and current_length < max_length: return "\n".join(lines) if len(lines) > 1 else None
lines.append("【相关记忆】")
for mem in relevant:
content = mem.get('content', '')[:80]
text = f"- {content}"
if current_length + len(text) > max_length:
break
lines.append(text)
current_length += len(text)
return "\n".join(lines) if lines else None
async def _get_foreshadow_reminders( async def _get_foreshadow_reminders(
self, self,
@@ -764,12 +1031,15 @@ class OneToOneContextBuilder:
logger.info(f" ✅ P0-大纲信息: {len(context.chapter_outline)}字符") logger.info(f" ✅ P0-大纲信息: {len(context.chapter_outline)}字符")
# === P1-重要信息 === # === P1-重要信息 ===
# 1. 获取上一章内容的最后500字 # 1. 获取上一章内容的最后500字和上一章摘要
if chapter_number > 1: if chapter_number > 1:
# 查找前一章:不假设序号连续,取 chapter_number < 当前章 中最大的
prev_chapter_result = await db.execute( prev_chapter_result = await db.execute(
select(Chapter) select(Chapter)
.where(Chapter.project_id == chapter.project_id) .where(Chapter.project_id == chapter.project_id)
.where(Chapter.chapter_number == chapter_number - 1) .where(Chapter.chapter_number < chapter_number)
.order_by(Chapter.chapter_number.desc())
.limit(1)
) )
prev_chapter = prev_chapter_result.scalar_one_or_none() prev_chapter = prev_chapter_result.scalar_one_or_none()
@@ -780,11 +1050,33 @@ class OneToOneContextBuilder:
else: else:
context.continuation_point = content[-500:] context.continuation_point = content[-500:]
logger.info(f" ✅ P1-上一章内容(最后500字): {len(context.continuation_point)}字符") logger.info(f" ✅ P1-上一章内容(最后500字): {len(context.continuation_point)}字符")
# 获取上一章摘要(优先从记忆系统获取,其次使用章节摘要)
summary_result = await db.execute(
select(StoryMemory.content)
.where(StoryMemory.project_id == chapter.project_id)
.where(StoryMemory.chapter_id == prev_chapter.id)
.where(StoryMemory.memory_type == 'chapter_summary')
.limit(1)
)
summary_mem = summary_result.scalar_one_or_none()
if summary_mem:
context.previous_chapter_summary = summary_mem[:300]
logger.info(f" ✅ P1-上一章摘要(记忆): {len(context.previous_chapter_summary)}字符")
elif prev_chapter.summary:
context.previous_chapter_summary = prev_chapter.summary[:300]
logger.info(f" ✅ P1-上一章摘要(章节): {len(context.previous_chapter_summary)}字符")
else:
context.previous_chapter_summary = None
logger.info(f" ⚠️ P1-上一章摘要: 无")
else: else:
context.continuation_point = None context.continuation_point = None
context.previous_chapter_summary = None
logger.info(f" ⚠️ P1-上一章内容: 无") logger.info(f" ⚠️ P1-上一章内容: 无")
else: else:
context.continuation_point = None context.continuation_point = None
context.previous_chapter_summary = None
logger.info(f" ✅ P1-第1章无需上一章内容") logger.info(f" ✅ P1-第1章无需上一章内容")
# 2. 根据structure中的characters获取角色信息(含职业) # 2. 根据structure中的characters获取角色信息(含职业)
@@ -792,7 +1084,12 @@ class OneToOneContextBuilder:
if outline and outline.structure: if outline and outline.structure:
try: try:
structure = json.loads(outline.structure) structure = json.loads(outline.structure)
character_names = structure.get('characters', []) raw_characters = structure.get('characters', [])
# characters可能是字符串列表或字典列表,统一提取为名称字符串列表
character_names = [
c['name'] if isinstance(c, dict) else c
for c in raw_characters
]
logger.info(f" 📋 从structure提取角色: {character_names}") logger.info(f" 📋 从structure提取角色: {character_names}")
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
@@ -853,7 +1150,7 @@ class OneToOneContextBuilder:
min_importance=0.0 min_importance=0.0
) )
# 降低相关度阈值0.4,提高召回率 # 过滤相关度阈值0.6
filtered_memories = [ filtered_memories = [
mem for mem in relevant_memories mem for mem in relevant_memories
if mem.get('similarity', 0) > 0.6 if mem.get('similarity', 0) > 0.6
@@ -867,7 +1164,7 @@ class OneToOneContextBuilder:
memory_lines.append(f"- (相关度:{similarity:.2f}) {content}") memory_lines.append(f"- (相关度:{similarity:.2f}) {content}")
context.relevant_memories = "\n".join(memory_lines) context.relevant_memories = "\n".join(memory_lines)
logger.info(f" ✅ P2-相关记忆: {len(filtered_memories)}条 (相关度>0.4, 共搜索{len(relevant_memories)}条)") logger.info(f" ✅ P2-相关记忆: {len(filtered_memories)}条 (相关度>0.6, 共搜索{len(relevant_memories)}条)")
else: else:
context.relevant_memories = None context.relevant_memories = None
logger.info(f" ⚠️ P2-相关记忆: 无符合条件的记忆 (共搜索到{len(relevant_memories)}条)") logger.info(f" ⚠️ P2-相关记忆: 无符合条件的记忆 (共搜索到{len(relevant_memories)}条)")
@@ -885,6 +1182,7 @@ class OneToOneContextBuilder:
"chapter_number": chapter_number, "chapter_number": chapter_number,
"has_previous_content": context.continuation_point is not None, "has_previous_content": context.continuation_point is not None,
"previous_content_length": len(context.continuation_point or ""), "previous_content_length": len(context.continuation_point or ""),
"previous_summary_length": len(context.previous_chapter_summary or ""),
"outline_length": len(context.chapter_outline), "outline_length": len(context.chapter_outline),
"characters_length": len(context.chapter_characters), "characters_length": len(context.chapter_characters),
"careers_length": len(context.chapter_careers or ""), "careers_length": len(context.chapter_careers or ""),
@@ -1093,14 +1391,61 @@ class OneToOneContextBuilder:
# 副职业也只显示引用 # 副职业也只显示引用
info_lines.append(f" - {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})") info_lines.append(f" - {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})")
# === 角色关系信息 ===
if not c.is_organization:
from sqlalchemy import or_
rels_result = await db.execute(
select(CharacterRelationship).where(
CharacterRelationship.project_id == project_id,
or_(
CharacterRelationship.character_from_id == c.id,
CharacterRelationship.character_to_id == c.id
)
)
)
rels = rels_result.scalars().all()
if rels:
related_ids = set()
for r in rels:
related_ids.add(r.character_from_id)
related_ids.add(r.character_to_id)
related_ids.discard(c.id)
if related_ids:
names_result = await db.execute(
select(Character.id, Character.name).where(Character.id.in_(related_ids))
)
name_map = {row.id: row.name for row in names_result}
rel_parts = []
for r in rels:
if r.character_from_id == c.id:
target_name = name_map.get(r.character_to_id, "未知")
else:
target_name = name_map.get(r.character_from_id, "未知")
rel_name = r.relationship_name or "相关"
rel_parts.append(f"{target_name}{rel_name}")
info_lines.append(f" 关系网络: {''.join(rel_parts)}")
# === 组织特有信息 === # === 组织特有信息 ===
if c.is_organization: if c.is_organization:
if c.organization_type: if c.organization_type:
info_lines.append(f" 组织类型: {c.organization_type}") info_lines.append(f" 组织类型: {c.organization_type}")
if c.organization_purpose: if c.organization_purpose:
info_lines.append(f" 组织目的: {c.organization_purpose[:100]}") info_lines.append(f" 组织目的: {c.organization_purpose[:100]}")
if c.organization_members: # 从 OrganizationMember 表动态查询组织成员
info_lines.append(f" 组织成员: {c.organization_members[:100]}") org_result = await db.execute(
select(Organization).where(Organization.character_id == c.id)
)
org = org_result.scalar_one_or_none()
if org:
members_result = await db.execute(
select(OrganizationMember, Character.name).join(
Character, OrganizationMember.character_id == Character.id
).where(OrganizationMember.organization_id == org.id)
)
members = members_result.all()
if members:
member_parts = [f"{name}{m.position}" for m, name in members]
info_lines.append(f" 组织成员: {''.join(member_parts)[:100]}")
# 组合完整信息 # 组合完整信息
full_info = "\n".join(info_lines) full_info = "\n".join(info_lines)