Build lightweight AI agent admin
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user