refactor:重构角色关系数据驱动,relationships字段改为从关系表动态生成,组织成员同理
This commit is contained in:
+193
-18
@@ -31,6 +31,90 @@ router = APIRouter(prefix="/characters", tags=["角色管理"])
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def _build_relationships_summary(character_id: str, project_id: str, db: AsyncSession) -> str:
|
||||
"""从 character_relationships 表构建角色关系摘要文本"""
|
||||
from sqlalchemy import or_
|
||||
|
||||
# 查询该角色参与的所有关系
|
||||
rels_result = await db.execute(
|
||||
select(CharacterRelationship).where(
|
||||
CharacterRelationship.project_id == project_id,
|
||||
or_(
|
||||
CharacterRelationship.character_from_id == character_id,
|
||||
CharacterRelationship.character_to_id == character_id
|
||||
)
|
||||
)
|
||||
)
|
||||
rels = rels_result.scalars().all()
|
||||
|
||||
if not rels:
|
||||
return ""
|
||||
|
||||
# 收集所有相关角色ID
|
||||
related_ids = set()
|
||||
for r in rels:
|
||||
related_ids.add(r.character_from_id)
|
||||
related_ids.add(r.character_to_id)
|
||||
related_ids.discard(character_id)
|
||||
|
||||
if not related_ids:
|
||||
return ""
|
||||
|
||||
# 批量查询角色名称
|
||||
chars_result = await db.execute(
|
||||
select(Character.id, Character.name).where(Character.id.in_(related_ids))
|
||||
)
|
||||
name_map = {row.id: row.name for row in chars_result}
|
||||
|
||||
# 构建摘要
|
||||
parts = []
|
||||
for r in rels:
|
||||
if r.character_from_id == character_id:
|
||||
target_name = name_map.get(r.character_to_id, "未知")
|
||||
rel_name = r.relationship_name or "相关"
|
||||
else:
|
||||
target_name = name_map.get(r.character_from_id, "未知")
|
||||
rel_name = r.relationship_name or "相关"
|
||||
parts.append(f"与{target_name}:{rel_name}")
|
||||
|
||||
return ";".join(parts)
|
||||
|
||||
|
||||
async def _build_org_members_summary(character_id: str, db: AsyncSession) -> str:
|
||||
"""从 organization_members 表构建组织成员摘要文本"""
|
||||
# 先查找该角色对应的 Organization 记录
|
||||
org_result = await db.execute(
|
||||
select(Organization).where(Organization.character_id == character_id)
|
||||
)
|
||||
org = org_result.scalar_one_or_none()
|
||||
if not org:
|
||||
return ""
|
||||
|
||||
# 查询该组织的所有成员
|
||||
members_result = await db.execute(
|
||||
select(OrganizationMember).where(OrganizationMember.organization_id == org.id)
|
||||
)
|
||||
members = members_result.scalars().all()
|
||||
if not members:
|
||||
return ""
|
||||
|
||||
# 批量查询成员角色名称
|
||||
member_char_ids = [m.character_id for m in members]
|
||||
chars_result = await db.execute(
|
||||
select(Character.id, Character.name).where(Character.id.in_(member_char_ids))
|
||||
)
|
||||
name_map = {row.id: row.name for row in chars_result}
|
||||
|
||||
# 构建摘要
|
||||
parts = []
|
||||
for m in members:
|
||||
name = name_map.get(m.character_id, "未知")
|
||||
position = m.position or "成员"
|
||||
parts.append(f"{name}({position})")
|
||||
|
||||
return "、".join(parts)
|
||||
|
||||
|
||||
@router.get("", response_model=CharacterListResponse, summary="获取角色列表")
|
||||
async def get_characters(
|
||||
project_id: str,
|
||||
@@ -56,9 +140,12 @@ async def get_characters(
|
||||
)
|
||||
characters = result.scalars().all()
|
||||
|
||||
# 为组织类型的角色填充Organization表的额外字段,并添加职业信息
|
||||
# 为角色填充关系摘要、组织额外字段、职业信息
|
||||
enriched_characters = []
|
||||
for char in characters:
|
||||
# 从 character_relationships 表动态生成关系摘要
|
||||
rel_summary = await _build_relationships_summary(char.id, project_id, db)
|
||||
|
||||
char_dict = {
|
||||
"id": char.id,
|
||||
"project_id": char.project_id,
|
||||
@@ -70,10 +157,10 @@ async def get_characters(
|
||||
"personality": char.personality,
|
||||
"background": char.background,
|
||||
"appearance": char.appearance,
|
||||
"relationships": char.relationships,
|
||||
"relationships": rel_summary,
|
||||
"organization_type": char.organization_type,
|
||||
"organization_purpose": char.organization_purpose,
|
||||
"organization_members": char.organization_members,
|
||||
"organization_members": await _build_org_members_summary(char.id, db) if char.is_organization else "",
|
||||
"traits": char.traits,
|
||||
"avatar_url": char.avatar_url,
|
||||
"created_at": char.created_at,
|
||||
@@ -130,9 +217,12 @@ async def get_project_characters(
|
||||
)
|
||||
characters = result.scalars().all()
|
||||
|
||||
# 为组织类型的角色填充Organization表的额外字段,并添加职业信息
|
||||
# 为角色填充关系摘要、组织额外字段、职业信息
|
||||
enriched_characters = []
|
||||
for char in characters:
|
||||
# 从 character_relationships 表动态生成关系摘要
|
||||
rel_summary = await _build_relationships_summary(char.id, project_id, db)
|
||||
|
||||
char_dict = {
|
||||
"id": char.id,
|
||||
"project_id": char.project_id,
|
||||
@@ -144,10 +234,10 @@ async def get_project_characters(
|
||||
"personality": char.personality,
|
||||
"background": char.background,
|
||||
"appearance": char.appearance,
|
||||
"relationships": char.relationships,
|
||||
"relationships": rel_summary,
|
||||
"organization_type": char.organization_type,
|
||||
"organization_purpose": char.organization_purpose,
|
||||
"organization_members": char.organization_members,
|
||||
"organization_members": await _build_org_members_summary(char.id, db) if char.is_organization else "",
|
||||
"traits": char.traits,
|
||||
"avatar_url": char.avatar_url,
|
||||
"created_at": char.created_at,
|
||||
@@ -198,7 +288,51 @@ async def get_character(
|
||||
user_id = getattr(request.state, 'user_id', None)
|
||||
await verify_project_access(character.project_id, user_id, db)
|
||||
|
||||
return character
|
||||
# 从 character_relationships 表动态生成关系摘要
|
||||
rel_summary = await _build_relationships_summary(character.id, character.project_id, db)
|
||||
|
||||
char_dict = {
|
||||
"id": character.id,
|
||||
"project_id": character.project_id,
|
||||
"name": character.name,
|
||||
"age": character.age,
|
||||
"gender": character.gender,
|
||||
"is_organization": character.is_organization,
|
||||
"role_type": character.role_type,
|
||||
"personality": character.personality,
|
||||
"background": character.background,
|
||||
"appearance": character.appearance,
|
||||
"relationships": rel_summary,
|
||||
"organization_type": character.organization_type,
|
||||
"organization_purpose": character.organization_purpose,
|
||||
"organization_members": await _build_org_members_summary(character.id, db) if character.is_organization else "",
|
||||
"traits": character.traits,
|
||||
"avatar_url": character.avatar_url,
|
||||
"created_at": character.created_at,
|
||||
"updated_at": character.updated_at,
|
||||
"power_level": None,
|
||||
"location": None,
|
||||
"motto": None,
|
||||
"color": None,
|
||||
"main_career_id": character.main_career_id,
|
||||
"main_career_stage": character.main_career_stage,
|
||||
"sub_careers": json.loads(character.sub_careers) if character.sub_careers else None
|
||||
}
|
||||
|
||||
if character.is_organization:
|
||||
org_result = await db.execute(
|
||||
select(Organization).where(Organization.character_id == character.id)
|
||||
)
|
||||
org = org_result.scalar_one_or_none()
|
||||
if org:
|
||||
char_dict.update({
|
||||
"power_level": org.power_level,
|
||||
"location": org.location,
|
||||
"motto": org.motto,
|
||||
"color": org.color
|
||||
})
|
||||
|
||||
return char_dict
|
||||
|
||||
|
||||
@router.put("/{character_id}", response_model=CharacterResponse, summary="更新角色")
|
||||
@@ -372,7 +506,9 @@ async def update_character(
|
||||
character.sub_careers = sub_careers_json if isinstance(sub_careers_json, str) else json.dumps(sub_careers_data, ensure_ascii=False)
|
||||
logger.info(f"更新副职业信息:{character.name}")
|
||||
|
||||
# 更新 Character 表字段
|
||||
# 更新 Character 表字段(排除 relationships 和 organization_members,这些字段现在由结构化表驱动)
|
||||
update_data.pop('relationships', None)
|
||||
update_data.pop('organization_members', None)
|
||||
for field, value in update_data.items():
|
||||
setattr(character, field, value)
|
||||
|
||||
@@ -403,7 +539,8 @@ async def update_character(
|
||||
|
||||
logger.info(f"更新角色/组织成功:{character.name} (ID: {character_id})")
|
||||
|
||||
# 构建响应,确保sub_careers是list类型
|
||||
# 构建响应,从关系表动态生成 relationships
|
||||
rel_summary = await _build_relationships_summary(character_id, character.project_id, db)
|
||||
response_data = {
|
||||
"id": character.id,
|
||||
"project_id": character.project_id,
|
||||
@@ -415,10 +552,10 @@ async def update_character(
|
||||
"personality": character.personality,
|
||||
"background": character.background,
|
||||
"appearance": character.appearance,
|
||||
"relationships": character.relationships,
|
||||
"relationships": rel_summary,
|
||||
"organization_type": character.organization_type,
|
||||
"organization_purpose": character.organization_purpose,
|
||||
"organization_members": character.organization_members,
|
||||
"organization_members": await _build_org_members_summary(character.id, db) if character.is_organization else "",
|
||||
"traits": character.traits,
|
||||
"avatar_url": character.avatar_url,
|
||||
"created_at": character.created_at,
|
||||
@@ -510,7 +647,7 @@ async def create_character(
|
||||
await verify_project_access(character_data.project_id, user_id, db)
|
||||
|
||||
try:
|
||||
# 创建角色
|
||||
# 创建角色(不再写入 relationships 文本字段,关系统一由 character_relationships 表管理)
|
||||
character = Character(
|
||||
project_id=character_data.project_id,
|
||||
name=character_data.name,
|
||||
@@ -521,10 +658,8 @@ async def create_character(
|
||||
personality=character_data.personality,
|
||||
background=character_data.background,
|
||||
appearance=character_data.appearance,
|
||||
relationships=character_data.relationships,
|
||||
organization_type=character_data.organization_type,
|
||||
organization_purpose=character_data.organization_purpose,
|
||||
organization_members=character_data.organization_members,
|
||||
traits=character_data.traits,
|
||||
avatar_url=character_data.avatar_url,
|
||||
main_career_id=character_data.main_career_id,
|
||||
@@ -623,7 +758,49 @@ async def create_character(
|
||||
|
||||
logger.info(f"🎉 成功手动创建角色/组织: {character.name}")
|
||||
|
||||
return character
|
||||
# 构建响应(relationships 从关系表动态生成)
|
||||
char_dict = {
|
||||
"id": character.id,
|
||||
"project_id": character.project_id,
|
||||
"name": character.name,
|
||||
"age": character.age,
|
||||
"gender": character.gender,
|
||||
"is_organization": character.is_organization,
|
||||
"role_type": character.role_type,
|
||||
"personality": character.personality,
|
||||
"background": character.background,
|
||||
"appearance": character.appearance,
|
||||
"relationships": "",
|
||||
"organization_type": character.organization_type,
|
||||
"organization_purpose": character.organization_purpose,
|
||||
"organization_members": await _build_org_members_summary(character.id, db) if character.is_organization else "",
|
||||
"traits": character.traits,
|
||||
"avatar_url": character.avatar_url,
|
||||
"created_at": character.created_at,
|
||||
"updated_at": character.updated_at,
|
||||
"power_level": None,
|
||||
"location": None,
|
||||
"motto": None,
|
||||
"color": None,
|
||||
"main_career_id": character.main_career_id,
|
||||
"main_career_stage": character.main_career_stage,
|
||||
"sub_careers": json.loads(character.sub_careers) if character.sub_careers else None
|
||||
}
|
||||
|
||||
if character.is_organization:
|
||||
org_result = await db.execute(
|
||||
select(Organization).where(Organization.character_id == character.id)
|
||||
)
|
||||
org = org_result.scalar_one_or_none()
|
||||
if org:
|
||||
char_dict.update({
|
||||
"power_level": org.power_level,
|
||||
"location": org.location,
|
||||
"motto": org.motto,
|
||||
"color": org.color
|
||||
})
|
||||
|
||||
return char_dict
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"手动创建角色失败: {str(e)}")
|
||||
@@ -878,7 +1055,7 @@ async def generate_character_stream(
|
||||
else:
|
||||
logger.warning(f"⚠️ AI返回的副职业名称未找到: {career_name}")
|
||||
|
||||
# 创建角色
|
||||
# 创建角色(不再写入 relationships 文本字段,关系统一由 character_relationships 表管理)
|
||||
character = Character(
|
||||
project_id=request.project_id,
|
||||
name=character_data.get("name", request.name or "未命名角色"),
|
||||
@@ -889,10 +1066,8 @@ async def generate_character_stream(
|
||||
personality=character_data.get("personality", ""),
|
||||
background=character_data.get("background", ""),
|
||||
appearance=character_data.get("appearance", ""),
|
||||
relationships=character_data.get("relationships_text", character_data.get("relationships", "")),
|
||||
organization_type=character_data.get("organization_type") if is_organization else None,
|
||||
organization_purpose=character_data.get("organization_purpose") if is_organization else None,
|
||||
organization_members=json.dumps(character_data.get("organization_members", []), ensure_ascii=False) if is_organization else None,
|
||||
traits=traits_json,
|
||||
main_career_id=main_career_id,
|
||||
main_career_stage=main_career_stage if main_career_id else None,
|
||||
|
||||
@@ -35,7 +35,6 @@ interface CharacterFormValues {
|
||||
role_type?: string;
|
||||
personality?: string;
|
||||
appearance?: string;
|
||||
relationships?: string;
|
||||
background?: string;
|
||||
main_career_id?: string;
|
||||
main_career_stage?: number;
|
||||
@@ -60,7 +59,6 @@ interface CharacterCreateData {
|
||||
role_type?: string;
|
||||
personality?: string;
|
||||
appearance?: string;
|
||||
relationships?: string;
|
||||
background?: string;
|
||||
main_career_id?: string;
|
||||
main_career_stage?: number;
|
||||
@@ -82,7 +80,6 @@ interface CharacterUpdateData {
|
||||
role_type?: string;
|
||||
personality?: string;
|
||||
appearance?: string;
|
||||
relationships?: string;
|
||||
background?: string;
|
||||
main_career_id?: string;
|
||||
main_career_stage?: number;
|
||||
@@ -271,7 +268,6 @@ export default function Characters() {
|
||||
createData.role_type = values.role_type || 'supporting';
|
||||
createData.personality = values.personality;
|
||||
createData.appearance = values.appearance;
|
||||
createData.relationships = values.relationships;
|
||||
createData.background = values.background;
|
||||
|
||||
// 职业字段
|
||||
@@ -288,7 +284,6 @@ export default function Characters() {
|
||||
// 组织字段
|
||||
createData.organization_type = values.organization_type;
|
||||
createData.organization_purpose = values.organization_purpose;
|
||||
createData.organization_members = values.organization_members;
|
||||
createData.background = values.background;
|
||||
createData.power_level = values.power_level;
|
||||
createData.location = values.location;
|
||||
@@ -1018,10 +1013,17 @@ export default function Characters() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 第三行:人际关系 */}
|
||||
<Form.Item label="人际关系" name="relationships" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="描述角色与其他角色的关系..." />
|
||||
</Form.Item>
|
||||
{/* 人际关系(只读,由关系管理页面维护) */}
|
||||
{editingCharacter?.relationships && (
|
||||
<Form.Item label="人际关系(由关系管理维护)" style={{ marginBottom: 12 }}>
|
||||
<Input.TextArea
|
||||
value={editingCharacter.relationships}
|
||||
readOnly
|
||||
autoSize={{ minRows: 1, maxRows: 3 }}
|
||||
style={{ backgroundColor: '#f5f5f5', cursor: 'default' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 第四行:角色背景 */}
|
||||
<Form.Item label="角色背景" name="background" style={{ marginBottom: 12 }}>
|
||||
@@ -1182,19 +1184,32 @@ export default function Characters() {
|
||||
<Input placeholder="描述组织的宗旨和目标..." />
|
||||
</Form.Item>
|
||||
|
||||
{/* 第三行:主要成员、所在地、代表颜色 */}
|
||||
{/* 第三行:主要成员(只读展示) */}
|
||||
<Form.Item
|
||||
label="主要成员"
|
||||
name="organization_members"
|
||||
style={{ marginBottom: 4 }}
|
||||
tooltip="成员信息由组织管理模块维护,此处仅展示"
|
||||
>
|
||||
<TextArea
|
||||
disabled
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
placeholder="暂无成员,请在组织管理中添加"
|
||||
style={{ color: '#333', backgroundColor: '#fafafa' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#8c8c8c' }}>
|
||||
💡 请前往「组织管理」页面添加或管理组织成员
|
||||
</div>
|
||||
|
||||
{/* 第四行:所在地、代表颜色 */}
|
||||
<Row gutter={12}>
|
||||
<Col span={10}>
|
||||
<Form.Item label="主要成员" name="organization_members" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="如:张三、李四" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="所在地" name="location" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="总部位置" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="代表颜色" name="color" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="如:金色" />
|
||||
</Form.Item>
|
||||
@@ -1289,12 +1304,7 @@ export default function Characters() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 第三行:人际关系 */}
|
||||
<Form.Item label="人际关系" name="relationships" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="描述角色与其他角色的关系..." />
|
||||
</Form.Item>
|
||||
|
||||
{/* 第四行:角色背景 */}
|
||||
{/* 第三行:角色背景 */}
|
||||
<Form.Item label="角色背景" name="background" style={{ marginBottom: 12 }}>
|
||||
<TextArea rows={2} placeholder="描述角色的背景故事..." />
|
||||
</Form.Item>
|
||||
@@ -1454,19 +1464,14 @@ export default function Characters() {
|
||||
<Input placeholder="描述组织的宗旨和目标..." />
|
||||
</Form.Item>
|
||||
|
||||
{/* 第三行:主要成员、所在地、代表颜色 */}
|
||||
{/* 第三行:所在地、代表颜色 */}
|
||||
<Row gutter={12}>
|
||||
<Col span={10}>
|
||||
<Form.Item label="主要成员" name="organization_members" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="如:张三、李四" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="所在地" name="location" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="总部位置" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="代表颜色" name="color" style={{ marginBottom: 12 }}>
|
||||
<Input placeholder="如:金色" />
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user