From 329cf7445609fdee431a6a7e54bfd4ac2813acce Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Tue, 19 Aug 2025 17:58:32 +0800 Subject: [PATCH] feat(X-Pack): Page embedding feature --- .../alembic/versions/036_modify_assistant.py | 27 ++++++ backend/apps/system/api/assistant.py | 37 ++------ backend/apps/system/crud/assistant_manage.py | 39 ++++++++ backend/apps/system/models/system_model.py | 2 + backend/common/core/sqlbot_cache.py | 22 +++-- backend/common/utils/whitelist.py | 1 + backend/pyproject.toml | 2 +- frontend/src/router/watch.ts | 2 +- frontend/src/views/embedded/page.vue | 91 ++++++++++++++++++- 9 files changed, 182 insertions(+), 41 deletions(-) create mode 100644 backend/alembic/versions/036_modify_assistant.py create mode 100644 backend/apps/system/crud/assistant_manage.py diff --git a/backend/alembic/versions/036_modify_assistant.py b/backend/alembic/versions/036_modify_assistant.py new file mode 100644 index 00000000..bb6d1a3a --- /dev/null +++ b/backend/alembic/versions/036_modify_assistant.py @@ -0,0 +1,27 @@ +"""036_modify_assistant + +Revision ID: 646e7ca28e0e +Revises: 29559ee607af +Create Date: 2025-08-18 16:12:46.041413 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '646e7ca28e0e' +down_revision = '29559ee607af' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('sys_assistant', sa.Column('app_id', sa.String(255), nullable=True, comment='app_id')) + op.add_column('sys_assistant', sa.Column('app_secret', sa.String(255), nullable=True, comment='app_secret')) + + +def downgrade(): + op.drop_column('sys_assistant', 'app_id') + op.drop_column('sys_assistant', 'app_secret') diff --git a/backend/apps/system/api/assistant.py b/backend/apps/system/api/assistant.py index 88c2adcc..bd93e5d9 100644 --- a/backend/apps/system/api/assistant.py +++ b/backend/apps/system/api/assistant.py @@ -2,10 +2,11 @@ from datetime import timedelta import json import os from typing import List, Optional -from fastapi import APIRouter, FastAPI, Form, HTTPException, Query, Request, Response, UploadFile +from fastapi import APIRouter, Form, HTTPException, Query, Request, Response, UploadFile from fastapi.responses import StreamingResponse -from sqlmodel import Session, select +from sqlmodel import select from apps.system.crud.assistant import get_assistant_info +from apps.system.crud.assistant_manage import dynamic_upgrade_cors, save from apps.system.models.system_model import AssistantModel from apps.system.schemas.auth import CacheName, CacheNamespace from apps.system.schemas.system_schema import AssistantBase, AssistantDTO, AssistantUiSchema, AssistantValidator @@ -13,10 +14,11 @@ from common.core.deps import SessionDep, Trans from common.core.security import create_access_token from common.core.sqlbot_cache import clear_cache from common.utils.time import get_timestamp -from starlette.middleware.cors import CORSMiddleware + from common.core.config import settings from common.utils.utils import get_origin_from_referer from sqlbot_xpack.file_utils import SQLBotFileUtils + router = APIRouter(tags=["system/assistant"], prefix="/system/assistant") @router.get("/info/{id}") @@ -104,16 +106,12 @@ async def ui(session: SessionDep, data: str = Form(), files: List[UploadFile] = @router.get("", response_model=list[AssistantModel]) async def query(session: SessionDep): - list_result = session.exec(select(AssistantModel).order_by(AssistantModel.name, AssistantModel.create_time)).all() + list_result = session.exec(select(AssistantModel).where(AssistantModel.type.in_([0, 1])).order_by(AssistantModel.name, AssistantModel.create_time)).all() return list_result @router.post("") async def add(request: Request, session: SessionDep, creator: AssistantBase): - db_model = AssistantModel.model_validate(creator) - db_model.create_time = get_timestamp() - session.add(db_model) - session.commit() - dynamic_upgrade_cors(request=request, session=session) + save(request, session, creator) @router.put("") @@ -147,26 +145,7 @@ async def delete(request: Request, session: SessionDep, id: int): session.commit() dynamic_upgrade_cors(request=request, session=session) -def dynamic_upgrade_cors(request: Request, session: Session): - list_result = session.exec(select(AssistantModel).order_by(AssistantModel.create_time)).all() - seen = set() - unique_domains = [] - for item in list_result: - if item.domain: - for domain in item.domain.split(','): - domain = domain.strip() - if domain and domain not in seen: - seen.add(domain) - unique_domains.append(domain) - app: FastAPI = request.app - cors_middleware = None - for middleware in app.user_middleware: - if middleware.cls == CORSMiddleware: - cors_middleware = middleware - break - if cors_middleware: - updated_origins = list(set(settings.all_cors_origins + unique_domains)) - cors_middleware.kwargs['allow_origins'] = updated_origins + diff --git a/backend/apps/system/crud/assistant_manage.py b/backend/apps/system/crud/assistant_manage.py new file mode 100644 index 00000000..bc958742 --- /dev/null +++ b/backend/apps/system/crud/assistant_manage.py @@ -0,0 +1,39 @@ + + +from fastapi import FastAPI, Request +from sqlmodel import Session, select +from starlette.middleware.cors import CORSMiddleware +from apps.system.schemas.system_schema import AssistantBase +from common.core.config import settings +from apps.system.models.system_model import AssistantModel +from common.utils.time import get_timestamp + + +def dynamic_upgrade_cors(request: Request, session: Session): + list_result = session.exec(select(AssistantModel).order_by(AssistantModel.create_time)).all() + seen = set() + unique_domains = [] + for item in list_result: + if item.domain: + for domain in item.domain.split(','): + domain = domain.strip() + if domain and domain not in seen: + seen.add(domain) + unique_domains.append(domain) + app: FastAPI = request.app + cors_middleware = None + for middleware in app.user_middleware: + if middleware.cls == CORSMiddleware: + cors_middleware = middleware + break + if cors_middleware: + updated_origins = list(set(settings.all_cors_origins + unique_domains)) + cors_middleware.kwargs['allow_origins'] = updated_origins + + +async def save(request: Request, session: Session, creator: AssistantBase): + db_model = AssistantModel.model_validate(creator) + db_model.create_time = get_timestamp() + session.add(db_model) + session.commit() + dynamic_upgrade_cors(request=request, session=session) \ No newline at end of file diff --git a/backend/apps/system/models/system_model.py b/backend/apps/system/models/system_model.py index c743533d..530a4853 100644 --- a/backend/apps/system/models/system_model.py +++ b/backend/apps/system/models/system_model.py @@ -50,6 +50,8 @@ class AssistantBaseModel(SQLModel): description: Optional[str] = Field(sa_type = Text(), nullable=True) configuration: Optional[str] = Field(sa_type = Text(), nullable=True) create_time: int = Field(default=0, sa_type=BigInteger()) + app_id: Optional[str] = Field(default=None, max_length=255, nullable=True) + app_secret: Optional[str] = Field(default=None, max_length=255, nullable=True) class AssistantModel(SnowflakeBase, AssistantBaseModel, table=True): __tablename__ = "sys_assistant" diff --git a/backend/common/core/sqlbot_cache.py b/backend/common/core/sqlbot_cache.py index cce07c06..ec6d5187 100644 --- a/backend/common/core/sqlbot_cache.py +++ b/backend/common/core/sqlbot_cache.py @@ -16,7 +16,7 @@ def custom_key_builder( kwargs: Dict[str, Any], cacheName: str, keyExpression: Optional[str] = None, -) -> str: +) -> str | list[str]: try: base_key = f"{namespace}:{cacheName}:" @@ -31,6 +31,8 @@ def custom_key_builder( if match := re.match(r"args\[(\d+)\]", keyExpression): index = int(match.group(1)) value = bound_args.args[index] + if isinstance(value, list): + return [f"{base_key}{v}" for v in value] return f"{base_key}{value}" # 支持属性路径格式 @@ -38,6 +40,8 @@ def custom_key_builder( value = bound_args.arguments[parts[0]] for part in parts[1:]: value = getattr(value, part) + if isinstance(value, list): + return [f"{base_key}{v}" for v in value] return f"{base_key}{value}" # 默认使用第一个参数作为key @@ -102,14 +106,16 @@ def clear_cache( cacheName=cacheName, keyExpression=keyExpression, ) + ket_list = cache_key if isinstance(cache_key, list) else [cache_key] 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}") + for temp_cache_key in ket_list: + if await backend.get(temp_cache_key): + if settings.CACHE_TYPE.lower() == "redis": + redis = backend.redis + await redis.delete(temp_cache_key) + else: + await backend.clear(key=temp_cache_key) + SQLBotLogUtil.debug(f"Cache cleared: {temp_cache_key}") return await func(*args, **kwargs) return wrapper diff --git a/backend/common/utils/whitelist.py b/backend/common/utils/whitelist.py index 45eaece1..e8654738 100644 --- a/backend/common/utils/whitelist.py +++ b/backend/common/utils/whitelist.py @@ -31,6 +31,7 @@ wlist = [ "/system/assistant/validator*", "/system/assistant/info/*", "/system/assistant/picture/*", + "/system/embedded*", "/datasource/uploadExcel" ] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bbdf7b9d..07417102 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "pyyaml (>=6.0.2,<7.0.0)", "fastapi-mcp (>=0.3.4,<0.4.0)", "tabulate>=0.9.0", - "sqlbot-xpack==0.0.3.16", + "sqlbot-xpack==0.0.3.17", "fastapi-cache2>=0.2.2", "sqlparse>=0.5.3", "redis>=6.2.0", diff --git a/frontend/src/router/watch.ts b/frontend/src/router/watch.ts index f6f2c622..fe08edd4 100644 --- a/frontend/src/router/watch.ts +++ b/frontend/src/router/watch.ts @@ -8,7 +8,7 @@ const appearanceStore = useAppearanceStoreWithOut() const userStore = useUserStore() const { wsCache } = useCache() const whiteList = ['/login'] -const assistantWhiteList = ['/assistant'] +const assistantWhiteList = ['/assistant', '/embeddedPage'] export const watchRouter = (router: any) => { router.beforeEach(async (to: any, from: any, next: any) => { await loadXpackStatic() diff --git a/frontend/src/views/embedded/page.vue b/frontend/src/views/embedded/page.vue index 42a6cabe..4aead20e 100644 --- a/frontend/src/views/embedded/page.vue +++ b/frontend/src/views/embedded/page.vue @@ -1,13 +1,100 @@