update: 1.新增职业管理模块和角色职业关联 2.章节分析自动更新角色职业状态 3.优化章节生成的角色信息构建 4.批量生成强制开启同步分析 5.章节内容批量生成增加系统提示
This commit is contained in:
@@ -297,6 +297,39 @@ class AutoCharacterService:
|
||||
) -> Dict[str, Any]:
|
||||
"""生成角色详细信息"""
|
||||
|
||||
# 🎯 获取项目职业列表
|
||||
from app.models.career import Career
|
||||
careers_result = await db.execute(
|
||||
select(Career)
|
||||
.where(Career.project_id == project.id)
|
||||
.order_by(Career.type, Career.name)
|
||||
)
|
||||
careers = careers_result.scalars().all()
|
||||
|
||||
# 构建职业信息摘要(包含最高阶段信息)
|
||||
careers_info = ""
|
||||
if careers:
|
||||
main_careers = [c for c in careers if c.type == 'main']
|
||||
sub_careers = [c for c in careers if c.type == 'sub']
|
||||
|
||||
if main_careers:
|
||||
careers_info += "\n\n可用主职业列表(请在career_info中填写职业名称和阶段):\n"
|
||||
for career in main_careers:
|
||||
careers_info += f"- 名称: {career.name}, 最高阶段: {career.max_stage}阶"
|
||||
if career.description:
|
||||
careers_info += f", 描述: {career.description[:50]}"
|
||||
careers_info += "\n"
|
||||
|
||||
if sub_careers:
|
||||
careers_info += "\n可用副职业列表(请在career_info中填写职业名称和阶段):\n"
|
||||
for career in sub_careers[:5]:
|
||||
careers_info += f"- 名称: {career.name}, 最高阶段: {career.max_stage}阶"
|
||||
if career.description:
|
||||
careers_info += f", 描述: {career.description[:50]}"
|
||||
careers_info += "\n"
|
||||
|
||||
careers_info += "\n⚠️ 重要提示:生成角色时,职业阶段不能超过该职业的最高阶段!\n"
|
||||
|
||||
# 构建角色生成提示词
|
||||
template = await PromptService.get_template(
|
||||
"AUTO_CHARACTER_GENERATION",
|
||||
@@ -315,7 +348,7 @@ class AutoCharacterService:
|
||||
location=project.world_location or "未设定",
|
||||
atmosphere=project.world_atmosphere or "未设定",
|
||||
rules=project.world_rules or "未设定",
|
||||
existing_characters=existing_chars_summary,
|
||||
existing_characters=existing_chars_summary + careers_info,
|
||||
plot_context="根据剧情需要引入的新角色",
|
||||
character_specification=json.dumps(spec, ensure_ascii=False, indent=2),
|
||||
mcp_references="" # 暂时不使用MCP增强
|
||||
@@ -367,6 +400,66 @@ class AutoCharacterService:
|
||||
|
||||
is_organization = character_data.get("is_organization", False)
|
||||
|
||||
# 提取职业信息(支持通过名称匹配)
|
||||
career_info = character_data.get("career_info", {})
|
||||
raw_main_career_name = career_info.get("main_career_name") if career_info else None
|
||||
main_career_stage = career_info.get("main_career_stage", 1) if career_info else None
|
||||
raw_sub_careers_data = career_info.get("sub_careers", []) if career_info else []
|
||||
|
||||
# 🔧 通过职业名称匹配数据库中的职业ID
|
||||
from app.models.career import Career, CharacterCareer
|
||||
main_career_id = None
|
||||
sub_careers_data = []
|
||||
|
||||
# 匹配主职业名称
|
||||
if raw_main_career_name and not is_organization:
|
||||
career_check = await db.execute(
|
||||
select(Career).where(
|
||||
Career.name == raw_main_career_name,
|
||||
Career.project_id == project_id,
|
||||
Career.type == 'main'
|
||||
)
|
||||
)
|
||||
matched_career = career_check.scalar_one_or_none()
|
||||
if matched_career:
|
||||
main_career_id = matched_career.id
|
||||
# ✅ 验证阶段不超过最高阶段
|
||||
if main_career_stage and main_career_stage > matched_career.max_stage:
|
||||
logger.warning(f" ⚠️ AI返回的主职业阶段({main_career_stage})超过最高阶段({matched_career.max_stage}),自动修正为最高阶段")
|
||||
main_career_stage = matched_career.max_stage
|
||||
logger.info(f" ✅ 主职业名称匹配成功: {raw_main_career_name} -> ID: {main_career_id}, 阶段: {main_career_stage}/{matched_career.max_stage}")
|
||||
else:
|
||||
logger.warning(f" ⚠️ AI返回的主职业名称未找到: {raw_main_career_name}")
|
||||
|
||||
# 匹配副职业名称
|
||||
if raw_sub_careers_data and not is_organization and isinstance(raw_sub_careers_data, list):
|
||||
for sub_data in raw_sub_careers_data[:2]:
|
||||
if isinstance(sub_data, dict):
|
||||
career_name = sub_data.get('career_name')
|
||||
if career_name:
|
||||
career_check = await db.execute(
|
||||
select(Career).where(
|
||||
Career.name == career_name,
|
||||
Career.project_id == project_id,
|
||||
Career.type == 'sub'
|
||||
)
|
||||
)
|
||||
matched_career = career_check.scalar_one_or_none()
|
||||
if matched_career:
|
||||
sub_stage = sub_data.get('stage', 1)
|
||||
# ✅ 验证阶段不超过最高阶段
|
||||
if sub_stage > matched_career.max_stage:
|
||||
logger.warning(f" ⚠️ AI返回的副职业阶段({sub_stage})超过最高阶段({matched_career.max_stage}),自动修正为最高阶段")
|
||||
sub_stage = matched_career.max_stage
|
||||
|
||||
sub_careers_data.append({
|
||||
'career_id': matched_career.id,
|
||||
'stage': sub_stage
|
||||
})
|
||||
logger.info(f" ✅ 副职业名称匹配成功: {career_name} -> ID: {matched_career.id}, 阶段: {sub_stage}/{matched_career.max_stage}")
|
||||
else:
|
||||
logger.warning(f" ⚠️ AI返回的副职业名称未找到: {career_name}")
|
||||
|
||||
# 创建角色
|
||||
character = Character(
|
||||
project_id=project_id,
|
||||
@@ -381,12 +474,40 @@ class AutoCharacterService:
|
||||
relationships=character_data.get("relationships_text", ""),
|
||||
organization_type=character_data.get("organization_type") if is_organization else None,
|
||||
organization_purpose=character_data.get("organization_purpose") if is_organization else None,
|
||||
traits=json.dumps(character_data.get("traits", []), ensure_ascii=False) if character_data.get("traits") else None
|
||||
traits=json.dumps(character_data.get("traits", []), ensure_ascii=False) if character_data.get("traits") else None,
|
||||
main_career_id=main_career_id,
|
||||
main_career_stage=main_career_stage if main_career_id else None,
|
||||
sub_careers=json.dumps(sub_careers_data, ensure_ascii=False) if sub_careers_data else None
|
||||
)
|
||||
|
||||
db.add(character)
|
||||
await db.flush()
|
||||
|
||||
# 处理主职业关联
|
||||
if main_career_id and not is_organization:
|
||||
char_career = CharacterCareer(
|
||||
character_id=character.id,
|
||||
career_id=main_career_id,
|
||||
career_type='main',
|
||||
current_stage=main_career_stage,
|
||||
stage_progress=0
|
||||
)
|
||||
db.add(char_career)
|
||||
logger.info(f" ✅ 创建主职业关联: {character.name} -> {raw_main_career_name}")
|
||||
|
||||
# 处理副职业关联
|
||||
if sub_careers_data and not is_organization:
|
||||
for sub_data in sub_careers_data:
|
||||
char_career = CharacterCareer(
|
||||
character_id=character.id,
|
||||
career_id=sub_data['career_id'],
|
||||
career_type='sub',
|
||||
current_stage=sub_data['stage'],
|
||||
stage_progress=0
|
||||
)
|
||||
db.add(char_career)
|
||||
logger.info(f" ✅ 创建副职业关联: {character.name}, 数量: {len(sub_careers_data)}")
|
||||
|
||||
# 如果是组织,创建Organization记录
|
||||
if is_organization:
|
||||
org = Organization(
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
"""职业生成服务"""
|
||||
from typing import Dict, Any, List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
import json
|
||||
|
||||
from app.models.project import Project
|
||||
from app.models.career import Career
|
||||
from app.services.ai_service import AIService
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CareerService:
|
||||
"""职业相关业务逻辑服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_career_generation_prompt(
|
||||
project: Project,
|
||||
main_career_count: int = 2,
|
||||
sub_career_count: int = 6
|
||||
) -> str:
|
||||
"""
|
||||
构建职业体系生成的提示词
|
||||
|
||||
Args:
|
||||
project: 项目对象
|
||||
main_career_count: 主职业数量
|
||||
sub_career_count: 副职业数量
|
||||
|
||||
Returns:
|
||||
完整的提示词
|
||||
"""
|
||||
project_context = f"""
|
||||
项目信息:
|
||||
- 书名:{project.title}
|
||||
- 类型:{project.genre or '未设定'}
|
||||
- 主题:{project.theme or '未设定'}
|
||||
- 时间背景:{project.world_time_period or '未设定'}
|
||||
- 地理位置:{project.world_location or '未设定'}
|
||||
- 氛围基调:{project.world_atmosphere or '未设定'}
|
||||
- 世界规则:{project.world_rules or '未设定'}
|
||||
"""
|
||||
|
||||
user_requirements = f"""
|
||||
生成要求:
|
||||
- 主职业数量:{main_career_count}个
|
||||
- 副职业数量:{sub_career_count}个
|
||||
- 主职业必须严格符合世界观规则,体现核心能力体系
|
||||
- 副职业可以更加自由灵活,包含生产、辅助、特殊类型
|
||||
"""
|
||||
|
||||
prompt = f"""{project_context}
|
||||
|
||||
{user_requirements}
|
||||
|
||||
请为这个小说项目生成完整的职业体系。返回JSON格式,结构如下:
|
||||
|
||||
{{
|
||||
"main_careers": [
|
||||
{{
|
||||
"name": "职业名称",
|
||||
"description": "职业描述(100-200字)",
|
||||
"category": "职业分类(如:战斗系、法术系、体修系等)",
|
||||
"stages": [
|
||||
{{"level": 1, "name": "阶段名称", "description": "阶段描述"}},
|
||||
{{"level": 2, "name": "阶段名称", "description": "阶段描述"}},
|
||||
...(共10个阶段)
|
||||
],
|
||||
"max_stage": 10,
|
||||
"requirements": "职业要求(如:需要特定天赋、资质等)",
|
||||
"special_abilities": "特殊能力描述",
|
||||
"worldview_rules": "世界观规则关联(说明该职业如何融入世界观)",
|
||||
"attribute_bonuses": {{"strength": "+10%", "intelligence": "+5%"}}
|
||||
}}
|
||||
],
|
||||
"sub_careers": [
|
||||
{{
|
||||
"name": "副职业名称",
|
||||
"description": "职业描述",
|
||||
"category": "生产系/辅助系/特殊系",
|
||||
"stages": [
|
||||
{{"level": 1, "name": "阶段名称", "description": "阶段描述"}},
|
||||
...(5-8个阶段)
|
||||
],
|
||||
"max_stage": 5,
|
||||
"requirements": "职业要求",
|
||||
"special_abilities": "特殊能力"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
重要注意事项:
|
||||
1. 主职业的阶段设定要详细,体现明确的成长路径,阶段名称要有特色
|
||||
2. 根据小说类型选择合适的职业:
|
||||
- 修仙类:剑修、体修、法修、符修等,阶段如:炼气、筑基、金丹、元婴...
|
||||
- 玄幻类:战士、法师、刺客等,阶段如:见习、初级、中级、高级...
|
||||
- 都市异能:异能者分类,阶段如:觉醒、初阶、中阶、高阶...
|
||||
- 科幻未来:基因战士、机甲师等,阶段如:E级、D级、C级、B级...
|
||||
3. 副职业要有实用性和趣味性,如:炼丹师、炼器师、阵法师、驯兽师、医师等
|
||||
4. 所有职业都要符合项目的整体世界观设定
|
||||
5. 阶段描述要简洁明了,体现该阶段的核心特征
|
||||
6. **只返回纯JSON对象,不要添加任何解释文字或markdown标记**
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
@staticmethod
|
||||
async def parse_and_save_careers(
|
||||
career_data: Dict[str, Any],
|
||||
project_id: str,
|
||||
db: AsyncSession
|
||||
) -> Dict[str, List[str]]:
|
||||
"""
|
||||
解析AI返回的职业数据并保存到数据库
|
||||
|
||||
Args:
|
||||
career_data: AI返回的职业数据(已解析为dict)
|
||||
project_id: 项目ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
{"main_careers": [...], "sub_careers": [...]} 创建的职业名称列表
|
||||
"""
|
||||
result = {
|
||||
"main_careers": [],
|
||||
"sub_careers": []
|
||||
}
|
||||
|
||||
# 保存主职业
|
||||
for idx, career_info in enumerate(career_data.get("main_careers", [])):
|
||||
try:
|
||||
stages_json = json.dumps(career_info.get("stages", []), ensure_ascii=False)
|
||||
attribute_bonuses = career_info.get("attribute_bonuses")
|
||||
attribute_bonuses_json = json.dumps(attribute_bonuses, ensure_ascii=False) if attribute_bonuses else None
|
||||
|
||||
career = Career(
|
||||
project_id=project_id,
|
||||
name=career_info.get("name", f"未命名主职业{idx+1}"),
|
||||
type="main",
|
||||
description=career_info.get("description"),
|
||||
category=career_info.get("category"),
|
||||
stages=stages_json,
|
||||
max_stage=career_info.get("max_stage", 10),
|
||||
requirements=career_info.get("requirements"),
|
||||
special_abilities=career_info.get("special_abilities"),
|
||||
worldview_rules=career_info.get("worldview_rules"),
|
||||
attribute_bonuses=attribute_bonuses_json,
|
||||
source="ai"
|
||||
)
|
||||
db.add(career)
|
||||
await db.flush()
|
||||
result["main_careers"].append(career.name)
|
||||
logger.info(f" ✅ 创建主职业:{career.name}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ 创建主职业失败:{str(e)}")
|
||||
continue
|
||||
|
||||
# 保存副职业
|
||||
for idx, career_info in enumerate(career_data.get("sub_careers", [])):
|
||||
try:
|
||||
stages_json = json.dumps(career_info.get("stages", []), ensure_ascii=False)
|
||||
attribute_bonuses = career_info.get("attribute_bonuses")
|
||||
attribute_bonuses_json = json.dumps(attribute_bonuses, ensure_ascii=False) if attribute_bonuses else None
|
||||
|
||||
career = Career(
|
||||
project_id=project_id,
|
||||
name=career_info.get("name", f"未命名副职业{idx+1}"),
|
||||
type="sub",
|
||||
description=career_info.get("description"),
|
||||
category=career_info.get("category"),
|
||||
stages=stages_json,
|
||||
max_stage=career_info.get("max_stage", 5),
|
||||
requirements=career_info.get("requirements"),
|
||||
special_abilities=career_info.get("special_abilities"),
|
||||
worldview_rules=career_info.get("worldview_rules"),
|
||||
attribute_bonuses=attribute_bonuses_json,
|
||||
source="ai"
|
||||
)
|
||||
db.add(career)
|
||||
await db.flush()
|
||||
result["sub_careers"].append(career.name)
|
||||
logger.info(f" ✅ 创建副职业:{career.name}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ 创建副职业失败:{str(e)}")
|
||||
continue
|
||||
|
||||
await db.commit()
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def get_project_careers_summary(project_id: str, db: AsyncSession) -> Dict[str, Any]:
|
||||
"""
|
||||
获取项目职业体系摘要
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
职业体系摘要信息
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Career).where(Career.project_id == project_id)
|
||||
)
|
||||
careers = result.scalars().all()
|
||||
|
||||
main_careers = []
|
||||
sub_careers = []
|
||||
|
||||
for career in careers:
|
||||
career_info = {
|
||||
"id": career.id,
|
||||
"name": career.name,
|
||||
"category": career.category,
|
||||
"max_stage": career.max_stage
|
||||
}
|
||||
|
||||
if career.type == "main":
|
||||
main_careers.append(career_info)
|
||||
else:
|
||||
sub_careers.append(career_info)
|
||||
|
||||
return {
|
||||
"main_careers": main_careers,
|
||||
"sub_careers": sub_careers,
|
||||
"total_count": len(careers)
|
||||
}
|
||||
|
||||
|
||||
# 创建全局服务实例
|
||||
career_service = CareerService()
|
||||
@@ -0,0 +1,398 @@
|
||||
"""职业更新服务 - 根据章节分析自动更新角色职业信息"""
|
||||
from typing import Dict, Any, List, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.character import Character
|
||||
from app.models.career import Career, CharacterCareer
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CareerUpdateService:
|
||||
"""职业更新服务 - 根据章节分析结果自动更新角色职业"""
|
||||
|
||||
@staticmethod
|
||||
async def update_careers_from_analysis(
|
||||
db: AsyncSession,
|
||||
project_id: str,
|
||||
character_states: List[Dict[str, Any]],
|
||||
chapter_id: str,
|
||||
chapter_number: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
根据章节分析结果更新角色职业
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
project_id: 项目ID
|
||||
character_states: 角色状态变化列表(来自PlotAnalysis)
|
||||
chapter_id: 章节ID
|
||||
chapter_number: 章节编号
|
||||
|
||||
Returns:
|
||||
更新结果字典,包含更新数量和变更日志
|
||||
"""
|
||||
if not character_states:
|
||||
logger.info("📋 角色状态列表为空,跳过职业更新")
|
||||
return {"updated_count": 0, "changes": []}
|
||||
|
||||
updated_count = 0
|
||||
changes_log = []
|
||||
|
||||
logger.info(f"🔍 开始分析第{chapter_number}章的角色职业变化...")
|
||||
|
||||
for char_state in character_states:
|
||||
char_name = char_state.get('character_name')
|
||||
career_changes = char_state.get('career_changes', {})
|
||||
|
||||
# 如果没有职业变化信息,跳过
|
||||
if not career_changes or not isinstance(career_changes, dict):
|
||||
continue
|
||||
|
||||
# 检查是否有实质性的职业变化
|
||||
main_stage_change = career_changes.get('main_career_stage_change', 0)
|
||||
sub_career_changes = career_changes.get('sub_career_changes', [])
|
||||
new_careers = career_changes.get('new_careers', [])
|
||||
|
||||
if main_stage_change == 0 and not sub_career_changes and not new_careers:
|
||||
continue
|
||||
|
||||
logger.info(f" 👤 检测到角色 [{char_name}] 有职业变化")
|
||||
|
||||
# 1. 查询角色
|
||||
char_result = await db.execute(
|
||||
select(Character).where(
|
||||
Character.name == char_name,
|
||||
Character.project_id == project_id
|
||||
)
|
||||
)
|
||||
character = char_result.scalar_one_or_none()
|
||||
|
||||
if not character:
|
||||
logger.warning(f" ⚠️ 角色不存在: {char_name},跳过")
|
||||
continue
|
||||
|
||||
# 2. 更新主职业阶段
|
||||
if main_stage_change != 0 and character.main_career_id:
|
||||
success = await CareerUpdateService._update_main_career_stage(
|
||||
db=db,
|
||||
character=character,
|
||||
stage_change=main_stage_change,
|
||||
chapter_number=chapter_number,
|
||||
career_changes=career_changes,
|
||||
changes_log=changes_log
|
||||
)
|
||||
if success:
|
||||
updated_count += 1
|
||||
|
||||
# 3. 更新副职业(如果有)
|
||||
if sub_career_changes and isinstance(sub_career_changes, list):
|
||||
for sub_change in sub_career_changes:
|
||||
success = await CareerUpdateService._update_sub_career_stage(
|
||||
db=db,
|
||||
character=character,
|
||||
project_id=project_id,
|
||||
sub_change=sub_change,
|
||||
chapter_number=chapter_number,
|
||||
changes_log=changes_log
|
||||
)
|
||||
if success:
|
||||
updated_count += 1
|
||||
|
||||
# 4. 添加新职业(如果有)
|
||||
if new_careers and isinstance(new_careers, list):
|
||||
for new_career_name in new_careers:
|
||||
success = await CareerUpdateService._add_new_career(
|
||||
db=db,
|
||||
character=character,
|
||||
project_id=project_id,
|
||||
career_name=new_career_name,
|
||||
chapter_number=chapter_number,
|
||||
changes_log=changes_log
|
||||
)
|
||||
if success:
|
||||
updated_count += 1
|
||||
|
||||
# 提交所有更改
|
||||
if updated_count > 0:
|
||||
await db.commit()
|
||||
logger.info(f"✅ 职业更新完成: 共更新了 {updated_count} 个角色的职业信息")
|
||||
else:
|
||||
logger.info("📋 本章没有角色职业变化")
|
||||
|
||||
return {
|
||||
"updated_count": updated_count,
|
||||
"changes": changes_log
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def _update_main_career_stage(
|
||||
db: AsyncSession,
|
||||
character: Character,
|
||||
stage_change: int,
|
||||
chapter_number: int,
|
||||
career_changes: Dict[str, Any],
|
||||
changes_log: List[Dict[str, Any]]
|
||||
) -> bool:
|
||||
"""更新主职业阶段"""
|
||||
try:
|
||||
# 查询主职业关联
|
||||
char_career_result = await db.execute(
|
||||
select(CharacterCareer).where(
|
||||
CharacterCareer.character_id == character.id,
|
||||
CharacterCareer.career_type == 'main'
|
||||
)
|
||||
)
|
||||
char_career = char_career_result.scalar_one_or_none()
|
||||
|
||||
if not char_career:
|
||||
logger.warning(f" ⚠️ {character.name} 没有主职业关联记录")
|
||||
return False
|
||||
|
||||
# 查询职业信息
|
||||
career_result = await db.execute(
|
||||
select(Career).where(Career.id == char_career.career_id)
|
||||
)
|
||||
career = career_result.scalar_one_or_none()
|
||||
|
||||
if not career:
|
||||
logger.warning(f" ⚠️ 职业ID {char_career.career_id} 不存在")
|
||||
return False
|
||||
|
||||
# 计算新阶段(不超过最大阶段,不低于1)
|
||||
old_stage = char_career.current_stage
|
||||
new_stage = min(max(1, old_stage + stage_change), career.max_stage)
|
||||
|
||||
# 如果没有实际变化,跳过
|
||||
if new_stage == old_stage:
|
||||
logger.info(f" 📊 {character.name} 的 {career.name} 已达到边界,无法变更")
|
||||
return False
|
||||
|
||||
# 更新CharacterCareer表
|
||||
char_career.current_stage = new_stage
|
||||
|
||||
# 同步更新Character表的冗余字段
|
||||
character.main_career_stage = new_stage
|
||||
|
||||
# 记录变更日志
|
||||
change_desc = f"{'晋升' if stage_change > 0 else '降级'}"
|
||||
breakthrough_desc = career_changes.get('career_breakthrough', '')
|
||||
|
||||
changes_log.append({
|
||||
'character': character.name,
|
||||
'career': career.name,
|
||||
'career_type': 'main',
|
||||
'old_stage': old_stage,
|
||||
'new_stage': new_stage,
|
||||
'change': stage_change,
|
||||
'chapter': chapter_number,
|
||||
'description': breakthrough_desc
|
||||
})
|
||||
|
||||
logger.info(
|
||||
f" ✨ {character.name} 的主职业 [{career.name}] "
|
||||
f"{old_stage}阶 → {new_stage}阶 ({change_desc})"
|
||||
)
|
||||
if breakthrough_desc:
|
||||
logger.info(f" 突破描述: {breakthrough_desc[:50]}...")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ 更新主职业失败: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _update_sub_career_stage(
|
||||
db: AsyncSession,
|
||||
character: Character,
|
||||
project_id: str,
|
||||
sub_change: Dict[str, Any],
|
||||
chapter_number: int,
|
||||
changes_log: List[Dict[str, Any]]
|
||||
) -> bool:
|
||||
"""更新副职业阶段"""
|
||||
try:
|
||||
career_name = sub_change.get('career_name')
|
||||
stage_change = sub_change.get('stage_change', 0)
|
||||
|
||||
if not career_name or stage_change == 0:
|
||||
return False
|
||||
|
||||
# 1. 查询职业(通过名称)
|
||||
career_result = await db.execute(
|
||||
select(Career).where(
|
||||
Career.name == career_name,
|
||||
Career.project_id == project_id,
|
||||
Career.type == 'sub'
|
||||
)
|
||||
)
|
||||
career = career_result.scalar_one_or_none()
|
||||
|
||||
if not career:
|
||||
logger.warning(f" ⚠️ 副职业 [{career_name}] 不存在")
|
||||
return False
|
||||
|
||||
# 2. 查询角色-职业关联
|
||||
char_career_result = await db.execute(
|
||||
select(CharacterCareer).where(
|
||||
CharacterCareer.character_id == character.id,
|
||||
CharacterCareer.career_id == career.id,
|
||||
CharacterCareer.career_type == 'sub'
|
||||
)
|
||||
)
|
||||
char_career = char_career_result.scalar_one_or_none()
|
||||
|
||||
if not char_career:
|
||||
logger.warning(f" ⚠️ {character.name} 没有 [{career_name}] 副职业")
|
||||
return False
|
||||
|
||||
# 3. 计算新阶段
|
||||
old_stage = char_career.current_stage
|
||||
new_stage = min(max(1, old_stage + stage_change), career.max_stage)
|
||||
|
||||
if new_stage == old_stage:
|
||||
return False
|
||||
|
||||
# 4. 更新阶段
|
||||
char_career.current_stage = new_stage
|
||||
|
||||
# 5. 同步更新Character表的sub_careers JSON字段
|
||||
import json
|
||||
sub_careers = json.loads(character.sub_careers) if character.sub_careers else []
|
||||
for sc in sub_careers:
|
||||
if sc.get('career_id') == career.id:
|
||||
sc['stage'] = new_stage
|
||||
break
|
||||
character.sub_careers = json.dumps(sub_careers, ensure_ascii=False)
|
||||
|
||||
# 6. 记录变更
|
||||
changes_log.append({
|
||||
'character': character.name,
|
||||
'career': career.name,
|
||||
'career_type': 'sub',
|
||||
'old_stage': old_stage,
|
||||
'new_stage': new_stage,
|
||||
'change': stage_change,
|
||||
'chapter': chapter_number
|
||||
})
|
||||
|
||||
logger.info(
|
||||
f" ✨ {character.name} 的副职业 [{career.name}] "
|
||||
f"{old_stage}阶 → {new_stage}阶"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ 更新副职业失败: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _add_new_career(
|
||||
db: AsyncSession,
|
||||
character: Character,
|
||||
project_id: str,
|
||||
career_name: str,
|
||||
chapter_number: int,
|
||||
changes_log: List[Dict[str, Any]]
|
||||
) -> bool:
|
||||
"""为角色添加新职业"""
|
||||
try:
|
||||
# 1. 查询职业
|
||||
career_result = await db.execute(
|
||||
select(Career).where(
|
||||
Career.name == career_name,
|
||||
Career.project_id == project_id
|
||||
)
|
||||
)
|
||||
career = career_result.scalar_one_or_none()
|
||||
|
||||
if not career:
|
||||
logger.warning(f" ⚠️ 职业 [{career_name}] 不存在,无法添加")
|
||||
return False
|
||||
|
||||
# 2. 检查是否已存在
|
||||
existing_result = await db.execute(
|
||||
select(CharacterCareer).where(
|
||||
CharacterCareer.character_id == character.id,
|
||||
CharacterCareer.career_id == career.id
|
||||
)
|
||||
)
|
||||
if existing_result.scalar_one_or_none():
|
||||
logger.info(f" 📋 {character.name} 已拥有 [{career_name}],跳过")
|
||||
return False
|
||||
|
||||
# 3. 根据职业类型添加
|
||||
if career.type == 'main':
|
||||
# 检查是否已有主职业
|
||||
if character.main_career_id:
|
||||
logger.warning(f" ⚠️ {character.name} 已有主职业,无法添加新主职业")
|
||||
return False
|
||||
|
||||
# 添加主职业
|
||||
import uuid
|
||||
new_char_career = CharacterCareer(
|
||||
id=str(uuid.uuid4()),
|
||||
character_id=character.id,
|
||||
career_id=career.id,
|
||||
career_type='main',
|
||||
current_stage=1
|
||||
)
|
||||
db.add(new_char_career)
|
||||
|
||||
# 更新Character表
|
||||
character.main_career_id = career.id
|
||||
character.main_career_stage = 1
|
||||
|
||||
logger.info(f" ✨ {character.name} 获得新主职业 [{career_name}]")
|
||||
|
||||
else: # sub职业
|
||||
# 检查副职业数量(最多2个)
|
||||
sub_count_result = await db.execute(
|
||||
select(CharacterCareer).where(
|
||||
CharacterCareer.character_id == character.id,
|
||||
CharacterCareer.career_type == 'sub'
|
||||
)
|
||||
)
|
||||
if len(sub_count_result.scalars().all()) >= 2:
|
||||
logger.warning(f" ⚠️ {character.name} 的副职业已达上限(2个)")
|
||||
return False
|
||||
|
||||
# 添加副职业
|
||||
import uuid
|
||||
new_char_career = CharacterCareer(
|
||||
id=str(uuid.uuid4()),
|
||||
character_id=character.id,
|
||||
career_id=career.id,
|
||||
career_type='sub',
|
||||
current_stage=1
|
||||
)
|
||||
db.add(new_char_career)
|
||||
|
||||
# 更新Character表的sub_careers JSON
|
||||
import json
|
||||
sub_careers = json.loads(character.sub_careers) if character.sub_careers else []
|
||||
sub_careers.append({
|
||||
'career_id': career.id,
|
||||
'stage': 1
|
||||
})
|
||||
character.sub_careers = json.dumps(sub_careers, ensure_ascii=False)
|
||||
|
||||
logger.info(f" ✨ {character.name} 获得新副职业 [{career_name}]")
|
||||
|
||||
# 记录变更
|
||||
changes_log.append({
|
||||
'character': character.name,
|
||||
'career': career.name,
|
||||
'career_type': career.type,
|
||||
'action': 'new',
|
||||
'chapter': chapter_number
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ 添加新职业失败: {str(e)}")
|
||||
return False
|
||||
@@ -745,6 +745,15 @@ class PromptService:
|
||||
- 特殊技能或知识
|
||||
- 符合世界观设定
|
||||
|
||||
7. **职业信息**(重要 - 如果项目上下文中包含职业列表):
|
||||
- 仔细查看项目上下文中的"可用主职业"和"可用副职业"列表
|
||||
- 主职业:必须从"可用主职业"列表中选择一个最符合角色设定的职业,填写其职业名称(name字段)
|
||||
- 主职业阶段:根据职业的阶段信息和角色实力,设定合理的当前阶段(1到职业的max_stage)
|
||||
- 副职业:可以从"可用副职业"列表中选择0-2个,每个包含职业名称和阶段
|
||||
- 如果项目没有职业列表,则不需要填写career_info字段
|
||||
- 职业选择必须与角色的背景故事、能力特点和故事定位高度契合
|
||||
- ⚠️ 重要:请填写职业的名称而非ID,系统会自动匹配
|
||||
|
||||
**重要格式要求:**
|
||||
1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字
|
||||
2. JSON字符串值的内容描述中严禁使用任何特殊符号(包括中文引号、英文引号、方括号、书名号等)
|
||||
@@ -781,7 +790,18 @@ class PromptService:
|
||||
"joined_at": "加入时间(可选)",
|
||||
"status": "active"
|
||||
}}
|
||||
]
|
||||
],
|
||||
|
||||
"career_info": {{
|
||||
"main_career_name": "从项目上下文的可用主职业列表中复制的职业名称",
|
||||
"main_career_stage": 5,
|
||||
"sub_careers": [
|
||||
{{
|
||||
"career_name": "从项目上下文的可用副职业列表中复制的职业名称",
|
||||
"stage": 3
|
||||
}}
|
||||
]
|
||||
}}
|
||||
}}
|
||||
|
||||
**关系类型参考(请从中选择或自定义):**
|
||||
@@ -947,6 +967,12 @@ class PromptService:
|
||||
- 关系变化
|
||||
- 关键行动和决策
|
||||
- 成长或退步
|
||||
- **💼 职业变化(重要 - 新增)**:
|
||||
- 如果角色在本章有职业相关的进展或突破,请详细分析
|
||||
- 主职业阶段变化: 是否晋级、突破或降级(用整数表示变化量,如: +1表示晋升一阶段,-1表示退步一阶段,0表示无变化)
|
||||
- 副职业变化: 是否学习新的副职业或副职业有所精进
|
||||
- 职业突破描述: 具体的突破过程、原因和标志性事件
|
||||
- 注意:只有当章节中明确描述了职业相关的成长、突破或变化时才填写此项
|
||||
|
||||
### 6. 关键情节点 (Plot Points)
|
||||
列出3-5个核心情节点:
|
||||
@@ -1054,6 +1080,13 @@ class PromptService:
|
||||
2. keyword必须是从章节原文中逐字复制的文本,长度8-25字
|
||||
3. keyword用于在前端标注文本位置,所以必须能在原文中精确找到
|
||||
4. 不要使用概括性语句或改写后的文字作为keyword
|
||||
5. **职业变化字段说明**:
|
||||
- career_changes是可选字段,只有当章节中明确描述了职业相关变化时才填写
|
||||
- main_career_stage_change: 整数,表示主职业阶段变化量(+1=晋升一阶,-1=退步一阶,0=无变化)
|
||||
- sub_career_changes: 数组,包含副职业的变化,每项包含career_name(职业名称)和stage_change(阶段变化量)
|
||||
- new_careers: 数组,包含新获得的职业名称(如果有)
|
||||
- career_breakthrough: 字符串,描述职业突破的具体过程和标志性事件
|
||||
- 如果角色没有职业变化,可以不填写career_changes字段或设为空对象
|
||||
|
||||
只返回JSON,不要其他说明。"""
|
||||
|
||||
@@ -1533,6 +1566,7 @@ class PromptService:
|
||||
3. 性格、背景要有深度和独特性
|
||||
4. 外貌描写要具体生动
|
||||
5. 特长和能力要符合角色定位
|
||||
6. **如果【已有角色】中包含职业列表,必须为角色设定职业**(参考下方职业信息要求)
|
||||
|
||||
**关系建立指导(非常重要):**
|
||||
- 仔细审视【已有角色】列表,思考新角色与哪些现有角色有联系
|
||||
@@ -1546,6 +1580,15 @@ class PromptService:
|
||||
2. JSON字符串值中严禁使用特殊符号(引号、方括号、书名号等)
|
||||
3. 所有专有名词直接书写,不使用任何符号包裹
|
||||
|
||||
【职业信息要求(重要)】
|
||||
如果【已有角色】部分包含"可用主职业列表"或"可用副职业列表",则必须:
|
||||
- 仔细查看可用的主职业和副职业列表
|
||||
- 根据角色的背景、能力、故事定位,选择最合适的职业
|
||||
- 主职业:从"可用主职业列表"中选择一个,填写职业名称(name字段)
|
||||
- 主职业阶段:根据职业的阶段信息和角色实力,设定合理的当前阶段
|
||||
- 副职业:可选择0-2个副职业,每个包含职业名称和阶段
|
||||
- ⚠️ 重要:必须填写职业的名称而非ID,系统会自动匹配
|
||||
|
||||
请严格按照以下JSON格式返回:
|
||||
{{
|
||||
"name": "角色姓名",
|
||||
@@ -1574,7 +1617,18 @@ class PromptService:
|
||||
"rank": 5,
|
||||
"loyalty": 80
|
||||
}}
|
||||
]
|
||||
],
|
||||
|
||||
"career_info": {{
|
||||
"main_career_name": "从可用主职业列表中选择的职业名称",
|
||||
"main_career_stage": 5,
|
||||
"sub_careers": [
|
||||
{{
|
||||
"career_name": "从可用副职业列表中选择的职业名称",
|
||||
"stage": 3
|
||||
}}
|
||||
]
|
||||
}}
|
||||
}}
|
||||
|
||||
**关系类型参考(从中选择或自定义):**
|
||||
@@ -1605,6 +1659,85 @@ class PromptService:
|
||||
|
||||
只返回纯JSON对象,不要有```json```这样的标记。"""
|
||||
|
||||
# 职业体系生成提示词
|
||||
CAREER_SYSTEM_GENERATION = """你是专业的游戏/小说职业体系设计师。请根据以下世界观信息,设计一个完整且合理的职业体系。
|
||||
|
||||
【项目信息】
|
||||
- 书名:{title}
|
||||
- 类型:{genre}
|
||||
- 主题:{theme}
|
||||
- 时间背景:{time_period}
|
||||
- 地理位置:{location}
|
||||
- 氛围基调:{atmosphere}
|
||||
- 世界规则:{rules}
|
||||
|
||||
【设计要求】
|
||||
1. **主职业(main_careers)**:
|
||||
- 根据世界观特点,决定需要多少个主职业
|
||||
- 主职业是角色的核心发展方向,直接影响战斗力或核心能力
|
||||
- 必须严格符合世界观规则,体现核心能力体系
|
||||
- 每个主职业的阶段数量可以不同:根据职业的复杂度、重要性、修炼难度等因素,为不同职业设定不同的max_stage
|
||||
|
||||
2. **副职业(sub_careers)**:
|
||||
- 根据世界需要,决定需要多少个副职业
|
||||
- 副职业包含生产、辅助、特殊技能类,丰富角色的能力维度
|
||||
- 每个副职业的阶段数量可以不同:简单的副职业可能只有3-5个阶段,复杂的可能有6-10个阶段
|
||||
- 不要让所有副职业都是相同的阶段数
|
||||
|
||||
3. **阶段设计(stages)**:
|
||||
- 每个职业的stages数组长度必须等于max_stage
|
||||
- 阶段名称要符合世界观文化背景和时代特征
|
||||
- 阶段描述要体现明确的能力提升和成长路径
|
||||
- 重要:确保职业间的阶段数量有差异,体现职业的多样性
|
||||
|
||||
【JSON格式】
|
||||
|
||||
{{
|
||||
"main_careers": [
|
||||
{{
|
||||
"name": "职业名称",
|
||||
"description": "职业描述(100-150字),说明职业特点和定位",
|
||||
"category": "职业分类(如:战斗系、法术系、体修系等)",
|
||||
"stages": [
|
||||
{{"level": 1, "name": "阶段1名称", "description": "阶段描述"}},
|
||||
{{"level": 2, "name": "阶段2名称", "description": "阶段描述"}},
|
||||
...数组长度应等于max_stage...
|
||||
],
|
||||
"max_stage": 根据职业复杂度自行决定的整数,
|
||||
"requirements": "职业要求和前置条件",
|
||||
"special_abilities": "职业特殊能力和特色",
|
||||
"worldview_rules": "与世界观规则的关联",
|
||||
"attribute_bonuses": {{"strength": "+10%", "intelligence": "+5%"}}
|
||||
}}
|
||||
],
|
||||
"sub_careers": [
|
||||
{{
|
||||
"name": "副职业名称",
|
||||
"description": "职业描述(80-120字)",
|
||||
"category": "生产系/辅助系/特殊系",
|
||||
"stages": [
|
||||
{{"level": 1, "name": "阶段1名称", "description": "阶段描述"}},
|
||||
...数组长度应等于max_stage...
|
||||
],
|
||||
"max_stage": 根据职业特性自行决定的整数,
|
||||
"requirements": "职业要求",
|
||||
"special_abilities": "特殊能力"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
【重要提示】
|
||||
- 职业的数量、类型完全由你根据世界观自行决定,不要受任何数字限制
|
||||
- **阶段数量多样性(关键)**:
|
||||
- 不同职业的max_stage必须不同,不要所有职业都是相同的阶段数
|
||||
- 主职业的阶段数建议范围:5-15个阶段(根据职业重要性和复杂度灵活设定)
|
||||
- 副职业的阶段数建议范围:3-10个阶段(根据职业特性灵活设定)
|
||||
- 例如:剑修可能有12个阶段,炼丹师可能有8个阶段,体修可能有10个阶段
|
||||
- 确保职业体系与世界观高度契合,符合该世界的逻辑和文化
|
||||
- 只返回纯JSON,不要添加markdown标记或其他解释文字
|
||||
|
||||
请让每个职业的阶段数有所不同,体现职业的独特性和多样性!"""
|
||||
|
||||
@staticmethod
|
||||
def format_prompt(template: str, **kwargs) -> str:
|
||||
"""
|
||||
@@ -2043,6 +2176,12 @@ class PromptService:
|
||||
"description": "根据剧情需求自动生成新角色的完整设定",
|
||||
"parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere", "rules",
|
||||
"existing_characters", "plot_context", "character_specification", "mcp_references"]
|
||||
},
|
||||
"CAREER_SYSTEM_GENERATION": {
|
||||
"name": "职业体系生成",
|
||||
"category": "世界构建",
|
||||
"description": "根据世界观自动生成完整的职业体系,包括主职业和副职业",
|
||||
"parameters": ["title", "genre", "theme", "time_period", "location", "atmosphere", "rules"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user