Files
HuiBrain/app/api/v1/settings.py
EduBrain Dev b17786b57b feat: v1.2.0 - 图片去重与管理、微信机器人优化、搜索设置可配置
主要功能:
- 图片上传时 OCR 内容去重(3个上传端点统一使用公共函数 _check_ocr_duplicate)
- 图片管理 Tab:展示所有图片、手动删除、一键去重
- 搜索结果详情弹窗增加删除按钮(带确认弹窗)
- 图片管理卡片点击查看详情(复用 showOcrDetailModal)
- 搜索限制和 LLM 批量判断数量可通过网站设置
- MiniMax API 调用添加 reasoning_split=True
- 企业微信机器人:WebSocket 长连接、图片搜索、配置化搜索数量
- 版本号升级至 1.2.0
2026-04-13 22:25:08 +08:00

224 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
系统设置 API 路由
提供运行时配置的读取和修改接口
"""
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.config import settings
router = APIRouter()
class SettingsResponse(BaseModel):
"""设置响应(隐藏敏感信息的中间位)"""
embedding_provider: str
embedding_model: str
embedding_dimensions: int
ocr_provider: str
llm_provider: str
minimax_base_url: str
minimax_embedding_model: str
minimax_chat_model: str
deepseek_base_url: str
deepseek_ocr_model: str
openai_base_url: str | None
chunk_size: int
chunk_overlap: int
search_limit: int
judge_batch_size: int
# API Key 只返回是否已配置(不返回实际值)
has_minimax_key: bool
has_deepseek_key: bool
has_openai_key: bool
has_zhipu_key: bool
has_dashscope_key: bool
has_aliyun_ocr: bool
has_tencent_ocr: bool
@router.get("", response_model=SettingsResponse, summary="获取当前设置")
async def get_settings():
"""获取当前系统设置API Key 脱敏)"""
return SettingsResponse(
embedding_provider=settings.EMBEDDING_PROVIDER.value,
embedding_model=settings.EMBEDDING_MODEL,
embedding_dimensions=settings.EMBEDDING_DIMENSIONS,
ocr_provider=settings.OCR_PROVIDER.value,
llm_provider=settings.LLM_PROVIDER,
minimax_base_url=settings.MINIMAX_BASE_URL,
minimax_embedding_model=settings.MINIMAX_EMBEDDING_MODEL,
minimax_chat_model=settings.MINIMAX_CHAT_MODEL,
deepseek_base_url=settings.DEEPSEEK_BASE_URL,
deepseek_ocr_model=settings.DEEPSEEK_OCR_MODEL,
openai_base_url=settings.OPENAI_BASE_URL,
chunk_size=settings.CHUNK_SIZE,
chunk_overlap=settings.CHUNK_OVERLAP,
search_limit=settings.SEARCH_LIMIT,
judge_batch_size=settings.JUDGE_BATCH_SIZE,
has_minimax_key=bool(settings.MINIMAX_API_KEY),
has_deepseek_key=bool(settings.DEEPSEEK_API_KEY),
has_openai_key=bool(settings.OPENAI_API_KEY),
has_zhipu_key=bool(settings.ZHIPU_API_KEY),
has_dashscope_key=bool(settings.DASHSCOPE_API_KEY),
has_aliyun_ocr=bool(settings.ALIYUN_OCR_ACCESS_KEY),
has_tencent_ocr=bool(settings.TENCENT_OCR_SECRET_ID),
)
class SettingsUpdate(BaseModel):
"""设置更新请求(所有字段可选)"""
embedding_provider: str | None = None
embedding_model: str | None = None
embedding_dimensions: int | None = None
ocr_provider: str | None = None
llm_provider: str | None = None
minimax_api_key: str | None = None
minimax_base_url: str | None = None
minimax_embedding_model: str | None = None
minimax_chat_model: str | None = None
deepseek_api_key: str | None = None
deepseek_base_url: str | None = None
deepseek_ocr_model: str | None = None
openai_api_key: str | None = None
openai_base_url: str | None = None
zhipu_api_key: str | None = None
dashscope_api_key: str | None = None
aliyun_ocr_access_key: str | None = None
aliyun_ocr_secret: str | None = None
tencent_ocr_secret_id: str | None = None
tencent_ocr_secret_key: str | None = None
chunk_size: int | None = None
chunk_overlap: int | None = None
search_limit: int | None = None
judge_batch_size: int | None = None
@router.put("", summary="更新设置")
async def update_settings(data: SettingsUpdate):
"""
更新系统设置(运行时生效)。
修改嵌入模型或 OCR 提供商后,对应的单例服务会自动重置。
"""
from app.config import EmbeddingProvider, OCRProvider
from app.services.embedding_service import EmbeddingService
from app.services.ocr_service import OCRService
from app.services.llm_service import LLMService
update_data = data.model_dump(exclude_none=True)
if not update_data:
raise HTTPException(status_code=400, detail="没有需要更新的字段")
provider_changed = False
# 映射字段名API 用 snake_caseSettings 用 UPPER_CASE
field_map = {
"embedding_provider": "EMBEDDING_PROVIDER",
"embedding_model": "EMBEDDING_MODEL",
"embedding_dimensions": "EMBEDDING_DIMENSIONS",
"ocr_provider": "OCR_PROVIDER",
"llm_provider": "LLM_PROVIDER",
"minimax_api_key": "MINIMAX_API_KEY",
"minimax_base_url": "MINIMAX_BASE_URL",
"minimax_embedding_model": "MINIMAX_EMBEDDING_MODEL",
"minimax_chat_model": "MINIMAX_CHAT_MODEL",
"deepseek_api_key": "DEEPSEEK_API_KEY",
"deepseek_base_url": "DEEPSEEK_BASE_URL",
"deepseek_ocr_model": "DEEPSEEK_OCR_MODEL",
"openai_api_key": "OPENAI_API_KEY",
"openai_base_url": "OPENAI_BASE_URL",
"zhipu_api_key": "ZHIPU_API_KEY",
"dashscope_api_key": "DASHSCOPE_API_KEY",
"aliyun_ocr_access_key": "ALIYUN_OCR_ACCESS_KEY",
"aliyun_ocr_secret": "ALIYUN_OCR_SECRET",
"tencent_ocr_secret_id": "TENCENT_OCR_SECRET_ID",
"tencent_ocr_secret_key": "TENCENT_OCR_SECRET_KEY",
"chunk_size": "CHUNK_SIZE",
"chunk_overlap": "CHUNK_OVERLAP",
"search_limit": "SEARCH_LIMIT",
"judge_batch_size": "JUDGE_BATCH_SIZE",
}
for api_field, config_field in field_map.items():
if api_field in update_data:
value = update_data[api_field]
# 枚举类型需要转换
if config_field == "EMBEDDING_PROVIDER":
value = EmbeddingProvider(value)
provider_changed = True
elif config_field == "OCR_PROVIDER":
value = OCRProvider(value)
provider_changed = True
setattr(settings, config_field, value)
# 如果提供商变了,重置单例
if provider_changed:
EmbeddingService.reset()
OCRService.reset()
LLMService.reset()
return {"message": "设置已更新", "provider_changed": provider_changed}
@router.get("/stats", summary="获取知识库统计")
async def get_stats():
"""获取知识库统计信息"""
from sqlalchemy import text
from app.database import async_session_factory
async with async_session_factory() as session:
result = await session.execute(text("""
SELECT
(SELECT COUNT(*) FROM knowledge_pages) AS page_count,
(SELECT COUNT(*) FROM knowledge_chunks) AS chunk_count,
(SELECT COUNT(*) FROM knowledge_chunks WHERE embedding IS NOT NULL) AS embedded_count,
(SELECT COUNT(*) FROM ocr_images) AS image_count,
(SELECT COUNT(*) FROM ocr_images WHERE status = 'completed') AS ocr_completed_count
"""))
row = result.fetchone()
return {
"page_count": row.page_count,
"chunk_count": row.chunk_count,
"embedded_count": row.embedded_count,
"image_count": row.image_count,
"ocr_completed_count": row.ocr_completed_count,
}
@router.post("/test/embedding", summary="测试嵌入模型连接")
async def test_embedding():
"""测试当前嵌入模型是否可用"""
try:
from app.services.embedding_service import EmbeddingService
result = await EmbeddingService.embed_single("测试")
return {"success": True, "dimension": len(result), "message": f"嵌入模型正常,维度: {len(result)}"}
except Exception as e:
return {"success": False, "message": f"嵌入模型测试失败: {str(e)}"}
@router.post("/test/llm", summary="测试 LLM 连接")
async def test_llm():
"""测试当前 LLM 是否可用"""
try:
from app.services.llm_service import LLMService
result = await LLMService.chat([
{"role": "user", "content": "请回复\"连接正常\""}
], max_tokens=20)
return {"success": True, "message": f"LLM 正常,回复: {result[:100]}"}
except Exception as e:
return {"success": False, "message": f"LLM 测试失败: {str(e)}"}
@router.post("/test/ocr", summary="测试 OCR使用示例文本")
async def test_ocr():
"""测试当前 OCR 提供商是否可用(不实际上传图片,只检查配置)"""
try:
from app.services.ocr_service import OCRService
provider = OCRService.get_instance()
return {"success": True, "message": f"OCR 提供商 {provider.name} 已就绪"}
except Exception as e:
return {"success": False, "message": f"OCR 测试失败: {str(e)}"}