update:1.后端新增API配置预设管理接口,支持API配置预设并保存到数据库 2.前端Settings页面重构为Tab布局,新增配置预设管理功能页面 3.优化角色/组织更新逻辑,修复组织字段同步问题 4.更新组织管理-组织成员UI显示,支持翻页显示和跳转
This commit is contained in:
@@ -246,11 +246,50 @@ async def update_character(
|
|||||||
|
|
||||||
# 更新字段
|
# 更新字段
|
||||||
update_data = character_update.model_dump(exclude_unset=True)
|
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():
|
for field, value in update_data.items():
|
||||||
setattr(character, field, value)
|
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.commit()
|
||||||
await db.refresh(character)
|
await db.refresh(character)
|
||||||
|
|
||||||
|
logger.info(f"更新角色/组织成功:{character.name} (ID: {character_id})")
|
||||||
return character
|
return character
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ async def update_organization(
|
|||||||
user_id = getattr(request.state, 'user_id', None)
|
user_id = getattr(request.state, 'user_id', None)
|
||||||
await verify_project_access(db_org.project_id, user_id, db)
|
await verify_project_access(db_org.project_id, user_id, db)
|
||||||
|
|
||||||
# 更新字段
|
# 更新 Organization 表字段
|
||||||
update_data = organization.model_dump(exclude_unset=True)
|
update_data = organization.model_dump(exclude_unset=True)
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
setattr(db_org, field, value)
|
setattr(db_org, field, value)
|
||||||
|
|||||||
+335
-3
@@ -4,14 +4,21 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Request, Depends
|
from fastapi import APIRouter, HTTPException, Request, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
import httpx
|
import httpx
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.settings import Settings
|
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.user_manager import User
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
from app.config import settings as app_settings, PROJECT_ROOT
|
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": error_msg,
|
||||||
"error_type": error_type,
|
"error_type": error_type,
|
||||||
"suggestions": suggestions
|
"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)
|
||||||
@@ -61,6 +61,12 @@ class CharacterUpdate(BaseModel):
|
|||||||
organization_purpose: Optional[str] = None
|
organization_purpose: Optional[str] = None
|
||||||
organization_members: Optional[str] = None
|
organization_members: Optional[str] = None
|
||||||
traits: 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):
|
class CharacterResponse(CharacterBase):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""设置相关的Pydantic模型"""
|
"""设置相关的Pydantic模型"""
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
@@ -34,4 +34,62 @@ class SettingsResponse(SettingsBase):
|
|||||||
id: str
|
id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
created_at: datetime
|
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")
|
||||||
@@ -374,7 +374,10 @@ class PromptService:
|
|||||||
2. **关系约束**:relationships_array只能引用本批次中已经出现的角色名称
|
2. **关系约束**:relationships_array只能引用本批次中已经出现的角色名称
|
||||||
3. **组织约束**:organization_memberships只能引用本批次中is_organization=true的实体名称
|
3. **组织约束**:organization_memberships只能引用本批次中is_organization=true的实体名称
|
||||||
4. **禁止幻觉**:不要引用任何不存在的角色或组织,如果没有可引用的就留空数组[]
|
4. **禁止幻觉**:不要引用任何不存在的角色或组织,如果没有可引用的就留空数组[]
|
||||||
5. intimacy_level是-100到100的整数(负值表示敌对仇恨关系),loyalty是0-100的整数
|
5. **数值范围约束**:
|
||||||
|
- intimacy_level:-100到100的整数(负值表示敌对仇恨关系)
|
||||||
|
- loyalty:0到100的整数
|
||||||
|
- **rank:0到10的整数**(职位等级,0最低,10最高)
|
||||||
6. 角色之间要形成合理的关系网络
|
6. 角色之间要形成合理的关系网络
|
||||||
|
|
||||||
**示例说明**:
|
**示例说明**:
|
||||||
@@ -1584,14 +1587,16 @@ class PromptService:
|
|||||||
1. **relationships数组必填**:至少要有1-3个与已有角色的关系(除非确实没有合理的关联)
|
1. **relationships数组必填**:至少要有1-3个与已有角色的关系(除非确实没有合理的关联)
|
||||||
2. **target_character_name必须精确匹配**:只能引用【已有角色】列表中的角色名称
|
2. **target_character_name必须精确匹配**:只能引用【已有角色】列表中的角色名称
|
||||||
3. organization_memberships只能引用已存在的组织名称
|
3. organization_memberships只能引用已存在的组织名称
|
||||||
4. intimacy_level是-100到100的整数:
|
4. **数值范围约束**:
|
||||||
- 80-100:至亲、挚友、深爱
|
- intimacy_level:-100到100的整数
|
||||||
- 50-79:亲密、友好
|
* 80-100:至亲、挚友、深爱
|
||||||
- 0-49:一般、普通
|
* 50-79:亲密、友好
|
||||||
- -1到-49:不和、敌视
|
* 0-49:一般、普通
|
||||||
- -50到-100:仇恨、死敌
|
* -1到-49:不和、敌视
|
||||||
5. loyalty是0-100的整数(仅用于组织成员)
|
* -50到-100:仇恨、死敌
|
||||||
6. status默认为"active",表示当前关系状态
|
- loyalty:0到100的整数(仅用于组织成员)
|
||||||
|
- **rank:0到10的整数**(职位等级,0最低,10最高)
|
||||||
|
5. status默认为"active",表示当前关系状态
|
||||||
|
|
||||||
**关系建立示例:**
|
**关系建立示例:**
|
||||||
- 如果新角色是主角的新队友,应该与主角建立"队友"或"朋友"关系
|
- 如果新角色是主角的新队友,应该与主角建立"队友"或"朋友"关系
|
||||||
|
|||||||
@@ -612,8 +612,12 @@ export default function Characters() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item label="主要成员" name="organization_members">
|
<Form.Item
|
||||||
<Input placeholder="如:张三、李四、王五" />
|
label="势力等级"
|
||||||
|
name="power_level"
|
||||||
|
tooltip="0-100的数值,表示组织的影响力"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -626,6 +630,10 @@ export default function Characters() {
|
|||||||
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
|
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="主要成员" name="organization_members">
|
||||||
|
<Input placeholder="如:张三、李四、王五" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item label="所在地" name="location">
|
<Form.Item label="所在地" name="location">
|
||||||
|
|||||||
+500
-466
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions } from 'antd';
|
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions } from 'antd';
|
||||||
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
|
import { useCharacterSync } from '../store/hooks';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
interface Organization {
|
interface Organization {
|
||||||
@@ -41,6 +42,7 @@ interface Character {
|
|||||||
export default function Organizations() {
|
export default function Organizations() {
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
const { currentProject } = useStore();
|
const { currentProject } = useStore();
|
||||||
|
const { refreshCharacters } = useCharacterSync();
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
|
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
|
||||||
const [members, setMembers] = useState<OrganizationMember[]>([]);
|
const [members, setMembers] = useState<OrganizationMember[]>([]);
|
||||||
@@ -216,7 +218,7 @@ export default function Organizations() {
|
|||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
width: isMobile ? 80 : undefined,
|
width: isMobile ? 100 : undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '职位',
|
title: '职位',
|
||||||
@@ -225,50 +227,53 @@ export default function Organizations() {
|
|||||||
render: (position: string, record: OrganizationMember) => (
|
render: (position: string, record: OrganizationMember) => (
|
||||||
<Tag color="blue">{position} {!isMobile && `(级别 ${record.rank})`}</Tag>
|
<Tag color="blue">{position} {!isMobile && `(级别 ${record.rank})`}</Tag>
|
||||||
),
|
),
|
||||||
|
width: isMobile ? 120 : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '忠诚度',
|
||||||
|
dataIndex: 'loyalty',
|
||||||
|
key: 'loyalty',
|
||||||
|
render: (loyalty: number) => (
|
||||||
|
<span style={{ color: loyalty >= 70 ? 'green' : loyalty >= 40 ? 'orange' : 'red' }}>
|
||||||
|
{loyalty}%
|
||||||
|
</span>
|
||||||
|
),
|
||||||
width: isMobile ? 80 : undefined,
|
width: isMobile ? 80 : undefined,
|
||||||
},
|
},
|
||||||
...(!isMobile ? [
|
{
|
||||||
{
|
title: '贡献度',
|
||||||
title: '忠诚度',
|
dataIndex: 'contribution',
|
||||||
dataIndex: 'loyalty',
|
key: 'contribution',
|
||||||
key: 'loyalty',
|
render: (contribution: number) => `${contribution}%`,
|
||||||
render: (loyalty: number) => (
|
width: isMobile ? 80 : undefined,
|
||||||
<span style={{ color: loyalty >= 70 ? 'green' : loyalty >= 40 ? 'orange' : 'red' }}>
|
},
|
||||||
{loyalty}%
|
{
|
||||||
</span>
|
title: '状态',
|
||||||
),
|
dataIndex: 'status',
|
||||||
},
|
key: 'status',
|
||||||
{
|
render: (status: string) => (
|
||||||
title: '贡献度',
|
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
||||||
dataIndex: 'contribution',
|
),
|
||||||
key: 'contribution',
|
width: isMobile ? 80 : undefined,
|
||||||
render: (contribution: number) => `${contribution}%`,
|
},
|
||||||
},
|
{
|
||||||
{
|
title: '加入时间',
|
||||||
title: '状态',
|
dataIndex: 'joined_at',
|
||||||
dataIndex: 'status',
|
key: 'joined_at',
|
||||||
key: 'status',
|
render: (time: string) => time || '-',
|
||||||
render: (status: string) => (
|
width: isMobile ? 120 : undefined,
|
||||||
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
},
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '加入时间',
|
|
||||||
dataIndex: 'joined_at',
|
|
||||||
key: 'joined_at',
|
|
||||||
render: (time: string) => time || '-',
|
|
||||||
}
|
|
||||||
] : []),
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
render: (_: unknown, record: OrganizationMember) => (
|
render: (_: unknown, record: OrganizationMember) => (
|
||||||
<Space>
|
<Space size={isMobile ? 0 : 'small'}>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => handleEditMember(record)}
|
onClick={() => handleEditMember(record)}
|
||||||
|
style={isMobile ? { padding: '4px' } : undefined}
|
||||||
>
|
>
|
||||||
{isMobile ? '' : '编辑'}
|
{isMobile ? '' : '编辑'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -278,12 +283,13 @@ export default function Organizations() {
|
|||||||
size="small"
|
size="small"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={() => handleRemoveMember(record.id)}
|
onClick={() => handleRemoveMember(record.id)}
|
||||||
|
style={isMobile ? { padding: '4px' } : undefined}
|
||||||
>
|
>
|
||||||
{isMobile ? '删除' : '移除'}
|
{isMobile ? '' : '移除'}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
width: isMobile ? 60 : undefined,
|
width: isMobile ? 50 : undefined,
|
||||||
fixed: isMobile ? 'right' as const : undefined,
|
fixed: isMobile ? 'right' as const : undefined,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -419,9 +425,24 @@ export default function Organizations() {
|
|||||||
columns={memberColumns}
|
columns={memberColumns}
|
||||||
dataSource={members}
|
dataSource={members}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
pagination={isMobile ? { simple: true, pageSize: 10 } : false}
|
pagination={
|
||||||
|
members.length > 5
|
||||||
|
? {
|
||||||
|
defaultPageSize: 5,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: !isMobile,
|
||||||
|
showTotal: (total) => `共 ${total} 名成员`,
|
||||||
|
pageSizeOptions: [5, 10, 20],
|
||||||
|
simple: isMobile,
|
||||||
|
position: ['bottomCenter'],
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
size="small"
|
size="small"
|
||||||
scroll={isMobile ? { x: 'max-content', y: 400 } : undefined}
|
scroll={{
|
||||||
|
x: isMobile ? 'max-content' : undefined,
|
||||||
|
y: members.length > 10 ? 500 : undefined,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -653,7 +674,20 @@ export default function Organizations() {
|
|||||||
await axios.put(`/api/organizations/${selectedOrg.id}`, values);
|
await axios.put(`/api/organizations/${selectedOrg.id}`, values);
|
||||||
message.success('组织信息更新成功');
|
message.success('组织信息更新成功');
|
||||||
setIsEditOrgModalOpen(false);
|
setIsEditOrgModalOpen(false);
|
||||||
loadOrganizations();
|
editOrgForm.resetFields();
|
||||||
|
|
||||||
|
// 重新获取更新后的组织列表
|
||||||
|
const res = await axios.get(`/api/organizations/project/${projectId}`);
|
||||||
|
setOrganizations(res.data);
|
||||||
|
|
||||||
|
// 更新当前选中的组织详情
|
||||||
|
const updatedOrg = res.data.find((org: Organization) => org.id === selectedOrg.id);
|
||||||
|
if (updatedOrg) {
|
||||||
|
setSelectedOrg(updatedOrg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新全局 store
|
||||||
|
await refreshCharacters();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('更新失败');
|
message.error('更新失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
+1045
-515
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,10 @@ import type {
|
|||||||
MCPTool,
|
MCPTool,
|
||||||
MCPToolCallRequest,
|
MCPToolCallRequest,
|
||||||
MCPToolCallResponse,
|
MCPToolCallResponse,
|
||||||
|
APIKeyPreset,
|
||||||
|
PresetCreateRequest,
|
||||||
|
PresetUpdateRequest,
|
||||||
|
PresetListResponse,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
@@ -197,6 +201,41 @@ export const settingsApi = {
|
|||||||
error_type?: string;
|
error_type?: string;
|
||||||
suggestions?: string[];
|
suggestions?: string[];
|
||||||
}>('/settings/test', params),
|
}>('/settings/test', params),
|
||||||
|
|
||||||
|
// API配置预设管理
|
||||||
|
getPresets: () =>
|
||||||
|
api.get<unknown, PresetListResponse>('/settings/presets'),
|
||||||
|
|
||||||
|
createPreset: (data: PresetCreateRequest) =>
|
||||||
|
api.post<unknown, APIKeyPreset>('/settings/presets', data),
|
||||||
|
|
||||||
|
updatePreset: (presetId: string, data: PresetUpdateRequest) =>
|
||||||
|
api.put<unknown, APIKeyPreset>(`/settings/presets/${presetId}`, data),
|
||||||
|
|
||||||
|
deletePreset: (presetId: string) =>
|
||||||
|
api.delete<unknown, { message: string; preset_id: string }>(`/settings/presets/${presetId}`),
|
||||||
|
|
||||||
|
activatePreset: (presetId: string) =>
|
||||||
|
api.post<unknown, { message: string; preset_id: string; preset_name: string }>(`/settings/presets/${presetId}/activate`),
|
||||||
|
|
||||||
|
testPreset: (presetId: string) =>
|
||||||
|
api.post<unknown, {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
response_time_ms?: number;
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
response_preview?: string;
|
||||||
|
details?: Record<string, boolean>;
|
||||||
|
error?: string;
|
||||||
|
error_type?: string;
|
||||||
|
suggestions?: string[];
|
||||||
|
}>(`/settings/presets/${presetId}/test`),
|
||||||
|
|
||||||
|
createPresetFromCurrent: (name: string, description?: string) =>
|
||||||
|
api.post<unknown, APIKeyPreset>('/settings/presets/from-current', null, {
|
||||||
|
params: { name, description }
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectApi = {
|
export const projectApi = {
|
||||||
|
|||||||
@@ -36,6 +36,43 @@ export interface SettingsUpdate {
|
|||||||
preferences?: string;
|
preferences?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API预设相关类型定义
|
||||||
|
export interface APIKeyPresetConfig {
|
||||||
|
api_provider: string;
|
||||||
|
api_key: string;
|
||||||
|
api_base_url?: string;
|
||||||
|
llm_model: string;
|
||||||
|
temperature: number;
|
||||||
|
max_tokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIKeyPreset {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
config: APIKeyPresetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresetCreateRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
config: APIKeyPresetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresetUpdateRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
config?: APIKeyPresetConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresetListResponse {
|
||||||
|
presets: APIKeyPreset[];
|
||||||
|
total: number;
|
||||||
|
active_preset_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// LinuxDO 授权 URL 响应
|
// LinuxDO 授权 URL 响应
|
||||||
export interface AuthUrlResponse {
|
export interface AuthUrlResponse {
|
||||||
auth_url: string;
|
auth_url: string;
|
||||||
@@ -622,22 +659,22 @@ export interface MCPPlugin {
|
|||||||
description?: string;
|
description?: string;
|
||||||
plugin_type: 'http' | 'stdio';
|
plugin_type: 'http' | 'stdio';
|
||||||
category: string;
|
category: string;
|
||||||
|
|
||||||
// HTTP类型字段
|
// HTTP类型字段
|
||||||
server_url?: string;
|
server_url?: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
|
||||||
// Stdio类型字段
|
// Stdio类型字段
|
||||||
command?: string;
|
command?: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
|
||||||
// 状态字段
|
// 状态字段
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
status: 'active' | 'inactive' | 'error';
|
status: 'active' | 'inactive' | 'error';
|
||||||
last_error?: string;
|
last_error?: string;
|
||||||
last_test_at?: string;
|
last_test_at?: string;
|
||||||
|
|
||||||
// 时间戳
|
// 时间戳
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user