Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
+461
View File
@@ -0,0 +1,461 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
配置管理器 - 三级配置获取工具
优先级: Redis 缓存 → 数据库 → env 配置文件
使用方式:
from app.config_manager import config_manager
# 获取单个配置
value = await config_manager.get("notify_email", "smtp_host")
# 获取整个分组
email_config = await config_manager.get_group("notify_email")
# 设置配置(同时写入数据库和 Redis)
await config_manager.set("notify_email", "smtp_host", "smtp.qq.com", db=db)
"""
import json
import logging
from typing import Any, Dict, List, Optional
from utils.redis import CacheManager
logger = logging.getLogger(__name__)
# Redis 缓存过期时间(秒),1 小时
CONFIG_CACHE_EXPIRE = 3600
# 配置分组到 env 配置文件字段的映射
# 格式: { "config_group": { "config_key": "SETTINGS_ATTR_NAME" } }
GROUP_ENV_MAPPING: Dict[str, Dict[str, str]] = {
# ===== OAuth SSO 配置 =====
"oauth_gitee": {
"client_id": "GITEE_CLIENT_ID",
"client_secret": "GITEE_CLIENT_SECRET",
"redirect_uri": "GITEE_REDIRECT_URI",
},
"oauth_github": {
"client_id": "GITHUB_CLIENT_ID",
"client_secret": "GITHUB_CLIENT_SECRET",
"redirect_uri": "GITHUB_REDIRECT_URI",
},
"oauth_qq": {
"app_id": "QQ_APP_ID",
"app_key": "QQ_APP_KEY",
"redirect_uri": "QQ_REDIRECT_URI",
},
"oauth_google": {
"client_id": "GOOGLE_CLIENT_ID",
"client_secret": "GOOGLE_CLIENT_SECRET",
"redirect_uri": "GOOGLE_REDIRECT_URI",
},
"oauth_wechat": {
"app_id": "WECHAT_APP_ID",
"app_secret": "WECHAT_APP_SECRET",
"redirect_uri": "WECHAT_REDIRECT_URI",
},
"oauth_microsoft": {
"client_id": "MICROSOFT_CLIENT_ID",
"client_secret": "MICROSOFT_CLIENT_SECRET",
"redirect_uri": "MICROSOFT_REDIRECT_URI",
},
"oauth_dingtalk": {
"app_id": "DINGTALK_APP_ID",
"app_secret": "DINGTALK_APP_SECRET",
"redirect_uri": "DINGTALK_REDIRECT_URI",
"h5_redirect_uri": "DINGTALK_H5_REDIRECT_URI",
},
"oauth_feishu": {
"app_id": "FEISHU_APP_ID",
"app_secret": "FEISHU_APP_SECRET",
"redirect_uri": "FEISHU_REDIRECT_URI",
"h5_redirect_uri": "FEISHU_H5_REDIRECT_URI",
},
"oauth_wecom": {
"corp_id": "WECOM_CORP_ID",
"agent_id": "WECOM_AGENT_ID",
"app_secret": "WECOM_APP_SECRET",
"redirect_uri": "WECOM_REDIRECT_URI",
"h5_redirect_uri": "WECOM_H5_REDIRECT_URI",
},
# ===== 消息通知配置 =====
"notify_email": {
"smtp_host": "SMTP_HOST",
"smtp_port": "SMTP_PORT",
"smtp_user": "SMTP_USER",
"smtp_password": "SMTP_PASSWORD",
"smtp_use_tls": "SMTP_USE_TLS",
"smtp_from_name": "SMTP_FROM_NAME",
"smtp_from_email": "SMTP_FROM_EMAIL",
},
"notify_dingtalk": {
"webhook_url": "DINGTALK_WEBHOOK_URL",
"webhook_secret": "DINGTALK_WEBHOOK_SECRET",
"agent_id": "DINGTALK_AGENT_ID",
"corp_id": "DINGTALK_CORP_ID",
"todo_pc_url": "DINGTALK_TODO_PC_URL",
"todo_app_url": "DINGTALK_TODO_APP_URL",
},
"notify_feishu": {
"webhook_url": "FEISHU_WEBHOOK_URL",
"webhook_secret": "FEISHU_WEBHOOK_SECRET",
},
"notify_wecom": {
"webhook_url": "WECOM_WEBHOOK_URL",
},
"notify_wechat_mp": {
"template_id": "WECHAT_MP_TEMPLATE_ID",
"url": "WECHAT_MP_URL",
"mini_appid": "WECHAT_MP_MINI_APPID",
"mini_page": "WECHAT_MP_MINI_PAGE",
},
# ===== 钉钉组织架构同步配置 =====
"sync_dingtalk": {
"corp_id": "DINGTALK_CORP_ID",
"app_key": "DINGTALK_APP_KEY",
"app_secret": "DINGTALK_APP_SECRET",
"sync_dept_id": "",
"sync_root_dept_id": "",
"enable_dept_event": "",
"enable_user_event": "",
"callback_token": "",
"callback_aes_key": "",
"callback_url": "",
},
# ===== 企业微信组织架构同步配置 =====
"sync_wecom": {
"corp_id": "WECOM_SYNC_CORP_ID",
"corp_secret": "WECOM_SYNC_CORP_SECRET",
"sync_dept_id": "",
"sync_root_dept_id": "",
"enable_dept_event": "",
"enable_user_event": "",
"callback_token": "",
"callback_aes_key": "",
"callback_url": "",
},
# ===== 飞书组织架构同步配置 =====
"sync_feishu": {
"app_id": "FEISHU_SYNC_APP_ID",
"app_secret": "FEISHU_SYNC_APP_SECRET",
"sync_dept_id": "",
"sync_root_dept_id": "",
"enable_dept_event": "",
"enable_user_event": "",
"encrypt_key": "",
"verification_token": "",
"callback_url": "",
},
"notify_sms": {
"provider": "SMS_PROVIDER",
"aliyun_access_key_id": "ALIYUN_SMS_ACCESS_KEY_ID",
"aliyun_access_key_secret": "ALIYUN_SMS_ACCESS_KEY_SECRET",
"aliyun_sign_name": "ALIYUN_SMS_SIGN_NAME",
"aliyun_template_code": "ALIYUN_SMS_TEMPLATE_CODE",
"tencent_secret_id": "TENCENT_SMS_SECRET_ID",
"tencent_secret_key": "TENCENT_SMS_SECRET_KEY",
"tencent_sdk_app_id": "TENCENT_SMS_SDK_APP_ID",
"tencent_sign_name": "TENCENT_SMS_SIGN_NAME",
"tencent_template_id": "TENCENT_SMS_TEMPLATE_ID",
},
}
# 敏感字段列表(API 返回时脱敏)
SECRET_KEYS = {
"client_secret", "app_secret", "app_key", "corp_secret",
"smtp_password", "webhook_secret",
"aliyun_access_key_secret", "tencent_secret_key",
"callback_token", "callback_aes_key",
"encrypt_key", "verification_token",
}
class ConfigManager:
"""
三级配置管理器
获取优先级: Redis → 数据库 → env 配置文件
写入: 同时写入数据库 + Redis
"""
def __init__(self):
self._cache = CacheManager(prefix="sys_config:")
# ========== 读取 ==========
async def get(self, group: str, key: str) -> Optional[str]:
"""
获取单个配置值
:param group: 配置分组
:param key: 配置键
:return: 配置值,三级都没有返回 None
"""
# 1. Redis
cache_key = f"{group}:{key}"
value = await self._cache.get(cache_key)
if value is not None:
return value
# 2. 数据库
value = await self._get_from_db(group, key)
if value is not None:
await self._cache.set(cache_key, value, expire=CONFIG_CACHE_EXPIRE)
return value
# 3. env 配置文件
value = self._get_from_env(group, key)
if value is not None:
# 回写到 Redis 缓存
await self._cache.set(cache_key, value, expire=CONFIG_CACHE_EXPIRE)
return value
async def get_group(self, group: str) -> Dict[str, Any]:
"""
获取整个分组的配置
:param group: 配置分组
:return: {key: value} 字典
"""
# 先尝试从 Redis 获取整个分组
group_cache_key = f"_group_:{group}"
cached = await self._cache.get(group_cache_key)
if cached is not None and isinstance(cached, dict):
return cached
# 从数据库获取该分组所有配置
db_configs = await self._get_group_from_db(group)
# 获取该分组的 env 映射
env_mapping = GROUP_ENV_MAPPING.get(group, {})
# 合并: 数据库值优先,缺失的用 env 补充
result = {}
for key in env_mapping:
if key in db_configs and db_configs[key] is not None and db_configs[key] != "":
result[key] = db_configs[key]
else:
env_value = self._get_from_env(group, key)
result[key] = env_value
# 数据库中可能有 env_mapping 之外的自定义 key
for key, value in db_configs.items():
if key not in result:
result[key] = value
# 缓存整个分组
await self._cache.set(group_cache_key, result, expire=CONFIG_CACHE_EXPIRE)
return result
async def get_all_groups(self) -> Dict[str, Dict[str, Any]]:
"""获取所有分组的配置"""
result = {}
for group in GROUP_ENV_MAPPING:
result[group] = await self.get_group(group)
return result
# ========== 写入 ==========
async def set(self, group: str, key: str, value: Optional[str], db_session=None) -> None:
"""
设置单个配置值(写入数据库 + 更新 Redis)
:param group: 配置分组
:param key: 配置键
:param value: 配置值
:param db_session: 数据库会话(可选,不传则自动创建)
"""
await self._set_to_db(group, key, value, db_session)
# 更新 Redis 单个 key 缓存
cache_key = f"{group}:{key}"
if value is not None:
await self._cache.set(cache_key, value, expire=CONFIG_CACHE_EXPIRE)
else:
await self._cache.delete(cache_key)
# 清除分组缓存(下次 get_group 会重新加载)
await self._invalidate_group_cache(group)
async def set_group(self, group: str, configs: Dict[str, Optional[str]], db_session=None) -> None:
"""
批量设置分组配置
:param group: 配置分组
:param configs: {key: value} 字典
:param db_session: 数据库会话
"""
for key, value in configs.items():
await self._set_to_db(group, key, value, db_session)
cache_key = f"{group}:{key}"
if value is not None:
await self._cache.set(cache_key, value, expire=CONFIG_CACHE_EXPIRE)
else:
await self._cache.delete(cache_key)
# 清除分组缓存
await self._invalidate_group_cache(group)
# ========== 缓存管理 ==========
async def invalidate(self, group: str, key: str) -> None:
"""清除单个配置的缓存"""
await self._cache.delete(f"{group}:{key}")
await self._invalidate_group_cache(group)
async def invalidate_group(self, group: str) -> None:
"""清除整个分组的缓存"""
env_mapping = GROUP_ENV_MAPPING.get(group, {})
for key in env_mapping:
await self._cache.delete(f"{group}:{key}")
await self._invalidate_group_cache(group)
async def invalidate_all(self) -> None:
"""清除所有配置缓存"""
await self._cache.delete_pattern("*")
async def warmup(self) -> None:
"""
预热: 启动时清除旧缓存,然后将数据库配置加载到 Redis
"""
logger.info("开始预热系统配置到 Redis...")
try:
await self.invalidate_all()
for group in GROUP_ENV_MAPPING:
await self.get_group(group)
logger.info("系统配置预热完成")
except Exception as e:
logger.warning(f"系统配置预热失败(将使用 env 配置文件兜底): {e}")
# ========== 内部方法 ==========
async def _invalidate_group_cache(self, group: str) -> None:
"""清除分组级缓存"""
await self._cache.delete(f"_group_:{group}")
async def _get_from_db(self, group: str, key: str) -> Optional[str]:
"""从数据库获取配置"""
try:
from app.database import AsyncSessionLocal
from core.system_config.model import SystemConfig
from sqlalchemy import select
async with AsyncSessionLocal() as db:
result = await db.execute(
select(SystemConfig.config_value).where(
SystemConfig.config_group == group,
SystemConfig.config_key == key,
SystemConfig.status == True, # noqa: E712
SystemConfig.is_deleted == False, # noqa: E712
)
)
row = result.scalar_one_or_none()
return row
except Exception as e:
logger.debug(f"从数据库获取配置失败 {group}.{key}: {e}")
return None
async def _get_group_from_db(self, group: str) -> Dict[str, str]:
"""从数据库获取整个分组"""
try:
from app.database import AsyncSessionLocal
from core.system_config.model import SystemConfig
from sqlalchemy import select
async with AsyncSessionLocal() as db:
result = await db.execute(
select(SystemConfig.config_key, SystemConfig.config_value).where(
SystemConfig.config_group == group,
SystemConfig.status == True, # noqa: E712
SystemConfig.is_deleted == False, # noqa: E712
)
)
return {row[0]: row[1] for row in result.all()}
except Exception as e:
logger.debug(f"从数据库获取分组配置失败 {group}: {e}")
return {}
async def _set_to_db(self, group: str, key: str, value: Optional[str], db_session=None) -> None:
"""写入数据库(upsert"""
from core.system_config.model import SystemConfig
from sqlalchemy import select
async def _do_upsert(db):
result = await db.execute(
select(SystemConfig).where(
SystemConfig.config_group == group,
SystemConfig.config_key == key,
SystemConfig.is_deleted == False, # noqa: E712
)
)
config = result.scalar_one_or_none()
if config:
config.config_value = value
else:
config = SystemConfig(
config_group=group,
config_key=key,
config_value=value,
is_secret=key in SECRET_KEYS,
status=True,
)
db.add(config)
await db.commit()
if db_session:
await _do_upsert(db_session)
else:
from app.database import AsyncSessionLocal
async with AsyncSessionLocal() as db:
await _do_upsert(db)
def _get_from_env(self, group: str, key: str) -> Optional[str]:
"""从 env 配置文件获取"""
from app.config import settings
env_mapping = GROUP_ENV_MAPPING.get(group, {})
attr_name = env_mapping.get(key)
if not attr_name:
return None
value = getattr(settings, attr_name, None)
if value is None:
return None
return str(value)
# ========== 工具方法 ==========
@staticmethod
def mask_value(value: Optional[str]) -> Optional[str]:
"""脱敏处理"""
if not value:
return value
if len(value) <= 6:
return "***"
return value[:3] + "***" + value[-3:]
@staticmethod
def get_group_list() -> List[Dict[str, str]]:
"""获取所有配置分组定义"""
groups = []
for group_key in GROUP_ENV_MAPPING:
groups.append({
"key": group_key,
"fields": list(GROUP_ENV_MAPPING[group_key].keys()),
})
return groups
@staticmethod
def is_secret_key(key: str) -> bool:
"""判断是否为敏感字段"""
return key in SECRET_KEYS
# 全局单例
config_manager = ConfigManager()