Files
HuiBrain/app/api/v1/settings.py
EduBrain Dev e8a861fda2 feat: 企业微信机器人集成到后端服务,网页端可配置和重启
- BotManager: 后端启动时自动检测配置并拉起机器人(后台线程)
- WeComBotService: 新增 start_async() 异步模式,支持嵌入事件循环
- settings API: 增加 bot 配置字段(enabled/bot_id/secret)
- settings API: 增加 bot/status 和 bot/restart 端点
- 前端设置页: 增加企业微信机器人配置卡片(启用开关/ID/Secret)
- 前端设置页: 增加机器人状态徽章和重启按钮
- Secret 脱敏显示,仅展示前4位
2026-04-14 13:42:04 +08:00

262 lines
9.7 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()
def _mask_secret(value: str | None, visible: int = 4) -> str | None:
"""对敏感字符串脱敏,保留前 visible 个字符"""
if not value:
return None
if len(value) <= visible:
return "****"
return value[:visible] + "****"
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
# 企业微信机器人
wework_bot_enabled: bool
wework_bot_id: str | None
wework_bot_secret_masked: str | None
@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),
wework_bot_enabled=settings.WEWORK_BOT_ENABLED,
wework_bot_id=settings.WEWORK_BOT_ID,
wework_bot_secret_masked=_mask_secret(settings.WEWORK_BOT_SECRET),
)
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
# 企业微信机器人
wework_bot_enabled: bool | None = None
wework_bot_id: str | None = None
wework_bot_secret: str | 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",
"wework_bot_enabled": "WEWORK_BOT_ENABLED",
"wework_bot_id": "WEWORK_BOT_ID",
"wework_bot_secret": "WEWORK_BOT_SECRET",
}
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)}"}
@router.post("/bot/restart", summary="重启企业微信机器人")
async def restart_bot():
"""重启企业微信机器人服务"""
from app.main import bot_manager
result = await bot_manager.restart()
return result
@router.get("/bot/status", summary="获取企业微信机器人状态")
async def get_bot_status():
"""获取企业微信机器人运行状态"""
from app.main import bot_manager
return bot_manager.get_status()