diff --git a/.env.example b/.env.example
index 10f5bd88..aa73850c 100644
--- a/.env.example
+++ b/.env.example
@@ -21,6 +21,9 @@ LOG_DIR="/opt/sqlbot/logs"
LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
SQL_DEBUG=False
+CACHE_TYPE="memory"
+CACHE_REDIS_URL="redis://127.0.0.1:6379"
+
# Postgres
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
diff --git a/backend/common/core/config.py b/backend/common/core/config.py
index f9026b09..482f4d61 100644
--- a/backend/common/core/config.py
+++ b/backend/common/core/config.py
@@ -61,6 +61,9 @@ class Settings(BaseSettings):
DEFAULT_PWD: str = "SQLBot@123456"
ASSISTANT_TOKEN_KEY: str = "X-SQLBOT-ASSISTANT-TOKEN"
+ CACHE_TYPE: Literal["redis", "memory", "None"] = "memory"
+ CACHE_REDIS_URL: str | None = None # Redis URL, e.g., "redis://[[username]:[password]]@localhost:6379/0"
+
LOG_LEVEL: str = "INFO" # DEBUG, INFO, WARNING, ERROR
LOG_DIR: str = "logs"
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
diff --git a/backend/common/core/sqlbot_cache.py b/backend/common/core/sqlbot_cache.py
index 15d58308..cce07c06 100644
--- a/backend/common/core/sqlbot_cache.py
+++ b/backend/common/core/sqlbot_cache.py
@@ -1,52 +1,12 @@
-import contextvars
from fastapi_cache import FastAPICache
from functools import partial, wraps
from typing import Optional, Any, Dict, Tuple
from inspect import signature
-from contextlib import asynccontextmanager
-import asyncio
-import random
-from collections import defaultdict
-
-from apps.system.schemas.auth import CacheName
-from apps.system.schemas.system_schema import UserInfoDTO
+from common.core.config import settings
from common.utils.utils import SQLBotLogUtil
+from fastapi_cache.backends.inmemory import InMemoryBackend
-# 使用contextvar来跟踪当前线程已持有的锁
-_held_locks = contextvars.ContextVar('held_locks', default=set())
-# 高效锁管理器
-class LockManager:
- _locks = defaultdict(asyncio.Lock)
-
- @classmethod
- def get_lock(cls, key: str) -> asyncio.Lock:
- return cls._locks[key]
-
-@asynccontextmanager
-async def _get_cache_lock(key: str):
- # 获取当前已持有的锁集合
- current_locks = _held_locks.get()
-
- # 如果已经持有这个锁,直接yield(锁传递)
- if key in current_locks:
- yield
- return
-
- # 否则获取锁并添加到当前上下文中
- lock = LockManager.get_lock(key)
- try:
- await lock.acquire()
- # 更新当前持有的锁集合
- new_locks = current_locks | {key}
- token = _held_locks.set(new_locks)
-
- yield
-
- finally:
- # 恢复之前的锁集合
- _held_locks.reset(token)
- if lock.locked():
- lock.release()
+from fastapi_cache.decorator import cache as original_cache
def custom_key_builder(
func: Any,
@@ -93,7 +53,6 @@ def cache(
*,
cacheName: str, # 必须提供cacheName
keyExpression: Optional[str] = None,
- jitter: int = 60, # 默认抖动60秒
):
def decorator(func):
# 预先生成key builder
@@ -105,40 +64,22 @@ def cache(
@wraps(func)
async def wrapper(*args, **kwargs):
+ if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none":
+ return await func(*args, **kwargs)
# 生成缓存键
cache_key = used_key_builder(
func=func,
- namespace=namespace,
+ namespace=str(namespace) if namespace else "",
args=args,
kwargs=kwargs
)
- # 防击穿锁
- async with _get_cache_lock(cache_key):
- backend = FastAPICache.get_backend()
-
- # 双重检查
- if (cached := await backend.get(cache_key)) is not None:
- SQLBotLogUtil.debug(f"Cache hit: {cache_key}")
- if CacheName.USER_INFO.value in cache_key:
- user = UserInfoDTO.model_validate(cached)
- SQLBotLogUtil.info(f"User cache hit: [uid: {user.id}, account: {user.account}, oid: {user.oid}]")
- return cached
-
- # 执行函数并缓存结果
- result = await func(*args, **kwargs)
-
- actual_expire = expire + random.randint(-jitter, jitter)
- if await backend.get(cache_key):
- await backend.clear(cache_key)
- await backend.set(cache_key, result, actual_expire)
-
- SQLBotLogUtil.debug(f"Cache set: {cache_key} (expire: {actual_expire}s)")
- if CacheName.USER_INFO.value in cache_key:
- user = UserInfoDTO.model_validate(result)
- SQLBotLogUtil.info(f"User cache set: [uid: {user.id}, account: {user.account}, oid: {user.oid}]")
- return result
-
+ return await original_cache(
+ expire=expire,
+ namespace=str(namespace) if namespace else "",
+ key_builder=lambda *_, **__: cache_key
+ )(func)(*args, **kwargs)
+
return wrapper
return decorator
@@ -148,30 +89,47 @@ def clear_cache(
cacheName: str,
keyExpression: Optional[str] = None,
):
- """精确清除单个缓存项的装饰器"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
+ if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none":
+ return await func(*args, **kwargs)
cache_key = custom_key_builder(
func=func,
- namespace=namespace,
+ namespace=str(namespace) if namespace else "",
args=args,
kwargs=kwargs,
cacheName=cacheName,
keyExpression=keyExpression,
)
-
- # 加锁防止竞争
- async with _get_cache_lock(cache_key):
- backend = FastAPICache.get_backend()
- result = None
- if await backend.get(cache_key):
- await backend.clear(cache_key)
- result = await func(*args, **kwargs)
- if await backend.get(cache_key):
- await backend.clear(cache_key)
- SQLBotLogUtil.info(f"Cache cleared: {cache_key}")
- return result
+ backend = FastAPICache.get_backend()
+ if await backend.get(cache_key):
+ if settings.CACHE_TYPE.lower() == "redis":
+ redis = backend.redis
+ await redis.delete(cache_key)
+ else:
+ await backend.clear(key=cache_key)
+ SQLBotLogUtil.debug(f"Cache cleared: {cache_key}")
+ return await func(*args, **kwargs)
return wrapper
- return decorator
\ No newline at end of file
+ return decorator
+
+
+def init_sqlbot_cache():
+ cache_type: str = settings.CACHE_TYPE
+ if cache_type == "memory":
+ FastAPICache.init(InMemoryBackend())
+ SQLBotLogUtil.info("SQLBot 使用内存缓存, 仅支持单进程模式")
+ elif cache_type == "redis":
+ from fastapi_cache.backends.redis import RedisBackend
+ import redis.asyncio as redis
+ from redis.asyncio.connection import ConnectionPool
+ redis_url = settings.CACHE_REDIS_URL or "redis://localhost:6379/0"
+ pool = ConnectionPool.from_url(url=redis_url)
+ redis_client = redis.Redis(connection_pool=pool)
+ FastAPICache.init(RedisBackend(redis_client), prefix="sqlbot-cache")
+ SQLBotLogUtil.info(f"SQLBot 使用Redis缓存, 可使用多进程模式")
+ else:
+ SQLBotLogUtil.warning("SQLBot 未启用缓存, 可使用多进程模式")
+
\ No newline at end of file
diff --git a/backend/main.py b/backend/main.py
index 54410660..c4571893 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -14,10 +14,9 @@ from alembic import command
from fastapi_mcp import FastApiMCP
from fastapi.staticfiles import StaticFiles
import sqlbot_xpack
-from fastapi_cache import FastAPICache
-from fastapi_cache.backends.inmemory import InMemoryBackend
from common.utils.utils import SQLBotLogUtil
+from common.core.sqlbot_cache import init_sqlbot_cache
def run_migrations():
alembic_cfg = Config("alembic.ini")
@@ -27,7 +26,7 @@ def run_migrations():
@asynccontextmanager
async def lifespan(app: FastAPI):
run_migrations()
- FastAPICache.init(InMemoryBackend())
+ init_sqlbot_cache()
init_dynamic_cors(app)
SQLBotLogUtil.info("✅ SQLBot 初始化完成")
yield
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 093e01ce..1de57be8 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -39,6 +39,7 @@ dependencies = [
"sqlbot-xpack==0.0.3.7",
"fastapi-cache2>=0.2.2",
"sqlparse>=0.5.3",
+ "redis>=6.2.0",
]
[[tool.uv.index]]
name = "default"
diff --git a/frontend/src/assets/embedded/icon_embedded.svg b/frontend/src/assets/embedded/icon_embedded.svg
new file mode 100644
index 00000000..077648fd
--- /dev/null
+++ b/frontend/src/assets/embedded/icon_embedded.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontend/src/assets/svg/menu/icon-setting.svg b/frontend/src/assets/svg/menu/icon-setting.svg
new file mode 100644
index 00000000..41335368
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon-setting.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_chat_filled.svg b/frontend/src/assets/svg/menu/icon_chat_filled.svg
new file mode 100644
index 00000000..93417643
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_chat_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_chat_outlined.svg b/frontend/src/assets/svg/menu/icon_chat_outlined.svg
new file mode 100644
index 00000000..e679fef4
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_chat_outlined.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_dashboard_filled.svg b/frontend/src/assets/svg/menu/icon_dashboard_filled.svg
new file mode 100644
index 00000000..7fd632b9
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_dashboard_filled.svg
@@ -0,0 +1,6 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_dashboard_outlined.svg b/frontend/src/assets/svg/menu/icon_dashboard_outlined.svg
new file mode 100644
index 00000000..a209c844
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_dashboard_outlined.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_database_filled.svg b/frontend/src/assets/svg/menu/icon_database_filled.svg
new file mode 100644
index 00000000..46c5d5cc
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_database_filled.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_database_outlined.svg b/frontend/src/assets/svg/menu/icon_database_outlined.svg
new file mode 100644
index 00000000..5a8f405f
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_database_outlined.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_dataset_filled.svg b/frontend/src/assets/svg/menu/icon_dataset_filled.svg
new file mode 100644
index 00000000..923fe6d6
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_dataset_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_dataset_outlined.svg b/frontend/src/assets/svg/menu/icon_dataset_outlined.svg
new file mode 100644
index 00000000..891ab45f
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_dataset_outlined.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_embedded_filled.svg b/frontend/src/assets/svg/menu/icon_embedded_filled.svg
new file mode 100644
index 00000000..70a595c8
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_embedded_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_embedded_outlined.svg b/frontend/src/assets/svg/menu/icon_embedded_outlined.svg
new file mode 100644
index 00000000..077648fd
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_embedded_outlined.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_member_filled.svg b/frontend/src/assets/svg/menu/icon_member_filled.svg
new file mode 100644
index 00000000..fded3173
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_member_filled.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_member_outlined.svg b/frontend/src/assets/svg/menu/icon_member_outlined.svg
new file mode 100644
index 00000000..0424c855
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_member_outlined.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_moments-categories_filled.svg b/frontend/src/assets/svg/menu/icon_moments-categories_filled.svg
new file mode 100644
index 00000000..7b4cde3b
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_moments-categories_filled.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_moments-categories_outlined.svg b/frontend/src/assets/svg/menu/icon_moments-categories_outlined.svg
new file mode 100644
index 00000000..88cef093
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_moments-categories_outlined.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/src/assets/svg/menu/icon_setting_filled.svg b/frontend/src/assets/svg/menu/icon_setting_filled.svg
new file mode 100644
index 00000000..8c1a1927
--- /dev/null
+++ b/frontend/src/assets/svg/menu/icon_setting_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/components/layout/MenuItem.vue b/frontend/src/components/layout/MenuItem.vue
index 31a9af05..2995ade5 100644
--- a/frontend/src/components/layout/MenuItem.vue
+++ b/frontend/src/components/layout/MenuItem.vue
@@ -1,34 +1,43 @@