2026-01-01 17:30:46 +08:00
|
|
|
|
"""章节上下文构建服务 - 实现RTCO框架的智能上下文构建"""
|
|
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.chapter import Chapter
|
|
|
|
|
|
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.memory import StoryMemory
|
2026-01-19 17:23:50 +08:00
|
|
|
|
from app.models.foreshadow import Foreshadow
|
2026-02-12 12:40:12 +08:00
|
|
|
|
from app.models.relationship import CharacterRelationship, Organization, OrganizationMember
|
2026-01-01 17:30:46 +08:00
|
|
|
|
from app.logger import get_logger
|
|
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
2026-02-06 16:44:09 +08:00
|
|
|
|
class OneToManyContext:
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
1-N模式章节上下文数据结构
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
采用RTCO框架的分层设计:
|
2026-02-12 12:40:12 +08:00
|
|
|
|
- P0-核心:大纲(含最近10章规划)、衔接锚点(500字+摘要)、字数要求
|
|
|
|
|
|
- P1-重要:角色(完整版含关系/组织/职业)、职业详情、情感基调
|
|
|
|
|
|
- P2-参考:记忆(始终启用,相关度>0.6)、伏笔提醒
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# === P0-核心信息 ===
|
|
|
|
|
|
chapter_outline: str = "" # 本章大纲(从expansion_plan构建)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
recent_chapters_context: Optional[str] = None # 最近10章expansion_plan摘要
|
|
|
|
|
|
continuation_point: Optional[str] = None # 衔接锚点(统一500字)
|
2026-02-06 16:44:09 +08:00
|
|
|
|
previous_chapter_summary: Optional[str] = None # 上一章剧情摘要
|
|
|
|
|
|
previous_chapter_events: Optional[List[str]] = None # 上一章关键事件
|
|
|
|
|
|
target_word_count: int = 3000
|
|
|
|
|
|
min_word_count: int = 2500
|
|
|
|
|
|
max_word_count: int = 4000
|
|
|
|
|
|
narrative_perspective: str = "第三人称"
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
# === 本章基本信息 ===
|
2026-02-06 16:44:09 +08:00
|
|
|
|
chapter_number: int = 1
|
|
|
|
|
|
chapter_title: str = ""
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
# === 项目基本信息 ===
|
2026-02-06 16:44:09 +08:00
|
|
|
|
title: str = ""
|
|
|
|
|
|
genre: str = ""
|
|
|
|
|
|
theme: str = ""
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# === P1-重要信息 ===
|
2026-02-12 12:40:12 +08:00
|
|
|
|
chapter_characters: str = "" # 完整版角色信息(含年龄、外貌、背景、关系、组织)
|
|
|
|
|
|
chapter_careers: Optional[str] = None # 独立的职业详情(含完整阶段体系)
|
2026-02-06 16:44:09 +08:00
|
|
|
|
emotional_tone: str = ""
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# === P2-参考信息 ===
|
2026-02-12 12:40:12 +08:00
|
|
|
|
relevant_memories: Optional[str] = None # 始终启用(相关度>0.6)
|
2026-02-06 16:44:09 +08:00
|
|
|
|
foreshadow_reminders: Optional[str] = None
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
# === 元信息 ===
|
2026-02-06 16:44:09 +08:00
|
|
|
|
context_stats: Dict[str, Any] = field(default_factory=dict)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
def get_total_context_length(self) -> int:
|
|
|
|
|
|
"""计算总上下文长度"""
|
|
|
|
|
|
total = 0
|
2026-02-12 12:40:12 +08:00
|
|
|
|
for field_name in ['chapter_outline', 'recent_chapters_context', 'continuation_point',
|
|
|
|
|
|
'chapter_characters', 'chapter_careers',
|
|
|
|
|
|
'relevant_memories', 'foreshadow_reminders',
|
|
|
|
|
|
'previous_chapter_summary']:
|
2026-01-01 17:30:46 +08:00
|
|
|
|
value = getattr(self, field_name, None)
|
|
|
|
|
|
if value:
|
|
|
|
|
|
total += len(value)
|
|
|
|
|
|
return total
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class OneToOneContext:
|
|
|
|
|
|
"""
|
|
|
|
|
|
1-1模式章节上下文数据结构
|
|
|
|
|
|
|
|
|
|
|
|
采用RTCO框架的分层设计:
|
|
|
|
|
|
- P0-核心:从outline.structure提取的大纲、字数要求
|
|
|
|
|
|
- P1-重要:上一章最后500字、从structure.characters获取的角色、本章职业体系
|
|
|
|
|
|
- P2-参考:伏笔提醒、相关记忆(相关度>0.6)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# === P0-核心信息 ===
|
|
|
|
|
|
chapter_outline: str = "" # 从outline.structure提取
|
|
|
|
|
|
target_word_count: int = 3000
|
|
|
|
|
|
min_word_count: int = 2500
|
|
|
|
|
|
max_word_count: int = 4000
|
|
|
|
|
|
narrative_perspective: str = "第三人称"
|
|
|
|
|
|
|
|
|
|
|
|
# === 本章基本信息 ===
|
|
|
|
|
|
chapter_number: int = 1
|
|
|
|
|
|
chapter_title: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
# === 项目基本信息 ===
|
|
|
|
|
|
title: str = ""
|
|
|
|
|
|
genre: str = ""
|
|
|
|
|
|
theme: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
# === P1-重要信息 ===
|
|
|
|
|
|
continuation_point: Optional[str] = None # 上一章最后500字
|
2026-02-12 12:40:12 +08:00
|
|
|
|
previous_chapter_summary: Optional[str] = None # 上一章剧情摘要
|
2026-02-06 16:44:09 +08:00
|
|
|
|
chapter_characters: str = "" # 从structure.characters获取
|
|
|
|
|
|
chapter_careers: Optional[str] = None # 本章涉及的职业完整信息
|
|
|
|
|
|
|
|
|
|
|
|
# === P2-参考信息 ===
|
|
|
|
|
|
foreshadow_reminders: Optional[str] = None
|
|
|
|
|
|
relevant_memories: Optional[str] = None # 相关度>0.6
|
|
|
|
|
|
|
|
|
|
|
|
# === 元信息 ===
|
|
|
|
|
|
context_stats: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
def get_total_context_length(self) -> int:
|
|
|
|
|
|
"""计算总上下文长度"""
|
|
|
|
|
|
total = 0
|
2026-02-12 12:40:12 +08:00
|
|
|
|
for field_name in ['chapter_outline', 'continuation_point', 'previous_chapter_summary',
|
|
|
|
|
|
'chapter_characters', 'chapter_careers', 'foreshadow_reminders',
|
|
|
|
|
|
'relevant_memories']:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
value = getattr(self, field_name, None)
|
|
|
|
|
|
if value:
|
|
|
|
|
|
total += len(value)
|
|
|
|
|
|
return total
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 1-N模式上下文构建器 ====================
|
|
|
|
|
|
|
|
|
|
|
|
class OneToManyContextBuilder:
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
1-N模式上下文构建器
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
上下文构建策略:
|
|
|
|
|
|
- 章节大纲:本章expansion_plan + 最近10章expansion_plan摘要
|
|
|
|
|
|
- 衔接锚点:统一上一章末尾500字 + 摘要
|
|
|
|
|
|
- 角色信息:完整版(含年龄、外貌、背景、关系、组织、职业)
|
|
|
|
|
|
- 职业详情:独立的chapter_careers字段,含完整阶段体系
|
|
|
|
|
|
- 相关记忆:始终启用(相关度>0.6)
|
|
|
|
|
|
- 伏笔提醒:始终启用
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# 配置常量
|
2026-02-12 12:40:12 +08:00
|
|
|
|
ENDING_LENGTH = 500 # 统一衔接长度500字
|
|
|
|
|
|
MEMORY_COUNT = 10 # 记忆条数
|
|
|
|
|
|
MEMORY_SIMILARITY_THRESHOLD = 0.6 # 记忆相关度阈值
|
|
|
|
|
|
RECENT_CHAPTERS_COUNT = 10 # 最近章节规划数量
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-01-19 17:23:50 +08:00
|
|
|
|
def __init__(self, memory_service=None, foreshadow_service=None):
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
初始化构建器
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
memory_service: 记忆服务实例(可选,用于检索相关记忆)
|
2026-01-19 17:23:50 +08:00
|
|
|
|
foreshadow_service: 伏笔服务实例(可选,用于获取伏笔提醒)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
self.memory_service = memory_service
|
2026-01-19 17:23:50 +08:00
|
|
|
|
self.foreshadow_service = foreshadow_service
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
async def build(
|
|
|
|
|
|
self,
|
|
|
|
|
|
chapter: Chapter,
|
|
|
|
|
|
project: Project,
|
|
|
|
|
|
outline: Optional[Outline],
|
|
|
|
|
|
user_id: str,
|
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
|
style_content: Optional[str] = None,
|
|
|
|
|
|
target_word_count: int = 3000,
|
|
|
|
|
|
temp_narrative_perspective: Optional[str] = None
|
2026-02-06 16:44:09 +08:00
|
|
|
|
) -> OneToManyContext:
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
构建章节生成所需的上下文(1-N模式)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
chapter: 章节对象
|
|
|
|
|
|
project: 项目对象
|
|
|
|
|
|
outline: 大纲对象(可选)
|
|
|
|
|
|
user_id: 用户ID
|
|
|
|
|
|
db: 数据库会话
|
2026-02-12 12:40:12 +08:00
|
|
|
|
style_content: 写作风格内容(可选,不再使用,保留参数兼容性)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
target_word_count: 目标字数
|
|
|
|
|
|
temp_narrative_perspective: 临时叙事视角(可选,覆盖项目默认)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
OneToManyContext: 结构化的上下文对象
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
chapter_number = chapter.chapter_number
|
2026-02-06 16:44:09 +08:00
|
|
|
|
logger.info(f"📝 [1-N模式] 开始构建章节上下文: 第{chapter_number}章")
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 确定叙事视角
|
|
|
|
|
|
narrative_perspective = (
|
|
|
|
|
|
temp_narrative_perspective or
|
|
|
|
|
|
project.narrative_perspective or
|
|
|
|
|
|
"第三人称"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化上下文
|
2026-02-06 16:44:09 +08:00
|
|
|
|
context = OneToManyContext(
|
2026-01-01 17:30:46 +08:00
|
|
|
|
chapter_number=chapter_number,
|
|
|
|
|
|
chapter_title=chapter.title or "",
|
|
|
|
|
|
title=project.title or "",
|
|
|
|
|
|
genre=project.genre or "",
|
|
|
|
|
|
theme=project.theme or "",
|
|
|
|
|
|
target_word_count=target_word_count,
|
|
|
|
|
|
min_word_count=max(500, target_word_count - 500),
|
|
|
|
|
|
max_word_count=target_word_count + 1000,
|
|
|
|
|
|
narrative_perspective=narrative_perspective
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# === P0-核心信息(始终构建)===
|
2026-02-06 16:44:09 +08:00
|
|
|
|
context.chapter_outline = self._build_chapter_outline_1n(chapter, outline)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# === 最近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字 + 摘要)===
|
2026-01-01 17:30:46 +08:00
|
|
|
|
if chapter_number == 1:
|
|
|
|
|
|
context.continuation_point = None
|
2026-01-19 17:23:50 +08:00
|
|
|
|
context.previous_chapter_summary = None
|
|
|
|
|
|
context.previous_chapter_events = None
|
2026-01-01 17:30:46 +08:00
|
|
|
|
logger.info(" ✅ 第1章无需衔接锚点")
|
|
|
|
|
|
else:
|
2026-01-19 17:23:50 +08:00
|
|
|
|
ending_info = await self._get_last_ending_enhanced(
|
2026-02-12 12:40:12 +08:00
|
|
|
|
chapter, db, self.ENDING_LENGTH
|
2026-01-01 17:30:46 +08:00
|
|
|
|
)
|
2026-01-19 17:23:50 +08:00
|
|
|
|
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')
|
2026-02-12 12:40:12 +08:00
|
|
|
|
logger.info(f" ✅ 衔接锚点: {len(context.continuation_point or '')}字符")
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
# === P1-重要信息 ===
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# 角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情
|
|
|
|
|
|
characters_info, careers_info = await self._build_chapter_characters_1n(
|
2026-01-01 17:30:46 +08:00
|
|
|
|
chapter, project, outline, db
|
|
|
|
|
|
)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
context.chapter_characters = characters_info
|
|
|
|
|
|
context.chapter_careers = careers_info
|
2026-01-01 17:30:46 +08:00
|
|
|
|
context.emotional_tone = self._extract_emotional_tone(chapter, outline)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
logger.info(f" ✅ 角色信息: {len(context.chapter_characters)}字符")
|
|
|
|
|
|
logger.info(f" ✅ 职业信息: {len(context.chapter_careers or '')}字符")
|
|
|
|
|
|
|
|
|
|
|
|
# === P2-参考信息(始终启用)===
|
|
|
|
|
|
if self.memory_service:
|
|
|
|
|
|
context.relevant_memories = await self._get_relevant_memories_enhanced(
|
|
|
|
|
|
user_id, project.id, chapter_number,
|
|
|
|
|
|
context.chapter_outline, db
|
2026-01-01 17:30:46 +08:00
|
|
|
|
)
|
|
|
|
|
|
logger.info(f" ✅ 相关记忆: {len(context.relevant_memories or '')}字符")
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# === P2-伏笔提醒===
|
2026-01-19 17:23:50 +08:00
|
|
|
|
if self.foreshadow_service:
|
|
|
|
|
|
context.foreshadow_reminders = await self._get_foreshadow_reminders(
|
|
|
|
|
|
project.id, chapter_number, db
|
|
|
|
|
|
)
|
|
|
|
|
|
if context.foreshadow_reminders:
|
|
|
|
|
|
logger.info(f" ✅ 伏笔提醒: {len(context.foreshadow_reminders)}字符")
|
|
|
|
|
|
|
2026-01-01 17:30:46 +08:00
|
|
|
|
# === 统计信息 ===
|
|
|
|
|
|
context.context_stats = {
|
2026-02-06 16:44:09 +08:00
|
|
|
|
"mode": "one-to-many",
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"chapter_number": chapter_number,
|
|
|
|
|
|
"has_continuation": context.continuation_point is not None,
|
|
|
|
|
|
"continuation_length": len(context.continuation_point or ""),
|
|
|
|
|
|
"characters_length": len(context.chapter_characters),
|
2026-02-12 12:40:12 +08:00
|
|
|
|
"careers_length": len(context.chapter_careers or ""),
|
|
|
|
|
|
"recent_context_length": len(context.recent_chapters_context or ""),
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"memories_length": len(context.relevant_memories or ""),
|
2026-01-19 17:23:50 +08:00
|
|
|
|
"foreshadow_length": len(context.foreshadow_reminders or ""),
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"total_length": context.get_total_context_length()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
logger.info(f"📊 [1-N模式] 上下文构建完成: 总长度 {context.context_stats['total_length']} 字符")
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
def _build_chapter_outline_1n(
|
2026-01-01 17:30:46 +08:00
|
|
|
|
self,
|
|
|
|
|
|
chapter: Chapter,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
outline: Optional[Outline]
|
2026-01-01 17:30:46 +08:00
|
|
|
|
) -> str:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
"""构建1-N模式的章节大纲"""
|
|
|
|
|
|
# 优先使用 expansion_plan 的详细规划
|
|
|
|
|
|
if chapter.expansion_plan:
|
|
|
|
|
|
try:
|
|
|
|
|
|
plan = json.loads(chapter.expansion_plan)
|
2026-03-13 17:50:00 +08:00
|
|
|
|
# expansion_plan没有plot_summary这个键,
|
|
|
|
|
|
outline_content = f"""剧情摘要:{plan.get('plot_summary') or chapter.summary or '无'}
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
关键事件:
|
|
|
|
|
|
{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', '未设定')}"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
return outline_content
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 回退到大纲内容
|
|
|
|
|
|
return outline.content if outline else chapter.summary or '暂无大纲'
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
async def _build_chapter_characters_1n(
|
2026-01-01 17:30:46 +08:00
|
|
|
|
self,
|
|
|
|
|
|
chapter: Chapter,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
project: Project,
|
|
|
|
|
|
outline: Optional[Outline],
|
|
|
|
|
|
db: AsyncSession
|
2026-02-12 12:40:12 +08:00
|
|
|
|
) -> tuple[str, Optional[str]]:
|
|
|
|
|
|
"""构建1-N模式的角色信息(完整版:含年龄、外貌、背景、关系、组织、职业)+ 独立职业详情"""
|
|
|
|
|
|
from sqlalchemy import or_
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# 获取所有角色
|
|
|
|
|
|
characters_result = await db.execute(
|
|
|
|
|
|
select(Character).where(Character.project_id == project.id)
|
|
|
|
|
|
)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
all_characters = characters_result.scalars().all()
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
if not all_characters:
|
|
|
|
|
|
return "暂无角色信息", None
|
|
|
|
|
|
|
|
|
|
|
|
# 构建全局角色名称映射(用于关系查询)
|
|
|
|
|
|
all_char_map = {c.id: c.name for c in all_characters}
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# 从expansion_plan中提取角色焦点
|
|
|
|
|
|
filter_character_names = None
|
|
|
|
|
|
if chapter.expansion_plan:
|
|
|
|
|
|
try:
|
|
|
|
|
|
plan = json.loads(chapter.expansion_plan)
|
|
|
|
|
|
filter_character_names = plan.get('character_focus', [])
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# 筛选角色
|
2026-02-12 12:40:12 +08:00
|
|
|
|
characters = all_characters
|
2026-02-06 16:44:09 +08:00
|
|
|
|
if filter_character_names:
|
2026-02-12 12:40:12 +08:00
|
|
|
|
characters = [c for c in all_characters if c.name in filter_character_names]
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
if not characters:
|
2026-02-12 12:40:12 +08:00
|
|
|
|
return "暂无相关角色", None
|
|
|
|
|
|
|
|
|
|
|
|
# 限制最多10个角色
|
|
|
|
|
|
characters = characters[:10]
|
|
|
|
|
|
character_ids = [c.id for c in characters]
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# === 批量查询关系数据 ===
|
|
|
|
|
|
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_rels = rels_result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
# 按角色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)))
|
2026-02-06 16:44:09 +08:00
|
|
|
|
)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
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}")
|
2026-02-06 16:44:09 +08:00
|
|
|
|
if c.personality:
|
2026-02-12 12:40:12 +08:00
|
|
|
|
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))
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
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
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-01-19 17:23:50 +08:00
|
|
|
|
async def _get_last_ending_enhanced(
|
|
|
|
|
|
self,
|
|
|
|
|
|
chapter: Chapter,
|
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
|
max_length: int
|
|
|
|
|
|
) -> Dict[str, Any]:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
"""获取增强版衔接锚点(含上一章摘要和关键事件)"""
|
2026-01-19 17:23:50 +08:00
|
|
|
|
result_info = {
|
|
|
|
|
|
'ending_text': None,
|
|
|
|
|
|
'summary': None,
|
|
|
|
|
|
'key_events': []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if chapter.chapter_number <= 1:
|
|
|
|
|
|
return result_info
|
|
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# 查询上一章:不假设序号连续,取 chapter_number < 当前章 中最大的
|
2026-01-19 17:23:50 +08:00
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(Chapter)
|
|
|
|
|
|
.where(Chapter.project_id == chapter.project_id)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
.where(Chapter.chapter_number < chapter.chapter_number)
|
|
|
|
|
|
.order_by(Chapter.chapter_number.desc())
|
|
|
|
|
|
.limit(1)
|
2026-01-19 17:23:50 +08:00
|
|
|
|
)
|
|
|
|
|
|
prev_chapter = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
if not prev_chapter:
|
|
|
|
|
|
return result_info
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 提取结尾内容
|
|
|
|
|
|
if prev_chapter.content:
|
|
|
|
|
|
content = prev_chapter.content.strip()
|
|
|
|
|
|
if len(content) <= max_length:
|
|
|
|
|
|
result_info['ending_text'] = content
|
|
|
|
|
|
else:
|
|
|
|
|
|
result_info['ending_text'] = content[-max_length:]
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 获取上一章摘要
|
|
|
|
|
|
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:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
result_info['summary'] = summary_mem[:300]
|
2026-01-19 17:23:50 +08:00
|
|
|
|
elif prev_chapter.summary:
|
|
|
|
|
|
result_info['summary'] = prev_chapter.summary[:300]
|
|
|
|
|
|
elif prev_chapter.expansion_plan:
|
|
|
|
|
|
try:
|
|
|
|
|
|
plan = json.loads(prev_chapter.expansion_plan)
|
|
|
|
|
|
result_info['summary'] = plan.get('plot_summary', '')[:300]
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 提取上一章关键事件
|
|
|
|
|
|
if prev_chapter.expansion_plan:
|
|
|
|
|
|
try:
|
|
|
|
|
|
plan = json.loads(prev_chapter.expansion_plan)
|
|
|
|
|
|
key_events = plan.get('key_events', [])
|
|
|
|
|
|
if key_events:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
result_info['key_events'] = key_events[:5]
|
2026-01-19 17:23:50 +08:00
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
return result_info
|
|
|
|
|
|
|
2026-01-01 17:30:46 +08:00
|
|
|
|
def _extract_emotional_tone(
|
|
|
|
|
|
self,
|
|
|
|
|
|
chapter: Chapter,
|
|
|
|
|
|
outline: Optional[Outline]
|
|
|
|
|
|
) -> str:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
"""提取本章情感基调"""
|
2026-01-01 17:30:46 +08:00
|
|
|
|
if chapter.expansion_plan:
|
|
|
|
|
|
try:
|
|
|
|
|
|
plan = json.loads(chapter.expansion_plan)
|
|
|
|
|
|
tone = plan.get('emotional_tone')
|
|
|
|
|
|
if tone:
|
|
|
|
|
|
return tone
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if outline and outline.structure:
|
|
|
|
|
|
try:
|
|
|
|
|
|
structure = json.loads(outline.structure)
|
|
|
|
|
|
tone = structure.get('emotion') or structure.get('emotional_tone')
|
|
|
|
|
|
if tone:
|
|
|
|
|
|
return tone
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
return "未设定"
|
|
|
|
|
|
|
|
|
|
|
|
def _summarize_style(self, style_content: str) -> str:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
"""将风格描述压缩为关键要点"""
|
2026-01-01 17:30:46 +08:00
|
|
|
|
if not style_content:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
if len(style_content) <= self.STYLE_MAX_LENGTH:
|
|
|
|
|
|
return style_content
|
|
|
|
|
|
|
|
|
|
|
|
return style_content[:self.STYLE_MAX_LENGTH] + "..."
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_relevant_memories(
|
|
|
|
|
|
self,
|
|
|
|
|
|
user_id: str,
|
|
|
|
|
|
project_id: str,
|
|
|
|
|
|
chapter_number: int,
|
|
|
|
|
|
chapter_outline: str,
|
|
|
|
|
|
limit: int = 3
|
|
|
|
|
|
) -> Optional[str]:
|
2026-02-12 12:40:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取与本章最相关的记忆
|
|
|
|
|
|
|
|
|
|
|
|
注意:伏笔相关信息统一由 _get_foreshadow_reminders() 通过 foreshadow_service 提供,
|
|
|
|
|
|
此方法只负责获取故事记忆,不再从旧的 memory_service 获取伏笔信息。
|
|
|
|
|
|
"""
|
2026-01-01 17:30:46 +08:00
|
|
|
|
if not self.memory_service:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
relevant = await self.memory_service.search_memories(
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
|
query=chapter_outline,
|
|
|
|
|
|
limit=limit,
|
|
|
|
|
|
min_importance=self.MEMORY_IMPORTANCE_THRESHOLD
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
return self._format_memories(relevant, max_length=500)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ 获取相关记忆失败: {str(e)}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _format_memories(
|
|
|
|
|
|
self,
|
|
|
|
|
|
relevant: List[Dict[str, Any]],
|
|
|
|
|
|
max_length: int = 500
|
|
|
|
|
|
) -> str:
|
2026-02-12 12:40:12 +08:00
|
|
|
|
"""格式化记忆为简洁文本(纯记忆,不含伏笔)"""
|
|
|
|
|
|
if not relevant:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
lines = ["【相关记忆】"]
|
2026-01-01 17:30:46 +08:00
|
|
|
|
current_length = 0
|
|
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
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 len(lines) > 1 else None
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-01-19 17:23:50 +08:00
|
|
|
|
async def _get_foreshadow_reminders(
|
|
|
|
|
|
self,
|
|
|
|
|
|
project_id: str,
|
|
|
|
|
|
chapter_number: int,
|
|
|
|
|
|
db: AsyncSession
|
|
|
|
|
|
) -> Optional[str]:
|
|
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
获取伏笔提醒信息(增强版)
|
2026-01-19 17:23:50 +08:00
|
|
|
|
|
|
|
|
|
|
策略:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
1. 本章必须回收的伏笔(target_resolve_chapter_number == chapter_number)
|
|
|
|
|
|
2. 超期未回收的伏笔(target_resolve_chapter_number < chapter_number)
|
|
|
|
|
|
3. 即将到期的伏笔(target_resolve_chapter_number 在未来3章内)
|
2026-01-19 17:23:50 +08:00
|
|
|
|
"""
|
|
|
|
|
|
if not self.foreshadow_service:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
lines = []
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 本章必须回收的伏笔
|
|
|
|
|
|
must_resolve = await self.foreshadow_service.get_must_resolve_foreshadows(
|
2026-01-19 17:23:50 +08:00
|
|
|
|
db=db,
|
|
|
|
|
|
project_id=project_id,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
chapter_number=chapter_number
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if must_resolve:
|
|
|
|
|
|
lines.append("【🎯 本章必须回收的伏笔】")
|
|
|
|
|
|
for f in must_resolve:
|
|
|
|
|
|
lines.append(f"- {f.title}")
|
|
|
|
|
|
lines.append(f" 埋入章节:第{f.plant_chapter_number}章")
|
|
|
|
|
|
lines.append(f" 伏笔内容:{f.content[:100]}{'...' if len(f.content) > 100 else ''}")
|
|
|
|
|
|
if f.resolution_notes:
|
|
|
|
|
|
lines.append(f" 回收提示:{f.resolution_notes}")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 超期未回收的伏笔
|
|
|
|
|
|
overdue = await self.foreshadow_service.get_overdue_foreshadows(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
|
current_chapter=chapter_number
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if overdue:
|
|
|
|
|
|
lines.append("【⚠️ 超期待回收伏笔】")
|
|
|
|
|
|
for f in overdue[:3]: # 最多显示3个
|
|
|
|
|
|
overdue_chapters = chapter_number - (f.target_resolve_chapter_number or 0)
|
|
|
|
|
|
lines.append(f"- {f.title} [已超期{overdue_chapters}章]")
|
|
|
|
|
|
lines.append(f" 埋入章节:第{f.plant_chapter_number}章,原计划第{f.target_resolve_chapter_number}章回收")
|
|
|
|
|
|
lines.append(f" 伏笔内容:{f.content[:80]}...")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 即将到期的伏笔(未来3章内)
|
|
|
|
|
|
upcoming = await self.foreshadow_service.get_pending_resolve_foreshadows(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
|
current_chapter=chapter_number,
|
|
|
|
|
|
lookahead=3
|
2026-01-19 17:23:50 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# 过滤:只保留未来章节的,排除本章和超期的
|
|
|
|
|
|
upcoming_filtered = [f for f in upcoming
|
|
|
|
|
|
if (f.target_resolve_chapter_number or 0) > chapter_number]
|
|
|
|
|
|
|
|
|
|
|
|
if upcoming_filtered:
|
|
|
|
|
|
lines.append("【📋 即将到期的伏笔(仅供参考)】")
|
|
|
|
|
|
for f in upcoming_filtered[:3]: # 最多显示3个
|
|
|
|
|
|
remaining = (f.target_resolve_chapter_number or 0) - chapter_number
|
|
|
|
|
|
lines.append(f"- {f.title}(计划第{f.target_resolve_chapter_number}章回收,还有{remaining}章)")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines) if lines else None
|
2026-01-19 17:23:50 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ 获取伏笔提醒失败: {str(e)}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-01-01 17:30:46 +08:00
|
|
|
|
async def _build_story_skeleton(
|
|
|
|
|
|
self,
|
|
|
|
|
|
project_id: str,
|
|
|
|
|
|
chapter_number: int,
|
|
|
|
|
|
db: AsyncSession
|
|
|
|
|
|
) -> Optional[str]:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
"""构建故事骨架(每N章采样)"""
|
2026-01-01 17:30:46 +08:00
|
|
|
|
try:
|
|
|
|
|
|
result = await db.execute(
|
2026-02-06 16:44:09 +08:00
|
|
|
|
select(Chapter.id, Chapter.chapter_number, Chapter.title)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
.where(Chapter.project_id == project_id)
|
|
|
|
|
|
.where(Chapter.chapter_number < chapter_number)
|
|
|
|
|
|
.where(Chapter.content != None)
|
|
|
|
|
|
.where(Chapter.content != "")
|
|
|
|
|
|
.order_by(Chapter.chapter_number)
|
|
|
|
|
|
)
|
|
|
|
|
|
chapters = result.all()
|
|
|
|
|
|
|
|
|
|
|
|
if not chapters:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
skeleton_lines = ["【故事骨架】"]
|
2026-02-06 16:44:09 +08:00
|
|
|
|
for i, (ch_id, ch_num, ch_title) in enumerate(chapters):
|
2026-01-01 17:30:46 +08:00
|
|
|
|
if i % self.SKELETON_SAMPLE_INTERVAL == 0:
|
|
|
|
|
|
summary_result = await db.execute(
|
|
|
|
|
|
select(StoryMemory.content)
|
|
|
|
|
|
.where(StoryMemory.project_id == project_id)
|
2026-02-06 16:44:09 +08:00
|
|
|
|
.where(StoryMemory.chapter_id == ch_id)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
.where(StoryMemory.memory_type == 'chapter_summary')
|
|
|
|
|
|
.limit(1)
|
|
|
|
|
|
)
|
|
|
|
|
|
summary = summary_result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
if summary:
|
|
|
|
|
|
skeleton_lines.append(f"第{ch_num}章《{ch_title}》:{summary[:100]}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
skeleton_lines.append(f"第{ch_num}章《{ch_title}》")
|
|
|
|
|
|
|
|
|
|
|
|
if len(skeleton_lines) <= 1:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return "\n".join(skeleton_lines)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ 构建故事骨架失败: {str(e)}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# ==================== 1-1模式上下文构建器 ====================
|
|
|
|
|
|
|
|
|
|
|
|
class OneToOneContextBuilder:
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
1-1模式上下文构建器
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
上下文构建策略:
|
|
|
|
|
|
P0核心信息:
|
|
|
|
|
|
1. 从outline.structure的JSON中提取:summary, scenes, key_points, emotion, goal
|
|
|
|
|
|
2. target_word_count
|
|
|
|
|
|
|
|
|
|
|
|
P1重要信息:
|
|
|
|
|
|
1. 上一章完整内容的最后500字作为参考
|
|
|
|
|
|
2. 根据structure中的characters获取角色信息(含职业)
|
|
|
|
|
|
|
|
|
|
|
|
P2参考信息:
|
|
|
|
|
|
1. 伏笔提醒
|
|
|
|
|
|
2. 根据角色名检索相关记忆(相关度>0.6)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
def __init__(self, memory_service=None, foreshadow_service=None):
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
初始化构建器
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
memory_service: 记忆服务实例(可选)
|
|
|
|
|
|
foreshadow_service: 伏笔服务实例(可选)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
|
|
|
|
|
self.memory_service = memory_service
|
2026-02-06 16:44:09 +08:00
|
|
|
|
self.foreshadow_service = foreshadow_service
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
async def build(
|
2026-01-01 17:30:46 +08:00
|
|
|
|
self,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
chapter: Chapter,
|
|
|
|
|
|
project: Project,
|
|
|
|
|
|
outline: Optional[Outline],
|
2026-01-01 17:30:46 +08:00
|
|
|
|
user_id: str,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
db: AsyncSession,
|
|
|
|
|
|
target_word_count: int = 3000
|
|
|
|
|
|
) -> OneToOneContext:
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
构建1-1模式上下文
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
chapter: 章节对象
|
|
|
|
|
|
project: 项目对象
|
|
|
|
|
|
outline: 大纲对象
|
2026-01-01 17:30:46 +08:00
|
|
|
|
user_id: 用户ID
|
2026-02-06 16:44:09 +08:00
|
|
|
|
db: 数据库会话
|
|
|
|
|
|
target_word_count: 目标字数
|
|
|
|
|
|
|
2026-01-01 17:30:46 +08:00
|
|
|
|
Returns:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
OneToOneContext: 上下文对象
|
2026-01-01 17:30:46 +08:00
|
|
|
|
"""
|
2026-02-06 16:44:09 +08:00
|
|
|
|
chapter_number = chapter.chapter_number
|
|
|
|
|
|
logger.info(f"📝 [1-1模式] 开始构建上下文: 第{chapter_number}章")
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# 初始化上下文
|
|
|
|
|
|
context = OneToOneContext(
|
|
|
|
|
|
chapter_number=chapter_number,
|
|
|
|
|
|
chapter_title=chapter.title or "",
|
|
|
|
|
|
title=project.title or "",
|
|
|
|
|
|
genre=project.genre or "",
|
|
|
|
|
|
theme=project.theme or "",
|
|
|
|
|
|
target_word_count=target_word_count,
|
|
|
|
|
|
min_word_count=max(500, target_word_count - 500),
|
|
|
|
|
|
max_word_count=target_word_count + 1000,
|
|
|
|
|
|
narrative_perspective=project.narrative_perspective or "第三人称"
|
2026-01-01 17:30:46 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# === P0-核心信息 ===
|
|
|
|
|
|
context.chapter_outline = self._build_outline_from_structure(outline, chapter)
|
|
|
|
|
|
logger.info(f" ✅ P0-大纲信息: {len(context.chapter_outline)}字符")
|
|
|
|
|
|
|
|
|
|
|
|
# === P1-重要信息 ===
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# 1. 获取上一章内容的最后500字和上一章摘要
|
2026-02-06 16:44:09 +08:00
|
|
|
|
if chapter_number > 1:
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# 查找前一章:不假设序号连续,取 chapter_number < 当前章 中最大的
|
2026-02-06 16:44:09 +08:00
|
|
|
|
prev_chapter_result = await db.execute(
|
|
|
|
|
|
select(Chapter)
|
|
|
|
|
|
.where(Chapter.project_id == chapter.project_id)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
.where(Chapter.chapter_number < chapter_number)
|
|
|
|
|
|
.order_by(Chapter.chapter_number.desc())
|
|
|
|
|
|
.limit(1)
|
2026-02-06 16:44:09 +08:00
|
|
|
|
)
|
|
|
|
|
|
prev_chapter = prev_chapter_result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
if prev_chapter and prev_chapter.content:
|
|
|
|
|
|
content = prev_chapter.content.strip()
|
|
|
|
|
|
if len(content) <= 500:
|
|
|
|
|
|
context.continuation_point = content
|
|
|
|
|
|
else:
|
|
|
|
|
|
context.continuation_point = content[-500:]
|
|
|
|
|
|
logger.info(f" ✅ P1-上一章内容(最后500字): {len(context.continuation_point)}字符")
|
2026-02-12 12:40:12 +08:00
|
|
|
|
|
|
|
|
|
|
# 获取上一章摘要(优先从记忆系统获取,其次使用章节摘要)
|
|
|
|
|
|
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-上一章摘要: 无")
|
2026-02-06 16:44:09 +08:00
|
|
|
|
else:
|
|
|
|
|
|
context.continuation_point = None
|
2026-02-12 12:40:12 +08:00
|
|
|
|
context.previous_chapter_summary = None
|
2026-02-06 16:44:09 +08:00
|
|
|
|
logger.info(f" ⚠️ P1-上一章内容: 无")
|
|
|
|
|
|
else:
|
|
|
|
|
|
context.continuation_point = None
|
2026-02-12 12:40:12 +08:00
|
|
|
|
context.previous_chapter_summary = None
|
2026-02-06 16:44:09 +08:00
|
|
|
|
logger.info(f" ✅ P1-第1章无需上一章内容")
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 根据structure中的characters获取角色信息(含职业)
|
|
|
|
|
|
character_names = []
|
|
|
|
|
|
if outline and outline.structure:
|
|
|
|
|
|
try:
|
|
|
|
|
|
structure = json.loads(outline.structure)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
raw_characters = structure.get('characters', [])
|
|
|
|
|
|
# characters可能是字符串列表或字典列表,统一提取为名称字符串列表
|
|
|
|
|
|
character_names = [
|
|
|
|
|
|
c['name'] if isinstance(c, dict) else c
|
|
|
|
|
|
for c in raw_characters
|
|
|
|
|
|
]
|
2026-02-06 16:44:09 +08:00
|
|
|
|
logger.info(f" 📋 从structure提取角色: {character_names}")
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if character_names:
|
|
|
|
|
|
# 获取角色基本信息
|
|
|
|
|
|
characters_result = await db.execute(
|
|
|
|
|
|
select(Character)
|
|
|
|
|
|
.where(Character.project_id == project.id)
|
|
|
|
|
|
.where(Character.name.in_(character_names))
|
|
|
|
|
|
)
|
|
|
|
|
|
characters = characters_result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
if characters:
|
|
|
|
|
|
# 构建包含职业信息的角色上下文和职业详情
|
|
|
|
|
|
characters_info, careers_info = await self._build_characters_and_careers(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
project_id=project.id,
|
|
|
|
|
|
characters=characters,
|
|
|
|
|
|
filter_character_names=character_names
|
|
|
|
|
|
)
|
|
|
|
|
|
context.chapter_characters = characters_info
|
|
|
|
|
|
context.chapter_careers = careers_info
|
|
|
|
|
|
logger.info(f" ✅ P1-角色信息: {len(context.chapter_characters)}字符")
|
|
|
|
|
|
logger.info(f" ✅ P1-职业信息: {len(context.chapter_careers or '')}字符")
|
|
|
|
|
|
else:
|
|
|
|
|
|
context.chapter_characters = "暂无角色信息"
|
|
|
|
|
|
context.chapter_careers = None
|
|
|
|
|
|
logger.info(f" ⚠️ P1-角色信息: 筛选后无匹配角色")
|
|
|
|
|
|
else:
|
|
|
|
|
|
context.chapter_characters = "暂无角色信息"
|
|
|
|
|
|
context.chapter_careers = None
|
|
|
|
|
|
logger.info(f" ⚠️ P1-角色信息: 无")
|
|
|
|
|
|
|
|
|
|
|
|
# === P2-参考信息 ===
|
|
|
|
|
|
# 1. 伏笔提醒
|
|
|
|
|
|
if self.foreshadow_service:
|
|
|
|
|
|
context.foreshadow_reminders = await self._get_foreshadow_reminders(
|
|
|
|
|
|
project.id, chapter_number, db
|
|
|
|
|
|
)
|
|
|
|
|
|
if context.foreshadow_reminders:
|
|
|
|
|
|
logger.info(f" ✅ P2-伏笔提醒: {len(context.foreshadow_reminders)}字符")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f" ⚠️ P2-伏笔提醒: 无")
|
|
|
|
|
|
|
2026-02-07 15:39:15 +08:00
|
|
|
|
# 2. 根据大纲内容检索相关记忆(相关度>0.4)
|
|
|
|
|
|
if self.memory_service and context.chapter_outline:
|
2026-02-06 16:44:09 +08:00
|
|
|
|
try:
|
2026-02-07 15:39:15 +08:00
|
|
|
|
# 使用大纲内容作为查询(截取前500字符以避免过长)
|
|
|
|
|
|
query_text = context.chapter_outline[:500].replace('\n', ' ')
|
|
|
|
|
|
logger.info(f" 🔍 记忆查询关键词: {query_text[:100]}...")
|
2026-02-06 16:44:09 +08:00
|
|
|
|
|
|
|
|
|
|
relevant_memories = await self.memory_service.search_memories(
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
project_id=project.id,
|
|
|
|
|
|
query=query_text,
|
2026-02-07 15:39:15 +08:00
|
|
|
|
limit=15,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
min_importance=0.0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# 过滤相关度阈值为0.6
|
2026-02-06 16:44:09 +08:00
|
|
|
|
filtered_memories = [
|
|
|
|
|
|
mem for mem in relevant_memories
|
|
|
|
|
|
if mem.get('similarity', 0) > 0.6
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if filtered_memories:
|
|
|
|
|
|
memory_lines = ["【相关记忆】"]
|
2026-02-07 15:39:15 +08:00
|
|
|
|
for mem in filtered_memories[:10]: # 最多显示10条
|
2026-02-06 16:44:09 +08:00
|
|
|
|
similarity = mem.get('similarity', 0)
|
|
|
|
|
|
content = mem.get('content', '')[:100]
|
|
|
|
|
|
memory_lines.append(f"- (相关度:{similarity:.2f}) {content}")
|
|
|
|
|
|
|
|
|
|
|
|
context.relevant_memories = "\n".join(memory_lines)
|
2026-02-12 12:40:12 +08:00
|
|
|
|
logger.info(f" ✅ P2-相关记忆: {len(filtered_memories)}条 (相关度>0.6, 共搜索{len(relevant_memories)}条)")
|
2026-02-06 16:44:09 +08:00
|
|
|
|
else:
|
|
|
|
|
|
context.relevant_memories = None
|
2026-02-07 15:39:15 +08:00
|
|
|
|
logger.info(f" ⚠️ P2-相关记忆: 无符合条件的记忆 (共搜索到{len(relevant_memories)}条)")
|
2026-02-06 16:44:09 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f" ❌ 检索相关记忆失败: {str(e)}")
|
|
|
|
|
|
context.relevant_memories = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
context.relevant_memories = None
|
2026-02-07 15:39:15 +08:00
|
|
|
|
logger.info(f" ⚠️ P2-相关记忆: 无大纲内容或记忆服务不可用")
|
2026-02-06 16:44:09 +08:00
|
|
|
|
|
|
|
|
|
|
# === 统计信息 ===
|
|
|
|
|
|
context.context_stats = {
|
|
|
|
|
|
"mode": "one-to-one",
|
|
|
|
|
|
"chapter_number": chapter_number,
|
|
|
|
|
|
"has_previous_content": context.continuation_point is not None,
|
|
|
|
|
|
"previous_content_length": len(context.continuation_point or ""),
|
2026-02-12 12:40:12 +08:00
|
|
|
|
"previous_summary_length": len(context.previous_chapter_summary or ""),
|
2026-02-06 16:44:09 +08:00
|
|
|
|
"outline_length": len(context.chapter_outline),
|
|
|
|
|
|
"characters_length": len(context.chapter_characters),
|
|
|
|
|
|
"careers_length": len(context.chapter_careers or ""),
|
|
|
|
|
|
"foreshadow_length": len(context.foreshadow_reminders or ""),
|
|
|
|
|
|
"memories_length": len(context.relevant_memories or ""),
|
|
|
|
|
|
"total_length": context.get_total_context_length()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"📊 [1-1模式] 上下文构建完成: 总长度 {context.context_stats['total_length']} 字符")
|
|
|
|
|
|
|
|
|
|
|
|
return context
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
def _build_outline_from_structure(
|
2026-01-01 17:30:46 +08:00
|
|
|
|
self,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
outline: Optional[Outline],
|
|
|
|
|
|
chapter: Chapter
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
"""从outline.structure提取大纲信息(1-1模式专用)"""
|
|
|
|
|
|
if outline and outline.structure:
|
|
|
|
|
|
try:
|
|
|
|
|
|
structure = json.loads(outline.structure)
|
|
|
|
|
|
|
|
|
|
|
|
outline_parts = []
|
|
|
|
|
|
|
|
|
|
|
|
if structure.get('summary'):
|
|
|
|
|
|
outline_parts.append(f"【章节概要】\n{structure['summary']}")
|
|
|
|
|
|
|
|
|
|
|
|
if structure.get('scenes'):
|
|
|
|
|
|
scenes_text = "\n".join([f"- {scene}" for scene in structure['scenes']])
|
|
|
|
|
|
outline_parts.append(f"【场景设定】\n{scenes_text}")
|
|
|
|
|
|
|
|
|
|
|
|
if structure.get('key_points'):
|
|
|
|
|
|
points_text = "\n".join([f"- {point}" for point in structure['key_points']])
|
|
|
|
|
|
outline_parts.append(f"【情节要点】\n{points_text}")
|
|
|
|
|
|
|
|
|
|
|
|
if structure.get('emotion'):
|
|
|
|
|
|
outline_parts.append(f"【情感基调】\n{structure['emotion']}")
|
|
|
|
|
|
|
|
|
|
|
|
if structure.get('goal'):
|
|
|
|
|
|
outline_parts.append(f"【叙事目标】\n{structure['goal']}")
|
|
|
|
|
|
|
|
|
|
|
|
return "\n\n".join(outline_parts)
|
|
|
|
|
|
|
|
|
|
|
|
except json.JSONDecodeError as e:
|
|
|
|
|
|
logger.error(f" ❌ 解析outline.structure失败: {e}")
|
|
|
|
|
|
return outline.content if outline else "暂无大纲"
|
|
|
|
|
|
else:
|
|
|
|
|
|
return outline.content if outline else "暂无大纲"
|
|
|
|
|
|
|
|
|
|
|
|
async def _build_characters_and_careers(
|
|
|
|
|
|
self,
|
|
|
|
|
|
db: AsyncSession,
|
2026-01-01 17:30:46 +08:00
|
|
|
|
project_id: str,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
characters: list,
|
|
|
|
|
|
filter_character_names: Optional[list] = None
|
|
|
|
|
|
) -> tuple[str, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
构建角色信息和职业信息(1-1模式专用)
|
|
|
|
|
|
获取角色的完整数据,并关联查询每个职业的完整数据
|
|
|
|
|
|
分别返回角色信息和职业信息
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
db: 数据库会话
|
|
|
|
|
|
project_id: 项目ID
|
|
|
|
|
|
characters: 角色列表
|
|
|
|
|
|
filter_character_names: 筛选的角色名称列表
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
tuple: (角色信息字符串, 职业信息字符串)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not characters:
|
|
|
|
|
|
return '暂无角色信息', None
|
|
|
|
|
|
|
|
|
|
|
|
# 如果提供了筛选名单,只保留匹配的角色
|
|
|
|
|
|
if filter_character_names:
|
|
|
|
|
|
filtered_characters = [c for c in characters if c.name in filter_character_names]
|
|
|
|
|
|
if not filtered_characters:
|
|
|
|
|
|
logger.warning(f"筛选后无匹配角色,使用全部角色。筛选名单: {filter_character_names}")
|
|
|
|
|
|
filtered_characters = characters
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f"根据筛选名单保留 {len(filtered_characters)}/{len(characters)} 个角色: {[c.name for c in filtered_characters]}")
|
|
|
|
|
|
characters = filtered_characters
|
|
|
|
|
|
|
|
|
|
|
|
# 获取角色ID列表
|
|
|
|
|
|
character_ids = [c.id for c in characters]
|
|
|
|
|
|
if not character_ids:
|
|
|
|
|
|
return '暂无角色信息', None
|
|
|
|
|
|
|
|
|
|
|
|
# 重新查询角色的完整数据(确保获取所有字段)
|
|
|
|
|
|
full_characters_result = await db.execute(
|
|
|
|
|
|
select(Character).where(Character.id.in_(character_ids))
|
2026-01-01 17:30:46 +08:00
|
|
|
|
)
|
2026-02-06 16:44:09 +08:00
|
|
|
|
full_characters = {c.id: c for c in full_characters_result.scalars().all()}
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# 获取所有角色的职业关联数据
|
|
|
|
|
|
character_careers_result = await db.execute(
|
|
|
|
|
|
select(CharacterCareer).where(CharacterCareer.character_id.in_(character_ids))
|
|
|
|
|
|
)
|
|
|
|
|
|
character_careers = character_careers_result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
# 收集所有需要查询的职业ID
|
|
|
|
|
|
career_ids = set()
|
|
|
|
|
|
for cc in character_careers:
|
|
|
|
|
|
career_ids.add(cc.career_id)
|
|
|
|
|
|
|
|
|
|
|
|
# 查询所有相关职业的完整数据
|
|
|
|
|
|
careers_map = {}
|
|
|
|
|
|
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()}
|
|
|
|
|
|
logger.info(f" 📋 查询到 {len(careers_map)} 个职业的完整数据")
|
|
|
|
|
|
|
|
|
|
|
|
# 构建角色ID到职业关联数据的映射
|
|
|
|
|
|
char_career_relations = {}
|
|
|
|
|
|
for cc in character_careers:
|
|
|
|
|
|
if cc.character_id not in char_career_relations:
|
|
|
|
|
|
char_career_relations[cc.character_id] = {'main': [], 'sub': []}
|
|
|
|
|
|
|
|
|
|
|
|
# 保存完整的CharacterCareer对象
|
|
|
|
|
|
if cc.career_type == 'main':
|
|
|
|
|
|
char_career_relations[cc.character_id]['main'].append(cc)
|
|
|
|
|
|
else:
|
|
|
|
|
|
char_career_relations[cc.character_id]['sub'].append(cc)
|
|
|
|
|
|
|
|
|
|
|
|
# 构建角色信息字符串
|
|
|
|
|
|
characters_info_parts = []
|
|
|
|
|
|
for char_id in character_ids[:10]: # 限制最多10个角色
|
|
|
|
|
|
c = full_characters.get(char_id)
|
|
|
|
|
|
if not c:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# === 角色基本信息 ===
|
|
|
|
|
|
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 char_id in char_career_relations:
|
|
|
|
|
|
career_relations = char_career_relations[char_id]
|
|
|
|
|
|
|
|
|
|
|
|
# 主职业
|
|
|
|
|
|
if career_relations['main']:
|
|
|
|
|
|
for cc in career_relations['main']:
|
|
|
|
|
|
career = careers_map.get(cc.career_id)
|
|
|
|
|
|
if career:
|
|
|
|
|
|
# 解析职业的完整阶段信息
|
|
|
|
|
|
try:
|
|
|
|
|
|
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
|
|
|
|
|
|
current_stage_info = None
|
|
|
|
|
|
for stage in stages:
|
|
|
|
|
|
if stage.get('level') == cc.current_stage:
|
|
|
|
|
|
current_stage_info = stage
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
stage_name = current_stage_info.get('name', f'第{cc.current_stage}阶') if current_stage_info else f'第{cc.current_stage}阶'
|
|
|
|
|
|
except (json.JSONDecodeError, AttributeError, TypeError) as e:
|
|
|
|
|
|
logger.warning(f"解析职业阶段信息失败: {e}")
|
|
|
|
|
|
stage_name = f'第{cc.current_stage}阶'
|
|
|
|
|
|
stage_desc = ''
|
|
|
|
|
|
|
|
|
|
|
|
# 构建主职业信息(只显示引用,详细信息在下面的"本章职业"部分)
|
|
|
|
|
|
info_lines.append(f" 主职业: {career.name} ({cc.current_stage}/{career.max_stage}阶 - {stage_name})")
|
|
|
|
|
|
|
|
|
|
|
|
# 副职业
|
|
|
|
|
|
if career_relations['sub']:
|
|
|
|
|
|
info_lines.append(f" 副职业:")
|
|
|
|
|
|
for cc in career_relations['sub']:
|
|
|
|
|
|
career = careers_map.get(cc.career_id)
|
|
|
|
|
|
if career:
|
|
|
|
|
|
# 解析副职业阶段信息
|
|
|
|
|
|
try:
|
|
|
|
|
|
stages = json.loads(career.stages) if isinstance(career.stages, str) else career.stages
|
|
|
|
|
|
current_stage_info = None
|
|
|
|
|
|
for stage in stages:
|
|
|
|
|
|
if stage.get('level') == cc.current_stage:
|
|
|
|
|
|
current_stage_info = stage
|
|
|
|
|
|
break
|
|
|
|
|
|
stage_name = current_stage_info.get('name', f'第{cc.current_stage}阶') if current_stage_info else f'第{cc.current_stage}阶'
|
|
|
|
|
|
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})")
|
|
|
|
|
|
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# === 角色关系信息 ===
|
|
|
|
|
|
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)}")
|
|
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
# === 组织特有信息 ===
|
|
|
|
|
|
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]}")
|
2026-02-12 12:40:12 +08:00
|
|
|
|
# 从 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]}")
|
2026-02-06 16:44:09 +08:00
|
|
|
|
|
|
|
|
|
|
# 组合完整信息
|
|
|
|
|
|
full_info = "\n".join(info_lines)
|
|
|
|
|
|
characters_info_parts.append(full_info)
|
|
|
|
|
|
|
|
|
|
|
|
characters_result = "\n\n".join(characters_info_parts)
|
|
|
|
|
|
logger.info(f" ✅ 构建了 {len(characters_info_parts)} 个角色的完整信息,总长度: {len(characters_result)} 字符")
|
|
|
|
|
|
|
|
|
|
|
|
# === 构建职业信息部分 ===
|
|
|
|
|
|
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) as e:
|
|
|
|
|
|
logger.warning(f"解析职业阶段失败: {e}")
|
|
|
|
|
|
career_lines.append(f" 阶段体系: 共{career.max_stage}阶")
|
|
|
|
|
|
|
|
|
|
|
|
# 职业要求
|
|
|
|
|
|
if career.requirements:
|
|
|
|
|
|
career_lines.append(f" 职业要求: {career.requirements}")
|
|
|
|
|
|
|
|
|
|
|
|
# 特殊能力
|
|
|
|
|
|
if career.special_abilities:
|
|
|
|
|
|
career_lines.append(f" 特殊能力: {career.special_abilities}")
|
|
|
|
|
|
|
|
|
|
|
|
# 世界观规则
|
|
|
|
|
|
if career.worldview_rules:
|
|
|
|
|
|
career_lines.append(f" 世界观规则: {career.worldview_rules}")
|
|
|
|
|
|
|
|
|
|
|
|
# 属性加成
|
|
|
|
|
|
if career.attribute_bonuses:
|
|
|
|
|
|
try:
|
|
|
|
|
|
bonuses = json.loads(career.attribute_bonuses) if isinstance(career.attribute_bonuses, str) else career.attribute_bonuses
|
|
|
|
|
|
if bonuses:
|
|
|
|
|
|
bonus_str = ", ".join([f"{k}:{v}" for k, v in bonuses.items()])
|
|
|
|
|
|
career_lines.append(f" 属性加成: {bonus_str}")
|
|
|
|
|
|
except (json.JSONDecodeError, AttributeError, TypeError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
careers_info_parts.append("\n".join(career_lines))
|
|
|
|
|
|
|
|
|
|
|
|
careers_result = None
|
|
|
|
|
|
if careers_info_parts: # 有职业数据就返回
|
|
|
|
|
|
careers_result = "\n\n".join(careers_info_parts)
|
|
|
|
|
|
logger.info(f" ✅ 构建了 {len(careers_map)} 个职业的完整信息,总长度: {len(careers_result)} 字符")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f" ⚠️ 本章无涉及职业")
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
return characters_result, careers_result
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
async def _get_foreshadow_reminders(
|
2026-01-01 17:30:46 +08:00
|
|
|
|
self,
|
2026-02-06 16:44:09 +08:00
|
|
|
|
project_id: str,
|
|
|
|
|
|
chapter_number: int,
|
|
|
|
|
|
db: AsyncSession
|
|
|
|
|
|
) -> Optional[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取伏笔提醒信息(增强版)
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
策略:
|
|
|
|
|
|
1. 本章必须回收的伏笔(target_resolve_chapter_number == chapter_number)
|
|
|
|
|
|
2. 超期未回收的伏笔(target_resolve_chapter_number < chapter_number)
|
|
|
|
|
|
3. 即将到期的伏笔(target_resolve_chapter_number 在未来3章内)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.foreshadow_service:
|
|
|
|
|
|
return None
|
2026-01-01 17:30:46 +08:00
|
|
|
|
|
2026-02-06 16:44:09 +08:00
|
|
|
|
try:
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 本章必须回收的伏笔
|
|
|
|
|
|
must_resolve = await self.foreshadow_service.get_must_resolve_foreshadows(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
|
chapter_number=chapter_number
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if must_resolve:
|
|
|
|
|
|
lines.append("【🎯 本章必须回收的伏笔】")
|
|
|
|
|
|
for f in must_resolve:
|
|
|
|
|
|
lines.append(f"- {f.title}")
|
|
|
|
|
|
lines.append(f" 埋入章节:第{f.plant_chapter_number}章")
|
|
|
|
|
|
lines.append(f" 伏笔内容:{f.content[:100]}{'...' if len(f.content) > 100 else ''}")
|
|
|
|
|
|
if f.resolution_notes:
|
|
|
|
|
|
lines.append(f" 回收提示:{f.resolution_notes}")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 超期未回收的伏笔
|
|
|
|
|
|
overdue = await self.foreshadow_service.get_overdue_foreshadows(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
|
current_chapter=chapter_number
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if overdue:
|
|
|
|
|
|
lines.append("【⚠️ 超期待回收伏笔】")
|
|
|
|
|
|
for f in overdue[:3]: # 最多显示3个
|
|
|
|
|
|
overdue_chapters = chapter_number - (f.target_resolve_chapter_number or 0)
|
|
|
|
|
|
lines.append(f"- {f.title} [已超期{overdue_chapters}章]")
|
|
|
|
|
|
lines.append(f" 埋入章节:第{f.plant_chapter_number}章,原计划第{f.target_resolve_chapter_number}章回收")
|
|
|
|
|
|
lines.append(f" 伏笔内容:{f.content[:80]}...")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 即将到期的伏笔(未来3章内)
|
|
|
|
|
|
upcoming = await self.foreshadow_service.get_pending_resolve_foreshadows(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
|
current_chapter=chapter_number,
|
|
|
|
|
|
lookahead=3
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 过滤:只保留未来章节的,排除本章和超期的
|
|
|
|
|
|
upcoming_filtered = [f for f in upcoming
|
|
|
|
|
|
if (f.target_resolve_chapter_number or 0) > chapter_number]
|
|
|
|
|
|
|
|
|
|
|
|
if upcoming_filtered:
|
|
|
|
|
|
lines.append("【📋 即将到期的伏笔(仅供参考)】")
|
|
|
|
|
|
for f in upcoming_filtered[:3]: # 最多显示3个
|
|
|
|
|
|
remaining = (f.target_resolve_chapter_number or 0) - chapter_number
|
|
|
|
|
|
lines.append(f"- {f.title}(计划第{f.target_resolve_chapter_number}章回收,还有{remaining}章)")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines) if lines else None
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ 获取伏笔提醒失败: {str(e)}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|