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
@@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Redis管理模块
"""
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Redis管理API(异步版本)
"""
import logging
from fastapi import APIRouter, HTTPException
from core.redis_manager.schema import (
RedisKeyDetailSchema,
RedisKeyCreateSchema,
RedisKeyUpdateSchema,
RedisKeySearchSchema,
RedisKeyListResponse,
RedisDatabaseListResponse,
RedisKeyRenameSchema,
RedisKeyExpireSchema,
RedisBatchDeleteSchema,
RedisFlushDBSchema,
RedisOperationResponse
)
from core.redis_manager.service import AsyncRedisManagerService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/redis_manager", tags=["Redis管理"])
@router.get("/databases", response_model=RedisDatabaseListResponse, summary="获取所有Redis数据库信息")
async def get_redis_databases():
"""获取所有Redis数据库信息"""
try:
service = AsyncRedisManagerService(db_index=0)
databases, total_keys = await service.get_all_databases()
await service.close()
return {
'databases': databases,
'total_keys': total_keys
}
except Exception as e:
logger.error(f"Failed to get redis databases: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{db_index}/keys/search", response_model=RedisKeyListResponse, summary="搜索Redis键")
async def search_redis_keys(db_index: int, search: RedisKeySearchSchema):
"""搜索Redis键"""
try:
service = AsyncRedisManagerService(db_index=db_index)
keys, total = await service.search_keys(
pattern=search.pattern,
key_type=search.key_type,
page=search.page,
page_size=search.page_size
)
await service.close()
return {
'total': total,
'keys': keys,
'page': search.page,
'page_size': search.page_size
}
except Exception as e:
logger.error(f"Failed to search redis keys: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{db_index}/keys/{key:path}", response_model=RedisKeyDetailSchema, summary="获取Redis键详情")
async def get_redis_key_detail(db_index: int, key: str):
"""获取Redis键详情"""
try:
service = AsyncRedisManagerService(db_index=db_index)
detail = await service.get_key_detail(key)
await service.close()
return detail
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to get redis key detail: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{db_index}/keys", response_model=RedisOperationResponse, summary="创建Redis键")
async def create_redis_key(db_index: int, data: RedisKeyCreateSchema):
"""创建Redis键"""
try:
service = AsyncRedisManagerService(db_index=db_index)
success = await service.create_key(
key=data.key,
key_type=data.type,
value=data.value,
ttl=data.ttl
)
await service.close()
return {
'success': success,
'message': f"Key '{data.key}' created successfully"
}
except ValueError as e:
return {
'success': False,
'message': str(e)
}
except Exception as e:
logger.error(f"Failed to create redis key: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{db_index}/keys/{key:path}", response_model=RedisOperationResponse, summary="更新Redis键")
async def update_redis_key(db_index: int, key: str, data: RedisKeyUpdateSchema):
"""更新Redis键"""
try:
service = AsyncRedisManagerService(db_index=db_index)
success = await service.update_key(
key=key,
value=data.value,
ttl=data.ttl
)
await service.close()
return {
'success': success,
'message': f"Key '{key}' updated successfully"
}
except ValueError as e:
return {
'success': False,
'message': str(e)
}
except Exception as e:
logger.error(f"Failed to update redis key: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{db_index}/keys/{key:path}", response_model=RedisOperationResponse, summary="删除Redis键")
async def delete_redis_key(db_index: int, key: str):
"""删除Redis键"""
try:
service = AsyncRedisManagerService(db_index=db_index)
success = await service.delete_key(key)
await service.close()
return {
'success': success,
'message': f"Key '{key}' deleted successfully" if success else f"Key '{key}' not found"
}
except Exception as e:
logger.error(f"Failed to delete redis key: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{db_index}/keys/batch-delete", response_model=RedisOperationResponse, summary="批量删除Redis键")
async def batch_delete_redis_keys(db_index: int, data: RedisBatchDeleteSchema):
"""批量删除Redis键"""
try:
service = AsyncRedisManagerService(db_index=db_index)
count = await service.batch_delete_keys(data.keys)
await service.close()
return {
'success': True,
'message': f"Deleted {count} keys",
'data': {'deleted_count': count}
}
except Exception as e:
logger.error(f"Failed to batch delete redis keys: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{db_index}/keys/rename", response_model=RedisOperationResponse, summary="重命名Redis键")
async def rename_redis_key(db_index: int, data: RedisKeyRenameSchema):
"""重命名Redis键"""
try:
service = AsyncRedisManagerService(db_index=db_index)
success = await service.rename_key(data.old_key, data.new_key)
await service.close()
return {
'success': success,
'message': f"Key renamed from '{data.old_key}' to '{data.new_key}'"
}
except ValueError as e:
return {
'success': False,
'message': str(e)
}
except Exception as e:
logger.error(f"Failed to rename redis key: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{db_index}/keys/expire", response_model=RedisOperationResponse, summary="设置Redis键过期时间")
async def set_redis_key_expire(db_index: int, data: RedisKeyExpireSchema):
"""设置Redis键过期时间"""
try:
service = AsyncRedisManagerService(db_index=db_index)
success = await service.set_expire(data.key, data.ttl)
await service.close()
ttl_msg = "永不过期" if data.ttl == -1 else f"{data.ttl}秒后过期"
return {
'success': success,
'message': f"Key '{data.key}' set to {ttl_msg}"
}
except ValueError as e:
return {
'success': False,
'message': str(e)
}
except Exception as e:
logger.error(f"Failed to set redis key expire: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{db_index}/flush", response_model=RedisOperationResponse, summary="清空Redis数据库")
async def flush_redis_database(db_index: int, data: RedisFlushDBSchema):
"""清空Redis数据库"""
try:
service = AsyncRedisManagerService(db_index=db_index)
success = await service.flush_db(confirm=data.confirm)
await service.close()
return {
'success': success,
'message': f"Database {db_index} flushed successfully"
}
except ValueError as e:
return {
'success': False,
'message': str(e)
}
except Exception as e:
logger.error(f"Failed to flush redis database: {e}")
raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,131 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Redis管理Schema
"""
from datetime import datetime
from typing import Optional, List, Any, Dict
from pydantic import BaseModel, Field
from app.base_schema import CSTDatetime
class RedisKeySchema(BaseModel):
"""Redis键Schema"""
key: str
type: str # string, list, set, zset, hash
ttl: int # -1表示永不过期,-2表示已过期
size: Optional[int] = None # 键的大小(字节)
length: Optional[int] = None # 集合/列表的元素数量
encoding: Optional[str] = None # 编码方式
class RedisKeyDetailSchema(BaseModel):
"""Redis键详情Schema"""
key: str
type: str
ttl: int
value: Any # 根据类型不同,值的格式也不同
size: Optional[int] = None
encoding: Optional[str] = None
created_at: Optional[CSTDatetime] = None
class RedisKeyCreateSchema(BaseModel):
"""创建Redis键Schema"""
key: str = Field(..., description="键名")
type: str = Field(..., description="数据类型: string, list, set, zset, hash")
value: Any = Field(..., description="")
ttl: Optional[int] = Field(None, description="过期时间(秒),-1表示永不过期")
class RedisKeyUpdateSchema(BaseModel):
"""更新Redis键Schema"""
value: Any = Field(..., description="新值")
ttl: Optional[int] = Field(None, description="过期时间(秒)")
class RedisStringValueSchema(BaseModel):
"""String类型值Schema"""
value: str
class RedisListValueSchema(BaseModel):
"""List类型值Schema"""
values: List[str]
class RedisSetValueSchema(BaseModel):
"""Set类型值Schema"""
members: List[str]
class RedisZSetValueSchema(BaseModel):
"""ZSet类型值Schema"""
members: List[Dict[str, Any]] # [{"member": "xxx", "score": 1.0}]
class RedisHashValueSchema(BaseModel):
"""Hash类型值Schema"""
fields: Dict[str, str]
class RedisKeySearchSchema(BaseModel):
"""Redis键搜索Schema"""
pattern: str = Field(default="*", description="搜索模式,支持通配符")
key_type: Optional[str] = Field(None, description="键类型过滤")
page: int = Field(default=1, ge=1, description="页码")
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
class RedisKeyListResponse(BaseModel):
"""Redis键列表响应Schema"""
total: int
keys: List[RedisKeySchema]
page: int
page_size: int
class RedisDatabaseSchema(BaseModel):
"""Redis数据库Schema"""
db_index: int
keys_count: int
expires_count: int
avg_ttl: int
class RedisDatabaseListResponse(BaseModel):
"""Redis数据库列表响应Schema"""
databases: List[RedisDatabaseSchema]
total_keys: int
class RedisKeyRenameSchema(BaseModel):
"""重命名键Schema"""
old_key: str
new_key: str
class RedisKeyExpireSchema(BaseModel):
"""设置过期时间Schema"""
key: str
ttl: int # 秒
class RedisBatchDeleteSchema(BaseModel):
"""批量删除Schema"""
keys: List[str]
class RedisFlushDBSchema(BaseModel):
"""清空数据库Schema"""
db_index: int
confirm: bool = Field(..., description="确认清空")
class RedisOperationResponse(BaseModel):
"""操作响应Schema"""
success: bool
message: str
data: Optional[Any] = None
@@ -0,0 +1,506 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Redis管理服务(异步版本)
"""
import logging
from typing import List, Dict, Any, Optional, Tuple
import redis.asyncio as aioredis
from app.config import settings
logger = logging.getLogger(__name__)
class AsyncRedisManagerService:
"""异步Redis管理服务"""
def __init__(self, db_index: int = 0):
"""
初始化Redis连接
Args:
db_index: Redis数据库索引(0-15
"""
redis_host = settings.REDIS_HOST
redis_port = settings.REDIS_PORT
redis_password = settings.REDIS_PASSWORD
if redis_password == '':
redis_password = None
# 不自动解码,因为可能有二进制数据
self.client = aioredis.Redis(
host=redis_host,
port=redis_port,
db=db_index,
password=redis_password,
decode_responses=False # 不自动解码,手动处理
)
self.db_index = db_index
self.host = redis_host
self.port = redis_port
self.password = redis_password
def _safe_decode(self, value: any) -> str:
"""安全解码Redis值"""
if value is None:
return ''
if isinstance(value, bytes):
try:
return value.decode('utf-8')
except UnicodeDecodeError:
import base64
return f"<binary data: {base64.b64encode(value).decode('ascii')}>"
return str(value)
async def get_all_databases(self) -> Tuple[List[Dict[str, Any]], int]:
"""
获取所有Redis数据库信息
Returns:
(数据库列表, 总键数)
"""
databases = []
total_keys = 0
# Redis默认有16个数据库(0-15
for db_idx in range(16):
try:
temp_client = aioredis.Redis(
host=self.host,
port=self.port,
db=db_idx,
password=self.password,
decode_responses=True
)
# 获取数据库信息
info = await temp_client.info('keyspace')
db_key = f'db{db_idx}'
if db_key in info:
db_info = info[db_key]
keys_count = db_info.get('keys', 0)
expires_count = db_info.get('expires', 0)
avg_ttl = db_info.get('avg_ttl', 0)
else:
keys_count = 0
expires_count = 0
avg_ttl = 0
databases.append({
'db_index': db_idx,
'keys_count': keys_count,
'expires_count': expires_count,
'avg_ttl': avg_ttl
})
total_keys += keys_count
await temp_client.aclose()
except Exception as e:
logger.error(f"Failed to get info for db{db_idx}: {e}")
databases.append({
'db_index': db_idx,
'keys_count': 0,
'expires_count': 0,
'avg_ttl': 0
})
return databases, total_keys
async def search_keys(
self,
pattern: str = "*",
key_type: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Dict[str, Any]], int]:
"""
搜索Redis键
Args:
pattern: 搜索模式
key_type: 键类型过滤
page: 页码
page_size: 每页数量
Returns:
(键列表, 总数)
"""
try:
# 使用SCAN命令遍历所有键
all_keys = []
cursor = 0
while True:
cursor, keys = await self.client.scan(
cursor,
match=pattern.encode() if isinstance(pattern, str) else pattern,
count=100
)
all_keys.extend([self._safe_decode(k) for k in keys])
if cursor == 0:
break
logger.info(f"Scanned {len(all_keys)} keys with pattern '{pattern}'")
# 按类型过滤
if key_type:
filtered_keys = []
for key in all_keys:
ktype = await self.client.type(key)
ktype_str = self._safe_decode(ktype)
if ktype_str == key_type:
filtered_keys.append(key)
logger.info(f"Filtered to {len(filtered_keys)} keys of type '{key_type}'")
all_keys = filtered_keys
total = len(all_keys)
logger.info(f"Total keys: {total}, returning page {page} with {page_size} items per page")
# 分页
start = (page - 1) * page_size
end = start + page_size
page_keys = all_keys[start:end]
# 获取键的详细信息
keys_info = []
for key in page_keys:
try:
key_info = await self._get_key_info(key)
keys_info.append(key_info)
except Exception as e:
logger.error(f"Failed to get info for key {key}: {e}")
keys_info.append({
'key': key,
'type': 'unknown',
'ttl': -1,
'size': None,
'length': None,
'encoding': None
})
return keys_info, total
except Exception as e:
logger.error(f"Failed to search keys: {e}")
raise
async def _get_key_info(self, key: str) -> Dict[str, Any]:
"""获取键的基本信息"""
key_bytes = key.encode() if isinstance(key, str) else key
key_type = self._safe_decode(await self.client.type(key_bytes))
ttl = await self.client.ttl(key_bytes)
info = {
'key': key,
'type': key_type,
'ttl': ttl,
'encoding': None
}
# 获取编码信息
try:
if await self.client.exists(key_bytes):
encoding = await self.client.object('encoding', key_bytes)
info['encoding'] = self._safe_decode(encoding) if encoding else None
except Exception:
pass
# 获取大小和长度
if key_type == 'string':
value = await self.client.get(key_bytes)
info['size'] = len(value) if value else 0
elif key_type == 'list':
info['length'] = await self.client.llen(key_bytes)
elif key_type == 'set':
info['length'] = await self.client.scard(key_bytes)
elif key_type == 'zset':
info['length'] = await self.client.zcard(key_bytes)
elif key_type == 'hash':
info['length'] = await self.client.hlen(key_bytes)
return info
async def get_key_detail(self, key: str) -> Dict[str, Any]:
"""
获取键的详细信息
Args:
key: 键名
Returns:
键的详细信息
"""
key_bytes = key.encode() if isinstance(key, str) else key
if not await self.client.exists(key_bytes):
raise ValueError(f"Key '{key}' does not exist")
key_type = self._safe_decode(await self.client.type(key_bytes))
ttl = await self.client.ttl(key_bytes)
detail = {
'key': key,
'type': key_type,
'ttl': ttl,
'encoding': None
}
# 获取编码信息
try:
encoding = await self.client.object('encoding', key_bytes)
detail['encoding'] = self._safe_decode(encoding) if encoding else None
except Exception:
pass
# 根据类型获取值
if key_type == 'string':
value = await self.client.get(key_bytes)
detail['value'] = self._safe_decode(value)
detail['size'] = len(value) if value else 0
elif key_type == 'list':
values = await self.client.lrange(key_bytes, 0, -1)
detail['value'] = [self._safe_decode(v) for v in values]
detail['length'] = len(values)
elif key_type == 'set':
members = await self.client.smembers(key_bytes)
detail['value'] = [self._safe_decode(m) for m in members]
detail['length'] = len(members)
elif key_type == 'zset':
members = await self.client.zrange(key_bytes, 0, -1, withscores=True)
detail['value'] = [{'member': self._safe_decode(m), 'score': s} for m, s in members]
detail['length'] = len(members)
elif key_type == 'hash':
hash_data = await self.client.hgetall(key_bytes)
detail['value'] = {self._safe_decode(k): self._safe_decode(v) for k, v in hash_data.items()}
detail['length'] = len(hash_data)
return detail
async def create_key(self, key: str, key_type: str, value: Any, ttl: Optional[int] = None) -> bool:
"""
创建Redis键
Args:
key: 键名
key_type: 数据类型
value: 值
ttl: 过期时间(秒)
Returns:
是否成功
"""
try:
# 检查键是否已存在
if await self.client.exists(key):
raise ValueError(f"Key '{key}' already exists")
# 根据类型设置值
if key_type == 'string':
await self.client.set(key, value)
elif key_type == 'list':
if isinstance(value, list):
await self.client.rpush(key, *value)
else:
raise ValueError("List type requires a list value")
elif key_type == 'set':
if isinstance(value, list):
await self.client.sadd(key, *value)
else:
raise ValueError("Set type requires a list value")
elif key_type == 'zset':
if isinstance(value, list):
mapping = {item['member']: item['score'] for item in value}
await self.client.zadd(key, mapping)
else:
raise ValueError("ZSet type requires a list of {member, score} dicts")
elif key_type == 'hash':
if isinstance(value, dict):
await self.client.hset(key, mapping=value)
else:
raise ValueError("Hash type requires a dict value")
else:
raise ValueError(f"Unsupported type: {key_type}")
# 设置过期时间
if ttl and ttl > 0:
await self.client.expire(key, ttl)
return True
except Exception as e:
logger.error(f"Failed to create key {key}: {e}")
raise
async def update_key(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
"""
更新Redis键
Args:
key: 键名
value: 新值
ttl: 过期时间(秒)
Returns:
是否成功
"""
try:
if not await self.client.exists(key):
raise ValueError(f"Key '{key}' does not exist")
key_type = self._safe_decode(await self.client.type(key))
# 删除旧值
await self.client.delete(key)
# 设置新值
if key_type == 'string':
await self.client.set(key, value)
elif key_type == 'list':
if isinstance(value, list):
await self.client.rpush(key, *value)
else:
raise ValueError("List type requires a list value")
elif key_type == 'set':
if isinstance(value, list):
await self.client.sadd(key, *value)
else:
raise ValueError("Set type requires a list value")
elif key_type == 'zset':
if isinstance(value, list):
mapping = {item['member']: item['score'] for item in value}
await self.client.zadd(key, mapping)
else:
raise ValueError("ZSet type requires a list of {member, score} dicts")
elif key_type == 'hash':
if isinstance(value, dict):
await self.client.hset(key, mapping=value)
else:
raise ValueError("Hash type requires a dict value")
# 设置过期时间
if ttl is not None:
if ttl > 0:
await self.client.expire(key, ttl)
elif ttl == -1:
await self.client.persist(key)
return True
except Exception as e:
logger.error(f"Failed to update key {key}: {e}")
raise
async def delete_key(self, key: str) -> bool:
"""
删除Redis键
Args:
key: 键名
Returns:
是否成功
"""
try:
result = await self.client.delete(key)
return result > 0
except Exception as e:
logger.error(f"Failed to delete key {key}: {e}")
raise
async def batch_delete_keys(self, keys: List[str]) -> int:
"""
批量删除Redis键
Args:
keys: 键名列表
Returns:
删除的键数量
"""
try:
if not keys:
return 0
return await self.client.delete(*keys)
except Exception as e:
logger.error(f"Failed to batch delete keys: {e}")
raise
async def rename_key(self, old_key: str, new_key: str) -> bool:
"""
重命名键
Args:
old_key: 旧键名
new_key: 新键名
Returns:
是否成功
"""
try:
if not await self.client.exists(old_key):
raise ValueError(f"Key '{old_key}' does not exist")
if await self.client.exists(new_key):
raise ValueError(f"Key '{new_key}' already exists")
await self.client.rename(old_key, new_key)
return True
except Exception as e:
logger.error(f"Failed to rename key {old_key} to {new_key}: {e}")
raise
async def set_expire(self, key: str, ttl: int) -> bool:
"""
设置键的过期时间
Args:
key: 键名
ttl: 过期时间(秒),-1表示永不过期
Returns:
是否成功
"""
try:
if not await self.client.exists(key):
raise ValueError(f"Key '{key}' does not exist")
if ttl == -1:
await self.client.persist(key)
else:
await self.client.expire(key, ttl)
return True
except Exception as e:
logger.error(f"Failed to set expire for key {key}: {e}")
raise
async def flush_db(self, confirm: bool = False) -> bool:
"""
清空当前数据库
Args:
confirm: 确认清空
Returns:
是否成功
"""
if not confirm:
raise ValueError("Must confirm to flush database")
try:
await self.client.flushdb()
return True
except Exception as e:
logger.error(f"Failed to flush database: {e}")
raise
async def close(self):
"""关闭连接"""
if self.client:
await self.client.aclose()