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

This commit is contained in:
xiamuceer
2025-12-15 15:58:57 +08:00
parent 247156d2c1
commit a753c75b9c
12 changed files with 2163 additions and 1041 deletions
+39
View File
@@ -246,11 +246,50 @@ async def update_character(
# 更新字段
update_data = character_update.model_dump(exclude_unset=True)
# 如果是组织,需要同步更新 Organization 表的字段
org_fields = {}
if character.is_organization:
# 提取需要同步到 Organization 表的字段
if 'power_level' in update_data:
org_fields['power_level'] = update_data.pop('power_level')
if 'location' in update_data:
org_fields['location'] = update_data.pop('location')
if 'motto' in update_data:
org_fields['motto'] = update_data.pop('motto')
if 'color' in update_data:
org_fields['color'] = update_data.pop('color')
# 更新 Character 表字段
for field, value in update_data.items():
setattr(character, field, value)
# 如果是组织且有需要同步的字段,更新 Organization 表
if character.is_organization and org_fields:
org_result = await db.execute(
select(Organization).where(Organization.character_id == character_id)
)
org = org_result.scalar_one_or_none()
if org:
for field, value in org_fields.items():
setattr(org, field, value)
logger.info(f"同步更新组织详情:{character.name}")
else:
# 如果 Organization 记录不存在,自动创建
org = Organization(
character_id=character_id,
project_id=character.project_id,
member_count=0,
**org_fields
)
db.add(org)
logger.info(f"自动创建组织详情:{character.name}")
await db.commit()
await db.refresh(character)
logger.info(f"更新角色/组织成功:{character.name} (ID: {character_id})")
return character
+1 -1
View File
@@ -194,7 +194,7 @@ async def update_organization(
user_id = getattr(request.state, 'user_id', None)
await verify_project_access(db_org.project_id, user_id, db)
# 更新字段
# 更新 Organization 表字段
update_data = organization.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_org, field, value)
+334 -2
View File
@@ -4,14 +4,21 @@
from fastapi import APIRouter, HTTPException, Request, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
from pathlib import Path
from pydantic import BaseModel
from datetime import datetime
import httpx
import json
import time
from app.database import get_db
from app.models.settings import Settings
from app.schemas.settings import SettingsCreate, SettingsUpdate, SettingsResponse
from app.schemas.settings import (
SettingsCreate, SettingsUpdate, SettingsResponse,
APIKeyPreset, APIKeyPresetConfig, PresetCreateRequest,
PresetUpdateRequest, PresetResponse, PresetListResponse
)
from app.user_manager import User
from app.logger import get_logger
from app.config import settings as app_settings, PROJECT_ROOT
@@ -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)
+6
View File
@@ -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):
"""角色响应模型"""
+59 -1
View File
@@ -1,6 +1,6 @@
"""设置相关的Pydantic模型"""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from typing import Optional, List
from datetime import datetime
@@ -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")
+14 -9
View File
@@ -374,7 +374,10 @@ class PromptService:
2. **关系约束**relationships_array只能引用本批次中已经出现的角色名称
3. **组织约束**organization_memberships只能引用本批次中is_organization=true的实体名称
4. **禁止幻觉**:不要引用任何不存在的角色或组织,如果没有可引用的就留空数组[]
5. intimacy_level是-100到100的整数(负值表示敌对仇恨关系),loyalty是0-100的整数
5. **数值范围约束**
- intimacy_level-100到100的整数(负值表示敌对仇恨关系)
- loyalty0到100的整数
- **rank:0到10的整数**(职位等级,0最低,10最高)
6. 角色之间要形成合理的关系网络
**示例说明**
@@ -1584,14 +1587,16 @@ class PromptService:
1. **relationships数组必填**:至少要有1-3个与已有角色的关系(除非确实没有合理的关联)
2. **target_character_name必须精确匹配**:只能引用【已有角色】列表中的角色名称
3. organization_memberships只能引用已存在的组织名称
4. intimacy_level是-100到100的整数
- 80-100:至亲、挚友、深爱
- 50-79:亲密、友好
- 0-49一般、普通
- -1到-49:不和、敌视
- -50到-100:仇恨、死敌
5. loyalty是0-100的整数(仅用于组织成员)
6. status默认为"active",表示当前关系状态
4. **数值范围约束**
- intimacy_level-100到100的整数
* 80-100:至亲、挚友、深爱
* 50-79亲密、友好
* 0-49:一般、普通
* -1到-49:不和、敌视
* -50到-100:仇恨、死敌
- loyalty:0到100的整数(仅用于组织成员)
- **rank:0到10的整数**(职位等级,0最低,10最高)
5. status默认为"active",表示当前关系状态
**关系建立示例:**
- 如果新角色是主角的新队友,应该与主角建立"队友""朋友"关系
+10 -2
View File
@@ -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
View File
@@ -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>
</>
);
}
+45 -11
View File
@@ -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
View File
@@ -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>
</>
);
}
+39
View File
@@ -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 = {
+37
View File
@@ -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;