refactor:重构角色关系数据驱动,relationships字段改为从关系表动态生成,组织成员同理

This commit is contained in:
xiamuceer-j
2026-02-12 12:39:38 +08:00
parent b9aaf5d6a7
commit e3b2a2bee4
2 changed files with 229 additions and 49 deletions
+193 -18
View File
@@ -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 -30
View File
@@ -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="描述角色与其他角色的关系..." />
{/* 人际关系(只读,由关系管理页面维护) */}
{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>
{/* 第三行:主要成员、所在地、代表颜色 */}
<Row gutter={12}>
<Col span={10}>
<Form.Item label="主要成员" name="organization_members" style={{ marginBottom: 12 }}>
<Input placeholder="如:张三、李四" />
{/* 第三行:主要成员(只读展示) */}
<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>
</Col>
<Col span={8}>
<div style={{ marginBottom: 12, fontSize: 12, color: '#8c8c8c' }}>
💡
</div>
{/* 第四行:所在地、代表颜色 */}
<Row gutter={12}>
<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>