diff --git a/backend/app/api/characters.py b/backend/app/api/characters.py
index 30fabc4..fe7921d 100644
--- a/backend/app/api/characters.py
+++ b/backend/app/api/characters.py
@@ -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
diff --git a/backend/app/api/organizations.py b/backend/app/api/organizations.py
index 63e1c4f..8a5cdb8 100644
--- a/backend/app/api/organizations.py
+++ b/backend/app/api/organizations.py
@@ -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)
diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py
index 2de4cc5..a992b9e 100644
--- a/backend/app/api/settings.py
+++ b/backend/app/api/settings.py
@@ -4,14 +4,21 @@
from fastapi import APIRouter, HTTPException, Request, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
-from typing import Dict, Any, List
+from typing import Dict, Any, List, Optional
from pathlib import Path
from pydantic import BaseModel
+from datetime import datetime
import httpx
+import json
+import time
from app.database import get_db
from app.models.settings import Settings
-from app.schemas.settings import SettingsCreate, SettingsUpdate, SettingsResponse
+from app.schemas.settings import (
+ SettingsCreate, SettingsUpdate, SettingsResponse,
+ APIKeyPreset, APIKeyPresetConfig, PresetCreateRequest,
+ PresetUpdateRequest, PresetResponse, PresetListResponse
+)
from app.user_manager import User
from app.logger import get_logger
from app.config import settings as app_settings, PROJECT_ROOT
@@ -462,4 +469,329 @@ async def test_api_connection(data: ApiTestRequest):
"error": error_msg,
"error_type": error_type,
"suggestions": suggestions
- }
\ No newline at end of file
+ }
+
+
+# ========== 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)
\ No newline at end of file
diff --git a/backend/app/schemas/character.py b/backend/app/schemas/character.py
index 369c7fd..0ee4f8e 100644
--- a/backend/app/schemas/character.py
+++ b/backend/app/schemas/character.py
@@ -61,6 +61,12 @@ class CharacterUpdate(BaseModel):
organization_purpose: Optional[str] = None
organization_members: Optional[str] = None
traits: Optional[str] = None
+
+ # 组织额外字段(会同步到Organization表)
+ power_level: Optional[int] = Field(None, description="组织势力等级(0-100)")
+ location: Optional[str] = Field(None, description="组织所在地")
+ motto: Optional[str] = Field(None, description="组织格言/口号")
+ color: Optional[str] = Field(None, description="组织代表颜色")
class CharacterResponse(CharacterBase):
diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py
index 03b48f6..21005ce 100644
--- a/backend/app/schemas/settings.py
+++ b/backend/app/schemas/settings.py
@@ -1,6 +1,6 @@
"""设置相关的Pydantic模型"""
from pydantic import BaseModel, Field, ConfigDict
-from typing import Optional
+from typing import Optional, List
from datetime import datetime
@@ -34,4 +34,62 @@ class SettingsResponse(SettingsBase):
id: str
user_id: str
created_at: datetime
- updated_at: datetime
\ No newline at end of file
+ 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")
\ No newline at end of file
diff --git a/backend/app/services/prompt_service.py b/backend/app/services/prompt_service.py
index 906b6ba..103b5d4 100644
--- a/backend/app/services/prompt_service.py
+++ b/backend/app/services/prompt_service.py
@@ -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",表示当前关系状态
**关系建立示例:**
- 如果新角色是主角的新队友,应该与主角建立"队友"或"朋友"关系
diff --git a/frontend/src/pages/Characters.tsx b/frontend/src/pages/Characters.tsx
index b43f5a0..a167af0 100644
--- a/frontend/src/pages/Characters.tsx
+++ b/frontend/src/pages/Characters.tsx
@@ -612,8 +612,12 @@ export default function Characters() {
-
-
+
+
@@ -626,6 +630,10 @@ export default function Characters() {
+
+
+
+
diff --git a/frontend/src/pages/MCPPlugins.tsx b/frontend/src/pages/MCPPlugins.tsx
index d63b8d5..642bc35 100644
--- a/frontend/src/pages/MCPPlugins.tsx
+++ b/frontend/src/pages/MCPPlugins.tsx
@@ -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([]);
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: (
-
-
{result.message}
-
- {/* 显示详细的测试结果 */}
- {result.suggestions && result.suggestions.length > 0 && (
-
-
测试详情:
-
- {result.suggestions.map((suggestion: string, index: number) => (
-
- {suggestion}
-
- ))}
-
-
- )}
-
- {/* 显示工具数量 */}
- {result.tools_count !== undefined && (
-
- 🔧 可用工具数: {result.tools_count}
-
- )}
-
- {/* 显示响应时间 */}
- {result.response_time_ms !== undefined && (
-
- ⏱️ 响应时间: {result.response_time_ms}ms
-
- )}
-
-
-
- ✓ 插件状态已自动更新为"运行中"
-
+
+
+
+ ✓ {result.message}
+
+
+ {(result.tools_count !== undefined || result.response_time_ms !== undefined) && (
+
+ {result.tools_count !== undefined && (
+
+ 可用工具数:
+ {result.tools_count}
+
+ )}
+ {result.response_time_ms !== undefined && (
+
+ 响应时间:
+ {result.response_time_ms}ms
+
+ )}
+
+ )}
+
+
),
});
} else {
- Modal.error({
- title: '❌ 测试失败',
- width: 700,
+ modal.error({
+ title: '测试失败',
+ centered: true,
+ width: isMobile ? '90%' : 600,
content: (
-
-
{result.message}
+
+
- {/* 显示错误信息 */}
{result.error && (
-
-
错误信息:
-
+
+ 错误信息:
+
{result.error}
-
+
)}
- {/* 显示建议 */}
{result.suggestions && result.suggestions.length > 0 && (
-
),
});
@@ -340,319 +316,321 @@ export default function MCPPluginsPage() {
};
return (
-
+ <>
+ {contextHolder}
- {/* 顶部导航卡片 */}
-
- {/* 装饰性背景元素 */}
-
-
-
-
-
-
-
-
-
-
- MCP插件管理
-
-
-
- 扩展AI能力,连接外部工具与服务
-
-
-
-
-
- }
- 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';
- }}
- >
- 返回主页
-
- }
- onClick={handleCreate}
- style={{
- borderRadius: 12,
- background: 'rgba(255, 193, 7, 0.95)',
- border: '1px solid rgba(255, 255, 255, 0.3)',
- boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)',
- color: '#fff',
- fontWeight: 600
- }}
- >
- 添加插件
-
-
-
-
-
- {/* 使用提示 */}
-
-
- 什么是 MCP 插件?
-
- }
- description={
-
-
- • MCP (Model Context Protocol) 是一个标准化的协议,允许 AI 调用外部工具获取数据。
-
-
- • 通过添加 MCP 插件,AI 可以访问搜索引擎、数据库、API 等外部服务,增强创作能力。
-
-
- }
- type="info"
- showIcon={false}
+
+ {/* 顶部导航卡片 */}
+
-
+ >
+ {/* 装饰性背景元素 */}
+
+
+
- {/* 主内容区 */}
-
-
- {/* 插件列表 */}
-
- {plugins.length === 0 ? (
-
- } onClick={handleCreate}>
- 添加第一个插件
-
-
- ) : (
-
- {plugins.map((plugin) => (
-
+
+
+
+
+
+ MCP插件管理
+
+
+
+ 扩展AI能力,连接外部工具与服务
+
+
+
+
+
+ }
+ onClick={() => navigate('/')}
style={{
- borderRadius: 8,
- border: '1px solid #f0f0f0',
+ 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';
}}
>
-
+
}
+ onClick={handleCreate}
+ style={{
+ borderRadius: 12,
+ background: 'rgba(255, 193, 7, 0.95)',
+ border: '1px solid rgba(255, 255, 255, 0.3)',
+ boxShadow: '0 4px 16px rgba(255, 193, 7, 0.4)',
+ color: '#fff',
+ fontWeight: 600
+ }}
+ >
+ 添加插件
+
+
+
+
+
+ {/* 使用提示 */}
+
+
+ 什么是 MCP 插件?
+
+ }
+ description={
+
+
+ • MCP (Model Context Protocol) 是一个标准化的协议,允许 AI 调用外部工具获取数据。
+
+
+ • 通过添加 MCP 插件,AI 可以访问搜索引擎、数据库、API 等外部服务,增强创作能力。
+
+
+ }
+ type="info"
+ showIcon={false}
+ style={{
+ marginTop: isMobile ? 16 : 24,
+ borderRadius: 12,
+ background: 'rgba(230, 247, 255, 0.6)',
+ border: '1px solid rgba(145, 213, 255, 0.6)',
+ backdropFilter: 'blur(5px)'
+ }}
+ />
+
+
+ {/* 主内容区 */}
+
+
+ {/* 插件列表 */}
+
+ {plugins.length === 0 ? (
+
+ } onClick={handleCreate}>
+ 添加第一个插件
+
+
+ ) : (
+
+ {plugins.map((plugin) => (
+
-
-
-
-
- {plugin.display_name || plugin.plugin_name}
-
- {getStatusTag(plugin)}
-
- {plugin.plugin_type?.toUpperCase() || 'UNKNOWN'}
-
- {plugin.category && plugin.category !== 'general' && (
-
{plugin.category}
+
+
+
+
+
+ {plugin.display_name || plugin.plugin_name}
+
+ {getStatusTag(plugin)}
+
+ {plugin.plugin_type?.toUpperCase() || 'UNKNOWN'}
+
+ {plugin.category && plugin.category !== 'general' && (
+ {plugin.category}
+ )}
+
+ {plugin.description && (
+
+ {plugin.description}
+
)}
-
- {plugin.description && (
-
+
+ {(() => {
+ // 脱敏处理:隐藏URL中的API Key
+ const url = plugin.server_url;
+ try {
+ const urlObj = new URL(url);
+ // 替换查询参数中的敏感信息
+ const params = new URLSearchParams(urlObj.search);
+ let maskedUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
+
+ const sensitiveKeys = ['apiKey', 'api_key', 'key', 'token', 'secret', 'password', 'auth'];
+ let hasParams = false;
+
+ params.forEach((value, key) => {
+ const isSensitive = sensitiveKeys.some(k => key.toLowerCase().includes(k.toLowerCase()));
+ const maskedValue = isSensitive ? '***' : value;
+ maskedUrl += (hasParams ? '&' : '?') + `${key}=${maskedValue}`;
+ hasParams = true;
+ });
+
+ return maskedUrl;
+ } catch {
+ // 如果URL解析失败,尝试简单替换
+ return url.replace(/([?&])(apiKey|api_key|key|token|secret|password|auth)=([^&]+)/gi, '$1$2=***');
+ }
+ })()}
+
+
+ )}
+
+ {plugin.plugin_type === 'stdio' && plugin.command && (
+
+
+ {plugin.command} {plugin.args?.join(' ')}
+
+
+ )}
+
+ {/* 显示最后错误信息 */}
+ {plugin.last_error && (
+
+ 错误: {plugin.last_error}
+
+ )}
+
+
+
+
+
+ handleToggle(plugin, checked)}
+ size={isMobile ? 'small' : 'default'}
style={{
- margin: 0,
- fontSize: isMobile ? '12px' : '13px',
+ flexShrink: 0,
+ height: isMobile ? 16 : 22,
+ minHeight: isMobile ? 16 : 22,
+ lineHeight: isMobile ? '16px' : '22px'
}}
- ellipsis={{ rows: 2 }}
- >
- {plugin.description}
-
- )}
-
- {/* 只显示有值的URL或命令,脱敏处理敏感信息 */}
- {plugin.plugin_type === 'http' && plugin.server_url && (
-
-
- {(() => {
- // 脱敏处理:隐藏URL中的API Key
- const url = plugin.server_url;
- try {
- const urlObj = new URL(url);
- // 替换查询参数中的敏感信息
- const params = new URLSearchParams(urlObj.search);
- let maskedUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
-
- const sensitiveKeys = ['apiKey', 'api_key', 'key', 'token', 'secret', 'password', 'auth'];
- let hasParams = false;
-
- params.forEach((value, key) => {
- const isSensitive = sensitiveKeys.some(k => key.toLowerCase().includes(k.toLowerCase()));
- const maskedValue = isSensitive ? '***' : value;
- maskedUrl += (hasParams ? '&' : '?') + `${key}=${maskedValue}`;
- hasParams = true;
- });
-
- return maskedUrl;
- } catch {
- // 如果URL解析失败,尝试简单替换
- return url.replace(/([?&])(apiKey|api_key|key|token|secret|password|auth)=([^&]+)/gi, '$1$2=***');
- }
- })()}
-
-
- )}
-
- {plugin.plugin_type === 'stdio' && plugin.command && (
-
-
- {plugin.command} {plugin.args?.join(' ')}
-
-
- )}
-
- {/* 显示最后错误信息 */}
- {plugin.last_error && (
-
- 错误: {plugin.last_error}
-
- )}
+ />
+
+
+ }
+ onClick={() => handleTest(plugin.id)}
+ loading={testingPluginId === plugin.id}
+ size={isMobile ? 'small' : 'middle'}
+ />
+
+
+ }
+ onClick={() => handleViewTools(plugin.id)}
+ disabled={!plugin.enabled || plugin.status !== 'active'}
+ size={isMobile ? 'small' : 'middle'}
+ />
+
+
+ }
+ onClick={() => handleEdit(plugin)}
+ size={isMobile ? 'small' : 'middle'}
+ />
+
+
+ }
+ onClick={() => handleDelete(plugin)}
+ size={isMobile ? 'small' : 'middle'}
+ />
+
-
-
-
- handleToggle(plugin, checked)}
- size={isMobile ? 'small' : 'default'}
- style={{
- flexShrink: 0,
- height: isMobile ? 16 : 22,
- minHeight: isMobile ? 16 : 22,
- lineHeight: isMobile ? '16px' : '22px'
- }}
- />
-
-
- }
- onClick={() => handleTest(plugin.id)}
- loading={testingPluginId === plugin.id}
- size={isMobile ? 'small' : 'middle'}
- />
-
-
- }
- onClick={() => handleViewTools(plugin.id)}
- disabled={!plugin.enabled || plugin.status !== 'active'}
- size={isMobile ? 'small' : 'middle'}
- />
-
-
- }
- onClick={() => handleEdit(plugin)}
- size={isMobile ? 'small' : 'middle'}
- />
-
-
- }
- onClick={() => handleDelete(plugin)}
- size={isMobile ? 'small' : 'middle'}
- />
-
-
-
-
- ))}
-
- )}
-
+
+ ))}
+
+ )}
+
+
-
- {/* 创建/编辑插件模态框 */}
- {
- setModalVisible(false);
- form.resetFields();
- }}
- onOk={() => form.submit()}
- width={isMobile ? '100%' : 600}
- confirmLoading={loading}
- okText="保存"
- cancelText="取消"
- >
-
-
-
-
-
-
-
+
+
+
+
+
- {/* 查看工具列表模态框 */}
- setViewingTools(null)}
- footer={[
- ,
- ]}
- width={isMobile ? '100%' : 700}
- >
- {viewingTools && (
-
- {viewingTools.tools.length === 0 ? (
-
- ) : (
- viewingTools.tools.map((tool, index) => (
-
-
-
-
- {tool.name}
-
-
- {tool.description && (
- {tool.description}
- )}
- {tool.inputSchema && (
-
-
- {JSON.stringify(tool.inputSchema, null, 2)}
-
-
- )}
-
-
- ))
- )}
-
- )}
-
-
+ {/* 查看工具列表模态框 */}
+
+
+ 可用工具列表
+ {viewingTools && viewingTools.tools.length > 0 && (
+ {viewingTools.tools.length} 个工具
+ )}
+
+ }
+ open={!!viewingTools}
+ onCancel={() => setViewingTools(null)}
+ footer={[
+ ,
+ ]}
+ width={isMobile ? '95%' : 800}
+ centered
+ styles={{
+ body: {
+ maxHeight: isMobile ? '60vh' : '70vh',
+ overflowY: 'auto',
+ padding: isMobile ? '16px' : '24px'
+ }
+ }}
+ >
+ {viewingTools && (
+
+ {viewingTools.tools.length === 0 ? (
+
+ ) : (
+ viewingTools.tools.map((tool, index) => (
+
+
+ {tool.name}
+
+
+ #{index + 1}
+
+
+ }
+ >
+
+ {tool.description && (
+
+
+ 描述:
+
+
+ {tool.description}
+
+
+ )}
+ {tool.inputSchema && (
+
+
+ 输入参数:
+
+
+ {JSON.stringify(tool.inputSchema, null, 2)}
+
+
+ )}
+
+
+ ))
+ )}
+
+ )}
+
+
+ >
);
}
\ No newline at end of file
diff --git a/frontend/src/pages/Organizations.tsx b/frontend/src/pages/Organizations.tsx
index ddb6b0e..9133d21 100644
--- a/frontend/src/pages/Organizations.tsx
+++ b/frontend/src/pages/Organizations.tsx
@@ -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
([]);
const [selectedOrg, setSelectedOrg] = useState(null);
const [members, setMembers] = useState([]);
@@ -216,7 +218,7 @@ export default function Organizations() {
{name}
),
- width: isMobile ? 80 : undefined,
+ width: isMobile ? 100 : undefined,
},
{
title: '职位',
@@ -225,50 +227,53 @@ export default function Organizations() {
render: (position: string, record: OrganizationMember) => (
{position} {!isMobile && `(级别 ${record.rank})`}
),
+ width: isMobile ? 120 : undefined,
+ },
+ {
+ title: '忠诚度',
+ dataIndex: 'loyalty',
+ key: 'loyalty',
+ render: (loyalty: number) => (
+ = 70 ? 'green' : loyalty >= 40 ? 'orange' : 'red' }}>
+ {loyalty}%
+
+ ),
width: isMobile ? 80 : undefined,
},
- ...(!isMobile ? [
- {
- title: '忠诚度',
- dataIndex: 'loyalty',
- key: 'loyalty',
- render: (loyalty: number) => (
- = 70 ? 'green' : loyalty >= 40 ? 'orange' : 'red' }}>
- {loyalty}%
-
- ),
- },
- {
- title: '贡献度',
- dataIndex: 'contribution',
- key: 'contribution',
- render: (contribution: number) => `${contribution}%`,
- },
- {
- title: '状态',
- dataIndex: 'status',
- key: 'status',
- render: (status: string) => (
- {getStatusText(status)}
- ),
- },
- {
- title: '加入时间',
- dataIndex: 'joined_at',
- key: 'joined_at',
- render: (time: string) => time || '-',
- }
- ] : []),
+ {
+ title: '贡献度',
+ dataIndex: 'contribution',
+ key: 'contribution',
+ render: (contribution: number) => `${contribution}%`,
+ width: isMobile ? 80 : undefined,
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ render: (status: string) => (
+ {getStatusText(status)}
+ ),
+ 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) => (
-
+
}
onClick={() => handleEditMember(record)}
+ style={isMobile ? { padding: '4px' } : undefined}
>
{isMobile ? '' : '编辑'}
@@ -278,12 +283,13 @@ export default function Organizations() {
size="small"
icon={}
onClick={() => handleRemoveMember(record.id)}
+ style={isMobile ? { padding: '4px' } : undefined}
>
- {isMobile ? '删除' : '移除'}
+ {isMobile ? '' : '移除'}
),
- 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,
+ }}
/>
@@ -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);
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index 65a3f26..bb61a3a 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -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([]);
+ const [presetsLoading, setPresetsLoading] = useState(false);
+ const [activePresetId, setActivePresetId] = useState();
+ const [editingPreset, setEditingPreset] = useState(null);
+ const [isPresetModalVisible, setIsPresetModalVisible] = useState(false);
+ const [testingPresetId, setTestingPresetId] = useState(null);
+ const [presetForm] = Form.useForm();
+
useEffect(() => {
loadSettings();
+ if (activeTab === 'presets') {
+ loadPresets();
+ }
}, []);
+ useEffect(() => {
+ if (activeTab === 'presets') {
+ loadPresets();
+ }
+ }, [activeTab]);
+
const loadSettings = async () => {
setInitialLoading(true);
try {
@@ -235,542 +256,1051 @@ export default function SettingsPage() {
}
};
- return (
-
-
-
-
- {/* 标题栏 */}
-
-
- }
- onClick={() => navigate('/')}
- type="text"
- size={isMobile ? 'middle' : 'large'}
- />
-
-
- {isMobile ? 'API 设置' : 'AI API 设置'}
-
-
-
+ // ========== 预设管理函数 ==========
-
- 配置你的AI API接口参数,这些设置将用于小说生成、角色创建等AI功能。
-
+ 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);
+ }
+ };
- {/* 默认配置提示 */}
- {isDefaultSettings && (
-
-
- 当前显示的是从服务器 .env 文件读取的默认配置。
-
-
- 点击"保存设置"后,配置将保存到数据库并同步更新到 .env 文件。
-
+ 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: (
+
+
+
+ ✓ API 连接正常
+
+
+
+
+
+ 提供商:
+ {result.provider?.toUpperCase() || 'N/A'}
+
+
+ 模型:
+ {result.model || 'N/A'}
+
+ {result.response_time_ms !== undefined && (
+
+ 响应时间:
+ {result.response_time_ms}ms
- }
- type="info"
- showIcon
- style={{ marginBottom: isMobile ? 12 : 16 }}
- />
- )}
+ )}
+
- {/* 已保存配置提示 */}
- {hasSettings && !isDefaultSettings && (
- )}
+
+ ),
+ });
+ } else {
+ modal.error({
+ title: '测试失败',
+ centered: true,
+ width: isMobile ? '90%' : 600,
+ content: (
+
+
- {/* 表单 */}
-
-
- API 提供商
-
-
-
-
- }
- name="api_provider"
- rules={[{ required: true, message: '请选择API提供商' }]}
- >
-
+ ),
+ });
+ }
+ } catch (error) {
+ message.error('测试失败');
+ console.error(error);
+ } finally {
+ setTestingPresetId(null);
+ }
+ };
-
- API 地址
-
-
-
-
- }
- name="api_base_url"
- rules={[
- { required: true, message: '请输入API地址' },
- { type: 'url', message: '请输入有效的URL' }
- ]}
- >
-
-
+ const handleCreateFromCurrent = () => {
+ const currentConfig = form.getFieldsValue();
+ presetForm.setFieldsValue({
+ name: '',
+ description: '',
+ ...currentConfig,
+ });
+ setEditingPreset(null);
+ setIsPresetModalVisible(true);
+ };
-
- 模型名称
-
-
-
-
- }
- name="llm_model"
- rules={[{ required: true, message: '请输入或选择模型名称' }]}
+ const getProviderColor = (provider: string) => {
+ switch (provider) {
+ case 'openai':
+ return 'blue';
+ case 'anthropic':
+ return 'purple';
+ case 'gemini':
+ return 'green';
+ default:
+ return 'default';
+ }
+ };
+
+ // ========== 渲染预设列表 ==========
+
+ const renderPresetsList = () => (
+
+
+
+ 管理你的API配置预设,快速切换不同的配置
+
+ } onClick={handleCreateFromCurrent}>
+ 从当前创建
+
+ } onClick={() => showPresetModal()}>
+ 新建预设
+
+
+
+
+ {presets.length === 0 ? (
+
+ } onClick={() => showPresetModal()}>
+ 创建第一个预设
+
+
+ ) : (
+ {
+ const isActive = preset.id === activePresetId;
+ return (
+ handlePresetActivate(preset.id, preset.name)}
+ >
+ 激活
+
+ ),
+
+ }
+ loading={testingPresetId === preset.id}
+ onClick={() => handlePresetTest(preset.id)}
+ >
+ 测试
+
+ ,
+ }
+ onClick={() => showPresetModal(preset)}
+ >
+ 编辑
+ ,
+ handlePresetDelete(preset.id)}
+ disabled={isActive}
+ okText="确定"
+ cancelText="取消"
+ >
+ }
+ disabled={isActive}
+ >
+ 删除
+
+ ,
+ ].filter(Boolean)}
>
-
-
-
- 温度参数
-
-
-
-
- }
- name="temperature"
- >
-
-
-
-
- 最大 Token 数
-
-
-
-
- }
- name="max_tokens"
- rules={[
- { required: true, message: '请输入最大token数' },
- { type: 'number', min: 1, message: '请输入大于0的数字' }
- ]}
- >
-
-
-
- {/* 测试结果展示 */}
- {showTestResult && testResult && (
-
- {testResult.success ? (
-
- ) : (
-
- )}
-
- {testResult.message}
-
+ {preset.name}
+ {isActive && 激活中}
}
description={
-
- {testResult.success ? (
-
- {testResult.response_time_ms && (
-
- ⚡ 响应时间: {testResult.response_time_ms} ms
-
- )}
- {testResult.response_preview && (
-
-
AI 响应预览:
-
{testResult.response_preview}
-
- )}
-
- ✓ API 配置正确,可以正常使用
-
-
- ) : (
-
- {testResult.error && (
-
- 错误信息: {testResult.error}
-
- )}
- {testResult.error_type && (
-
- 错误类型: {testResult.error_type}
-
- )}
- {testResult.suggestions && testResult.suggestions.length > 0 && (
-
-
- 💡 解决建议:
-
-
- {testResult.suggestions.map((suggestion, index) => (
- - {suggestion}
- ))}
-
-
- )}
-
- )}
-
- }
- type={testResult.success ? 'success' : 'error'}
- closable
- onClose={() => setShowTestResult(false)}
- style={{ marginBottom: isMobile ? 16 : 24 }}
- />
- )}
-
- {/* 操作按钮 */}
-
- {isMobile ? (
- // 移动端:垂直堆叠布局
-
- }
- htmlType="submit"
- loading={loading}
- block
- style={{
- background: 'var(--color-primary)',
- border: 'none',
- height: '44px'
- }}
- >
- 保存设置
-
- }
- onClick={handleTestConnection}
- loading={testingApi}
- block
- style={{
- borderColor: 'var(--color-success)',
- color: 'var(--color-success)',
- fontWeight: 500,
- height: '44px'
- }}
- >
- {testingApi ? '测试中...' : '测试连接'}
-
-
- }
- onClick={handleReset}
- style={{ flex: 1, height: '44px' }}
- >
- 重置
-
- {hasSettings && (
- }
- onClick={handleDelete}
- loading={loading}
- style={{ flex: 1, height: '44px' }}
- >
- 删除
-
+
+ {preset.description && (
+ {preset.description}
)}
+
+
+ {preset.config.api_provider.toUpperCase()}
+
+ {preset.config.llm_model}
+ 温度: {preset.config.temperature}
+ Tokens: {preset.config.max_tokens}
+
+
+ 创建于: {new Date(preset.created_at).toLocaleString()}
+
-
- ) : (
- // 桌面端:删除在左边,测试、重置和保存在右边
-
- {/* 左侧:删除按钮 */}
- {hasSettings ? (
-
}
- onClick={handleDelete}
- loading={loading}
- style={{
- minWidth: '100px'
- }}
- >
- 删除配置
-
- ) : (
-
// 占位符,保持右侧按钮位置
+ }
+ />
+
+ );
+ }}
+ />
+ )}
+
+
+ );
+
+ return (
+ <>
+ {contextHolder}
+
+
+ {/* 顶部导航卡片 */}
+
+ {/* 装饰性背景元素 */}
+
+
+
+
+
+
+
+
+
+ AI API 设置
+
+
+ 配置AI接口参数,管理多个API配置预设
+
+
+
+
+
+ }
+ 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';
+ }}
+ >
+ 返回主页
+
+
+
+
+
+
+ {/* 主内容卡片 */}
+
+
+
+ {/* 默认配置提示 */}
+ {isDefaultSettings && (
+
+
+ 当前显示的是从服务器 .env 文件读取的默认配置。
+
+
+ 点击"保存设置"后,配置将保存到数据库并同步更新到 .env 文件。
+
+
+ }
+ type="info"
+ showIcon
+ style={{ marginBottom: isMobile ? 12 : 16 }}
+ />
)}
- {/* 右侧:测试、重置和保存按钮组 */}
-
- }
- onClick={handleTestConnection}
- loading={testingApi}
- style={{
- borderColor: 'var(--color-success)',
- color: 'var(--color-success)',
- fontWeight: 500,
- minWidth: '100px'
- }}
+ {/* 已保存配置提示 */}
+ {hasSettings && !isDefaultSettings && (
+
+ )}
+
+ {/* 表单 */}
+
+
-
- )}
-
-
-
-
-
+
+ API 提供商
+
+
+
+
+ }
+ name="api_provider"
+ rules={[{ required: true, message: '请选择API提供商' }]}
+ >
+
+
+
+
+ API 密钥
+
+
+
+
+ }
+ name="api_key"
+ rules={[{ required: true, message: '请输入API密钥' }]}
+ >
+
+
+
+
+ API 地址
+
+
+
+
+ }
+ name="api_base_url"
+ rules={[
+ { required: true, message: '请输入API地址' },
+ { type: 'url', message: '请输入有效的URL' }
+ ]}
+ >
+
+
+
+
+ 模型名称
+
+
+
+
+ }
+ name="llm_model"
+ rules={[{ required: true, message: '请输入或选择模型名称' }]}
+ >
+
+
+
+ 温度参数
+
+
+
+
+ }
+ name="temperature"
+ >
+
+
+
+
+ 最大 Token 数
+
+
+
+
+ }
+ name="max_tokens"
+ rules={[
+ { required: true, message: '请输入最大token数' },
+ { type: 'number', min: 1, message: '请输入大于0的数字' }
+ ]}
+ >
+
+
+
+ {/* 测试结果展示 */}
+ {showTestResult && testResult && (
+
+ {testResult.success ? (
+
+ ) : (
+
+ )}
+
+ {testResult.message}
+
+
+ }
+ description={
+
+ {testResult.success ? (
+
+ {testResult.response_time_ms && (
+
+ ⚡ 响应时间: {testResult.response_time_ms} ms
+
+ )}
+ {testResult.response_preview && (
+
+
AI 响应预览:
+
{testResult.response_preview}
+
+ )}
+
+ ✓ API 配置正确,可以正常使用
+
+
+ ) : (
+
+ {testResult.error && (
+
+ 错误信息: {testResult.error}
+
+ )}
+ {testResult.error_type && (
+
+ 错误类型: {testResult.error_type}
+
+ )}
+ {testResult.suggestions && testResult.suggestions.length > 0 && (
+
+
+ 💡 解决建议:
+
+
+ {testResult.suggestions.map((suggestion, index) => (
+ - {suggestion}
+ ))}
+
+
+ )}
+
+ )}
+
+ }
+ type={testResult.success ? 'success' : 'error'}
+ closable
+ onClose={() => setShowTestResult(false)}
+ style={{ marginBottom: isMobile ? 16 : 24 }}
+ />
+ )}
+
+ {/* 操作按钮 */}
+
+ {isMobile ? (
+ // 移动端:垂直堆叠布局
+
+ }
+ htmlType="submit"
+ loading={loading}
+ block
+ style={{
+ background: 'var(--color-primary)',
+ border: 'none',
+ height: '44px'
+ }}
+ >
+ 保存设置
+
+ }
+ onClick={handleTestConnection}
+ loading={testingApi}
+ block
+ style={{
+ borderColor: 'var(--color-success)',
+ color: 'var(--color-success)',
+ fontWeight: 500,
+ height: '44px'
+ }}
+ >
+ {testingApi ? '测试中...' : '测试连接'}
+
+
+ }
+ onClick={handleReset}
+ style={{ flex: 1, height: '44px' }}
+ >
+ 重置
+
+ {hasSettings && (
+ }
+ onClick={handleDelete}
+ loading={loading}
+ style={{ flex: 1, height: '44px' }}
+ >
+ 删除
+
+ )}
+
+
+ ) : (
+ // 桌面端:删除在左边,测试、重置和保存在右边
+
+ {/* 左侧:删除按钮 */}
+ {hasSettings ? (
+
}
+ onClick={handleDelete}
+ loading={loading}
+ style={{
+ minWidth: '100px'
+ }}
+ >
+ 删除配置
+
+ ) : (
+
// 占位符,保持右侧按钮位置
+ )}
+
+ {/* 右侧:测试、重置和保存按钮组 */}
+
+ }
+ onClick={handleTestConnection}
+ loading={testingApi}
+ style={{
+ borderColor: 'var(--color-success)',
+ color: 'var(--color-success)',
+ fontWeight: 500,
+ minWidth: '100px'
+ }}
+ >
+ {testingApi ? '测试中...' : '测试'}
+
+ }
+ onClick={handleReset}
+ style={{
+ minWidth: '100px'
+ }}
+ >
+ 重置
+
+ }
+ htmlType="submit"
+ loading={loading}
+ style={{
+ background: 'var(--color-primary)',
+ border: 'none',
+ minWidth: '120px',
+ fontWeight: 500
+ }}
+ >
+ 保存
+
+
+
+ )}
+
+
+
+
+ ),
+ },
+ {
+ key: 'presets',
+ label: '配置预设',
+ children: renderPresetsList(),
+ },
+ ]}
+ />
+
+
+
+ {/* 预设编辑对话框 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ >
);
}
\ No newline at end of file
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 35fdbb0..17fde57 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -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('/settings/presets'),
+
+ createPreset: (data: PresetCreateRequest) =>
+ api.post('/settings/presets', data),
+
+ updatePreset: (presetId: string, data: PresetUpdateRequest) =>
+ api.put(`/settings/presets/${presetId}`, data),
+
+ deletePreset: (presetId: string) =>
+ api.delete(`/settings/presets/${presetId}`),
+
+ activatePreset: (presetId: string) =>
+ api.post(`/settings/presets/${presetId}/activate`),
+
+ testPreset: (presetId: string) =>
+ api.post;
+ error?: string;
+ error_type?: string;
+ suggestions?: string[];
+ }>(`/settings/presets/${presetId}/test`),
+
+ createPresetFromCurrent: (name: string, description?: string) =>
+ api.post('/settings/presets/from-current', null, {
+ params: { name, description }
+ }),
};
export const projectApi = {
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 437c401..d6d7693 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -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;
@@ -622,22 +659,22 @@ export interface MCPPlugin {
description?: string;
plugin_type: 'http' | 'stdio';
category: string;
-
+
// HTTP类型字段
server_url?: string;
headers?: Record;
-
+
// Stdio类型字段
command?: string;
args?: string[];
env?: Record;
-
+
// 状态字段
enabled: boolean;
status: 'active' | 'inactive' | 'error';
last_error?: string;
last_test_at?: string;
-
+
// 时间戳
created_at: string;
}