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..02cd2d82 100644
--- a/backend/common/core/sqlbot_cache.py
+++ b/backend/common/core/sqlbot_cache.py
@@ -7,11 +7,9 @@ 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())
# 高效锁管理器
@@ -105,6 +103,8 @@ 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,
@@ -116,13 +116,9 @@ def cache(
# 防击穿锁
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
# 执行函数并缓存结果
@@ -134,9 +130,6 @@ def cache(
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 wrapper
@@ -152,6 +145,8 @@ def clear_cache(
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,
@@ -170,8 +165,27 @@ def clear_cache(
result = await func(*args, **kwargs)
if await backend.get(cache_key):
await backend.clear(cache_key)
- SQLBotLogUtil.info(f"Cache cleared: {cache_key}")
+ SQLBotLogUtil.debug(f"Cache cleared: {cache_key}")
return result
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/i18n/en.json b/frontend/src/i18n/en.json
index ee534d96..383cddca 100644
--- a/frontend/src/i18n/en.json
+++ b/frontend/src/i18n/en.json
@@ -32,6 +32,7 @@
"password_reset_successful": "Password reset successful"
},
"dashboard": {
+ "new_tab": "New Tab",
"length_limit64": "Field length must be between 1 and 64",
"sort_column": "Sort field",
"sort_type": "Sort type",
diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json
index 6ceb58e2..ca01a0fb 100644
--- a/frontend/src/i18n/zh-CN.json
+++ b/frontend/src/i18n/zh-CN.json
@@ -34,6 +34,7 @@
"password_reset_successful": "重置密码成功"
},
"dashboard": {
+ "new_tab": "新建Tab",
"length_limit64": "字段长度需要在1-64之间",
"sort_column": "排序字段",
"sort_type": "排序方式",
diff --git a/frontend/src/style.less b/frontend/src/style.less
index 685e6f51..4680870a 100644
--- a/frontend/src/style.less
+++ b/frontend/src/style.less
@@ -183,3 +183,11 @@ body {
.ed-dialog {
border-radius: 12px !important;
}
+
+.tox-tinymce-inline {
+ z-index: 10 !important;
+}
+
+.ed-message-box__container {
+ display: block!important;
+}
diff --git a/frontend/src/views/chat/component/charts/Line.ts b/frontend/src/views/chat/component/charts/Line.ts
index bbf0424e..d28f29b9 100644
--- a/frontend/src/views/chat/component/charts/Line.ts
+++ b/frontend/src/views/chat/component/charts/Line.ts
@@ -53,10 +53,6 @@ export class Line extends BaseG2Chart {
nice: true,
},
},
- interaction: {
- tooltip: { series: series.length > 0, shared: true },
- },
-
children: [
{
type: 'line',
diff --git a/frontend/src/views/dashboard/canvas/CanvasCore.vue b/frontend/src/views/dashboard/canvas/CanvasCore.vue
index 37b806c0..9eb1c755 100644
--- a/frontend/src/views/dashboard/canvas/CanvasCore.vue
+++ b/frontend/src/views/dashboard/canvas/CanvasCore.vue
@@ -16,7 +16,7 @@ const { t } = useI18n()
const dashboardStore = dashboardStoreWithOut()
const canvasLocked = ref(false) // Is the canvas movement locked, Default false
const emits = defineEmits(['parentAddItemBox'])
-const { curComponentId, fullscreenFlag } = storeToRefs(dashboardStore)
+const { curComponentId, curComponent, fullscreenFlag } = storeToRefs(dashboardStore)
let currentInstance: any
const props = defineProps({
canvasId: {
@@ -409,6 +409,10 @@ function removeItemById(id: number) {
const index = canvasComponentData.value.findIndex((item) => item.id === id)
if (index >= 0) {
removeItem(index)
+ renderOk.value = false
+ nextTick(() => {
+ renderOk.value = true
+ })
}
}
@@ -736,8 +740,13 @@ function containerMouseDown(e: MouseEvent) {
}
infoBox.value.startX = e.pageX
infoBox.value.startY = e.pageY
- e.preventDefault()
- e.stopPropagation()
+ // @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ if (isMainCanvas(props.canvasId) && curComponent.value?.editing) {
+ // do SQtext
+ } else {
+ e.preventDefault()
+ e.stopPropagation()
+ }
}
function getNowPosition(addSizeX: number, addSizeY: number, moveXSize: number, moveYSize: number) {
diff --git a/frontend/src/views/dashboard/canvas/CanvasShape.vue b/frontend/src/views/dashboard/canvas/CanvasShape.vue
index 3d93959f..f49de87f 100644
--- a/frontend/src/views/dashboard/canvas/CanvasShape.vue
+++ b/frontend/src/views/dashboard/canvas/CanvasShape.vue
@@ -1,6 +1,6 @@
@@ -78,7 +80,7 @@ const shapeClick = (e: MouseEvent) => {
:canvas-id="canvasId"
@enlarge-view="() => emits('enlargeView')"
>
-
+
diff --git a/frontend/src/views/dashboard/common/ResourceGroupOpt.vue b/frontend/src/views/dashboard/common/ResourceGroupOpt.vue
index 24da8906..d71353c7 100644
--- a/frontend/src/views/dashboard/common/ResourceGroupOpt.vue
+++ b/frontend/src/views/dashboard/common/ResourceGroupOpt.vue
@@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const emits = defineEmits(['finish'])
+const resource = ref(null)
const state = reactive({
id: null,
opt: null,
@@ -84,21 +85,11 @@ const resourceFormRules = ref({
name: [
{
required: true,
- message: state.placeholder,
- trigger: 'change',
- },
- {
- required: true,
- message: state.placeholder,
- trigger: 'blur',
- },
- {
min: 1,
max: 64,
message: t('dashboard.length_limit64'),
trigger: 'change',
},
- { required: true, trigger: 'blur' },
],
pid: [
{
@@ -131,23 +122,28 @@ const propsTree = {
const showPid = false
const saveResource = () => {
- const params = {
- id: state.id,
- node_type: state.nodeType,
- name: resourceForm.name,
- opt: state.opt,
- pid: resourceForm.pid,
- type: 'dashboard',
- level: state.nodeType === 'folder' ? 0 : 1,
- }
- saveDashboardResource(params, function (rsp: any) {
- const messageTips = t('common.save_success')
- ElMessage({
- type: 'success',
- message: messageTips,
- })
- emits('finish', { opt: state.opt, resourceId: rsp.id })
- resetForm()
+ // @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ resource.value?.validate((result) => {
+ if (result) {
+ const params = {
+ id: state.id,
+ node_type: state.nodeType,
+ name: resourceForm.name,
+ opt: state.opt,
+ pid: resourceForm.pid,
+ type: 'dashboard',
+ level: state.nodeType === 'folder' ? 0 : 1,
+ }
+ saveDashboardResource(params, function (rsp: any) {
+ const messageTips = t('common.save_success')
+ ElMessage({
+ type: 'success',
+ message: messageTips,
+ })
+ emits('finish', { opt: state.opt, resourceId: rsp.id })
+ resetForm()
+ })
+ }
})
}
diff --git a/frontend/src/views/dashboard/components/sq-tab/index.vue b/frontend/src/views/dashboard/components/sq-tab/index.vue
index 815ad3a7..652026b1 100644
--- a/frontend/src/views/dashboard/components/sq-tab/index.vue
+++ b/frontend/src/views/dashboard/components/sq-tab/index.vue
@@ -69,7 +69,7 @@ const state = reactive({
function addTab() {
const newTab = {
name: guid('tab'),
- title: 'New Tab',
+ title: t('dashboard.new_tab'),
componentData: [],
closable: true,
}
diff --git a/frontend/src/views/dashboard/components/sq-text/index.vue b/frontend/src/views/dashboard/components/sq-text/index.vue
index a3febbaf..6a58018e 100644
--- a/frontend/src/views/dashboard/components/sq-text/index.vue
+++ b/frontend/src/views/dashboard/components/sq-text/index.vue
@@ -124,6 +124,10 @@ onMounted(() => {
overflow-y: auto !important;
position: relative;
padding: 12px !important;
+ div::-webkit-scrollbar {
+ width: 0 !important;
+ height: 0 !important;
+ }
.rich-text-empty {
position: absolute;
width: calc(100% - 24px);
diff --git a/frontend/src/views/dashboard/components/sq-view/index.vue b/frontend/src/views/dashboard/components/sq-view/index.vue
index 0bf0ff36..f9c089c9 100644
--- a/frontend/src/views/dashboard/components/sq-view/index.vue
+++ b/frontend/src/views/dashboard/components/sq-view/index.vue
@@ -102,6 +102,10 @@ defineExpose({
background: #fff;
padding: 12px !important;
border-radius: 12px;
+ div::-webkit-scrollbar {
+ width: 0 !important;
+ height: 0 !important;
+ }
.header-bar {
height: 32px;
display: flex;
diff --git a/frontend/src/views/dashboard/editor/Toolbar.vue b/frontend/src/views/dashboard/editor/Toolbar.vue
index 572eb58d..5a6e1906 100644
--- a/frontend/src/views/dashboard/editor/Toolbar.vue
+++ b/frontend/src/views/dashboard/editor/Toolbar.vue
@@ -94,6 +94,7 @@ const editCanvasName = () => {
const closeEditCanvasName = () => {
nameEdit.value = false
if (!inputName.value || !inputName.value.trim()) {
+ ElMessage.warning(t('dashboard.length_1_64_characters'))
return
}
if (inputName.value.trim() === dashboardInfo.value.name) {
diff --git a/frontend/src/views/dashboard/editor/index.vue b/frontend/src/views/dashboard/editor/index.vue
index c70eeafd..1c8dc224 100644
--- a/frontend/src/views/dashboard/editor/index.vue
+++ b/frontend/src/views/dashboard/editor/index.vue
@@ -36,6 +36,8 @@ const addComponent = (componentType: string, viewInfo?: any) => {
const subTabName = guid('tab')
// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
component.propValue[0].name = subTabName
+ // @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ component.propValue[0].title = t('dashboard.new_tab')
component.activeTabName = subTabName
}
component.y = 100