update:优化章节上下文构建系统和大纲查找逻辑,优先使用outline_id直接关联
This commit is contained in:
+361
-318
@@ -20,6 +20,7 @@ from app.models.project import Project
|
||||
from app.models.outline import Outline
|
||||
from app.models.character import Character
|
||||
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.writing_style import WritingStyle
|
||||
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
|
||||
|
||||
|
||||
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(
|
||||
db: AsyncSession,
|
||||
project_id: str,
|
||||
@@ -711,11 +519,75 @@ async def build_characters_info_with_careers(
|
||||
if not character_ids:
|
||||
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(
|
||||
select(CharacterCareer).where(CharacterCareer.character_id.in_(character_ids))
|
||||
)
|
||||
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到职业信息的映射
|
||||
char_career_map = {}
|
||||
for cc in character_careers:
|
||||
@@ -741,9 +613,52 @@ async def build_characters_info_with_careers(
|
||||
# 构建角色信息字符串
|
||||
characters_info_parts = []
|
||||
for c in characters:
|
||||
# 基本信息
|
||||
# 基本信息(含存活状态标记)
|
||||
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 = ""
|
||||
@@ -764,6 +679,55 @@ async def build_characters_info_with_careers(
|
||||
sub_list.append(f"{sub['name']}({stage_desc})")
|
||||
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 = ""
|
||||
if c.personality:
|
||||
@@ -771,7 +735,7 @@ async def build_characters_info_with_careers(
|
||||
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)
|
||||
|
||||
return "\n".join(characters_info_parts)
|
||||
@@ -903,6 +867,63 @@ async def analyze_chapter_background(
|
||||
)
|
||||
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):
|
||||
"""重试时更新任务状态,让前端能感知到重试进度"""
|
||||
@@ -924,7 +945,7 @@ async def analyze_chapter_background(
|
||||
except Exception as callback_error:
|
||||
logger.warning(f"⚠️ 更新重试状态失败: {callback_error}")
|
||||
|
||||
# 3. 使用PlotAnalyzer分析章节(传入已有伏笔列表和重试回调)
|
||||
# 3. 使用PlotAnalyzer分析章节(传入已有伏笔列表、角色信息和重试回调)
|
||||
analyzer = PlotAnalyzer(ai_service)
|
||||
analysis_result = await analyzer.analyze_chapter(
|
||||
chapter_number=chapter.chapter_number,
|
||||
@@ -932,7 +953,8 @@ async def analyze_chapter_background(
|
||||
content=chapter.content,
|
||||
word_count=chapter.word_count or len(chapter.content),
|
||||
existing_foreshadows=existing_foreshadows,
|
||||
on_retry=on_retry_callback
|
||||
on_retry=on_retry_callback,
|
||||
characters_info=characters_info
|
||||
)
|
||||
|
||||
if not analysis_result:
|
||||
@@ -1131,6 +1153,72 @@ async def analyze_chapter_background(
|
||||
else:
|
||||
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'):
|
||||
try:
|
||||
@@ -1319,13 +1407,21 @@ async def generate_chapter_content_stream(
|
||||
outline_mode = project.outline_mode if project else 'one-to-many'
|
||||
logger.info(f"📋 项目大纲模式: {outline_mode}")
|
||||
|
||||
# 获取对应的大纲
|
||||
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)
|
||||
)
|
||||
# 获取对应的大纲(优先使用 chapter.outline_id 直接关联)
|
||||
if current_chapter.outline_id:
|
||||
outline_result = await db_session.execute(
|
||||
select(Outline)
|
||||
.where(Outline.id == current_chapter.outline_id)
|
||||
.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()
|
||||
|
||||
# 获取写作风格
|
||||
@@ -1429,6 +1525,7 @@ async def generate_chapter_content_stream(
|
||||
genre=project.genre or '未设定',
|
||||
narrative_perspective=chapter_perspective,
|
||||
previous_chapter_content=chapter_context.continuation_point,
|
||||
previous_chapter_summary=chapter_context.previous_chapter_summary or '(无上一章摘要)',
|
||||
characters_info=chapter_context.chapter_characters or '暂无角色信息',
|
||||
chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
|
||||
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
|
||||
@@ -1476,9 +1573,10 @@ async def generate_chapter_content_stream(
|
||||
genre=project.genre or '未设定',
|
||||
narrative_perspective=chapter_perspective,
|
||||
characters_info=chapter_context.chapter_characters or '暂无角色信息',
|
||||
chapter_careers=chapter_context.chapter_careers or '暂无职业信息',
|
||||
foreshadow_reminders=chapter_context.foreshadow_reminders or '暂无需要关注的伏笔',
|
||||
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 ''
|
||||
)
|
||||
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,
|
||||
genre=project.genre or '未设定',
|
||||
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}")
|
||||
|
||||
@@ -2658,49 +2759,19 @@ async def generate_single_chapter_for_batch(
|
||||
outline_mode = project.outline_mode if project else 'one-to-many'
|
||||
logger.info(f"📋 批量生成 - 项目大纲模式: {outline_mode}")
|
||||
|
||||
# 获取对应的大纲
|
||||
outline_result = await db_session.execute(
|
||||
select(Outline)
|
||||
.where(Outline.project_id == chapter.project_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解析失败,使用全部角色")
|
||||
# 获取对应的大纲(优先使用 chapter.outline_id 直接关联)
|
||||
if chapter.outline_id:
|
||||
outline_result = await db_session.execute(
|
||||
select(Outline).where(Outline.id == chapter.outline_id)
|
||||
)
|
||||
else:
|
||||
# 1-n模式:从chapter.expansion_plan中提取character_focus字段
|
||||
if chapter.expansion_plan:
|
||||
try:
|
||||
plan = json.loads(chapter.expansion_plan)
|
||||
filter_character_names = plan.get('character_focus', [])
|
||||
if filter_character_names:
|
||||
logger.info(f"📋 批量生成 - 1-n模式:从expansion_plan提取角色焦点 {filter_character_names}")
|
||||
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
|
||||
)
|
||||
# 回退到按序号查找
|
||||
outline_result = await db_session.execute(
|
||||
select(Outline)
|
||||
.where(Outline.project_id == chapter.project_id)
|
||||
.where(Outline.order_index == chapter.chapter_number)
|
||||
)
|
||||
outline = outline_result.scalar_one_or_none()
|
||||
|
||||
# 获取写作风格
|
||||
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('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':
|
||||
# 1-1模式
|
||||
if chapter_context.continuation_point:
|
||||
@@ -2817,7 +2844,8 @@ async def generate_single_chapter_for_batch(
|
||||
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 '暂无相关记忆'
|
||||
relevant_memories=chapter_context.relevant_memories or '暂无相关记忆',
|
||||
previous_chapter_summary=chapter_context.previous_chapter_summary or ''
|
||||
)
|
||||
else:
|
||||
# 第一章
|
||||
@@ -2837,17 +2865,16 @@ async def generate_single_chapter_for_batch(
|
||||
relevant_memories=chapter_context.relevant_memories or '暂无相关记忆'
|
||||
)
|
||||
else:
|
||||
# 1-n模式:使用原有的完整模板
|
||||
# 1-n模式:使用 context_builder 构建的结果,与单章生成保持一致
|
||||
if chapter_context.continuation_point:
|
||||
# 有前置内容,使用 WITH_CONTEXT 模板
|
||||
# 优先使用 context_builder 的摘要,其次使用传入的 previous_summary_context
|
||||
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
|
||||
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)
|
||||
base_prompt = PromptService.format_prompt(
|
||||
@@ -2855,15 +2882,16 @@ async def generate_single_chapter_for_batch(
|
||||
project_title=project.title,
|
||||
chapter_number=chapter.chapter_number,
|
||||
chapter_title=chapter.title,
|
||||
chapter_outline=chapter_outline_content,
|
||||
chapter_outline=chapter_context.chapter_outline,
|
||||
target_word_count=target_word_count,
|
||||
continuation_point=chapter_context.continuation_point,
|
||||
genre=project.genre 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 '暂无需要关注的伏笔',
|
||||
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 ''
|
||||
)
|
||||
else:
|
||||
@@ -2874,11 +2902,14 @@ async def generate_single_chapter_for_batch(
|
||||
project_title=project.title,
|
||||
chapter_number=chapter.chapter_number,
|
||||
chapter_title=chapter.title,
|
||||
chapter_outline=chapter_outline_content,
|
||||
chapter_outline=chapter_context.chapter_outline,
|
||||
target_word_count=target_word_count,
|
||||
genre=project.genre 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
|
||||
if outline_mode == 'one-to-one':
|
||||
# 1-1模式:从outline.structure中提取characters字段
|
||||
outline_result_temp = await temp_db.execute(
|
||||
select(Outline.structure)
|
||||
.where(Outline.project_id == chapter.project_id)
|
||||
.where(Outline.order_index == chapter.chapter_number)
|
||||
)
|
||||
# 1-1模式:从outline.structure中提取characters字段(优先使用 outline_id)
|
||||
if chapter.outline_id:
|
||||
outline_result_temp = await temp_db.execute(
|
||||
select(Outline.structure)
|
||||
.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()
|
||||
if outline_structure:
|
||||
try:
|
||||
@@ -3083,12 +3120,18 @@ async def regenerate_chapter_stream(
|
||||
filter_character_names=filter_character_names
|
||||
)
|
||||
|
||||
# 获取章节大纲
|
||||
outline_result = await temp_db.execute(
|
||||
select(Outline)
|
||||
.where(Outline.project_id == chapter.project_id)
|
||||
.where(Outline.order_index == chapter.chapter_number)
|
||||
)
|
||||
# 获取章节大纲(优先使用 chapter.outline_id 直接关联)
|
||||
if chapter.outline_id:
|
||||
outline_result = await temp_db.execute(
|
||||
select(Outline).where(Outline.id == chapter.outline_id)
|
||||
)
|
||||
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()
|
||||
|
||||
# 获取写作风格
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.models.character import Character
|
||||
from app.models.career import Career, CharacterCareer
|
||||
from app.models.memory import StoryMemory
|
||||
from app.models.foreshadow import Foreshadow
|
||||
from app.models.relationship import CharacterRelationship, Organization, OrganizationMember
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -24,14 +25,15 @@ class OneToManyContext:
|
||||
1-N模式章节上下文数据结构
|
||||
|
||||
采用RTCO框架的分层设计:
|
||||
- P0-核心:大纲、衔接锚点、字数要求
|
||||
- P1-重要:角色、情感基调、风格
|
||||
- P2-参考:记忆、故事骨架、伏笔提醒
|
||||
- P0-核心:大纲(含最近10章规划)、衔接锚点(500字+摘要)、字数要求
|
||||
- P1-重要:角色(完整版含关系/组织/职业)、职业详情、情感基调
|
||||
- P2-参考:记忆(始终启用,相关度>0.6)、伏笔提醒
|
||||
"""
|
||||
|
||||
# === P0-核心信息 ===
|
||||
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_events: Optional[List[str]] = None # 上一章关键事件
|
||||
target_word_count: int = 3000
|
||||
@@ -49,13 +51,12 @@ class OneToManyContext:
|
||||
theme: str = ""
|
||||
|
||||
# === P1-重要信息 ===
|
||||
chapter_characters: str = "" # 从character_focus筛选的角色
|
||||
chapter_characters: str = "" # 完整版角色信息(含年龄、外貌、背景、关系、组织)
|
||||
chapter_careers: Optional[str] = None # 独立的职业详情(含完整阶段体系)
|
||||
emotional_tone: str = ""
|
||||
style_instruction: str = ""
|
||||
|
||||
# === P2-参考信息 ===
|
||||
relevant_memories: Optional[str] = None
|
||||
story_skeleton: Optional[str] = None # 50章+启用
|
||||
relevant_memories: Optional[str] = None # 始终启用(相关度>0.6)
|
||||
foreshadow_reminders: Optional[str] = None
|
||||
|
||||
# === 元信息 ===
|
||||
@@ -64,9 +65,10 @@ class OneToManyContext:
|
||||
def get_total_context_length(self) -> int:
|
||||
"""计算总上下文长度"""
|
||||
total = 0
|
||||
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters',
|
||||
'relevant_memories', 'story_skeleton', 'style_instruction',
|
||||
'foreshadow_reminders', 'previous_chapter_summary']:
|
||||
for field_name in ['chapter_outline', 'recent_chapters_context', 'continuation_point',
|
||||
'chapter_characters', 'chapter_careers',
|
||||
'relevant_memories', 'foreshadow_reminders',
|
||||
'previous_chapter_summary']:
|
||||
value = getattr(self, field_name, None)
|
||||
if value:
|
||||
total += len(value)
|
||||
@@ -102,6 +104,7 @@ class OneToOneContext:
|
||||
|
||||
# === P1-重要信息 ===
|
||||
continuation_point: Optional[str] = None # 上一章最后500字
|
||||
previous_chapter_summary: Optional[str] = None # 上一章剧情摘要
|
||||
chapter_characters: str = "" # 从structure.characters获取
|
||||
chapter_careers: Optional[str] = None # 本章涉及的职业完整信息
|
||||
|
||||
@@ -115,8 +118,9 @@ class OneToOneContext:
|
||||
def get_total_context_length(self) -> int:
|
||||
"""计算总上下文长度"""
|
||||
total = 0
|
||||
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters',
|
||||
'chapter_careers', 'foreshadow_reminders', 'relevant_memories']:
|
||||
for field_name in ['chapter_outline', 'continuation_point', 'previous_chapter_summary',
|
||||
'chapter_characters', 'chapter_careers', 'foreshadow_reminders',
|
||||
'relevant_memories']:
|
||||
value = getattr(self, field_name, None)
|
||||
if value:
|
||||
total += len(value)
|
||||
@@ -129,23 +133,20 @@ class OneToManyContextBuilder:
|
||||
"""
|
||||
1-N模式上下文构建器
|
||||
|
||||
实现动态裁剪逻辑,根据章节序号自动调整上下文复杂度:
|
||||
- 第1章:无前置上下文,仅提供大纲和角色
|
||||
- 第2-10章:上一章结尾300字 + 涉及角色
|
||||
- 第11-50章:上一章结尾500字 + 相关记忆3条
|
||||
- 第51章+:上一章结尾500字 + 故事骨架 + 智能记忆5条
|
||||
上下文构建策略:
|
||||
- 章节大纲:本章expansion_plan + 最近10章expansion_plan摘要
|
||||
- 衔接锚点:统一上一章末尾500字 + 摘要
|
||||
- 角色信息:完整版(含年龄、外貌、背景、关系、组织、职业)
|
||||
- 职业详情:独立的chapter_careers字段,含完整阶段体系
|
||||
- 相关记忆:始终启用(相关度>0.6)
|
||||
- 伏笔提醒:始终启用
|
||||
"""
|
||||
|
||||
# 配置常量
|
||||
ENDING_LENGTH_SHORT = 300 # 1-10章:短衔接
|
||||
ENDING_LENGTH_NORMAL = 500 # 11章+:标准衔接
|
||||
MEMORY_COUNT_LIGHT = 3 # 11-50章:轻量记忆
|
||||
MEMORY_COUNT_FULL = 5 # 51章+:完整记忆
|
||||
SKELETON_THRESHOLD = 50 # 启用故事骨架的章节阈值
|
||||
SKELETON_SAMPLE_INTERVAL = 10 # 故事骨架采样间隔
|
||||
MEMORY_IMPORTANCE_THRESHOLD = 0.7 # 记忆重要性阈值
|
||||
STYLE_MAX_LENGTH = 200 # 风格描述最大长度
|
||||
MAX_CONTEXT_LENGTH = 3000 # 总上下文最大字符数
|
||||
ENDING_LENGTH = 500 # 统一衔接长度500字
|
||||
MEMORY_COUNT = 10 # 记忆条数
|
||||
MEMORY_SIMILARITY_THRESHOLD = 0.6 # 记忆相关度阈值
|
||||
RECENT_CHAPTERS_COUNT = 10 # 最近章节规划数量
|
||||
|
||||
def __init__(self, memory_service=None, foreshadow_service=None):
|
||||
"""
|
||||
@@ -178,7 +179,7 @@ class OneToManyContextBuilder:
|
||||
outline: 大纲对象(可选)
|
||||
user_id: 用户ID
|
||||
db: 数据库会话
|
||||
style_content: 写作风格内容(可选)
|
||||
style_content: 写作风格内容(可选,不再使用,保留参数兼容性)
|
||||
target_word_count: 目标字数
|
||||
temp_narrative_perspective: 临时叙事视角(可选,覆盖项目默认)
|
||||
|
||||
@@ -211,59 +212,47 @@ class OneToManyContextBuilder:
|
||||
# === P0-核心信息(始终构建)===
|
||||
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:
|
||||
context.continuation_point = None
|
||||
context.previous_chapter_summary = None
|
||||
context.previous_chapter_events = None
|
||||
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:
|
||||
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.previous_chapter_summary = ending_info.get('summary')
|
||||
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-重要信息 ===
|
||||
context.chapter_characters = await self._build_chapter_characters_1n(
|
||||
# 角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情
|
||||
characters_info, careers_info = await self._build_chapter_characters_1n(
|
||||
chapter, project, outline, db
|
||||
)
|
||||
context.chapter_characters = characters_info
|
||||
context.chapter_careers = careers_info
|
||||
context.emotional_tone = self._extract_emotional_tone(chapter, outline)
|
||||
logger.info(f" ✅ 角色信息: {len(context.chapter_characters)}字符")
|
||||
logger.info(f" ✅ 职业信息: {len(context.chapter_careers or '')}字符")
|
||||
|
||||
# 写作风格(摘要化)
|
||||
if style_content:
|
||||
context.style_instruction = self._summarize_style(style_content)
|
||||
|
||||
# === P2-参考信息(条件触发)===
|
||||
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(
|
||||
# === P2-参考信息(始终启用)===
|
||||
if self.memory_service:
|
||||
context.relevant_memories = await self._get_relevant_memories_enhanced(
|
||||
user_id, project.id, chapter_number,
|
||||
context.chapter_outline,
|
||||
limit=memory_limit
|
||||
context.chapter_outline, db
|
||||
)
|
||||
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-伏笔提醒===
|
||||
if self.foreshadow_service:
|
||||
context.foreshadow_reminders = await self._get_foreshadow_reminders(
|
||||
@@ -279,8 +268,9 @@ class OneToManyContextBuilder:
|
||||
"has_continuation": context.continuation_point is not None,
|
||||
"continuation_length": len(context.continuation_point or ""),
|
||||
"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 ""),
|
||||
"skeleton_length": len(context.story_skeleton or ""),
|
||||
"foreshadow_length": len(context.foreshadow_reminders or ""),
|
||||
"total_length": context.get_total_context_length()
|
||||
}
|
||||
@@ -321,16 +311,21 @@ class OneToManyContextBuilder:
|
||||
project: Project,
|
||||
outline: Optional[Outline],
|
||||
db: AsyncSession
|
||||
) -> str:
|
||||
"""构建1-N模式的角色信息(从expansion_plan提取character_focus)"""
|
||||
) -> tuple[str, Optional[str]]:
|
||||
"""构建1-N模式的角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情"""
|
||||
from sqlalchemy import or_
|
||||
|
||||
# 获取所有角色
|
||||
characters_result = await db.execute(
|
||||
select(Character).where(Character.project_id == project.id)
|
||||
)
|
||||
characters = characters_result.scalars().all()
|
||||
all_characters = characters_result.scalars().all()
|
||||
|
||||
if not characters:
|
||||
return "暂无角色信息"
|
||||
if not all_characters:
|
||||
return "暂无角色信息", None
|
||||
|
||||
# 构建全局角色名称映射(用于关系查询)
|
||||
all_char_map = {c.id: c.name for c in all_characters}
|
||||
|
||||
# 从expansion_plan中提取角色焦点
|
||||
filter_character_names = None
|
||||
@@ -342,26 +337,338 @@ class OneToManyContextBuilder:
|
||||
pass
|
||||
|
||||
# 筛选角色
|
||||
characters = all_characters
|
||||
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:
|
||||
return "暂无相关角色"
|
||||
return "暂无相关角色", None
|
||||
|
||||
# 构建精简的角色信息
|
||||
char_lines = []
|
||||
for c in characters[:10]:
|
||||
role_type = "主角" if c.role_type == "protagonist" else (
|
||||
"反派" if c.role_type == "antagonist" else "配角"
|
||||
# 限制最多10个角色
|
||||
characters = characters[:10]
|
||||
character_ids = [c.id for c in characters]
|
||||
|
||||
# === 批量查询关系数据 ===
|
||||
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:
|
||||
personality_brief = c.personality[:50]
|
||||
if len(c.personality) > 50:
|
||||
personality_brief += "..."
|
||||
char_lines.append(f"- {c.name}({role_type}): {personality_brief}")
|
||||
)
|
||||
all_rels = rels_result.scalars().all()
|
||||
|
||||
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(
|
||||
self,
|
||||
@@ -379,11 +686,13 @@ class OneToManyContextBuilder:
|
||||
if chapter.chapter_number <= 1:
|
||||
return result_info
|
||||
|
||||
# 查询上一章
|
||||
# 查询上一章:不假设序号连续,取 chapter_number < 当前章 中最大的
|
||||
result = await db.execute(
|
||||
select(Chapter)
|
||||
.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()
|
||||
|
||||
@@ -475,7 +784,12 @@ class OneToManyContextBuilder:
|
||||
chapter_outline: str,
|
||||
limit: int = 3
|
||||
) -> Optional[str]:
|
||||
"""获取与本章最相关的记忆"""
|
||||
"""
|
||||
获取与本章最相关的记忆
|
||||
|
||||
注意:伏笔相关信息统一由 _get_foreshadow_reminders() 通过 foreshadow_service 提供,
|
||||
此方法只负责获取故事记忆,不再从旧的 memory_service 获取伏笔信息。
|
||||
"""
|
||||
if not self.memory_service:
|
||||
return None
|
||||
|
||||
@@ -488,80 +802,33 @@ class OneToManyContextBuilder:
|
||||
min_importance=self.MEMORY_IMPORTANCE_THRESHOLD
|
||||
)
|
||||
|
||||
foreshadows = await self._get_due_foreshadows(
|
||||
user_id, project_id, chapter_number,
|
||||
lookahead=5
|
||||
)
|
||||
|
||||
return self._format_memories(relevant, foreshadows, max_length=500)
|
||||
return self._format_memories(relevant, max_length=500)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取相关记忆失败: {str(e)}")
|
||||
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(
|
||||
self,
|
||||
relevant: List[Dict[str, Any]],
|
||||
foreshadows: List[Dict[str, Any]],
|
||||
max_length: int = 500
|
||||
) -> str:
|
||||
"""格式化记忆为简洁文本"""
|
||||
lines = []
|
||||
"""格式化记忆为简洁文本(纯记忆,不含伏笔)"""
|
||||
if not relevant:
|
||||
return None
|
||||
|
||||
lines = ["【相关记忆】"]
|
||||
current_length = 0
|
||||
|
||||
if foreshadows:
|
||||
lines.append("【待回收伏笔】")
|
||||
for fs in foreshadows[:2]:
|
||||
text = f"- 第{fs['chapter']}章埋下:{fs['content']}"
|
||||
if current_length + len(text) > max_length:
|
||||
break
|
||||
lines.append(text)
|
||||
current_length += len(text)
|
||||
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)
|
||||
|
||||
if relevant and current_length < max_length:
|
||||
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
|
||||
return "\n".join(lines) if len(lines) > 1 else None
|
||||
|
||||
async def _get_foreshadow_reminders(
|
||||
self,
|
||||
@@ -764,12 +1031,15 @@ class OneToOneContextBuilder:
|
||||
logger.info(f" ✅ P0-大纲信息: {len(context.chapter_outline)}字符")
|
||||
|
||||
# === P1-重要信息 ===
|
||||
# 1. 获取上一章内容的最后500字
|
||||
# 1. 获取上一章内容的最后500字和上一章摘要
|
||||
if chapter_number > 1:
|
||||
# 查找前一章:不假设序号连续,取 chapter_number < 当前章 中最大的
|
||||
prev_chapter_result = await db.execute(
|
||||
select(Chapter)
|
||||
.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()
|
||||
|
||||
@@ -780,11 +1050,33 @@ class OneToOneContextBuilder:
|
||||
else:
|
||||
context.continuation_point = content[-500:]
|
||||
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:
|
||||
context.continuation_point = None
|
||||
context.previous_chapter_summary = None
|
||||
logger.info(f" ⚠️ P1-上一章内容: 无")
|
||||
else:
|
||||
context.continuation_point = None
|
||||
context.previous_chapter_summary = None
|
||||
logger.info(f" ✅ P1-第1章无需上一章内容")
|
||||
|
||||
# 2. 根据structure中的characters获取角色信息(含职业)
|
||||
@@ -792,7 +1084,12 @@ class OneToOneContextBuilder:
|
||||
if outline and outline.structure:
|
||||
try:
|
||||
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}")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -853,7 +1150,7 @@ class OneToOneContextBuilder:
|
||||
min_importance=0.0
|
||||
)
|
||||
|
||||
# 降低相关度阈值到0.4,提高召回率
|
||||
# 过滤相关度阈值为0.6
|
||||
filtered_memories = [
|
||||
mem for mem in relevant_memories
|
||||
if mem.get('similarity', 0) > 0.6
|
||||
@@ -867,7 +1164,7 @@ class OneToOneContextBuilder:
|
||||
memory_lines.append(f"- (相关度:{similarity:.2f}) {content}")
|
||||
|
||||
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:
|
||||
context.relevant_memories = None
|
||||
logger.info(f" ⚠️ P2-相关记忆: 无符合条件的记忆 (共搜索到{len(relevant_memories)}条)")
|
||||
@@ -885,6 +1182,7 @@ class OneToOneContextBuilder:
|
||||
"chapter_number": chapter_number,
|
||||
"has_previous_content": context.continuation_point is not None,
|
||||
"previous_content_length": len(context.continuation_point or ""),
|
||||
"previous_summary_length": len(context.previous_chapter_summary or ""),
|
||||
"outline_length": len(context.chapter_outline),
|
||||
"characters_length": len(context.chapter_characters),
|
||||
"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})")
|
||||
|
||||
# === 角色关系信息 ===
|
||||
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.organization_type:
|
||||
info_lines.append(f" 组织类型: {c.organization_type}")
|
||||
if c.organization_purpose:
|
||||
info_lines.append(f" 组织目的: {c.organization_purpose[:100]}")
|
||||
if c.organization_members:
|
||||
info_lines.append(f" 组织成员: {c.organization_members[:100]}")
|
||||
# 从 OrganizationMember 表动态查询组织成员
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user