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)
|
||||
|
||||
# 如果是组织,需要同步更新 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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+334
-2
@@ -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
|
||||
@@ -463,3 +470,328 @@ async def test_api_connection(data: ApiTestRequest):
|
||||
"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)
|
||||
@@ -62,6 +62,12 @@ class CharacterUpdate(BaseModel):
|
||||
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):
|
||||
"""角色响应模型"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""设置相关的Pydantic模型"""
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -35,3 +35,61 @@ class SettingsResponse(SettingsBase):
|
||||
user_id: str
|
||||
created_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只能引用本批次中已经出现的角色名称
|
||||
3. **组织约束**:organization_memberships只能引用本批次中is_organization=true的实体名称
|
||||
4. **禁止幻觉**:不要引用任何不存在的角色或组织,如果没有可引用的就留空数组[]
|
||||
5. intimacy_level是-100到100的整数(负值表示敌对仇恨关系),loyalty是0-100的整数
|
||||
5. **数值范围约束**:
|
||||
- intimacy_level:-100到100的整数(负值表示敌对仇恨关系)
|
||||
- loyalty:0到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",表示当前关系状态
|
||||
|
||||
**关系建立示例:**
|
||||
- 如果新角色是主角的新队友,应该与主角建立"队友"或"朋友"关系
|
||||
|
||||
@@ -612,8 +612,12 @@ export default function Characters() {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="主要成员" name="organization_members">
|
||||
<Input placeholder="如:张三、李四、王五" />
|
||||
<Form.Item
|
||||
label="势力等级"
|
||||
name="power_level"
|
||||
tooltip="0-100的数值,表示组织的影响力"
|
||||
>
|
||||
<InputNumber min={0} max={100} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -626,6 +630,10 @@ export default function Characters() {
|
||||
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="主要成员" name="organization_members">
|
||||
<Input placeholder="如:张三、李四、王五" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="所在地" name="location">
|
||||
|
||||
+142
-108
@@ -16,7 +16,6 @@ import {
|
||||
Spin,
|
||||
Empty,
|
||||
Alert,
|
||||
Descriptions,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd';
|
||||
@@ -41,6 +40,7 @@ export default function MCPPluginsPage() {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const [form] = Form.useForm();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [plugins, setPlugins] = useState<MCPPlugin[]>([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
@@ -150,124 +150,100 @@ export default function MCPPluginsPage() {
|
||||
await loadPlugins();
|
||||
|
||||
if (result.success) {
|
||||
Modal.success({
|
||||
title: '✅ 测试成功',
|
||||
width: 700,
|
||||
modal.success({
|
||||
title: '测试成功',
|
||||
centered: true,
|
||||
width: isMobile ? '90%' : 600,
|
||||
content: (
|
||||
<div>
|
||||
<p style={{ marginBottom: 16, fontSize: '15px', fontWeight: 500 }}>{result.message}</p>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)' }}>
|
||||
✓ {result.message}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{/* 显示详细的测试结果 */}
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>测试详情:</Text>
|
||||
{(result.tools_count !== undefined || result.response_time_ms !== undefined) && (
|
||||
<div style={{
|
||||
marginTop: 8,
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 4,
|
||||
fontSize: '13px',
|
||||
fontFamily: 'monospace',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto'
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
{result.suggestions.map((suggestion: string, index: number) => (
|
||||
<div key={index} style={{
|
||||
marginBottom: index < (result.suggestions?.length || 0) - 1 ? 8 : 0,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.6
|
||||
}}>
|
||||
{suggestion}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 显示工具数量 */}
|
||||
{result.tools_count !== undefined && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="secondary">🔧 可用工具数: <strong>{result.tools_count}</strong></Text>
|
||||
<div style={{ marginBottom: 8, fontSize: 14 }}>
|
||||
<Text type="secondary">可用工具数:</Text>
|
||||
<Text strong>{result.tools_count}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 显示响应时间 */}
|
||||
{result.response_time_ms !== undefined && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Text type="secondary">⏱️ 响应时间: <strong>{result.response_time_ms}ms</strong></Text>
|
||||
<div style={{ fontSize: 14 }}>
|
||||
<Text type="secondary">响应时间:</Text>
|
||||
<Text strong>{result.response_time_ms}ms</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: 4
|
||||
}}>
|
||||
<Text type="success" style={{ fontSize: '13px' }}>
|
||||
✓ 插件状态已自动更新为"运行中"
|
||||
</Text>
|
||||
</div>
|
||||
<Alert
|
||||
message='插件状态已自动更新为"运行中"'
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
Modal.error({
|
||||
title: '❌ 测试失败',
|
||||
width: 700,
|
||||
modal.error({
|
||||
title: '测试失败',
|
||||
centered: true,
|
||||
width: isMobile ? '90%' : 600,
|
||||
content: (
|
||||
<div>
|
||||
<p style={{ marginBottom: 16, fontSize: '15px', fontWeight: 500 }}>{result.message}</p>
|
||||
|
||||
{/* 显示错误信息 */}
|
||||
{result.error && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>错误信息:</Text>
|
||||
<div style={{
|
||||
marginTop: 8,
|
||||
padding: 12,
|
||||
background: 'var(--color-error-bg)',
|
||||
borderRadius: 4,
|
||||
color: 'var(--color-error)',
|
||||
fontSize: '13px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{result.error}
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Alert
|
||||
message={result.message || 'MCP插件测试失败'}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
{result.error && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-error-bg)',
|
||||
border: '1px solid var(--color-error-border)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
||||
<Text style={{ fontSize: 13, color: 'var(--color-error)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{result.error}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 显示建议 */}
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>💡 建议:</Text>
|
||||
<ul style={{ marginTop: 8, marginBottom: 0, paddingLeft: 20 }}>
|
||||
{result.suggestions.map((suggestion: string, index: number) => (
|
||||
<li key={index} style={{ marginBottom: 6, fontSize: '13px' }}>
|
||||
{suggestion}
|
||||
</li>
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-warning-bg)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 建议:</Text>
|
||||
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
|
||||
{result.suggestions.map((s: string, i: number) => (
|
||||
<li key={i} style={{ marginBottom: 4 }}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
background: 'var(--color-warning-bg)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
borderRadius: 4
|
||||
}}>
|
||||
<Text style={{ fontSize: '13px', color: '#ad6800' }}>
|
||||
⚠️ 插件状态已更新,请检查配置后重试
|
||||
</Text>
|
||||
</div>
|
||||
<Alert
|
||||
message="插件状态已更新,请检查配置后重试"
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
@@ -340,6 +316,8 @@ export default function MCPPluginsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
||||
@@ -687,49 +665,104 @@ export default function MCPPluginsPage() {
|
||||
|
||||
{/* 查看工具列表模态框 */}
|
||||
<Modal
|
||||
title="可用工具列表"
|
||||
title={
|
||||
<Space>
|
||||
<ToolOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<span>可用工具列表</span>
|
||||
{viewingTools && viewingTools.tools.length > 0 && (
|
||||
<Tag color="blue">{viewingTools.tools.length} 个工具</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
open={!!viewingTools}
|
||||
onCancel={() => setViewingTools(null)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setViewingTools(null)}>
|
||||
<Button key="close" type="primary" onClick={() => setViewingTools(null)}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
width={isMobile ? '100%' : 700}
|
||||
width={isMobile ? '95%' : 800}
|
||||
centered
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: isMobile ? '60vh' : '70vh',
|
||||
overflowY: 'auto',
|
||||
padding: isMobile ? '16px' : '24px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{viewingTools && (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{viewingTools.tools.length === 0 ? (
|
||||
<Empty description="该插件没有提供任何工具" />
|
||||
<Empty
|
||||
description="该插件没有提供任何工具"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
style={{ padding: '40px 0' }}
|
||||
/>
|
||||
) : (
|
||||
viewingTools.tools.map((tool, index) => (
|
||||
<Card key={index} size="small" style={{ borderRadius: 8 }}>
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="工具名称">
|
||||
<Text code strong>
|
||||
<Card
|
||||
key={index}
|
||||
size="small"
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
border: '1px solid var(--color-border-secondary)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
|
||||
}}
|
||||
title={
|
||||
<Space>
|
||||
<Text code strong style={{ fontSize: isMobile ? '13px' : '14px', color: 'var(--color-primary)' }}>
|
||||
{tool.name}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
<Tag color="processing" style={{ fontSize: '11px' }}>
|
||||
#{index + 1}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{tool.description && (
|
||||
<Descriptions.Item label="描述">{tool.description}</Descriptions.Item>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: isMobile ? '12px' : '13px', display: 'block', marginBottom: 4 }}>
|
||||
描述:
|
||||
</Text>
|
||||
<Paragraph
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: isMobile ? '12px' : '13px',
|
||||
padding: '8px 12px',
|
||||
background: 'var(--color-bg-layout)',
|
||||
borderRadius: 4,
|
||||
borderLeft: '3px solid var(--color-info)'
|
||||
}}
|
||||
>
|
||||
{tool.description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
{tool.inputSchema && (
|
||||
<Descriptions.Item label="输入参数">
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: isMobile ? '12px' : '13px', display: 'block', marginBottom: 4 }}>
|
||||
输入参数:
|
||||
</Text>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 8,
|
||||
background: '#f5f5f5',
|
||||
padding: isMobile ? '8px' : '12px',
|
||||
background: 'var(--color-bg-layout)',
|
||||
borderRadius: 4,
|
||||
fontSize: isMobile ? '11px' : '12px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '200px',
|
||||
border: '1px solid var(--color-border-secondary)',
|
||||
lineHeight: 1.6
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(tool.inputSchema, null, 2)}
|
||||
</pre>
|
||||
</Descriptions.Item>
|
||||
</div>
|
||||
)}
|
||||
</Descriptions>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
@@ -737,5 +770,6 @@ export default function MCPPluginsPage() {
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync } from '../store/hooks';
|
||||
import axios from 'axios';
|
||||
|
||||
interface Organization {
|
||||
@@ -41,6 +42,7 @@ interface Character {
|
||||
export default function Organizations() {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const { currentProject } = useStore();
|
||||
const { refreshCharacters } = useCharacterSync();
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
|
||||
const [members, setMembers] = useState<OrganizationMember[]>([]);
|
||||
@@ -216,7 +218,7 @@ export default function Organizations() {
|
||||
<span>{name}</span>
|
||||
</Space>
|
||||
),
|
||||
width: isMobile ? 80 : undefined,
|
||||
width: isMobile ? 100 : undefined,
|
||||
},
|
||||
{
|
||||
title: '职位',
|
||||
@@ -225,9 +227,8 @@ export default function Organizations() {
|
||||
render: (position: string, record: OrganizationMember) => (
|
||||
<Tag color="blue">{position} {!isMobile && `(级别 ${record.rank})`}</Tag>
|
||||
),
|
||||
width: isMobile ? 80 : undefined,
|
||||
width: isMobile ? 120 : undefined,
|
||||
},
|
||||
...(!isMobile ? [
|
||||
{
|
||||
title: '忠诚度',
|
||||
dataIndex: 'loyalty',
|
||||
@@ -237,12 +238,14 @@ export default function Organizations() {
|
||||
{loyalty}%
|
||||
</span>
|
||||
),
|
||||
width: isMobile ? 80 : undefined,
|
||||
},
|
||||
{
|
||||
title: '贡献度',
|
||||
dataIndex: 'contribution',
|
||||
key: 'contribution',
|
||||
render: (contribution: number) => `${contribution}%`,
|
||||
width: isMobile ? 80 : undefined,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
@@ -251,24 +254,26 @@ export default function Organizations() {
|
||||
render: (status: string) => (
|
||||
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
||||
),
|
||||
width: isMobile ? 80 : undefined,
|
||||
},
|
||||
{
|
||||
title: '加入时间',
|
||||
dataIndex: 'joined_at',
|
||||
key: 'joined_at',
|
||||
render: (time: string) => time || '-',
|
||||
}
|
||||
] : []),
|
||||
width: isMobile ? 120 : undefined,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: unknown, record: OrganizationMember) => (
|
||||
<Space>
|
||||
<Space size={isMobile ? 0 : 'small'}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEditMember(record)}
|
||||
style={isMobile ? { padding: '4px' } : undefined}
|
||||
>
|
||||
{isMobile ? '' : '编辑'}
|
||||
</Button>
|
||||
@@ -278,12 +283,13 @@ export default function Organizations() {
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemoveMember(record.id)}
|
||||
style={isMobile ? { padding: '4px' } : undefined}
|
||||
>
|
||||
{isMobile ? '删除' : '移除'}
|
||||
{isMobile ? '' : '移除'}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
width: isMobile ? 60 : undefined,
|
||||
width: isMobile ? 50 : undefined,
|
||||
fixed: isMobile ? 'right' as const : undefined,
|
||||
},
|
||||
];
|
||||
@@ -419,9 +425,24 @@ export default function Organizations() {
|
||||
columns={memberColumns}
|
||||
dataSource={members}
|
||||
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"
|
||||
scroll={isMobile ? { x: 'max-content', y: 400 } : undefined}
|
||||
scroll={{
|
||||
x: isMobile ? 'max-content' : undefined,
|
||||
y: members.length > 10 ? 500 : undefined,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
@@ -653,7 +674,20 @@ export default function Organizations() {
|
||||
await axios.put(`/api/organizations/${selectedOrg.id}`, values);
|
||||
message.success('组织信息更新成功');
|
||||
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) {
|
||||
message.error('更新失败');
|
||||
console.error(error);
|
||||
|
||||
+578
-48
@@ -1,19 +1,21 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Tooltip, Alert, Grid } from 'antd';
|
||||
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Tooltip, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col } from 'antd';
|
||||
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import { settingsApi } from '../services/api';
|
||||
import type { SettingsUpdate } from '../types';
|
||||
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
const { Option } = Select;
|
||||
const { useBreakpoint } = Grid;
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const screens = useBreakpoint();
|
||||
const isMobile = !screens.md; // md断点是768px
|
||||
const [form] = Form.useForm();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const [hasSettings, setHasSettings] = useState(false);
|
||||
@@ -33,10 +35,29 @@ export default function SettingsPage() {
|
||||
} | null>(null);
|
||||
const [showTestResult, setShowTestResult] = useState(false);
|
||||
|
||||
// 预设相关状态
|
||||
const [activeTab, setActiveTab] = useState('current');
|
||||
const [presets, setPresets] = useState<APIKeyPreset[]>([]);
|
||||
const [presetsLoading, setPresetsLoading] = useState(false);
|
||||
const [activePresetId, setActivePresetId] = useState<string | undefined>();
|
||||
const [editingPreset, setEditingPreset] = useState<APIKeyPreset | null>(null);
|
||||
const [isPresetModalVisible, setIsPresetModalVisible] = useState(false);
|
||||
const [testingPresetId, setTestingPresetId] = useState<string | null>(null);
|
||||
const [presetForm] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
if (activeTab === 'presets') {
|
||||
loadPresets();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'presets') {
|
||||
loadPresets();
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const loadSettings = async () => {
|
||||
setInitialLoading(true);
|
||||
try {
|
||||
@@ -235,22 +256,456 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 预设管理函数 ==========
|
||||
|
||||
const loadPresets = async () => {
|
||||
setPresetsLoading(true);
|
||||
try {
|
||||
const response = await settingsApi.getPresets();
|
||||
setPresets(response.presets);
|
||||
setActivePresetId(response.active_preset_id);
|
||||
} catch (error) {
|
||||
message.error('加载预设失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setPresetsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showPresetModal = (preset?: APIKeyPreset) => {
|
||||
if (preset) {
|
||||
setEditingPreset(preset);
|
||||
presetForm.setFieldsValue({
|
||||
name: preset.name,
|
||||
description: preset.description,
|
||||
...preset.config,
|
||||
});
|
||||
} else {
|
||||
setEditingPreset(null);
|
||||
presetForm.resetFields();
|
||||
presetForm.setFieldsValue({
|
||||
api_provider: 'openai',
|
||||
temperature: 0.7,
|
||||
max_tokens: 2000,
|
||||
});
|
||||
}
|
||||
setIsPresetModalVisible(true);
|
||||
};
|
||||
|
||||
const handlePresetCancel = () => {
|
||||
setIsPresetModalVisible(false);
|
||||
setEditingPreset(null);
|
||||
presetForm.resetFields();
|
||||
};
|
||||
|
||||
const handlePresetSave = async () => {
|
||||
try {
|
||||
const values = await presetForm.validateFields();
|
||||
const config: APIKeyPresetConfig = {
|
||||
api_provider: values.api_provider,
|
||||
api_key: values.api_key,
|
||||
api_base_url: values.api_base_url,
|
||||
llm_model: values.llm_model,
|
||||
temperature: values.temperature,
|
||||
max_tokens: values.max_tokens,
|
||||
};
|
||||
|
||||
if (editingPreset) {
|
||||
await settingsApi.updatePreset(editingPreset.id, {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
config,
|
||||
});
|
||||
message.success('预设已更新');
|
||||
} else {
|
||||
const request: PresetCreateRequest = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
config,
|
||||
};
|
||||
await settingsApi.createPreset(request);
|
||||
message.success('预设已创建');
|
||||
}
|
||||
|
||||
handlePresetCancel();
|
||||
loadPresets();
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePresetDelete = async (presetId: string) => {
|
||||
try {
|
||||
await settingsApi.deletePreset(presetId);
|
||||
message.success('预设已删除');
|
||||
loadPresets();
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.detail || '删除失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePresetActivate = async (presetId: string, presetName: string) => {
|
||||
try {
|
||||
await settingsApi.activatePreset(presetId);
|
||||
message.success(`已激活预设: ${presetName}`);
|
||||
loadPresets();
|
||||
loadSettings(); // 重新加载当前配置
|
||||
} catch (error) {
|
||||
message.error('激活失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePresetTest = async (presetId: string) => {
|
||||
setTestingPresetId(presetId);
|
||||
try {
|
||||
const result = await settingsApi.testPreset(presetId);
|
||||
if (result.success) {
|
||||
modal.success({
|
||||
title: '测试成功',
|
||||
centered: true,
|
||||
width: isMobile ? '90%' : 600,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
|
||||
<Typography.Text strong style={{ color: 'var(--color-success)' }}>
|
||||
✓ API 连接正常
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-bg-layout)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<div style={{ marginBottom: 8, fontSize: 14 }}>
|
||||
<Text type="secondary">提供商:</Text>
|
||||
<Text strong>{result.provider?.toUpperCase() || 'N/A'}</Text>
|
||||
</div>
|
||||
<div style={{ marginBottom: 8, fontSize: 14 }}>
|
||||
<Text type="secondary">模型:</Text>
|
||||
<Text strong>{result.model || 'N/A'}</Text>
|
||||
</div>
|
||||
{result.response_time_ms !== undefined && (
|
||||
<div style={{ fontSize: 14 }}>
|
||||
<Text type="secondary">响应时间:</Text>
|
||||
<Text strong>{result.response_time_ms}ms</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
message="预设配置测试通过,可以正常使用"
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
modal.error({
|
||||
title: '测试失败',
|
||||
centered: true,
|
||||
width: isMobile ? '90%' : 600,
|
||||
content: (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Alert
|
||||
message={result.message || 'API 测试失败'}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
{result.error && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-error-bg)',
|
||||
border: '1px solid var(--color-error-border)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>错误信息:</Text>
|
||||
<Text style={{ fontSize: 13, color: 'var(--color-error)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{result.error}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.suggestions && result.suggestions.length > 0 && (
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: 'var(--color-warning-bg)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 建议:</Text>
|
||||
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
|
||||
{result.suggestions.map((s, i) => (
|
||||
<li key={i} style={{ marginBottom: 4 }}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Alert
|
||||
message="预设配置存在问题,请检查后重试"
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('测试失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setTestingPresetId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateFromCurrent = () => {
|
||||
const currentConfig = form.getFieldsValue();
|
||||
presetForm.setFieldsValue({
|
||||
name: '',
|
||||
description: '',
|
||||
...currentConfig,
|
||||
});
|
||||
setEditingPreset(null);
|
||||
setIsPresetModalVisible(true);
|
||||
};
|
||||
|
||||
const getProviderColor = (provider: string) => {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return 'blue';
|
||||
case 'anthropic':
|
||||
return 'purple';
|
||||
case 'gemini':
|
||||
return 'green';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 渲染预设列表 ==========
|
||||
|
||||
const renderPresetsList = () => (
|
||||
<Spin spinning={presetsLoading}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text type="secondary">管理你的API配置预设,快速切换不同的配置</Text>
|
||||
<Space>
|
||||
<Button icon={<CopyOutlined />} onClick={handleCreateFromCurrent}>
|
||||
从当前创建
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showPresetModal()}>
|
||||
新建预设
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{presets.length === 0 ? (
|
||||
<Empty
|
||||
description="暂无预设配置"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
style={{ margin: '40px 0' }}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => showPresetModal()}>
|
||||
创建第一个预设
|
||||
</Button>
|
||||
</Empty>
|
||||
) : (
|
||||
<List
|
||||
dataSource={presets}
|
||||
renderItem={(preset) => {
|
||||
const isActive = preset.id === activePresetId;
|
||||
return (
|
||||
<List.Item
|
||||
key={preset.id}
|
||||
style={{
|
||||
background: isActive ? '#f0f5ff' : 'transparent',
|
||||
padding: '16px',
|
||||
marginBottom: '8px',
|
||||
border: isActive ? '2px solid #1890ff' : '1px solid #f0f0f0',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
actions={[
|
||||
!isActive && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handlePresetActivate(preset.id, preset.name)}
|
||||
>
|
||||
激活
|
||||
</Button>
|
||||
),
|
||||
<Tooltip title="测试连接">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={testingPresetId === preset.id}
|
||||
onClick={() => handlePresetTest(preset.id)}
|
||||
>
|
||||
测试
|
||||
</Button>
|
||||
</Tooltip>,
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => showPresetModal(preset)}
|
||||
>
|
||||
编辑
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
title="确定删除此预设吗?"
|
||||
onConfirm={() => handlePresetDelete(preset.id)}
|
||||
disabled={isActive}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={isActive}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
].filter(Boolean)}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
isActive && (
|
||||
<CheckCircleOutlined
|
||||
style={{ fontSize: '24px', color: '#52c41a' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span style={{ fontWeight: 'bold' }}>{preset.name}</span>
|
||||
{isActive && <Tag color="success">激活中</Tag>}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{preset.description && (
|
||||
<div style={{ color: '#666' }}>{preset.description}</div>
|
||||
)}
|
||||
<Space wrap>
|
||||
<Tag color={getProviderColor(preset.config.api_provider)}>
|
||||
{preset.config.api_provider.toUpperCase()}
|
||||
</Tag>
|
||||
<Tag>{preset.config.llm_model}</Tag>
|
||||
<Tag>温度: {preset.config.temperature}</Tag>
|
||||
<Tag>Tokens: {preset.config.max_tokens}</Tag>
|
||||
</Space>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
创建于: {new Date(preset.created_at).toLocaleString()}
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Spin>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
background: 'var(--color-bg-base)',
|
||||
padding: isMobile ? '16px 12px' : '40px 24px'
|
||||
background: 'linear-gradient(180deg, var(--color-bg-base) 0%, #EEF2F3 100%)',
|
||||
padding: isMobile ? '20px 16px' : '40px 24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: isMobile ? '100%' : 800,
|
||||
margin: '0 auto'
|
||||
maxWidth: 1400,
|
||||
margin: '0 auto',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
{/* 顶部导航卡片 */}
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'var(--color-bg-container)',
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, #5A9BA5 50%, var(--color-primary-hover) 100%)',
|
||||
borderRadius: isMobile ? 16 : 24,
|
||||
boxShadow: '0 12px 40px rgba(77, 128, 136, 0.25), 0 4px 12px rgba(0, 0, 0, 0.06)',
|
||||
marginBottom: isMobile ? 20 : 24,
|
||||
border: 'none',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.08)', pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', bottom: -40, left: '30%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.05)', pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', top: '50%', right: '15%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255, 255, 255, 0.06)', pointerEvents: 'none' }} />
|
||||
|
||||
<Row align="middle" justify="space-between" gutter={[16, 16]} style={{ position: 'relative', zIndex: 1 }}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Space direction="vertical" size={4}>
|
||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, color: '#fff', textShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
||||
<SettingOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} />
|
||||
AI API 设置
|
||||
</Title>
|
||||
<Text style={{ fontSize: isMobile ? 12 : 14, color: 'rgba(255,255,255,0.85)', marginLeft: isMobile ? 40 : 48 }}>
|
||||
配置AI接口参数,管理多个API配置预设
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Space size={12} style={{ display: 'flex', justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/')}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255, 255, 255, 0.15)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
color: '#fff',
|
||||
backdropFilter: 'blur(10px)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}}
|
||||
>
|
||||
返回主页
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 主内容卡片 */}
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
borderRadius: isMobile ? 12 : 16,
|
||||
boxShadow: 'var(--shadow-card)',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||
flex: 1,
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
@@ -258,45 +713,15 @@ export default function SettingsPage() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: 'current',
|
||||
label: '当前配置',
|
||||
children: (
|
||||
<Space direction="vertical" size={isMobile ? 'middle' : 'large'} style={{ width: '100%' }}>
|
||||
{/* 标题栏 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<Space size={isMobile ? 'small' : 'middle'}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/')}
|
||||
type="text"
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
/>
|
||||
<Title
|
||||
level={isMobile ? 4 : 2}
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: isMobile ? '18px' : undefined
|
||||
}}
|
||||
>
|
||||
<SettingOutlined style={{ marginRight: 8, color: 'var(--color-primary)' }} />
|
||||
{isMobile ? 'API 设置' : 'AI API 设置'}
|
||||
</Title>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
type="secondary"
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
fontSize: isMobile ? '13px' : '14px',
|
||||
lineHeight: isMobile ? '1.5' : '1.6'
|
||||
}}
|
||||
>
|
||||
配置你的AI API接口参数,这些设置将用于小说生成、角色创建等AI功能。
|
||||
</Paragraph>
|
||||
|
||||
{/* 默认配置提示 */}
|
||||
{isDefaultSettings && (
|
||||
@@ -769,8 +1194,113 @@ export default function SettingsPage() {
|
||||
</Form>
|
||||
</Spin>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'presets',
|
||||
label: '配置预设',
|
||||
children: renderPresetsList(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 预设编辑对话框 */}
|
||||
<Modal
|
||||
title={editingPreset ? '编辑预设' : '创建预设'}
|
||||
open={isPresetModalVisible}
|
||||
onOk={handlePresetSave}
|
||||
onCancel={handlePresetCancel}
|
||||
width={isMobile ? '90%' : 600}
|
||||
centered
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={presetForm}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="预设名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入预设名称' },
|
||||
{ max: 50, message: '名称不能超过50个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:工作账号-GPT4" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="预设描述"
|
||||
rules={[{ max: 200, message: '描述不能超过200个字符' }]}
|
||||
>
|
||||
<TextArea rows={2} placeholder="例如:用于日常写作任务(可选)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_provider"
|
||||
label="API 提供商"
|
||||
rules={[{ required: true, message: '请选择API提供商' }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="openai">OpenAI</Select.Option>
|
||||
<Select.Option value="anthropic">Anthropic (Claude)</Select.Option>
|
||||
<Select.Option value="gemini">Google Gemini</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label="API Key"
|
||||
rules={[{ required: true, message: '请输入API Key' }]}
|
||||
>
|
||||
<Input.Password placeholder="sk-..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_base_url" label="API Base URL">
|
||||
<Input placeholder="https://api.openai.com/v1(可选)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="llm_model"
|
||||
label="模型名称"
|
||||
rules={[{ required: true, message: '请输入模型名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:gpt-4, claude-3-opus-20240229" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="temperature"
|
||||
label="温度参数"
|
||||
rules={[{ required: true, message: '请输入温度参数' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="0.7"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="max_tokens"
|
||||
label="最大 Tokens"
|
||||
rules={[{ required: true, message: '请输入最大tokens' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={100000}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="2000"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,10 @@ import type {
|
||||
MCPTool,
|
||||
MCPToolCallRequest,
|
||||
MCPToolCallResponse,
|
||||
APIKeyPreset,
|
||||
PresetCreateRequest,
|
||||
PresetUpdateRequest,
|
||||
PresetListResponse,
|
||||
} from '../types';
|
||||
|
||||
const api = axios.create({
|
||||
@@ -197,6 +201,41 @@ export const settingsApi = {
|
||||
error_type?: string;
|
||||
suggestions?: string[];
|
||||
}>('/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 = {
|
||||
|
||||
@@ -36,6 +36,43 @@ export interface SettingsUpdate {
|
||||
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 响应
|
||||
export interface AuthUrlResponse {
|
||||
auth_url: string;
|
||||
|
||||
Reference in New Issue
Block a user