update:1.后端新增API配置预设管理接口,支持API配置预设并保存到数据库 2.前端Settings页面重构为Tab布局,新增配置预设管理功能页面 3.优化角色/组织更新逻辑,修复组织字段同步问题 4.更新组织管理-组织成员UI显示,支持翻页显示和跳转

This commit is contained in:
xiamuceer
2025-12-15 15:58:57 +08:00
parent 247156d2c1
commit a753c75b9c
12 changed files with 2163 additions and 1041 deletions
+39
View File
@@ -246,11 +246,50 @@ async def update_character(
# 更新字段
update_data = character_update.model_dump(exclude_unset=True)
# 如果是组织,需要同步更新 Organization 表的字段
org_fields = {}
if character.is_organization:
# 提取需要同步到 Organization 表的字段
if 'power_level' in update_data:
org_fields['power_level'] = update_data.pop('power_level')
if 'location' in update_data:
org_fields['location'] = update_data.pop('location')
if 'motto' in update_data:
org_fields['motto'] = update_data.pop('motto')
if 'color' in update_data:
org_fields['color'] = update_data.pop('color')
# 更新 Character 表字段
for field, value in update_data.items():
setattr(character, field, value)
# 如果是组织且有需要同步的字段,更新 Organization 表
if character.is_organization and org_fields:
org_result = await db.execute(
select(Organization).where(Organization.character_id == character_id)
)
org = org_result.scalar_one_or_none()
if org:
for field, value in org_fields.items():
setattr(org, field, value)
logger.info(f"同步更新组织详情:{character.name}")
else:
# 如果 Organization 记录不存在,自动创建
org = Organization(
character_id=character_id,
project_id=character.project_id,
member_count=0,
**org_fields
)
db.add(org)
logger.info(f"自动创建组织详情:{character.name}")
await db.commit()
await db.refresh(character)
logger.info(f"更新角色/组织成功:{character.name} (ID: {character_id})")
return character
+1 -1
View File
@@ -194,7 +194,7 @@ async def update_organization(
user_id = getattr(request.state, 'user_id', None)
await verify_project_access(db_org.project_id, user_id, db)
# 更新字段
# 更新 Organization 表字段
update_data = organization.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_org, field, value)
+335 -3
View File
@@ -4,14 +4,21 @@
from fastapi import APIRouter, HTTPException, Request, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
from pathlib import Path
from pydantic import BaseModel
from datetime import datetime
import httpx
import json
import time
from app.database import get_db
from app.models.settings import Settings
from app.schemas.settings import SettingsCreate, SettingsUpdate, SettingsResponse
from app.schemas.settings import (
SettingsCreate, SettingsUpdate, SettingsResponse,
APIKeyPreset, APIKeyPresetConfig, PresetCreateRequest,
PresetUpdateRequest, PresetResponse, PresetListResponse
)
from app.user_manager import User
from app.logger import get_logger
from app.config import settings as app_settings, PROJECT_ROOT
@@ -462,4 +469,329 @@ async def test_api_connection(data: ApiTestRequest):
"error": error_msg,
"error_type": error_type,
"suggestions": suggestions
}
}
# ========== API配置预设管理(零数据库改动方案)==========
async def get_user_settings(user_id: str, db: AsyncSession) -> Settings:
"""获取用户settings,如果不存在则创建"""
result = await db.execute(
select(Settings).where(Settings.user_id == user_id)
)
settings = result.scalar_one_or_none()
if not settings:
# 创建默认设置
env_defaults = read_env_defaults()
settings = Settings(
user_id=user_id,
**env_defaults,
preferences='{}' # 初始化为空JSON
)
db.add(settings)
await db.commit()
await db.refresh(settings)
logger.info(f"用户 {user_id} 首次访问,已创建默认设置")
return settings
@router.get("/presets", response_model=PresetListResponse)
async def get_presets(
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""
获取所有API配置预设
从preferences字段读取预设列表
"""
settings = await get_user_settings(user.user_id, db)
# 解析preferences
try:
prefs = json.loads(settings.preferences or '{}')
except json.JSONDecodeError:
logger.warning(f"用户 {user.user_id} 的preferences字段JSON格式错误,重置为空")
prefs = {}
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
presets = api_presets.get('presets', [])
# 找到激活的预设
active_preset_id = next(
(p['id'] for p in presets if p.get('is_active')),
None
)
logger.info(f"用户 {user.user_id} 获取预设列表,共 {len(presets)}")
return {
"presets": presets,
"total": len(presets),
"active_preset_id": active_preset_id
}
@router.post("/presets", response_model=PresetResponse)
async def create_preset(
data: PresetCreateRequest,
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""
创建新预设
将预设添加到preferences字段的JSON中
"""
settings = await get_user_settings(user.user_id, db)
# 解析preferences
try:
prefs = json.loads(settings.preferences or '{}')
except json.JSONDecodeError:
prefs = {}
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
presets = api_presets.get('presets', [])
# 创建新预设
new_preset = {
"id": f"preset_{int(datetime.now().timestamp() * 1000)}",
"name": data.name,
"description": data.description,
"is_active": False,
"created_at": datetime.now().isoformat(),
"config": data.config.model_dump()
}
presets.append(new_preset)
# 保存回preferences
api_presets['presets'] = presets
prefs['api_presets'] = api_presets
settings.preferences = json.dumps(prefs, ensure_ascii=False)
await db.commit()
logger.info(f"用户 {user.user_id} 创建预设: {data.name}")
return new_preset
@router.put("/presets/{preset_id}", response_model=PresetResponse)
async def update_preset(
preset_id: str,
data: PresetUpdateRequest,
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""
更新预设
在preferences字段的JSON中更新指定预设
"""
settings = await get_user_settings(user.user_id, db)
# 解析preferences
try:
prefs = json.loads(settings.preferences or '{}')
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="配置数据格式错误")
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
presets = api_presets.get('presets', [])
# 找到并更新预设
target_preset = next((p for p in presets if p['id'] == preset_id), None)
if not target_preset:
raise HTTPException(status_code=404, detail="预设不存在")
# 更新字段
if data.name is not None:
target_preset['name'] = data.name
if data.description is not None:
target_preset['description'] = data.description
if data.config is not None:
target_preset['config'] = data.config.model_dump()
# 保存回preferences
prefs['api_presets'] = api_presets
settings.preferences = json.dumps(prefs, ensure_ascii=False)
await db.commit()
logger.info(f"用户 {user.user_id} 更新预设: {preset_id}")
return target_preset
@router.delete("/presets/{preset_id}")
async def delete_preset(
preset_id: str,
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""
删除预设
从preferences字段的JSON中删除指定预设
"""
settings = await get_user_settings(user.user_id, db)
# 解析preferences
try:
prefs = json.loads(settings.preferences or '{}')
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="配置数据格式错误")
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
presets = api_presets.get('presets', [])
# 找到预设
target_preset = next((p for p in presets if p['id'] == preset_id), None)
if not target_preset:
raise HTTPException(status_code=404, detail="预设不存在")
# 检查是否是激活的预设
if target_preset.get('is_active'):
raise HTTPException(status_code=400, detail="无法删除激活中的预设,请先激活其他预设")
# 删除预设
presets = [p for p in presets if p['id'] != preset_id]
# 保存回preferences
api_presets['presets'] = presets
prefs['api_presets'] = api_presets
settings.preferences = json.dumps(prefs, ensure_ascii=False)
await db.commit()
logger.info(f"用户 {user.user_id} 删除预设: {preset_id}")
return {"message": "预设已删除", "preset_id": preset_id}
@router.post("/presets/{preset_id}/activate")
async def activate_preset(
preset_id: str,
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""
激活预设
将预设的配置应用到Settings主字段
"""
settings = await get_user_settings(user.user_id, db)
# 解析preferences
try:
prefs = json.loads(settings.preferences or '{}')
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="配置数据格式错误")
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
presets = api_presets.get('presets', [])
# 找到目标预设
target_preset = next((p for p in presets if p['id'] == preset_id), None)
if not target_preset:
raise HTTPException(status_code=404, detail="预设不存在")
# 应用配置到Settings主字段
config = target_preset['config']
settings.api_provider = config['api_provider']
settings.api_key = config['api_key']
settings.api_base_url = config.get('api_base_url')
settings.llm_model = config['llm_model']
settings.temperature = config['temperature']
settings.max_tokens = config['max_tokens']
# 更新所有预设的is_active状态
for preset in presets:
preset['is_active'] = (preset['id'] == preset_id)
# 保存回preferences
prefs['api_presets'] = api_presets
settings.preferences = json.dumps(prefs, ensure_ascii=False)
await db.commit()
logger.info(f"用户 {user.user_id} 激活预设: {target_preset['name']}")
return {
"message": "预设已激活",
"preset_id": preset_id,
"preset_name": target_preset['name']
}
@router.post("/presets/{preset_id}/test")
async def test_preset(
preset_id: str,
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""
测试预设的API连接
"""
settings = await get_user_settings(user.user_id, db)
# 解析preferences
try:
prefs = json.loads(settings.preferences or '{}')
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="配置数据格式错误")
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
presets = api_presets.get('presets', [])
# 找到预设
target_preset = next((p for p in presets if p['id'] == preset_id), None)
if not target_preset:
raise HTTPException(status_code=404, detail="预设不存在")
# 使用现有的test_api_connection逻辑
config = target_preset['config']
test_request = ApiTestRequest(
api_key=config['api_key'],
api_base_url=config.get('api_base_url', ''),
provider=config['api_provider'],
llm_model=config['llm_model']
)
logger.info(f"用户 {user.user_id} 测试预设: {target_preset['name']}")
return await test_api_connection(test_request)
@router.post("/presets/from-current", response_model=PresetResponse)
async def create_preset_from_current(
name: str,
description: Optional[str] = None,
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""
从当前配置创建新预设
快捷方式:将当前激活的配置保存为新预设
"""
settings = await get_user_settings(user.user_id, db)
# 从当前Settings主字段读取配置
current_config = APIKeyPresetConfig(
api_provider=settings.api_provider,
api_key=settings.api_key,
api_base_url=settings.api_base_url,
llm_model=settings.llm_model,
temperature=settings.temperature,
max_tokens=settings.max_tokens
)
# 创建预设
create_request = PresetCreateRequest(
name=name,
description=description,
config=current_config
)
logger.info(f"用户 {user.user_id} 从当前配置创建预设: {name}")
return await create_preset(create_request, user, db)
+6
View File
@@ -61,6 +61,12 @@ class CharacterUpdate(BaseModel):
organization_purpose: Optional[str] = None
organization_members: Optional[str] = None
traits: Optional[str] = None
# 组织额外字段(会同步到Organization表)
power_level: Optional[int] = Field(None, description="组织势力等级(0-100)")
location: Optional[str] = Field(None, description="组织所在地")
motto: Optional[str] = Field(None, description="组织格言/口号")
color: Optional[str] = Field(None, description="组织代表颜色")
class CharacterResponse(CharacterBase):
+60 -2
View File
@@ -1,6 +1,6 @@
"""设置相关的Pydantic模型"""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from typing import Optional, List
from datetime import datetime
@@ -34,4 +34,62 @@ class SettingsResponse(SettingsBase):
id: str
user_id: str
created_at: datetime
updated_at: datetime
updated_at: datetime
# ========== API配置预设相关模型 ==========
class APIKeyPresetConfig(BaseModel):
"""预设配置内容"""
model_config = ConfigDict(protected_namespaces=())
api_provider: str = Field(..., description="API提供商")
api_key: str = Field(..., description="API密钥")
api_base_url: Optional[str] = Field(None, description="自定义API地址")
llm_model: str = Field(..., description="模型名称")
temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="温度参数")
max_tokens: int = Field(default=2000, ge=1, description="最大token数")
class APIKeyPreset(BaseModel):
"""API配置预设"""
model_config = ConfigDict(protected_namespaces=())
id: str = Field(..., description="预设ID")
name: str = Field(..., min_length=1, max_length=50, description="预设名称")
description: Optional[str] = Field(None, max_length=200, description="预设描述")
is_active: bool = Field(default=False, description="是否激活")
created_at: datetime = Field(..., description="创建时间")
config: APIKeyPresetConfig = Field(..., description="配置内容")
class PresetCreateRequest(BaseModel):
"""创建预设请求"""
model_config = ConfigDict(protected_namespaces=())
name: str = Field(..., min_length=1, max_length=50, description="预设名称")
description: Optional[str] = Field(None, max_length=200, description="预设描述")
config: APIKeyPresetConfig = Field(..., description="配置内容")
class PresetUpdateRequest(BaseModel):
"""更新预设请求"""
model_config = ConfigDict(protected_namespaces=())
name: Optional[str] = Field(None, min_length=1, max_length=50, description="预设名称")
description: Optional[str] = Field(None, max_length=200, description="预设描述")
config: Optional[APIKeyPresetConfig] = Field(None, description="配置内容")
class PresetResponse(APIKeyPreset):
"""预设响应"""
pass
class PresetListResponse(BaseModel):
"""预设列表响应"""
model_config = ConfigDict(protected_namespaces=())
presets: List[PresetResponse] = Field(..., description="预设列表")
total: int = Field(..., description="总数")
active_preset_id: Optional[str] = Field(None, description="当前激活的预设ID")
+14 -9
View File
@@ -374,7 +374,10 @@ class PromptService:
2. **关系约束**relationships_array只能引用本批次中已经出现的角色名称
3. **组织约束**organization_memberships只能引用本批次中is_organization=true的实体名称
4. **禁止幻觉**:不要引用任何不存在的角色或组织,如果没有可引用的就留空数组[]
5. intimacy_level是-100到100的整数(负值表示敌对仇恨关系),loyalty是0-100的整数
5. **数值范围约束**
- intimacy_level-100到100的整数(负值表示敌对仇恨关系)
- loyalty0到100的整数
- **rank:0到10的整数**(职位等级,0最低,10最高)
6. 角色之间要形成合理的关系网络
**示例说明**
@@ -1584,14 +1587,16 @@ class PromptService:
1. **relationships数组必填**:至少要有1-3个与已有角色的关系(除非确实没有合理的关联)
2. **target_character_name必须精确匹配**:只能引用【已有角色】列表中的角色名称
3. organization_memberships只能引用已存在的组织名称
4. intimacy_level是-100到100的整数
- 80-100:至亲、挚友、深爱
- 50-79:亲密、友好
- 0-49一般、普通
- -1到-49:不和、敌视
- -50到-100:仇恨、死敌
5. loyalty是0-100的整数(仅用于组织成员)
6. status默认为"active",表示当前关系状态
4. **数值范围约束**
- intimacy_level-100到100的整数
* 80-100:至亲、挚友、深爱
* 50-79亲密、友好
* 0-49:一般、普通
* -1到-49:不和、敌视
* -50到-100:仇恨、死敌
- loyalty:0到100的整数(仅用于组织成员)
- **rank:0到10的整数**(职位等级,0最低,10最高)
5. status默认为"active",表示当前关系状态
**关系建立示例:**
- 如果新角色是主角的新队友,应该与主角建立"队友""朋友"关系