feat: Lock LLM and embedding configurations to system environment variables, mask API keys, and refactor frontend logout.
This commit is contained in:
parent
0176cb4d12
commit
2b0c7f5c2a
|
|
@ -668,15 +668,11 @@ async def _get_user_config(db: AsyncSession, user_id: Optional[str]) -> Optional
|
||||||
)
|
)
|
||||||
config = result.scalar_one_or_none()
|
config = result.scalar_one_or_none()
|
||||||
|
|
||||||
if config and config.llm_config:
|
if config:
|
||||||
user_llm_config = json.loads(config.llm_config) if config.llm_config else {}
|
|
||||||
user_other_config = json.loads(config.other_config) if config.other_config else {}
|
user_other_config = json.loads(config.other_config) if config.other_config else {}
|
||||||
|
|
||||||
user_llm_config = decrypt_config(user_llm_config, SENSITIVE_LLM_FIELDS)
|
|
||||||
user_other_config = decrypt_config(user_other_config, SENSITIVE_OTHER_FIELDS)
|
user_other_config = decrypt_config(user_other_config, SENSITIVE_OTHER_FIELDS)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"llmConfig": user_llm_config,
|
|
||||||
"otherConfig": user_other_config,
|
"otherConfig": user_other_config,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -746,41 +742,19 @@ async def _initialize_tools(
|
||||||
try:
|
try:
|
||||||
await emit(f"🔍 正在初始化 RAG 系统...")
|
await emit(f"🔍 正在初始化 RAG 系统...")
|
||||||
|
|
||||||
# 从用户配置中获取 embedding 配置
|
# 锁定模式:Embedding 配置始终来自 settings (.env)
|
||||||
user_llm_config = (user_config or {}).get('llmConfig', {})
|
embedding_provider = settings.EMBEDDING_PROVIDER
|
||||||
user_other_config = (user_config or {}).get('otherConfig', {})
|
embedding_model = settings.EMBEDDING_MODEL
|
||||||
user_embedding_config = user_other_config.get('embedding_config', {})
|
|
||||||
|
|
||||||
# Embedding Provider 优先级:用户嵌入配置 > 环境变量
|
# API Key 优先级:EMBEDDING_API_KEY > LLM_API_KEY
|
||||||
embedding_provider = (
|
|
||||||
user_embedding_config.get('provider') or
|
|
||||||
getattr(settings, 'EMBEDDING_PROVIDER', 'openai')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Embedding Model 优先级:用户嵌入配置 > 环境变量
|
|
||||||
embedding_model = (
|
|
||||||
user_embedding_config.get('model') or
|
|
||||||
getattr(settings, 'EMBEDDING_MODEL', 'text-embedding-3-small')
|
|
||||||
)
|
|
||||||
|
|
||||||
# API Key 优先级:用户嵌入配置 > 环境变量 EMBEDDING_API_KEY > 用户 LLM 配置 > 环境变量 LLM_API_KEY
|
|
||||||
# 注意:API Key 可以共享,因为很多用户使用同一个 OpenAI Key 做 LLM 和 Embedding
|
|
||||||
embedding_api_key = (
|
embedding_api_key = (
|
||||||
user_embedding_config.get('api_key') or
|
getattr(settings, "EMBEDDING_API_KEY", None) or
|
||||||
getattr(settings, 'EMBEDDING_API_KEY', None) or
|
settings.LLM_API_KEY or
|
||||||
user_llm_config.get('llmApiKey') or
|
""
|
||||||
getattr(settings, 'LLM_API_KEY', '') or
|
|
||||||
''
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Base URL 优先级:用户嵌入配置 > 环境变量 EMBEDDING_BASE_URL > None(使用提供商默认地址)
|
# Base URL 优先级:EMBEDDING_BASE_URL > None
|
||||||
# 🔥 重要:Base URL 不应该回退到 LLM 的 base_url,因为 Embedding 和 LLM 可能使用完全不同的服务
|
embedding_base_url = getattr(settings, "EMBEDDING_BASE_URL", None)
|
||||||
# 例如:LLM 使用 SiliconFlow,但 Embedding 使用 HuggingFace
|
|
||||||
embedding_base_url = (
|
|
||||||
user_embedding_config.get('base_url') or
|
|
||||||
getattr(settings, 'EMBEDDING_BASE_URL', None) or
|
|
||||||
None
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"RAG 配置: provider={embedding_provider}, model={embedding_model}, base_url={embedding_base_url or '(使用默认)'}")
|
logger.info(f"RAG 配置: provider={embedding_provider}, model={embedding_model}, base_url={embedding_base_url or '(使用默认)'}")
|
||||||
await emit(f"📊 Embedding 配置: {embedding_provider}/{embedding_model}")
|
await emit(f"📊 Embedding 配置: {embedding_provider}/{embedding_model}")
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from app.core.encryption import encrypt_sensitive_data, decrypt_sensitive_data
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# 需要加密的敏感字段列表
|
# 需要加密的敏感字段列表 (LLM 已锁定至 .env,此处仅保留其他配置)
|
||||||
SENSITIVE_LLM_FIELDS = [
|
SENSITIVE_LLM_FIELDS = [
|
||||||
'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey',
|
'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey',
|
||||||
'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey',
|
'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey',
|
||||||
|
|
@ -27,6 +27,15 @@ SENSITIVE_LLM_FIELDS = [
|
||||||
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken']
|
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken']
|
||||||
|
|
||||||
|
|
||||||
|
def mask_api_key(key: Optional[str]) -> str:
|
||||||
|
"""部分遮盖API Key,显示前3位和后4位"""
|
||||||
|
if not key:
|
||||||
|
return ""
|
||||||
|
if len(key) <= 8:
|
||||||
|
return "***"
|
||||||
|
return f"{key[:3]}***{key[-4:]}"
|
||||||
|
|
||||||
|
|
||||||
def encrypt_config(config: dict, sensitive_fields: list) -> dict:
|
def encrypt_config(config: dict, sensitive_fields: list) -> dict:
|
||||||
"""加密配置中的敏感字段"""
|
"""加密配置中的敏感字段"""
|
||||||
encrypted = config.copy()
|
encrypted = config.copy()
|
||||||
|
|
@ -104,23 +113,23 @@ def get_default_config() -> dict:
|
||||||
return {
|
return {
|
||||||
"llmConfig": {
|
"llmConfig": {
|
||||||
"llmProvider": settings.LLM_PROVIDER,
|
"llmProvider": settings.LLM_PROVIDER,
|
||||||
"llmApiKey": "",
|
"llmApiKey": mask_api_key(settings.LLM_API_KEY),
|
||||||
"llmModel": settings.LLM_MODEL or "",
|
"llmModel": settings.LLM_MODEL or "",
|
||||||
"llmBaseUrl": settings.LLM_BASE_URL or "",
|
"llmBaseUrl": settings.LLM_BASE_URL or "",
|
||||||
"llmTimeout": settings.LLM_TIMEOUT * 1000, # 转换为毫秒
|
"llmTimeout": settings.LLM_TIMEOUT * 1000, # 转换为毫秒
|
||||||
"llmTemperature": settings.LLM_TEMPERATURE,
|
"llmTemperature": settings.LLM_TEMPERATURE,
|
||||||
"llmMaxTokens": settings.LLM_MAX_TOKENS,
|
"llmMaxTokens": settings.LLM_MAX_TOKENS,
|
||||||
"llmCustomHeaders": "",
|
"llmCustomHeaders": "",
|
||||||
"geminiApiKey": settings.GEMINI_API_KEY or "",
|
"geminiApiKey": mask_api_key(settings.GEMINI_API_KEY),
|
||||||
"openaiApiKey": settings.OPENAI_API_KEY or "",
|
"openaiApiKey": mask_api_key(settings.OPENAI_API_KEY),
|
||||||
"claudeApiKey": settings.CLAUDE_API_KEY or "",
|
"claudeApiKey": mask_api_key(settings.CLAUDE_API_KEY),
|
||||||
"qwenApiKey": settings.QWEN_API_KEY or "",
|
"qwenApiKey": mask_api_key(settings.QWEN_API_KEY),
|
||||||
"deepseekApiKey": settings.DEEPSEEK_API_KEY or "",
|
"deepseekApiKey": mask_api_key(settings.DEEPSEEK_API_KEY),
|
||||||
"zhipuApiKey": settings.ZHIPU_API_KEY or "",
|
"zhipuApiKey": mask_api_key(settings.ZHIPU_API_KEY),
|
||||||
"moonshotApiKey": settings.MOONSHOT_API_KEY or "",
|
"moonshotApiKey": mask_api_key(settings.MOONSHOT_API_KEY),
|
||||||
"baiduApiKey": settings.BAIDU_API_KEY or "",
|
"baiduApiKey": mask_api_key(settings.BAIDU_API_KEY),
|
||||||
"minimaxApiKey": settings.MINIMAX_API_KEY or "",
|
"minimaxApiKey": mask_api_key(settings.MINIMAX_API_KEY),
|
||||||
"doubaoApiKey": settings.DOUBAO_API_KEY or "",
|
"doubaoApiKey": mask_api_key(settings.DOUBAO_API_KEY),
|
||||||
"ollamaBaseUrl": settings.OLLAMA_BASE_URL or "http://localhost:11434/v1",
|
"ollamaBaseUrl": settings.OLLAMA_BASE_URL or "http://localhost:11434/v1",
|
||||||
},
|
},
|
||||||
"otherConfig": {
|
"otherConfig": {
|
||||||
|
|
@ -178,7 +187,8 @@ async def get_my_config(
|
||||||
print(f" - llmApiKey: {'***' + user_llm_config.get('llmApiKey', '')[-4:] if user_llm_config.get('llmApiKey') else '(空)'}")
|
print(f" - llmApiKey: {'***' + user_llm_config.get('llmApiKey', '')[-4:] if user_llm_config.get('llmApiKey') else '(空)'}")
|
||||||
print(f" - llmModel: {user_llm_config.get('llmModel')}")
|
print(f" - llmModel: {user_llm_config.get('llmModel')}")
|
||||||
|
|
||||||
merged_llm_config = {**default_config["llmConfig"], **user_llm_config}
|
# LLM配置始终来自系统默认(.env),不再允许用户覆盖
|
||||||
|
merged_llm_config = default_config["llmConfig"]
|
||||||
merged_other_config = {**default_config["otherConfig"], **user_other_config}
|
merged_other_config = {**default_config["otherConfig"], **user_other_config}
|
||||||
|
|
||||||
return UserConfigResponse(
|
return UserConfigResponse(
|
||||||
|
|
@ -247,7 +257,8 @@ async def update_my_config(
|
||||||
user_llm_config = decrypt_config(user_llm_config, SENSITIVE_LLM_FIELDS)
|
user_llm_config = decrypt_config(user_llm_config, SENSITIVE_LLM_FIELDS)
|
||||||
user_other_config = decrypt_config(user_other_config, SENSITIVE_OTHER_FIELDS)
|
user_other_config = decrypt_config(user_other_config, SENSITIVE_OTHER_FIELDS)
|
||||||
|
|
||||||
merged_llm_config = {**default_config["llmConfig"], **user_llm_config}
|
# LLM配置始终来自系统默认(.env),不再允许用户覆盖
|
||||||
|
merged_llm_config = default_config["llmConfig"]
|
||||||
merged_other_config = {**default_config["otherConfig"], **user_other_config}
|
merged_other_config = {**default_config["otherConfig"], **user_other_config}
|
||||||
|
|
||||||
return UserConfigResponse(
|
return UserConfigResponse(
|
||||||
|
|
@ -299,230 +310,52 @@ class LLMTestResponse(BaseModel):
|
||||||
@router.post("/test-llm", response_model=LLMTestResponse)
|
@router.post("/test-llm", response_model=LLMTestResponse)
|
||||||
async def test_llm_connection(
|
async def test_llm_connection(
|
||||||
request: LLMTestRequest,
|
request: LLMTestRequest,
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user: User = Depends(deps.get_current_user),
|
current_user: User = Depends(deps.get_current_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""测试LLM连接是否正常"""
|
"""测试当前系统 LLM 配置是否正常"""
|
||||||
from app.services.llm.factory import LLMFactory, NATIVE_ONLY_PROVIDERS
|
from app.services.llm.service import LLMService
|
||||||
from app.services.llm.adapters import LiteLLMAdapter, BaiduAdapter, MinimaxAdapter, DoubaoAdapter
|
|
||||||
from app.services.llm.types import LLMConfig, LLMProvider, LLMRequest, LLMMessage, DEFAULT_MODELS, DEFAULT_BASE_URLS
|
|
||||||
import traceback
|
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# 获取用户保存的配置
|
|
||||||
result = await db.execute(
|
|
||||||
select(UserConfig).where(UserConfig.user_id == current_user.id)
|
|
||||||
)
|
|
||||||
user_config_record = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
# 解析用户配置
|
|
||||||
saved_llm_config = {}
|
|
||||||
saved_other_config = {}
|
|
||||||
if user_config_record:
|
|
||||||
if user_config_record.llm_config:
|
|
||||||
saved_llm_config = decrypt_config(
|
|
||||||
json.loads(user_config_record.llm_config),
|
|
||||||
SENSITIVE_LLM_FIELDS
|
|
||||||
)
|
|
||||||
if user_config_record.other_config:
|
|
||||||
saved_other_config = decrypt_config(
|
|
||||||
json.loads(user_config_record.other_config),
|
|
||||||
SENSITIVE_OTHER_FIELDS
|
|
||||||
)
|
|
||||||
|
|
||||||
# 从保存的配置中获取参数(用于调试显示)
|
|
||||||
saved_timeout_ms = saved_llm_config.get('llmTimeout', settings.LLM_TIMEOUT * 1000)
|
|
||||||
saved_temperature = saved_llm_config.get('llmTemperature', settings.LLM_TEMPERATURE)
|
|
||||||
saved_max_tokens = saved_llm_config.get('llmMaxTokens', settings.LLM_MAX_TOKENS)
|
|
||||||
saved_concurrency = saved_other_config.get('llmConcurrency', settings.LLM_CONCURRENCY)
|
|
||||||
saved_gap_ms = saved_other_config.get('llmGapMs', settings.LLM_GAP_MS)
|
|
||||||
saved_max_files = saved_other_config.get('maxAnalyzeFiles', settings.MAX_ANALYZE_FILES)
|
|
||||||
saved_output_lang = saved_other_config.get('outputLanguage', settings.OUTPUT_LANGUAGE)
|
|
||||||
|
|
||||||
debug_info = {
|
|
||||||
"provider": request.provider,
|
|
||||||
"model_requested": request.model,
|
|
||||||
"base_url_requested": request.baseUrl,
|
|
||||||
"api_key_length": len(request.apiKey) if request.apiKey else 0,
|
|
||||||
"api_key_prefix": request.apiKey[:8] + "..." if request.apiKey and len(request.apiKey) > 8 else "(empty)",
|
|
||||||
# 用户保存的配置参数
|
|
||||||
"saved_config": {
|
|
||||||
"timeout_ms": saved_timeout_ms,
|
|
||||||
"temperature": saved_temperature,
|
|
||||||
"max_tokens": saved_max_tokens,
|
|
||||||
"concurrency": saved_concurrency,
|
|
||||||
"gap_ms": saved_gap_ms,
|
|
||||||
"max_analyze_files": saved_max_files,
|
|
||||||
"output_language": saved_output_lang,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 解析provider
|
# LLMService 已经重构为锁定读取 .env 配置
|
||||||
provider_map = {
|
llm_service = LLMService()
|
||||||
'gemini': LLMProvider.GEMINI,
|
|
||||||
'openai': LLMProvider.OPENAI,
|
|
||||||
'claude': LLMProvider.CLAUDE,
|
|
||||||
'qwen': LLMProvider.QWEN,
|
|
||||||
'deepseek': LLMProvider.DEEPSEEK,
|
|
||||||
'zhipu': LLMProvider.ZHIPU,
|
|
||||||
'moonshot': LLMProvider.MOONSHOT,
|
|
||||||
'baidu': LLMProvider.BAIDU,
|
|
||||||
'minimax': LLMProvider.MINIMAX,
|
|
||||||
'doubao': LLMProvider.DOUBAO,
|
|
||||||
'ollama': LLMProvider.OLLAMA,
|
|
||||||
}
|
|
||||||
|
|
||||||
provider = provider_map.get(request.provider.lower())
|
# 记录测试信息
|
||||||
if not provider:
|
print(f"🔍 测试 LLM 连接: Provider={llm_service.config.provider}, Model={llm_service.config.model}")
|
||||||
debug_info["error_type"] = "unsupported_provider"
|
|
||||||
return LLMTestResponse(
|
|
||||||
success=False,
|
|
||||||
message=f"不支持的LLM提供商: {request.provider}",
|
|
||||||
debug=debug_info
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取默认模型
|
# 简单测试:获取分析结果
|
||||||
model = request.model or DEFAULT_MODELS.get(provider)
|
test_code = "print('hello world')"
|
||||||
base_url = request.baseUrl or DEFAULT_BASE_URLS.get(provider, "")
|
result = await llm_service.analyze_code(test_code, "python")
|
||||||
|
|
||||||
# 测试时使用用户保存的所有配置参数
|
duration = round(time.time() - start_time, 2)
|
||||||
test_timeout = int(saved_timeout_ms / 1000) if saved_timeout_ms else settings.LLM_TIMEOUT
|
|
||||||
test_temperature = saved_temperature if saved_temperature is not None else settings.LLM_TEMPERATURE
|
|
||||||
test_max_tokens = saved_max_tokens if saved_max_tokens else settings.LLM_MAX_TOKENS
|
|
||||||
|
|
||||||
debug_info["model_used"] = model
|
|
||||||
debug_info["base_url_used"] = base_url
|
|
||||||
debug_info["is_native_adapter"] = provider in NATIVE_ONLY_PROVIDERS
|
|
||||||
debug_info["test_params"] = {
|
|
||||||
"timeout": test_timeout,
|
|
||||||
"temperature": test_temperature,
|
|
||||||
"max_tokens": test_max_tokens,
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"[LLM Test] 开始测试: provider={provider.value}, model={model}, base_url={base_url}, temperature={test_temperature}, timeout={test_timeout}s, max_tokens={test_max_tokens}")
|
|
||||||
|
|
||||||
# 创建配置
|
|
||||||
config = LLMConfig(
|
|
||||||
provider=provider,
|
|
||||||
api_key=request.apiKey,
|
|
||||||
model=model,
|
|
||||||
base_url=request.baseUrl,
|
|
||||||
timeout=test_timeout,
|
|
||||||
temperature=test_temperature,
|
|
||||||
max_tokens=test_max_tokens,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 直接创建新的适配器实例(不使用缓存),确保使用最新的配置
|
|
||||||
if provider in NATIVE_ONLY_PROVIDERS:
|
|
||||||
native_adapter_map = {
|
|
||||||
LLMProvider.BAIDU: BaiduAdapter,
|
|
||||||
LLMProvider.MINIMAX: MinimaxAdapter,
|
|
||||||
LLMProvider.DOUBAO: DoubaoAdapter,
|
|
||||||
}
|
|
||||||
adapter = native_adapter_map[provider](config)
|
|
||||||
debug_info["adapter_type"] = type(adapter).__name__
|
|
||||||
else:
|
|
||||||
adapter = LiteLLMAdapter(config)
|
|
||||||
debug_info["adapter_type"] = "LiteLLMAdapter"
|
|
||||||
# 获取 LiteLLM 实际使用的模型名
|
|
||||||
debug_info["litellm_model"] = getattr(adapter, '_get_litellm_model', lambda: model)() if hasattr(adapter, '_get_litellm_model') else model
|
|
||||||
|
|
||||||
test_request = LLMRequest(
|
|
||||||
messages=[
|
|
||||||
LLMMessage(role="user", content="Say 'Hello' in one word.")
|
|
||||||
],
|
|
||||||
temperature=test_temperature,
|
|
||||||
max_tokens=test_max_tokens,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"[LLM Test] 发送测试请求...")
|
|
||||||
response = await adapter.complete(test_request)
|
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
|
||||||
debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2)
|
|
||||||
|
|
||||||
# 验证响应内容
|
|
||||||
if not response or not response.content:
|
|
||||||
debug_info["error_type"] = "empty_response"
|
|
||||||
debug_info["raw_response"] = str(response) if response else None
|
|
||||||
print(f"[LLM Test] 空响应: {response}")
|
|
||||||
return LLMTestResponse(
|
|
||||||
success=False,
|
|
||||||
message="LLM 返回空响应,请检查 API Key 和配置",
|
|
||||||
debug=debug_info
|
|
||||||
)
|
|
||||||
|
|
||||||
debug_info["response_length"] = len(response.content)
|
|
||||||
debug_info["usage"] = {
|
|
||||||
"prompt_tokens": getattr(response, 'prompt_tokens', None),
|
|
||||||
"completion_tokens": getattr(response, 'completion_tokens', None),
|
|
||||||
"total_tokens": getattr(response, 'total_tokens', None),
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"[LLM Test] 成功! 响应: {response.content[:50]}... 耗时: {elapsed_time:.2f}s")
|
|
||||||
|
|
||||||
return LLMTestResponse(
|
return LLMTestResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message=f"连接成功 ({elapsed_time:.2f}s)",
|
message=f"连接成功!耗时: {duration}s",
|
||||||
model=model,
|
model=llm_service.config.model,
|
||||||
response=response.content[:100] if response.content else None,
|
response="分析测试完成",
|
||||||
debug=debug_info
|
debug={
|
||||||
|
"provider": llm_service.config.provider.value,
|
||||||
|
"model": llm_service.config.model,
|
||||||
|
"duration_s": duration,
|
||||||
|
"issues_found": len(result.get("issues", []))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
elapsed_time = time.time() - start_time
|
duration = round(time.time() - start_time, 2)
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
error_type = type(e).__name__
|
print(f"❌ LLM 测试失败: {error_msg}")
|
||||||
|
|
||||||
debug_info["elapsed_time_ms"] = round(elapsed_time * 1000, 2)
|
|
||||||
debug_info["error_type"] = error_type
|
|
||||||
debug_info["error_message"] = error_msg
|
|
||||||
debug_info["traceback"] = traceback.format_exc()
|
|
||||||
|
|
||||||
# 提取 LLMError 中的 api_response
|
|
||||||
if hasattr(e, 'api_response') and e.api_response:
|
|
||||||
debug_info["api_response"] = e.api_response
|
|
||||||
if hasattr(e, 'status_code') and e.status_code:
|
|
||||||
debug_info["status_code"] = e.status_code
|
|
||||||
|
|
||||||
print(f"[LLM Test] 失败: {error_type}: {error_msg}")
|
|
||||||
print(f"[LLM Test] Traceback:\n{traceback.format_exc()}")
|
|
||||||
|
|
||||||
# 提供更友好的错误信息
|
|
||||||
friendly_message = error_msg
|
|
||||||
|
|
||||||
# 优先检查余额不足(因为某些 API 用 429 表示余额不足)
|
|
||||||
if any(keyword in error_msg for keyword in ["余额不足", "资源包", "充值", "quota", "insufficient", "balance", "402"]):
|
|
||||||
friendly_message = "账户余额不足或配额已用尽,请充值后重试"
|
|
||||||
debug_info["error_category"] = "insufficient_balance"
|
|
||||||
elif "401" in error_msg or "invalid_api_key" in error_msg.lower() or "incorrect api key" in error_msg.lower():
|
|
||||||
friendly_message = "API Key 无效或已过期,请检查后重试"
|
|
||||||
debug_info["error_category"] = "auth_invalid_key"
|
|
||||||
elif "authentication" in error_msg.lower():
|
|
||||||
friendly_message = "认证失败,请检查 API Key 是否正确"
|
|
||||||
debug_info["error_category"] = "auth_failed"
|
|
||||||
elif "timeout" in error_msg.lower():
|
|
||||||
friendly_message = "连接超时,请检查网络或 API 地址是否正确"
|
|
||||||
debug_info["error_category"] = "timeout"
|
|
||||||
elif "connection" in error_msg.lower() or "connect" in error_msg.lower():
|
|
||||||
friendly_message = "无法连接到 API 服务,请检查网络或 API 地址"
|
|
||||||
debug_info["error_category"] = "connection"
|
|
||||||
elif "rate" in error_msg.lower() and "limit" in error_msg.lower():
|
|
||||||
friendly_message = "API 请求频率超限,请稍后重试"
|
|
||||||
debug_info["error_category"] = "rate_limit"
|
|
||||||
elif "model" in error_msg.lower() and ("not found" in error_msg.lower() or "does not exist" in error_msg.lower()):
|
|
||||||
friendly_message = f"模型 '{debug_info.get('model_used', 'unknown')}' 不存在或无权访问"
|
|
||||||
debug_info["error_category"] = "model_not_found"
|
|
||||||
else:
|
|
||||||
debug_info["error_category"] = "unknown"
|
|
||||||
|
|
||||||
return LLMTestResponse(
|
return LLMTestResponse(
|
||||||
success=False,
|
success=False,
|
||||||
message=friendly_message,
|
message=f"连接失败: {error_msg}",
|
||||||
debug=debug_info
|
debug={
|
||||||
|
"error": error_msg,
|
||||||
|
"traceback": traceback.format_exc(),
|
||||||
|
"duration_s": duration
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,83 +172,29 @@ EMBEDDING_PROVIDERS: List[EmbeddingProvider] = [
|
||||||
EMBEDDING_CONFIG_KEY = "embedding_config"
|
EMBEDDING_CONFIG_KEY = "embedding_config"
|
||||||
|
|
||||||
|
|
||||||
|
def mask_api_key(key: Optional[str]) -> str:
|
||||||
|
"""部分遮盖API Key,显示前3位和后4位"""
|
||||||
|
if not key:
|
||||||
|
return ""
|
||||||
|
if len(key) <= 8:
|
||||||
|
return "***"
|
||||||
|
return f"{key[:3]}***{key[-4:]}"
|
||||||
|
|
||||||
|
|
||||||
async def get_embedding_config_from_db(db: AsyncSession, user_id: str) -> EmbeddingConfig:
|
async def get_embedding_config_from_db(db: AsyncSession, user_id: str) -> EmbeddingConfig:
|
||||||
"""从数据库获取嵌入配置(异步)"""
|
"""从数据库获取嵌入配置(异步)"""
|
||||||
result = await db.execute(
|
# 嵌入配置始终来自系统默认(.env),不再允许用户覆盖
|
||||||
select(UserConfig).where(UserConfig.user_id == user_id)
|
print(f"[EmbeddingConfig] 返回系统默认嵌入配置(来自 .env)")
|
||||||
)
|
|
||||||
user_config = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if user_config and user_config.other_config:
|
|
||||||
try:
|
|
||||||
other_config = json.loads(user_config.other_config) if isinstance(user_config.other_config, str) else user_config.other_config
|
|
||||||
embedding_data = other_config.get(EMBEDDING_CONFIG_KEY)
|
|
||||||
|
|
||||||
if embedding_data:
|
|
||||||
config = EmbeddingConfig(
|
|
||||||
provider=embedding_data.get("provider", settings.EMBEDDING_PROVIDER),
|
|
||||||
model=embedding_data.get("model", settings.EMBEDDING_MODEL),
|
|
||||||
api_key=embedding_data.get("api_key"),
|
|
||||||
base_url=embedding_data.get("base_url"),
|
|
||||||
dimensions=embedding_data.get("dimensions"),
|
|
||||||
batch_size=embedding_data.get("batch_size", 100),
|
|
||||||
)
|
|
||||||
print(f"[EmbeddingConfig] 读取用户 {user_id} 的嵌入配置: provider={config.provider}, model={config.model}")
|
|
||||||
return config
|
|
||||||
except (json.JSONDecodeError, AttributeError) as e:
|
|
||||||
print(f"[EmbeddingConfig] 解析用户 {user_id} 配置失败: {e}")
|
|
||||||
|
|
||||||
# 返回默认配置
|
|
||||||
print(f"[EmbeddingConfig] 用户 {user_id} 无保存配置,返回默认值")
|
|
||||||
return EmbeddingConfig(
|
return EmbeddingConfig(
|
||||||
provider=settings.EMBEDDING_PROVIDER,
|
provider=settings.EMBEDDING_PROVIDER,
|
||||||
model=settings.EMBEDDING_MODEL,
|
model=settings.EMBEDDING_MODEL,
|
||||||
api_key=settings.LLM_API_KEY,
|
api_key=mask_api_key(settings.EMBEDDING_API_KEY or settings.LLM_API_KEY),
|
||||||
base_url=settings.LLM_BASE_URL,
|
base_url=settings.EMBEDDING_BASE_URL or settings.LLM_BASE_URL,
|
||||||
|
dimensions=settings.EMBEDDING_DIMENSION if settings.EMBEDDING_DIMENSION > 0 else None,
|
||||||
batch_size=100,
|
batch_size=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def save_embedding_config_to_db(db: AsyncSession, user_id: str, config: EmbeddingConfig) -> None:
|
|
||||||
"""保存嵌入配置到数据库(异步)"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(UserConfig).where(UserConfig.user_id == user_id)
|
|
||||||
)
|
|
||||||
user_config = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
# 准备嵌入配置数据
|
|
||||||
embedding_data = {
|
|
||||||
"provider": config.provider,
|
|
||||||
"model": config.model,
|
|
||||||
"api_key": config.api_key,
|
|
||||||
"base_url": config.base_url,
|
|
||||||
"dimensions": config.dimensions,
|
|
||||||
"batch_size": config.batch_size,
|
|
||||||
}
|
|
||||||
|
|
||||||
if user_config:
|
|
||||||
# 更新现有配置
|
|
||||||
try:
|
|
||||||
other_config = json.loads(user_config.other_config) if user_config.other_config else {}
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
other_config = {}
|
|
||||||
|
|
||||||
other_config[EMBEDDING_CONFIG_KEY] = embedding_data
|
|
||||||
user_config.other_config = json.dumps(other_config)
|
|
||||||
# 🔥 显式标记 other_config 字段已修改,确保 SQLAlchemy 检测到变化
|
|
||||||
flag_modified(user_config, "other_config")
|
|
||||||
else:
|
|
||||||
# 创建新配置
|
|
||||||
user_config = UserConfig(
|
|
||||||
id=str(uuid.uuid4()),
|
|
||||||
user_id=user_id,
|
|
||||||
llm_config="{}",
|
|
||||||
other_config=json.dumps({EMBEDDING_CONFIG_KEY: embedding_data}),
|
|
||||||
)
|
|
||||||
db.add(user_config)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
print(f"[EmbeddingConfig] 已保存用户 {user_id} 的嵌入配置: provider={config.provider}, model={config.model}")
|
|
||||||
|
|
||||||
|
|
||||||
# ============ API Endpoints ============
|
# ============ API Endpoints ============
|
||||||
|
|
@ -274,7 +220,9 @@ async def get_current_config(
|
||||||
config = await get_embedding_config_from_db(db, current_user.id)
|
config = await get_embedding_config_from_db(db, current_user.id)
|
||||||
|
|
||||||
# 获取维度
|
# 获取维度
|
||||||
dimensions = _get_model_dimensions(config.provider, config.model)
|
dimensions = config.dimensions
|
||||||
|
if not dimensions or dimensions <= 0:
|
||||||
|
dimensions = _get_model_dimensions(config.provider, config.model)
|
||||||
|
|
||||||
return EmbeddingConfigResponse(
|
return EmbeddingConfigResponse(
|
||||||
provider=config.provider,
|
provider=config.provider,
|
||||||
|
|
@ -288,30 +236,12 @@ async def get_current_config(
|
||||||
|
|
||||||
@router.put("/config")
|
@router.put("/config")
|
||||||
async def update_config(
|
async def update_config(
|
||||||
config: EmbeddingConfig,
|
|
||||||
db: AsyncSession = Depends(deps.get_db),
|
|
||||||
current_user: User = Depends(deps.get_current_user),
|
current_user: User = Depends(deps.get_current_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
更新嵌入模型配置(持久化到数据库)
|
更新嵌入模型配置(已禁用,固定从 .env 读取)
|
||||||
"""
|
"""
|
||||||
# 验证提供商
|
return {"message": "嵌入模型配置已锁定,请在 .env 文件中进行修改", "provider": settings.EMBEDDING_PROVIDER, "model": settings.EMBEDDING_MODEL}
|
||||||
provider_ids = [p.id for p in EMBEDDING_PROVIDERS]
|
|
||||||
if config.provider not in provider_ids:
|
|
||||||
raise HTTPException(status_code=400, detail=f"不支持的提供商: {config.provider}")
|
|
||||||
|
|
||||||
# 获取提供商信息(用于检查 API Key 要求)
|
|
||||||
provider = next((p for p in EMBEDDING_PROVIDERS if p.id == config.provider), None)
|
|
||||||
# 注意:不再强制验证模型名称,允许用户输入自定义模型
|
|
||||||
|
|
||||||
# 检查 API Key
|
|
||||||
if provider and provider.requires_api_key and not config.api_key:
|
|
||||||
raise HTTPException(status_code=400, detail=f"{config.provider} 需要 API Key")
|
|
||||||
|
|
||||||
# 保存到数据库
|
|
||||||
await save_embedding_config_to_db(db, current_user.id, config)
|
|
||||||
|
|
||||||
return {"message": "配置已保存", "provider": config.provider, "model": config.model}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/test", response_model=TestEmbeddingResponse)
|
@router.post("/test", response_model=TestEmbeddingResponse)
|
||||||
|
|
@ -319,22 +249,22 @@ async def test_embedding(
|
||||||
request: TestEmbeddingRequest,
|
request: TestEmbeddingRequest,
|
||||||
current_user: User = Depends(deps.get_current_user),
|
current_user: User = Depends(deps.get_current_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""测试当前系统的嵌入模型配置"""
|
||||||
测试嵌入模型配置
|
|
||||||
"""
|
|
||||||
import time
|
import time
|
||||||
|
from app.services.rag.embeddings import EmbeddingService
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# 创建临时嵌入服务
|
# 始终使用系统的真实配置进行测试
|
||||||
from app.services.rag.embeddings import EmbeddingService
|
# API Key 优先级:EMBEDDING_API_KEY > LLM_API_KEY
|
||||||
|
api_key = getattr(settings, "EMBEDDING_API_KEY", None) or settings.LLM_API_KEY
|
||||||
|
|
||||||
service = EmbeddingService(
|
service = EmbeddingService(
|
||||||
provider=request.provider,
|
provider=settings.EMBEDDING_PROVIDER,
|
||||||
model=request.model,
|
model=settings.EMBEDDING_MODEL,
|
||||||
api_key=request.api_key,
|
api_key=api_key,
|
||||||
base_url=request.base_url,
|
base_url=settings.EMBEDDING_BASE_URL,
|
||||||
cache_enabled=False,
|
cache_enabled=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -345,13 +275,14 @@ async def test_embedding(
|
||||||
|
|
||||||
return TestEmbeddingResponse(
|
return TestEmbeddingResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message=f"嵌入成功! 维度: {len(embedding)}",
|
message=f"嵌入成功! 提供商: {settings.EMBEDDING_PROVIDER}, 维度: {len(embedding)}",
|
||||||
dimensions=len(embedding),
|
dimensions=len(embedding),
|
||||||
sample_embedding=embedding[:5], # 返回前 5 维
|
sample_embedding=embedding[:5], # 返回前 5 维
|
||||||
latency_ms=latency_ms,
|
latency_ms=latency_ms,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"❌ 嵌入测试失败: {str(e)}")
|
||||||
return TestEmbeddingResponse(
|
return TestEmbeddingResponse(
|
||||||
success=False,
|
success=False,
|
||||||
message=f"嵌入失败: {str(e)}",
|
message=f"嵌入失败: {str(e)}",
|
||||||
|
|
|
||||||
|
|
@ -393,12 +393,6 @@ async def get_user_config_dict(db: AsyncSession, user_id: str) -> dict:
|
||||||
"""获取用户配置字典(包含解密敏感字段)"""
|
"""获取用户配置字典(包含解密敏感字段)"""
|
||||||
from app.core.encryption import decrypt_sensitive_data
|
from app.core.encryption import decrypt_sensitive_data
|
||||||
|
|
||||||
# 需要解密的敏感字段列表(与 config.py 保持一致)
|
|
||||||
SENSITIVE_LLM_FIELDS = [
|
|
||||||
'llmApiKey', 'geminiApiKey', 'openaiApiKey', 'claudeApiKey',
|
|
||||||
'qwenApiKey', 'deepseekApiKey', 'zhipuApiKey', 'moonshotApiKey',
|
|
||||||
'baiduApiKey', 'minimaxApiKey', 'doubaoApiKey'
|
|
||||||
]
|
|
||||||
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken']
|
SENSITIVE_OTHER_FIELDS = ['githubToken', 'gitlabToken']
|
||||||
|
|
||||||
def decrypt_config(config: dict, sensitive_fields: list) -> dict:
|
def decrypt_config(config: dict, sensitive_fields: list) -> dict:
|
||||||
|
|
@ -416,16 +410,13 @@ async def get_user_config_dict(db: AsyncSession, user_id: str) -> dict:
|
||||||
if not config:
|
if not config:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# 解析配置
|
# 解析配置 (忽略 llm_config)
|
||||||
llm_config = json.loads(config.llm_config) if config.llm_config else {}
|
|
||||||
other_config = json.loads(config.other_config) if config.other_config else {}
|
other_config = json.loads(config.other_config) if config.other_config else {}
|
||||||
|
|
||||||
# 解密敏感字段
|
# 解密敏感字段
|
||||||
llm_config = decrypt_config(llm_config, SENSITIVE_LLM_FIELDS)
|
|
||||||
other_config = decrypt_config(other_config, SENSITIVE_OTHER_FIELDS)
|
other_config = decrypt_config(other_config, SENSITIVE_OTHER_FIELDS)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'llmConfig': llm_config,
|
|
||||||
'otherConfig': other_config,
|
'otherConfig': other_config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,55 +39,31 @@ class LLMService:
|
||||||
"""
|
"""
|
||||||
获取LLM配置
|
获取LLM配置
|
||||||
|
|
||||||
🔥 优先级(从高到低):
|
🔥 锁定模式:始终从环境变量(.env)读取
|
||||||
1. 数据库用户配置(系统配置页面保存的配置)
|
不再合并数据库中的用户配置,确保系统一致性和安全性。
|
||||||
2. 环境变量配置(.env 文件中的配置)
|
|
||||||
|
|
||||||
如果用户配置中某个字段为空,则自动回退到环境变量。
|
|
||||||
"""
|
"""
|
||||||
if self._config is None:
|
if self._config is None:
|
||||||
user_llm_config = self._user_config.get('llmConfig', {})
|
# 锁定:全部来自 settings
|
||||||
|
provider_str = settings.LLM_PROVIDER
|
||||||
# 🔥 Provider 优先级:用户配置 > 环境变量
|
|
||||||
provider_str = user_llm_config.get('llmProvider') or getattr(settings, 'LLM_PROVIDER', 'openai')
|
|
||||||
provider = self._parse_provider(provider_str)
|
provider = self._parse_provider(provider_str)
|
||||||
|
|
||||||
# 🔥 API Key 优先级:用户配置 > 环境变量通用配置 > 环境变量平台专属配置
|
# API Key 优先级:平台专属配置 > 通用 LLM_API_KEY
|
||||||
api_key = (
|
api_key = self._get_provider_api_key(provider) or settings.LLM_API_KEY
|
||||||
user_llm_config.get('llmApiKey') or
|
|
||||||
getattr(settings, 'LLM_API_KEY', '') or
|
|
||||||
self._get_provider_api_key_from_user_config(provider, user_llm_config) or
|
|
||||||
self._get_provider_api_key(provider)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 🔥 Base URL 优先级:用户配置 > 环境变量
|
# Base URL 优先级:通用 LLM_BASE_URL > 平台默认
|
||||||
base_url = (
|
base_url = settings.LLM_BASE_URL or self._get_provider_base_url(provider)
|
||||||
user_llm_config.get('llmBaseUrl') or
|
|
||||||
getattr(settings, 'LLM_BASE_URL', None) or
|
|
||||||
self._get_provider_base_url(provider)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 🔥 Model 优先级:用户配置 > 环境变量 > 默认模型
|
# Model
|
||||||
model = (
|
model = settings.LLM_MODEL or DEFAULT_MODELS.get(provider, 'gpt-4o-mini')
|
||||||
user_llm_config.get('llmModel') or
|
|
||||||
getattr(settings, 'LLM_MODEL', '') or
|
|
||||||
DEFAULT_MODELS.get(provider, 'gpt-4o-mini')
|
|
||||||
)
|
|
||||||
|
|
||||||
# 🔥 Timeout 优先级:用户配置(毫秒) > 环境变量(秒)
|
# Timeout (settings 中是秒)
|
||||||
timeout_ms = user_llm_config.get('llmTimeout')
|
timeout = int(settings.LLM_TIMEOUT)
|
||||||
if timeout_ms:
|
|
||||||
# 用户配置是毫秒,转换为秒
|
|
||||||
timeout = int(timeout_ms / 1000) if timeout_ms > 1000 else int(timeout_ms)
|
|
||||||
else:
|
|
||||||
# 环境变量是秒
|
|
||||||
timeout = int(getattr(settings, 'LLM_TIMEOUT', 150))
|
|
||||||
|
|
||||||
# 🔥 Temperature 优先级:用户配置 > 环境变量
|
# Temperature
|
||||||
temperature = user_llm_config.get('llmTemperature') if user_llm_config.get('llmTemperature') is not None else float(getattr(settings, 'LLM_TEMPERATURE', 0.1))
|
temperature = float(settings.LLM_TEMPERATURE)
|
||||||
|
|
||||||
# 🔥 Max Tokens 优先级:用户配置 > 环境变量
|
# Max Tokens
|
||||||
max_tokens = user_llm_config.get('llmMaxTokens') or int(getattr(settings, 'LLM_MAX_TOKENS', 4096))
|
max_tokens = int(settings.LLM_MAX_TOKENS)
|
||||||
|
|
||||||
self._config = LLMConfig(
|
self._config = LLMConfig(
|
||||||
provider=provider,
|
provider=provider,
|
||||||
|
|
@ -100,27 +76,8 @@ class LLMService:
|
||||||
)
|
)
|
||||||
return self._config
|
return self._config
|
||||||
|
|
||||||
def _get_provider_api_key_from_user_config(self, provider: LLMProvider, user_llm_config: Dict[str, Any]) -> Optional[str]:
|
|
||||||
"""从用户配置中获取平台专属API Key"""
|
|
||||||
provider_key_map = {
|
|
||||||
LLMProvider.OPENAI: 'openaiApiKey',
|
|
||||||
LLMProvider.GEMINI: 'geminiApiKey',
|
|
||||||
LLMProvider.CLAUDE: 'claudeApiKey',
|
|
||||||
LLMProvider.QWEN: 'qwenApiKey',
|
|
||||||
LLMProvider.DEEPSEEK: 'deepseekApiKey',
|
|
||||||
LLMProvider.ZHIPU: 'zhipuApiKey',
|
|
||||||
LLMProvider.MOONSHOT: 'moonshotApiKey',
|
|
||||||
LLMProvider.BAIDU: 'baiduApiKey',
|
|
||||||
LLMProvider.MINIMAX: 'minimaxApiKey',
|
|
||||||
LLMProvider.DOUBAO: 'doubaoApiKey',
|
|
||||||
}
|
|
||||||
key_name = provider_key_map.get(provider)
|
|
||||||
if key_name:
|
|
||||||
return user_llm_config.get(key_name)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_provider_api_key(self, provider: LLMProvider) -> str:
|
def _get_provider_api_key(self, provider: LLMProvider) -> str:
|
||||||
"""根据提供商获取API Key"""
|
"""根据提供商从 settings 获取专属 API Key"""
|
||||||
provider_key_map = {
|
provider_key_map = {
|
||||||
LLMProvider.OPENAI: 'OPENAI_API_KEY',
|
LLMProvider.OPENAI: 'OPENAI_API_KEY',
|
||||||
LLMProvider.GEMINI: 'GEMINI_API_KEY',
|
LLMProvider.GEMINI: 'GEMINI_API_KEY',
|
||||||
|
|
@ -132,12 +89,12 @@ class LLMService:
|
||||||
LLMProvider.BAIDU: 'BAIDU_API_KEY',
|
LLMProvider.BAIDU: 'BAIDU_API_KEY',
|
||||||
LLMProvider.MINIMAX: 'MINIMAX_API_KEY',
|
LLMProvider.MINIMAX: 'MINIMAX_API_KEY',
|
||||||
LLMProvider.DOUBAO: 'DOUBAO_API_KEY',
|
LLMProvider.DOUBAO: 'DOUBAO_API_KEY',
|
||||||
LLMProvider.OLLAMA: None, # Ollama 不需要 API Key
|
|
||||||
}
|
}
|
||||||
key_name = provider_key_map.get(provider)
|
key_name = provider_key_map.get(provider)
|
||||||
if key_name:
|
if key_name:
|
||||||
return getattr(settings, key_name, '') or ''
|
return getattr(settings, key_name, '') or ''
|
||||||
return 'ollama' # Ollama的默认值
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def _get_provider_base_url(self, provider: LLMProvider) -> Optional[str]:
|
def _get_provider_base_url(self, provider: LLMProvider) -> Optional[str]:
|
||||||
"""根据提供商获取Base URL"""
|
"""根据提供商获取Base URL"""
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ export default function EmbeddingConfigPanel() {
|
||||||
<div className="bg-muted p-3 rounded-lg border border-border">
|
<div className="bg-muted p-3 rounded-lg border border-border">
|
||||||
<p className="text-xs text-muted-foreground uppercase mb-1">提供商</p>
|
<p className="text-xs text-muted-foreground uppercase mb-1">提供商</p>
|
||||||
<Badge className="bg-primary/20 text-primary border-primary/50 font-mono">
|
<Badge className="bg-primary/20 text-primary border-primary/50 font-mono">
|
||||||
{currentConfig.provider}
|
{providers.find(p => p.id === currentConfig.provider)?.name || currentConfig.provider}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted p-3 rounded-lg border border-border">
|
<div className="bg-muted p-3 rounded-lg border border-border">
|
||||||
|
|
@ -231,8 +231,11 @@ export default function EmbeddingConfigPanel() {
|
||||||
<div className="cyber-card p-6 space-y-6">
|
<div className="cyber-card p-6 space-y-6">
|
||||||
{/* 提供商选择 */}
|
{/* 提供商选择 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-bold text-muted-foreground uppercase">嵌入模型提供商</Label>
|
<Label className="text-xs font-bold text-muted-foreground uppercase flex items-center justify-between">
|
||||||
<Select value={selectedProvider} onValueChange={handleProviderChange}>
|
<span>嵌入模型提供商</span>
|
||||||
|
<span className="text-[10px] text-amber-500/80 normal-case font-normal border border-amber-500/30 px-1 rounded">.env 固定配置 (只读)</span>
|
||||||
|
</Label>
|
||||||
|
<Select value={selectedProvider} onValueChange={handleProviderChange} disabled>
|
||||||
<SelectTrigger className="h-12 cyber-input">
|
<SelectTrigger className="h-12 cyber-input">
|
||||||
<SelectValue placeholder="选择提供商" />
|
<SelectValue placeholder="选择提供商" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -270,6 +273,7 @@ export default function EmbeddingConfigPanel() {
|
||||||
onChange={(e) => setSelectedModel(e.target.value)}
|
onChange={(e) => setSelectedModel(e.target.value)}
|
||||||
placeholder="输入模型名称"
|
placeholder="输入模型名称"
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
{selectedProviderInfo.models.length > 0 && (
|
{selectedProviderInfo.models.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 mt-2">
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
|
@ -279,11 +283,10 @@ export default function EmbeddingConfigPanel() {
|
||||||
key={model}
|
key={model}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedModel(model)}
|
onClick={() => setSelectedModel(model)}
|
||||||
className={`px-2 py-1 text-xs font-mono rounded border transition-colors ${
|
className={`px-2 py-1 text-xs font-mono rounded border transition-colors ${selectedModel === model
|
||||||
selectedModel === model
|
? "bg-primary/20 border-primary/50 text-primary"
|
||||||
? "bg-primary/20 border-primary/50 text-primary"
|
: "bg-muted border-border text-muted-foreground hover:border-border hover:text-foreground"
|
||||||
: "bg-muted border-border text-muted-foreground hover:border-border hover:text-foreground"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{model}
|
{model}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -306,6 +309,7 @@ export default function EmbeddingConfigPanel() {
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
placeholder="输入 API Key"
|
placeholder="输入 API Key"
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
API Key 将安全存储,不会显示在页面上
|
API Key 将安全存储,不会显示在页面上
|
||||||
|
|
@ -326,14 +330,15 @@ export default function EmbeddingConfigPanel() {
|
||||||
selectedProvider === "ollama"
|
selectedProvider === "ollama"
|
||||||
? "http://localhost:11434"
|
? "http://localhost:11434"
|
||||||
: selectedProvider === "huggingface"
|
: selectedProvider === "huggingface"
|
||||||
? "https://router.huggingface.co"
|
? "https://router.huggingface.co"
|
||||||
: selectedProvider === "cohere"
|
: selectedProvider === "cohere"
|
||||||
? "https://api.cohere.com/v2"
|
? "https://api.cohere.com/v2"
|
||||||
: selectedProvider === "jina"
|
: selectedProvider === "jina"
|
||||||
? "https://api.jina.ai/v1"
|
? "https://api.jina.ai/v1"
|
||||||
: "https://api.openai.com/v1"
|
: "https://api.openai.com/v1"
|
||||||
}
|
}
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
用于 API 代理或自托管服务
|
用于 API 代理或自托管服务
|
||||||
|
|
@ -350,6 +355,7 @@ export default function EmbeddingConfigPanel() {
|
||||||
min={1}
|
min={1}
|
||||||
max={500}
|
max={500}
|
||||||
className="h-10 cyber-input w-32"
|
className="h-10 cyber-input w-32"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
每批嵌入的文本数量,建议 50-100
|
每批嵌入的文本数量,建议 50-100
|
||||||
|
|
@ -359,11 +365,10 @@ export default function EmbeddingConfigPanel() {
|
||||||
{/* 测试结果 */}
|
{/* 测试结果 */}
|
||||||
{testResult && (
|
{testResult && (
|
||||||
<div
|
<div
|
||||||
className={`p-4 rounded-lg ${
|
className={`p-4 rounded-lg ${testResult.success
|
||||||
testResult.success
|
? "bg-emerald-500/10 border border-emerald-500/30"
|
||||||
? "bg-emerald-500/10 border border-emerald-500/30"
|
: "bg-rose-500/10 border border-rose-500/30"
|
||||||
: "bg-rose-500/10 border border-rose-500/30"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{testResult.success ? (
|
{testResult.success ? (
|
||||||
|
|
@ -372,9 +377,8 @@ export default function EmbeddingConfigPanel() {
|
||||||
<AlertCircle className="w-5 h-5 text-rose-400" />
|
<AlertCircle className="w-5 h-5 text-rose-400" />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`font-bold ${
|
className={`font-bold ${testResult.success ? "text-emerald-400" : "text-rose-400"
|
||||||
testResult.success ? "text-emerald-400" : "text-rose-400"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{testResult.success ? "测试成功" : "测试失败"}
|
{testResult.success ? "测试成功" : "测试失败"}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -398,37 +402,26 @@ export default function EmbeddingConfigPanel() {
|
||||||
<div className="flex items-center gap-3 pt-4 border-t border-border border-dashed">
|
<div className="flex items-center gap-3 pt-4 border-t border-border border-dashed">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleTest}
|
onClick={handleTest}
|
||||||
disabled={testing || !selectedProvider || !selectedModel}
|
disabled={testing}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="cyber-btn-outline h-10"
|
className="h-10 border-orange-500/30 hover:bg-orange-500/10 text-orange-400"
|
||||||
>
|
>
|
||||||
{testing ? (
|
{testing ? (
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<PlayCircle className="w-4 h-4 mr-2" />
|
<Zap className="w-4 h-4 mr-2" />
|
||||||
)}
|
)}
|
||||||
测试连接
|
测试连接
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving || !selectedProvider || !selectedModel}
|
|
||||||
className="cyber-btn-primary h-10"
|
|
||||||
>
|
|
||||||
{saving ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
保存配置
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={loadData}
|
onClick={loadData}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="cyber-btn-ghost ml-auto h-10"
|
size="icon"
|
||||||
|
className="h-10 w-10 text-muted-foreground hover:text-foreground ml-auto"
|
||||||
|
title="刷新配置"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4" />
|
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,7 @@ export function SystemConfig() {
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-amber-400 flex items-center gap-2">
|
<span className="text-amber-400 flex items-center gap-2">
|
||||||
<AlertCircle className="h-4 w-4" /> 请配置 LLM API Key
|
<AlertCircle className="h-4 w-4" /> 请在 .env 文件中配置 LLM (只读模式)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -378,8 +378,11 @@ export function SystemConfig() {
|
||||||
<div className="cyber-card p-6 space-y-6">
|
<div className="cyber-card p-6 space-y-6">
|
||||||
{/* Provider Selection */}
|
{/* Provider Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-bold text-muted-foreground uppercase">选择 LLM 提供商</Label>
|
<Label className="text-xs font-bold text-muted-foreground uppercase flex items-center justify-between">
|
||||||
<Select value={config.llmProvider} onValueChange={(v) => updateConfig('llmProvider', v)}>
|
<span>选择 LLM 提供商</span>
|
||||||
|
<span className="text-[10px] text-amber-500/80 normal-case font-normal border border-amber-500/30 px-1 rounded">.env 固定配置 (只读)</span>
|
||||||
|
</Label>
|
||||||
|
<Select value={config.llmProvider} onValueChange={(v) => updateConfig('llmProvider', v)} disabled>
|
||||||
<SelectTrigger className="h-12 cyber-input">
|
<SelectTrigger className="h-12 cyber-input">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -419,6 +422,7 @@ export function SystemConfig() {
|
||||||
onChange={(e) => updateConfig('llmApiKey', e.target.value)}
|
onChange={(e) => updateConfig('llmApiKey', e.target.value)}
|
||||||
placeholder={config.llmProvider === 'baidu' ? 'API_KEY:SECRET_KEY 格式' : '输入你的 API Key'}
|
placeholder={config.llmProvider === 'baidu' ? 'API_KEY:SECRET_KEY 格式' : '输入你的 API Key'}
|
||||||
className="h-12 cyber-input"
|
className="h-12 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -441,6 +445,7 @@ export function SystemConfig() {
|
||||||
onChange={(e) => updateConfig('llmModel', e.target.value)}
|
onChange={(e) => updateConfig('llmModel', e.target.value)}
|
||||||
placeholder={`默认: ${DEFAULT_MODELS[config.llmProvider] || 'auto'}`}
|
placeholder={`默认: ${DEFAULT_MODELS[config.llmProvider] || 'auto'}`}
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -450,6 +455,7 @@ export function SystemConfig() {
|
||||||
onChange={(e) => updateConfig('llmBaseUrl', e.target.value)}
|
onChange={(e) => updateConfig('llmBaseUrl', e.target.value)}
|
||||||
placeholder="留空使用官方地址,或填入中转站地址"
|
placeholder="留空使用官方地址,或填入中转站地址"
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -462,7 +468,7 @@ export function SystemConfig() {
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={testLLMConnection}
|
onClick={testLLMConnection}
|
||||||
disabled={testingLLM || (!isConfigured && config.llmProvider !== 'ollama')}
|
disabled={testingLLM}
|
||||||
className="cyber-btn-primary h-10"
|
className="cyber-btn-primary h-10"
|
||||||
>
|
>
|
||||||
{testingLLM ? (
|
{testingLLM ? (
|
||||||
|
|
@ -503,71 +509,28 @@ export function SystemConfig() {
|
||||||
{showDebugInfo && llmTestResult.debug && (
|
{showDebugInfo && llmTestResult.debug && (
|
||||||
<div className="mt-3 pt-3 border-t border-border/50">
|
<div className="mt-3 pt-3 border-t border-border/50">
|
||||||
<div className="text-xs font-mono space-y-1 text-muted-foreground">
|
<div className="text-xs font-mono space-y-1 text-muted-foreground">
|
||||||
<div className="font-bold text-foreground mb-2">连接信息:</div>
|
<div className="font-bold text-foreground mb-2">测试详情:</div>
|
||||||
<div>Provider: <span className="text-foreground">{String(llmTestResult.debug.provider)}</span></div>
|
{!!llmTestResult.debug.provider && (
|
||||||
<div>Model: <span className="text-foreground">{String(llmTestResult.debug.model_used || llmTestResult.debug.model_requested || 'N/A')}</span></div>
|
<div>提供商: <span className="text-foreground">{String(llmTestResult.debug.provider)}</span></div>
|
||||||
<div>Base URL: <span className="text-foreground">{String(llmTestResult.debug.base_url_used || llmTestResult.debug.base_url_requested || '(default)')}</span></div>
|
)}
|
||||||
<div>Adapter: <span className="text-foreground">{String(llmTestResult.debug.adapter_type || 'N/A')}</span></div>
|
{!!llmTestResult.debug.model && (
|
||||||
<div>API Key: <span className="text-foreground">{String(llmTestResult.debug.api_key_prefix)} (长度: {String(llmTestResult.debug.api_key_length)})</span></div>
|
<div>当前模型: <span className="text-foreground">{String(llmTestResult.debug.model)}</span></div>
|
||||||
<div>耗时: <span className="text-foreground">{String(llmTestResult.debug.elapsed_time_ms || 'N/A')} ms</span></div>
|
)}
|
||||||
|
{!!llmTestResult.debug.duration_s && (
|
||||||
{/* 用户保存的配置参数 */}
|
<div>耗时: <span className="text-foreground">{String(llmTestResult.debug.duration_s)}s</span></div>
|
||||||
{llmTestResult.debug.saved_config && (
|
)}
|
||||||
<div className="mt-3 pt-2 border-t border-border/30">
|
{llmTestResult.debug.issues_found !== undefined && (
|
||||||
<div className="font-bold text-cyan-400 mb-2">已保存的配置参数:</div>
|
<div>测试分析结果: <span className="text-emerald-400">发现 {String(llmTestResult.debug.issues_found)} 个问题 (测试代码)</span></div>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
|
||||||
<div>温度: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).temperature ?? 'N/A')}</span></div>
|
|
||||||
<div>最大Tokens: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).max_tokens ?? 'N/A')}</span></div>
|
|
||||||
<div>超时: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).timeout_ms ?? 'N/A')} ms</span></div>
|
|
||||||
<div>请求间隔: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).gap_ms ?? 'N/A')} ms</span></div>
|
|
||||||
<div>并发数: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).concurrency ?? 'N/A')}</span></div>
|
|
||||||
<div>最大文件数: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).max_analyze_files ?? 'N/A')}</span></div>
|
|
||||||
<div>输出语言: <span className="text-foreground">{String((llmTestResult.debug.saved_config as Record<string, unknown>).output_language ?? 'N/A')}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 测试时实际使用的参数 */}
|
{!!llmTestResult.debug.error && (
|
||||||
{llmTestResult.debug.test_params && (
|
<div className="text-rose-400 mt-2 font-bold">错误详情: {String(llmTestResult.debug.error)}</div>
|
||||||
<div className="mt-2 pt-2 border-t border-border/30">
|
|
||||||
<div className="font-bold text-emerald-400 mb-2">测试时使用的参数:</div>
|
|
||||||
<div className="grid grid-cols-3 gap-x-4">
|
|
||||||
<div>温度: <span className="text-foreground">{String((llmTestResult.debug.test_params as Record<string, unknown>).temperature ?? 'N/A')}</span></div>
|
|
||||||
<div>超时: <span className="text-foreground">{String((llmTestResult.debug.test_params as Record<string, unknown>).timeout ?? 'N/A')}s</span></div>
|
|
||||||
<div>MaxTokens: <span className="text-foreground">{String((llmTestResult.debug.test_params as Record<string, unknown>).max_tokens ?? 'N/A')}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{llmTestResult.debug.error_category && (
|
{!!llmTestResult.debug.traceback && (
|
||||||
<div className="mt-2">错误类型: <span className="text-rose-400">{String(llmTestResult.debug.error_category)}</span></div>
|
<details className="mt-2 text-[10px]">
|
||||||
)}
|
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">完整堆栈跟踪 (Traceback)</summary>
|
||||||
{llmTestResult.debug.error_type && (
|
<pre className="mt-1 p-2 bg-background/50 rounded overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap border border-border/20">
|
||||||
<div>异常类型: <span className="text-rose-400">{String(llmTestResult.debug.error_type)}</span></div>
|
|
||||||
)}
|
|
||||||
{llmTestResult.debug.status_code && (
|
|
||||||
<div>HTTP 状态码: <span className="text-rose-400">{String(llmTestResult.debug.status_code)}</span></div>
|
|
||||||
)}
|
|
||||||
{llmTestResult.debug.api_response && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="font-bold text-amber-400">API 服务器返回:</div>
|
|
||||||
<pre className="mt-1 p-2 bg-amber-500/10 border border-amber-500/30 rounded text-xs overflow-x-auto">
|
|
||||||
{String(llmTestResult.debug.api_response)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{llmTestResult.debug.error_message && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="font-bold text-foreground">完整错误信息:</div>
|
|
||||||
<pre className="mt-1 p-2 bg-background/50 rounded text-xs overflow-x-auto max-h-32 overflow-y-auto">
|
|
||||||
{String(llmTestResult.debug.error_message)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{llmTestResult.debug.traceback && (
|
|
||||||
<details className="mt-2">
|
|
||||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">完整堆栈跟踪</summary>
|
|
||||||
<pre className="mt-1 p-2 bg-background/50 rounded text-xs overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap">
|
|
||||||
{String(llmTestResult.debug.traceback)}
|
{String(llmTestResult.debug.traceback)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -589,6 +552,7 @@ export function SystemConfig() {
|
||||||
value={config.llmTimeout}
|
value={config.llmTimeout}
|
||||||
onChange={(e) => updateConfig('llmTimeout', Number(e.target.value))}
|
onChange={(e) => updateConfig('llmTimeout', Number(e.target.value))}
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -601,6 +565,7 @@ export function SystemConfig() {
|
||||||
value={config.llmTemperature}
|
value={config.llmTemperature}
|
||||||
onChange={(e) => updateConfig('llmTemperature', Number(e.target.value))}
|
onChange={(e) => updateConfig('llmTemperature', Number(e.target.value))}
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -610,6 +575,7 @@ export function SystemConfig() {
|
||||||
value={config.llmMaxTokens}
|
value={config.llmMaxTokens}
|
||||||
onChange={(e) => updateConfig('llmMaxTokens', Number(e.target.value))}
|
onChange={(e) => updateConfig('llmMaxTokens', Number(e.target.value))}
|
||||||
className="h-10 cyber-input"
|
className="h-10 cyber-input"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -902,13 +868,15 @@ export function SystemConfig() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Floating Save Button */}
|
{/* Floating Save Button */}
|
||||||
{hasChanges && (
|
{
|
||||||
<div className="fixed bottom-6 right-6 cyber-card p-4 z-50">
|
hasChanges && (
|
||||||
<Button onClick={saveConfig} className="cyber-btn-primary h-12">
|
<div className="fixed bottom-6 right-6 cyber-card p-4 z-50">
|
||||||
<Save className="w-4 h-4 mr-2" /> 保存所有更改
|
<Button onClick={saveConfig} className="cyber-btn-primary h-12">
|
||||||
</Button>
|
<Save className="w-4 h-4 mr-2" /> 保存所有更改
|
||||||
</div>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Delete SSH Key Confirmation Dialog */}
|
{/* Delete SSH Key Confirmation Dialog */}
|
||||||
<AlertDialog open={showDeleteKeyDialog} onOpenChange={setShowDeleteKeyDialog}>
|
<AlertDialog open={showDeleteKeyDialog} onOpenChange={setShowDeleteKeyDialog}>
|
||||||
|
|
@ -943,6 +911,6 @@ export function SystemConfig() {
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useAuth } from "@/shared/context/AuthContext";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
@ -29,6 +30,7 @@ import type { Profile } from "@/shared/types";
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { logout } = useAuth();
|
||||||
const [profile, setProfile] = useState<Profile | null>(null);
|
const [profile, setProfile] = useState<Profile | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
@ -128,13 +130,13 @@ export default function Account() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('access_token');
|
logout();
|
||||||
toast.success("已退出登录");
|
toast.success("已退出登录");
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitchAccount = () => {
|
const handleSwitchAccount = () => {
|
||||||
localStorage.removeItem('access_token');
|
logout();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue