From 0b4a765115f84cb930db8bd44aa6bf772cc67054 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Wed, 27 May 2026 11:24:17 +0800 Subject: [PATCH 001/113] fix: Advanced assistant application failed to display the application name correctly --- frontend/src/views/embedded/page.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/embedded/page.vue b/frontend/src/views/embedded/page.vue index 3889d41a0..7878d0ae6 100644 --- a/frontend/src/views/embedded/page.vue +++ b/frontend/src/views/embedded/page.vue @@ -186,7 +186,7 @@ onBeforeMount(async () => { if (rawData.logo) { logo.value = baseUrl + rawData.logo } - + rawData['name'] = rawData['name'] || res['name'] for (const key in customSet) { if ( Object.prototype.hasOwnProperty.call(customSet, key) && From faa5c9f2e02ce9e7d0b8d93fc23d982139e871ef Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Fri, 29 May 2026 15:18:52 +0800 Subject: [PATCH 002/113] perf: Lazy loading for third-party platform organization structure --- frontend/src/api/system.ts | 3 +- frontend/src/i18n/en.json | 1 + frontend/src/i18n/ko-KR.json | 1 + frontend/src/i18n/zh-CN.json | 1 + frontend/src/i18n/zh-TW.json | 1 + .../src/views/system/user/SyncUserDing.vue | 133 +++++++++++++++--- 6 files changed, 123 insertions(+), 17 deletions(-) diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index f9ed3ec10..112bd4fbc 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -27,6 +27,7 @@ export const modelApi = { query: (id: number) => request.get(`/system/aimodel/${id}`), setDefault: (id: number) => request.put(`/system/aimodel/default/${id}`), check: (data: any) => request.fetchStream('/system/aimodel/status', data), - platform: (id: number) => request.get(`/system/platform/org/${id}`), + platform: (id: number, lazy?: number, pid?: string) => + request.get(`/system/platform/org/${id}`, { params: { lazy, pid } }), userSync: (data: any) => request.post(`/system/platform/user/sync`, data), } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 3bc95e678..6fc46be93 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -38,6 +38,7 @@ "field_details": "Field Details", "integration": "Platform integration needs to be enabled.", "the_existing_user": "If the user already exists, overwrite the existing user.", + "lazy_load": "Lazy Load", "sync_users": "Sync Users", "sync_wechat_users": "Sync WeChat Users", "sync_dingtalk_users": "Sync DingTalk Users", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index f0d3cf631..a95df5f80 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -38,6 +38,7 @@ "field_details": "필드 세부 정보", "integration": "플랫폼 통합을 활성화해야 합니다.", "the_existing_user": "해당 사용자가 이미 존재하는 경우 기존 사용자를 덮어씁니다.", + "lazy_load": "지연 로딩", "sync_users": "사용자 동기화", "sync_wechat_users": "위챗 사용자 동기화", "sync_dingtalk_users": "딩톡 사용자 동기화", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index fda9d25fd..e13832170 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -38,6 +38,7 @@ "field_details": "字段详情", "integration": "需开启平台对接", "the_existing_user": "若用户已存在,覆盖旧用户", + "lazy_load": "懒加载", "sync_users": "同步用户", "sync_wechat_users": "同步企业微信用户", "sync_dingtalk_users": "同步钉钉用户", diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index 87b71cd5d..bd9fe135e 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -38,6 +38,7 @@ "field_details": "欄位詳情", "integration": "需開啟平台對接", "the_existing_user": "若使用者已存在,覆蓋舊使用者", + "lazy_load": "懶加載", "sync_users": "同步使用者", "sync_wechat_users": "同步企業微信使用者", "sync_dingtalk_users": "同步釘釘使用者", diff --git a/frontend/src/views/system/user/SyncUserDing.vue b/frontend/src/views/system/user/SyncUserDing.vue index 8ad2faef3..cf0de88ce 100644 --- a/frontend/src/views/system/user/SyncUserDing.vue +++ b/frontend/src/views/system/user/SyncUserDing.vue @@ -1,13 +1,17 @@ -
+
+ + diff --git a/frontend/src/views/system/model/Model.vue b/frontend/src/views/system/model/Model.vue index f370f3d94..ddd760a07 100644 --- a/frontend/src/views/system/model/Model.vue +++ b/frontend/src/views/system/model/Model.vue @@ -16,6 +16,7 @@ import { getModelTypeName } from '@/entity/CommonEntity.ts' import { useI18n } from 'vue-i18n' import { get_supplier } from '@/entity/supplier' import { highlightKeyword } from '@/utils/xss' +import AuthorizedWorkspaceDialogForModel from '@/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue' interface Model { name: string @@ -249,6 +250,15 @@ const deleteHandler = (item: any) => { }) } +const AuthorizedWorkspaceDialogForModelRef = ref() + +const handleAuthorizedSpace = (item: any) => { + AuthorizedWorkspaceDialogForModelRef.value.open(item.id) +} +const handleEditWorkspaceList = (item: any) => { + AuthorizedWorkspaceDialogForModelRef.value.open(item.id) +} + const clickModel = (ele: any) => { activeStep.value = 1 supplierChang(ele) @@ -394,6 +404,8 @@ const submit = (item: any) => { @edit="handleEditModel(ele)" @del="deleteHandler" @default="handleDefault(ele)" + @authorized-space="handleAuthorizedSpace(ele)" + @edit-workspace-list="handleEditWorkspaceList(ele)" > @@ -468,6 +480,7 @@ const submit = (item: any) => {
+ From a6ca7af2e98b6a4f3a0ddbc3ff687351550d1414 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Tue, 2 Jun 2026 15:20:06 +0800 Subject: [PATCH 013/113] feat(Model Configuration): Supported Authorized Space --- .../system/workspace/AuthorizedWorkspaceDialogForModel.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue b/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue index 049dffd63..d4fc32a64 100644 --- a/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue +++ b/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue @@ -200,7 +200,7 @@ const emits = defineEmits(['refresh']) const handleConfirm = () => { workspaceModelMappingUpdate( oid, - checkTableList.value.map((ele: any) => ele.id) + checkTableList.value.map((ele: any) => `${ele.id}`) ).then(() => { beforeClose() emits('refresh') From 23ca654bc4913b5ebb8915246bb8c9d7606013c2 Mon Sep 17 00:00:00 2001 From: ulleo Date: Tue, 2 Jun 2026 16:19:51 +0800 Subject: [PATCH 014/113] feat(Application): Application can configure specified model --- backend/apps/chat/task/llm.py | 10 ++- backend/apps/system/api/aimodel.py | 4 +- backend/apps/system/models/system_model.py | 68 ++++++++++++------- frontend/src/api/system.ts | 1 + frontend/src/views/system/embedded/iframe.vue | 44 ++++++------ 5 files changed, 75 insertions(+), 52 deletions(-) diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index 85d89dbaf..26af4845d 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -41,6 +41,7 @@ from apps.datasource.embedding.ds_embedding import get_ds_embedding from apps.datasource.models.datasource import CoreDatasource from apps.db.db import exec_sql, get_version, check_connection +from apps.system.crud.aimodel_manage import get_ai_model_list_by_workspace from apps.system.crud.assistant import AssistantOutDs, AssistantOutDsFactory, get_assistant_ds from apps.system.crud.parameter_manage import get_groups from apps.system.schemas.system_schema import AssistantOutDsSchema @@ -176,11 +177,16 @@ def __init__(self, session: Session, current_user: CurrentUser, chat_question: C @classmethod async def create(cls, *args, **kwargs): specialized_model_id = None + _ai_model_list = [] if args[3]: + if args[1]: + ws_id = args[1].oid + _ai_model_list = get_ai_model_list_by_workspace(args[0], ws_id) if args[3].enable_custom_model: if args[3].custom_model: - specialized_model_id = args[3].custom_model - print("use custom model: id[" + args[3].custom_model + "]") + if any(str(model.id) == str(args[3].custom_model) for model in _ai_model_list): + specialized_model_id = args[3].custom_model + print("use custom model: id[" + specialized_model_id + "]") config: LLMConfig = await get_default_config(specialized_model_id) instance = cls(*args, **kwargs, config=config) diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py index 1cd146cb0..1a198e5c8 100644 --- a/backend/apps/system/api/aimodel.py +++ b/backend/apps/system/api/aimodel.py @@ -249,11 +249,11 @@ async def update_model_ws_mapping_by_id( return [str(ws_id) for ws_id in ws_ids] -@router.get("/list_by_ws", response_model=AiModelBrief, summary=f"{PLACEHOLDER_PREFIX}system_model_query", +@router.get("/list/by_ws", response_model=List[AiModelBrief], summary=f"{PLACEHOLDER_PREFIX}system_model_query", description=f"{PLACEHOLDER_PREFIX}system_model_query") @require_permissions(permission=SqlbotPermission(role=['ws_admin'])) async def get_model_by_ws( session: SessionDep, current_user: CurrentUser ): - return get_ai_model_list_by_workspace(session, current_user.workspace_id) + return get_ai_model_list_by_workspace(session, current_user.oid) diff --git a/backend/apps/system/models/system_model.py b/backend/apps/system/models/system_model.py index 2bc52780f..6b3602329 100644 --- a/backend/apps/system/models/system_model.py +++ b/backend/apps/system/models/system_model.py @@ -1,6 +1,8 @@ - from typing import Optional + +from pydantic import field_serializer from sqlmodel import BigInteger, Field, Text, SQLModel + from common.core.models import SnowflakeBase from common.core.schemas import BaseCreatorDTO @@ -9,84 +11,98 @@ class AiModelBase: supplier: int = Field(nullable=False) name: str = Field(max_length=255, nullable=False) model_type: int = Field(nullable=False) - base_model: str = Field(max_length = 255, nullable=False) + base_model: str = Field(max_length=255, nullable=False) default_model: bool = Field(default=False, nullable=False) + class AiModelDetail(SnowflakeBase, AiModelBase, table=True): - __tablename__ = "ai_model" - api_key: str | None = Field(default=None, nullable=True, sa_type=Text()) - api_domain: str = Field(nullable=False, sa_type=Text()) - protocol: int = Field(nullable=False, default = 1) - config: str = Field(sa_type = Text()) - status: int = Field(nullable=False, default = 1) - create_time: int = Field(default=0, sa_type=BigInteger()) - + __tablename__ = "ai_model" + api_key: str | None = Field(default=None, nullable=True, sa_type=Text()) + api_domain: str = Field(nullable=False, sa_type=Text()) + protocol: int = Field(nullable=False, default=1) + config: str = Field(sa_type=Text()) + status: int = Field(nullable=False, default=1) + create_time: int = Field(default=0, sa_type=BigInteger()) + + class AiModelWorkspaceMapping(SnowflakeBase, table=True): __tablename__ = "ai_model_workspace_mapping" ai_model_id: int = Field(default=None, nullable=True, sa_type=BigInteger()) workspace_id: int = Field(default=None, nullable=True, sa_type=BigInteger()) + class AiModelBrief(SQLModel): id: int name: str default_model: bool supplier: int + @field_serializer("id") + def id_to_str(self, v: int) -> str: + return str(v) + + class WorkspaceBase(SQLModel): name: str = Field(max_length=255, nullable=False) + class WorkspaceEditor(WorkspaceBase, BaseCreatorDTO): pass - + + class WorkspaceModel(SnowflakeBase, WorkspaceBase, table=True): __tablename__ = "sys_workspace" create_time: int = Field(default=0, sa_type=BigInteger()) - + + class UserWsBaseModel(SQLModel): uid: int = Field(nullable=False, sa_type=BigInteger()) oid: int = Field(nullable=False, sa_type=BigInteger()) - weight: int = Field(default=0, nullable=False) - + weight: int = Field(default=0, nullable=False) + + class UserWsModel(SnowflakeBase, UserWsBaseModel, table=True): __tablename__ = "sys_user_ws" - + class AssistantBaseModel(SQLModel): name: str = Field(max_length=255, nullable=False) type: int = Field(nullable=False, default=0) domain: str = Field(max_length=255, nullable=False) - description: Optional[str] = Field(sa_type = Text(), nullable=True) - configuration: Optional[str] = Field(sa_type = Text(), nullable=True) + 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_id: Optional[str] = Field(default=None, max_length=255, nullable=True) app_secret: Optional[str] = Field(default=None, max_length=255, nullable=True) oid: Optional[int] = Field(nullable=True, sa_type=BigInteger(), default=1) enable_custom_model: Optional[bool] = Field(default=False, nullable=True) custom_model: Optional[str] = Field(default=None, max_length=255, nullable=True) + class AssistantModel(SnowflakeBase, AssistantBaseModel, table=True): __tablename__ = "sys_assistant" - + class AuthenticationBaseModel(SQLModel): name: str = Field(max_length=255, nullable=False) type: int = Field(nullable=False, default=0) - config: Optional[str] = Field(sa_type = Text(), nullable=True) - - + config: Optional[str] = Field(sa_type=Text(), nullable=True) + + class AuthenticationModel(SnowflakeBase, AuthenticationBaseModel, table=True): __tablename__ = "sys_authentication" create_time: Optional[int] = Field(default=0, sa_type=BigInteger()) enable: bool = Field(default=False, nullable=False) valid: bool = Field(default=False, nullable=False) - + class ApiKeyBaseModel(SQLModel): access_key: str = Field(max_length=255, nullable=False) secret_key: str = Field(max_length=255, nullable=False) create_time: int = Field(default=0, sa_type=BigInteger()) - uid: int = Field(default=0,nullable=False, sa_type=BigInteger()) + uid: int = Field(default=0, nullable=False, sa_type=BigInteger()) status: bool = Field(default=True, nullable=False) - + + class ApiKeyModel(SnowflakeBase, ApiKeyBaseModel, table=True): - __tablename__ = "sys_apikey" \ No newline at end of file + __tablename__ = "sys_apikey" diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index f854c6087..4d56fecb1 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -30,4 +30,5 @@ export const modelApi = { platform: (id: number, lazy?: number, pid?: string) => request.post(`/system/platform/org/${id}`, { lazy, pid }), userSync: (data: any) => request.post(`/system/platform/user/sync`, data), + list_by_ws: () => request.get(`/system/aimodel/list/by_ws`), } diff --git a/frontend/src/views/system/embedded/iframe.vue b/frontend/src/views/system/embedded/iframe.vue index cf2fcc8ae..292c72e1c 100644 --- a/frontend/src/views/system/embedded/iframe.vue +++ b/frontend/src/views/system/embedded/iframe.vue @@ -101,25 +101,23 @@ const dsListOptions = ref([]) const embeddedListWithSearch = computed(() => { if (!keywords.value) return embeddedList.value return embeddedList.value.filter((ele: any) => - ele.name.toLowerCase().includes(keywords.value.toLowerCase()), + ele.name.toLowerCase().includes(keywords.value.toLowerCase()) ) }) interface Model { + id: number name: string - model_type: string - base_model: string - id: string default_model: boolean supplier: number } -const modelList =ref>([]) +const modelList = ref>([]) const searchModels = () => { searchLoading.value = true modelApi - .queryAll() + .list_by_ws() .then((res: any) => { modelList.value = res }) @@ -307,8 +305,8 @@ const validateUrl = (_: any, value: any, callback: any) => { if (value === '') { callback( new Error( - t('datasource.please_enter') + t('common.empty') + t('embedded.cross_domain_settings'), - ), + t('datasource.please_enter') + t('common.empty') + t('embedded.cross_domain_settings') + ) ) } else { // var Expression = /(https?:\/\/)?([\da-z\.-]+)\.([a-z]{2,6})(:\d{1,5})?([\/\w\.-]*)*\/?(#[\S]+)?/ // eslint-disable-line @@ -352,7 +350,7 @@ const dsRules = { const validatePass = (_: any, value: any, callback: any) => { if (value === '') { callback( - new Error(t('datasource.please_enter') + t('common.empty') + t('embedded.interface_url')), + new Error(t('datasource.please_enter') + t('common.empty') + t('embedded.interface_url')) ) } else { // var Expression = /(https?:\/\/)?([\da-z\.-]+)\.([a-z]{2,6})(:\d{1,5})?([\/\w\.-]*)*\/?(#[\S]+)?/ // eslint-disable-line @@ -470,7 +468,7 @@ const saveEmbedded = () => { if (!currentEmbedded.id) { delete obj.id } - if (obj.custom_model == undefined){ + if (obj.custom_model == undefined) { obj.custom_model = '' } req(obj).then(() => { @@ -526,29 +524,29 @@ const handleEmbedded = (row: any) => { } const copyJsCode = () => { copy(jsCodeElement.value) - .then(function() { + .then(function () { ElMessage.success(t('embedded.copy_successful')) }) - .catch(function() { + .catch(function () { ElMessage.error(t('embedded.copy_failed')) }) } const copyJsCodeFull = () => { copy(jsCodeElementFull.value) - .then(function() { + .then(function () { ElMessage.success(t('embedded.copy_successful')) }) - .catch(function() { + .catch(function () { ElMessage.error(t('embedded.copy_failed')) }) } const copyCode = () => { copy(scriptElement.value) - .then(function() { + .then(function () { ElMessage.success(t('embedded.copy_successful')) }) - .catch(function() { + .catch(function () { ElMessage.error(t('embedded.copy_failed')) }) } @@ -821,13 +819,16 @@ const saveHandler = () => { - - {{t('embedded.enableCustomModel')}} + + {{ t('embedded.enableCustomModel') }} - + { /> -
@@ -1013,7 +1013,7 @@ const saveHandler = () => {
{{ t('embedded.set_data_source') }} {{ $t('embedded.open_the_query') }} + >{{ $t('embedded.open_the_query') }}
From d57cf3aee634c3387347fce29a99e6d641799db9 Mon Sep 17 00:00:00 2001 From: ulleo Date: Tue, 2 Jun 2026 16:57:53 +0800 Subject: [PATCH 015/113] feat(Application): Application can configure specified model --- backend/apps/system/api/assistant.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/apps/system/api/assistant.py b/backend/apps/system/api/assistant.py index 5db3f4032..ac583924d 100644 --- a/backend/apps/system/api/assistant.py +++ b/backend/apps/system/api/assistant.py @@ -222,6 +222,8 @@ def get_db_type(type): async def query(session: SessionDep, current_user: CurrentUser): list_result = session.exec(select(AssistantModel).where(AssistantModel.oid == current_user.oid, AssistantModel.type != 4).order_by(AssistantModel.name, AssistantModel.create_time)).all() + for model in list_result: + model.enable_custom_model = model.enable_custom_model or False return list_result From 76b5176b0e394740fd07c0d1769c0c792e8cb3b4 Mon Sep 17 00:00:00 2001 From: junjun Date: Wed, 3 Jun 2026 12:17:37 +0800 Subject: [PATCH 016/113] fix: Fix SQL injection vulnerability --- backend/apps/datasource/crud/datasource.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 372e4ee37..48ab794bf 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -328,18 +328,19 @@ def preview(session: SessionDep, current_user: CurrentUser, id: int, data: Table if fields is None or len(fields) == 0: return {"fields": [], "data": [], "sql": ''} + table = session.query(CoreTable).filter(CoreTable.id == data.table.id).first() conf = DatasourceConf(**json.loads(aes_decrypt(ds.configuration))) if ds.type != "excel" else get_engine_config() sql: str = "" if ds.type == "mysql" or ds.type == "doris" or ds.type == "starrocks" or ds.type == "hive": - sql = f"""SELECT `{"`, `".join(fields)}` FROM `{data.table.table_name}` + sql = f"""SELECT `{"`, `".join(fields)}` FROM `{table.table_name}` {where} LIMIT 100""" elif ds.type == "sqlServer": - sql = f"""SELECT TOP 100 [{"], [".join(fields)}] FROM [{conf.dbSchema}].[{data.table.table_name}] + sql = f"""SELECT TOP 100 [{"], [".join(fields)}] FROM [{conf.dbSchema}].[{table.table_name}] {where} """ elif ds.type == "pg" or ds.type == "excel" or ds.type == "redshift" or ds.type == "kingbase": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}" + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{table.table_name}" {where} LIMIT 100""" elif ds.type == "oracle": @@ -348,25 +349,25 @@ def preview(session: SessionDep, current_user: CurrentUser, id: int, data: Table # ORDER BY "{fields[0]}" # OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY""" sql = f"""SELECT * FROM - (SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}" + (SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{table.table_name}" {where} ORDER BY "{fields[0]}") WHERE ROWNUM <= 100 """ elif ds.type == "ck": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{data.table.table_name}" + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{table.table_name}" {where} LIMIT 100""" elif ds.type == "dm": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}" + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{table.table_name}" {where} LIMIT 100""" elif ds.type == "es": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{data.table.table_name}" + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{table.table_name}" {where} LIMIT 100""" elif ds.type == "sqlite": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{data.table.table_name}" + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{table.table_name}" {where} LIMIT 100""" return exec_sql(ds, sql, True) From b950664978237eb3ed697bf4d6f0d06b4a787d5b Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Wed, 3 Jun 2026 15:46:24 +0800 Subject: [PATCH 017/113] feat(Model Configuration): Supported Authorized Space --- .../system/workspace/AuthorizedWorkspaceDialogForModel.vue | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue b/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue index d4fc32a64..01cb84f52 100644 --- a/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue +++ b/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue @@ -91,10 +91,7 @@ {{ $t('common.cancel') }} - {{ - $t('common.confirm2') - }} - + {{ $t('common.confirm2') }} From 49e7bb1245d6ab7cafdc969e63b921a4d69429d9 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Wed, 3 Jun 2026 15:50:38 +0800 Subject: [PATCH 018/113] fix: Style optimization --- frontend/src/views/system/embedded/iframe.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/views/system/embedded/iframe.vue b/frontend/src/views/system/embedded/iframe.vue index 292c72e1c..ac923e7ff 100644 --- a/frontend/src/views/system/embedded/iframe.vue +++ b/frontend/src/views/system/embedded/iframe.vue @@ -1529,6 +1529,9 @@ const saveHandler = () => { display: flex; align-items: center; flex-direction: column; + &:hover { + box-shadow: 0px 6px 24px 0px #1f232914; + } .title { font-weight: 500; From 835eefcb39b595248ea006f3cfce445d2f49167e Mon Sep 17 00:00:00 2001 From: ulleo Date: Wed, 3 Jun 2026 15:52:44 +0800 Subject: [PATCH 019/113] fix: remove svg image support in upload files --- backend/apps/system/api/assistant.py | 2 +- backend/apps/system/crud/parameter_manage.py | 2 +- backend/pyproject.toml | 2 +- frontend/src/i18n/en.json | 8 ++++---- frontend/src/i18n/ko-KR.json | 8 ++++---- frontend/src/i18n/zh-CN.json | 8 ++++---- frontend/src/i18n/zh-TW.json | 8 ++++---- frontend/src/views/system/appearance/index.vue | 4 ++-- frontend/src/views/system/embedded/SetUi.vue | 4 ++-- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/backend/apps/system/api/assistant.py b/backend/apps/system/api/assistant.py index ac583924d..4dbf1aa09 100644 --- a/backend/apps/system/api/assistant.py +++ b/backend/apps/system/api/assistant.py @@ -130,7 +130,7 @@ async def ui(session: SessionDep, data: str = Form(), files: List[UploadFile] = file.filename = file_name if flag_name == 'logo' or flag_name == 'float_icon': try: - SQLBotFileUtils.check_file(file=file, file_types=[".jpg", ".png", ".svg"], + SQLBotFileUtils.check_file(file=file, file_types=[".jpg", ".jpeg", ".png"], limit_file_size=(10 * 1024 * 1024)) except ValueError as e: error_msg = str(e) diff --git a/backend/apps/system/crud/parameter_manage.py b/backend/apps/system/crud/parameter_manage.py index f80edd29c..65aaf164a 100644 --- a/backend/apps/system/crud/parameter_manage.py +++ b/backend/apps/system/crud/parameter_manage.py @@ -15,7 +15,7 @@ async def get_groups(session: SessionDep, flag: str) -> list[SysArgModel]: async def save_parameter_args(session: SessionDep, request: Request): allow_file_mapping = { - """ "test_logo": { "types": [".jpg", ".jpeg", ".png", ".svg"], "size": 5 * 1024 * 1024 } """ + """ "test_logo": { "types": [".jpg", ".jpeg", ".png"], "size": 5 * 1024 * 1024 } """ } form_data = await request.form() files = form_data.getlist("files") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bb1d0aee1..2dd9f076e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -39,7 +39,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.5.19,<0.0.6.0", + "sqlbot-xpack>=0.0.5.22,<0.0.6.0", "fastapi-cache2>=0.2.2", "sqlparse>=0.5.3", "redis>=6.2.0", diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 9f18e1864..50d54abc0 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -733,7 +733,7 @@ "display_settings": "Display Settings", "header_text_color": "Header Text Color", "app_logo": "App Logo", - "maximum_size_10mb": "Recommended size: 32 x 32, supports JPG, PNG, and SVG, maximum size: 10MB", + "maximum_size_10mb": "Recommended size: 32 x 32, supports JPG, PNG, maximum size: 10MB", "replace": "Replace", "default_icon_position": "Default Icon Position", "draggable_position": "Draggable Position", @@ -846,11 +846,11 @@ "website_logo": "Website Logo", "tab": "Tab", "replace_image": "Replace Image", - "larger_than_200kb": "Logo displayed at the top of the website: Recommended size: 48 x 48 pixels, supports JPG, PNG, and SVG, and no larger than 200KB", + "larger_than_200kb": "Logo displayed at the top of the website: Recommended size: 48 x 48 pixels, supports JPG, PNG, and no larger than 200KB", "login_logo": "System Logo", - "larger_than_200kb_de": "Logo on the right side of the login page: Recommended size: 204 x 52 pixels, supports JPG, PNG, and SVG, and no larger than 200KB", + "larger_than_200kb_de": "Logo on the right side of the login page: Recommended size: 204 x 52 pixels, supports JPG, PNG, and no larger than 200KB", "login_background_image": "Login Background Image", - "larger_than_5mb": "Background image on the left: Recommended size: 576 x 900 for vector images, 1152 x 1800 for bitmap images, supports JPG, PNG, and SVG, and no larger than 5MB", + "larger_than_5mb": "Background image on the left: Recommended size: 576 x 900 for vector images, 1152 x 1800 for bitmap images, supports JPG, PNG, and no larger than 5MB", "website_name": "Website Name", "on_webpage_tabs": "Platform name displayed on webpage tabs", "welcome_message": "Display a welcome message", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 046da13f5..63542c4ef 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -733,7 +733,7 @@ "display_settings": "표시 설정", "header_text_color": "헤더 텍스트 색상", "app_logo": "애플리케이션 로고", - "maximum_size_10mb": "권장 크기 32 x 32, JPG, PNG, SVG 지원, 크기 10MB 이하", + "maximum_size_10mb": "권장 크기 32 x 32, JPG, PNG 지원, 크기 10MB 이하", "replace": "교체", "default_icon_position": "아이콘 기본 위치", "draggable_position": "드래그 가능한 위치", @@ -846,11 +846,11 @@ "website_logo": "웹사이트 로고", "tab": "페이지 탭", "replace_image": "이미지 교체", - "larger_than_200kb": "상단 웹사이트에 표시되는 로고, 권장 크기 48 x 48, JPG, PNG, SVG 지원, 크기 200KB 이하", + "larger_than_200kb": "상단 웹사이트에 표시되는 로고, 권장 크기 48 x 48, JPG, PNG 지원, 크기 200KB 이하", "login_logo": "시스템 로고", - "larger_than_200kb_de": "로그인 페이지 오른쪽 로고, 권장 크기 204*52, JPG, PNG, SVG 지원, 크기 200KB 이하", + "larger_than_200kb_de": "로그인 페이지 오른쪽 로고, 권장 크기 204*52, JPG, PNG 지원, 크기 200KB 이하", "login_background_image": "로그인 배경 이미지", - "larger_than_5mb": "왼쪽 배경 이미지, 벡터 이미지 권장 크기 576*900, 비트맵 권장 크기 1152*1800; JPG, PNG, SVG 지원, 크기 5MB 이하", + "larger_than_5mb": "왼쪽 배경 이미지, 벡터 이미지 권장 크기 576*900, 비트맵 권장 크기 1152*1800; JPG, PNG 지원, 크기 5MB 이하", "website_name": "웹사이트 이름", "on_webpage_tabs": "웹페이지 탭에 표시되는 플랫폼 이름", "welcome_message": "환영 메시지 표시", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index eebaceb38..61498a89f 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -733,7 +733,7 @@ "display_settings": "显示设置", "header_text_color": "头部文本颜色", "app_logo": "应用 Logo", - "maximum_size_10mb": "建议尺寸 32 x 32,支持 JPG、PNG、SVG,大小不超过 10MB", + "maximum_size_10mb": "建议尺寸 32 x 32,支持 JPG、PNG,大小不超过 10MB", "replace": "替换", "default_icon_position": "图标默认位置", "draggable_position": "可拖拽位置", @@ -846,11 +846,11 @@ "website_logo": "网站 Logo", "tab": "页签", "replace_image": "替换图片", - "larger_than_200kb": "顶部网站显示的 Logo,建议尺寸 48 x 48,支持 JPG、PNG、SVG,大小不超过 200KB", + "larger_than_200kb": "顶部网站显示的 Logo,建议尺寸 48 x 48,支持 JPG、PNG,大小不超过 200KB", "login_logo": "系统 Logo", - "larger_than_200kb_de": "登录页面右侧 Logo,建议尺寸 204*52,支持 JPG、PNG、SVG,大小不超过 200KB", + "larger_than_200kb_de": "登录页面右侧 Logo,建议尺寸 204*52,支持 JPG、PNG,大小不超过 200KB", "login_background_image": "登录背景图", - "larger_than_5mb": "左侧背景图,矢量图建议尺寸 576*900,位图建议尺寸 1152*1800;支持 JPG、PNG、SVG,大小不超过 5M", + "larger_than_5mb": "左侧背景图,矢量图建议尺寸 576*900,位图建议尺寸 1152*1800;支持 JPG、PNG,大小不超过 5M", "website_name": "网站名称", "on_webpage_tabs": "显示在网页 Tab 的平台名称", "welcome_message": "显示欢迎语", diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index c53114cae..f851e9f43 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -733,7 +733,7 @@ "display_settings": "顯示設定", "header_text_color": "頭部文字顏色", "app_logo": "應用 Logo", - "maximum_size_10mb": "建議尺寸 32 x 32,支援 JPG、PNG、SVG,大小不超過 10MB", + "maximum_size_10mb": "建議尺寸 32 x 32,支援 JPG、PNG,大小不超過 10MB", "replace": "取代", "default_icon_position": "圖示預設位置", "draggable_position": "可拖曳位置", @@ -846,11 +846,11 @@ "website_logo": "網站 Logo", "tab": "頁籤", "replace_image": "取代圖片", - "larger_than_200kb": "頂部網站顯示的 Logo,建議尺寸 48 x 48,支援 JPG、PNG、SVG,大小不超過 200KB", + "larger_than_200kb": "頂部網站顯示的 Logo,建議尺寸 48 x 48,支援 JPG、PNG,大小不超過 200KB", "login_logo": "系統 Logo", - "larger_than_200kb_de": "登入頁面右側 Logo,建議尺寸 204*52,支援 JPG、PNG、SVG,大小不超過 200KB", + "larger_than_200kb_de": "登入頁面右側 Logo,建議尺寸 204*52,支援 JPG、PNG,大小不超過 200KB", "login_background_image": "登入背景圖", - "larger_than_5mb": "左側背景圖,向量圖建議尺寸 576*900,點陣圖建議尺寸 1152*1800;支援 JPG、PNG、SVG,大小不超過 5M", + "larger_than_5mb": "左側背景圖,向量圖建議尺寸 576*900,點陣圖建議尺寸 1152*1800;支援 JPG、PNG,大小不超過 5M", "website_name": "網站名稱", "on_webpage_tabs": "顯示在網頁 Tab 的平台名稱", "welcome_message": "顯示歡迎語", diff --git a/frontend/src/views/system/appearance/index.vue b/frontend/src/views/system/appearance/index.vue index 4c574c155..29fe967d3 100644 --- a/frontend/src/views/system/appearance/index.vue +++ b/frontend/src/views/system/appearance/index.vue @@ -89,7 +89,7 @@ :name="ele.type" :show-file-list="false" class="upload-demo" - accept=".jpeg,.jpg,.png,.gif,.svg" + accept=".jpeg,.jpg,.png" :before-upload="(e: any) => beforeUpload(e, ele)" :http-request="uploadImg" > @@ -378,7 +378,7 @@ const defaultTopForm = { showDoc: '0', showAbout: '0', pc_welcome: t('embedded.i_am_sqlbot'), - pc_welcome_desc: t("qa.hint_description"), + pc_welcome_desc: t('qa.hint_description'), } const topForm = reactive<{ diff --git a/frontend/src/views/system/embedded/SetUi.vue b/frontend/src/views/system/embedded/SetUi.vue index c22cc1728..9d11a8dcf 100644 --- a/frontend/src/views/system/embedded/SetUi.vue +++ b/frontend/src/views/system/embedded/SetUi.vue @@ -280,7 +280,7 @@ defineExpose({ @@ -296,7 +296,7 @@ defineExpose({ From db6cfc2139c856c2163d0905a2e88d3af8141a97 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Wed, 3 Jun 2026 16:03:05 +0800 Subject: [PATCH 020/113] feat(Model Configuration): Supported Authorized Space --- frontend/src/i18n/en.json | 1 + frontend/src/i18n/ko-KR.json | 1 + frontend/src/i18n/zh-CN.json | 1 + frontend/src/i18n/zh-TW.json | 1 + .../system/workspace/AuthorizedWorkspaceDialogForModel.vue | 6 +++--- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 50d54abc0..56de1e23b 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -582,6 +582,7 @@ "member_feng_yibudao": "Do you want to remove the member: {msg}?", "select_member": "Select member", "selected_2_people": "Selected: {msg} people", + "selected_number": "Selected: {msg} items", "clear": "Clear", "historical_dialogue": "No historical dialogue", "rename_a_workspace": "Rename a workspace", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 63542c4ef..8ba552ed0 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -582,6 +582,7 @@ "member_feng_yibudao": "멤버를 제거하시겠습니까: {msg}?", "select_member": "멤버 선택", "selected_2_people": "선택됨: {msg}명", + "selected_number": "선택됨: {msg}개", "clear": "지우기", "historical_dialogue": "과거 대화가 없습니다", "rename_a_workspace": "작업 공간 이름 바꾸기", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 61498a89f..1222f067d 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -582,6 +582,7 @@ "member_feng_yibudao": "是否移除成员:{msg}?", "select_member": "选择成员", "selected_2_people": "已选:{msg} 人", + "selected_number": "已选:{msg} 个", "clear": "清空", "historical_dialogue": "暂无历史对话", "rename_a_workspace": "重命名工作空间", diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index f851e9f43..92cf89c0f 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -582,6 +582,7 @@ "member_feng_yibudao": "是否移除成員:{msg}?", "select_member": "選擇成員", "selected_2_people": "已選:{msg} 人", + "selected_number": "已選:{msg} 個", "clear": "清空", "historical_dialogue": "暫無歷史對話", "rename_a_workspace": "重新命名工作區", diff --git a/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue b/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue index 01cb84f52..301f5b6a5 100644 --- a/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue +++ b/frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue @@ -59,7 +59,7 @@
- {{ $t('workspace.selected_2_people', { msg: checkTableList.length }) }} + {{ $t('workspace.selected_number', { msg: checkTableList.length }) }} @@ -183,11 +183,11 @@ const open = async (id: any) => { : [] checkedWorkspace.value = workspaceMappingResult.status === 'fulfilled' - ? workspace.value.filter((ele: any) => workspaceMappingResult.value.includes(ele.id)) + ? workspace.value.filter((ele: any) => workspaceMappingResult.value.includes(`${ele.id}`)) : [] handleCheckedWorkspaceChange( workspaceMappingResult.status === 'fulfilled' - ? workspace.value.filter((ele: any) => workspaceMappingResult.value.includes(ele.id)) + ? workspace.value.filter((ele: any) => workspaceMappingResult.value.includes(`${ele.id}`)) : [] ) loading.value = false From c0802dbadc993fecd1126a9ca1b0b49bea66aec1 Mon Sep 17 00:00:00 2001 From: xuwei-fit2cloud Date: Wed, 3 Jun 2026 16:22:02 +0800 Subject: [PATCH 021/113] Update contact QR code in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9451714e1..c5a258844 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ docker run -d \ 如你有更多问题,可以加入我们的技术交流群与我们交流。 -contact_me_qr +contact_me_qr ## UI 展示 From f691b3e03ab1014561933290cda84986a5f814e6 Mon Sep 17 00:00:00 2001 From: ulleo Date: Wed, 3 Jun 2026 17:28:08 +0800 Subject: [PATCH 022/113] feat(Application): Application can configure specified model --- backend/apps/swagger/locales/en.json | 5 +++++ backend/apps/swagger/locales/zh.json | 5 +++++ backend/apps/system/api/aimodel.py | 12 ++++++------ backend/apps/system/schemas/system_schema.py | 4 ++-- frontend/src/views/system/embedded/Page.vue | 2 ++ 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/apps/swagger/locales/en.json b/backend/apps/swagger/locales/en.json index 512f7d2cc..2ac5bf16b 100644 --- a/backend/apps/swagger/locales/en.json +++ b/backend/apps/swagger/locales/en.json @@ -92,6 +92,11 @@ "system_model_create": "Save Model", "system_model_update": "Update Model", "system_model_del": "Delete Model", + "enable_custom_model": "Enable Custom Model", + "custom_model": "Custom Model", + "system_model_ws_mapping": "Query Model-Workspace Authorization Relationships", + "system_model_ws_mapping_update": "Update Model-Workspace Authorization Relationships", + "system_model_list_by_ws": "Get Model List by Workspace", "model_name": "Name", "model_type": "Type", "base_model": "Base Model", diff --git a/backend/apps/swagger/locales/zh.json b/backend/apps/swagger/locales/zh.json index b9552c4d7..4acf45050 100644 --- a/backend/apps/swagger/locales/zh.json +++ b/backend/apps/swagger/locales/zh.json @@ -92,6 +92,11 @@ "system_model_create": "保存模型", "system_model_update": "更新模型", "system_model_del": "删除模型", + "enable_custom_model": "启用自定义模型", + "custom_model": "自定义模型", + "system_model_ws_mapping": "查询模型授权工作空间的关联关系", + "system_model_ws_mapping_update": "更新模型授权工作空间的关联关系", + "system_model_list_by_ws": "根据工作空间获取模型列表", "model_name": "名称", "model_type": "类型", "base_model": "基础模型", diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py index 1a198e5c8..1c69333b6 100644 --- a/backend/apps/system/api/aimodel.py +++ b/backend/apps/system/api/aimodel.py @@ -192,8 +192,8 @@ async def delete_model( session.commit() -@router.get("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_query", - description=f"{PLACEHOLDER_PREFIX}system_model_query") +@router.get("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping", + description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def get_model_ws_mapping_by_id( session: SessionDep, @@ -214,8 +214,8 @@ async def get_model_ws_mapping_by_id( return [str(ws_id) for ws_id in ws_ids] -@router.put("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_query", - description=f"{PLACEHOLDER_PREFIX}system_model_query") +@router.put("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_update", + description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_update") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def update_model_ws_mapping_by_id( session: SessionDep, @@ -249,8 +249,8 @@ async def update_model_ws_mapping_by_id( return [str(ws_id) for ws_id in ws_ids] -@router.get("/list/by_ws", response_model=List[AiModelBrief], summary=f"{PLACEHOLDER_PREFIX}system_model_query", - description=f"{PLACEHOLDER_PREFIX}system_model_query") +@router.get("/list/by_ws", response_model=List[AiModelBrief], summary=f"{PLACEHOLDER_PREFIX}system_model_list_by_ws", + description=f"{PLACEHOLDER_PREFIX}system_model_list_by_ws") @require_permissions(permission=SqlbotPermission(role=['ws_admin'])) async def get_model_by_ws( session: SessionDep, diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index 75eebc39c..90185ba98 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -111,8 +111,8 @@ class AssistantBase(BaseModel): configuration: Optional[str] = Field(default=None, description=f"{PLACEHOLDER_PREFIX}assistant_configuration") description: Optional[str] = Field(default=None, description=f"{PLACEHOLDER_PREFIX}assistant_description") oid: Optional[int] = Field(default=1, description=f"{PLACEHOLDER_PREFIX}oid") - enable_custom_model: Optional[bool] = Field(default=False, description=f"{PLACEHOLDER_PREFIX}oid") - custom_model: Optional[str] = Field(description=f"{PLACEHOLDER_PREFIX}oid") + enable_custom_model: Optional[bool] = Field(default=False, description=f"{PLACEHOLDER_PREFIX}enable_custom_model") + custom_model: Optional[str] = Field(description=f"{PLACEHOLDER_PREFIX}custom_model") class AssistantDTO(AssistantBase, BaseCreatorDTO): diff --git a/frontend/src/views/system/embedded/Page.vue b/frontend/src/views/system/embedded/Page.vue index 3492f1ca6..2fd33201d 100644 --- a/frontend/src/views/system/embedded/Page.vue +++ b/frontend/src/views/system/embedded/Page.vue @@ -65,6 +65,8 @@ const defaultForm = { type: 4, configuration: null, description: null, + enable_custom_model: false, + custom_model: null, } const pageForm = ref
(cloneDeep(defaultForm)) From 2e1b6fb37f421aefd23d46586f3dc21d3bdb6f67 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Thu, 4 Jun 2026 10:19:45 +0800 Subject: [PATCH 023/113] fix: Style optimization --- frontend/src/i18n/en.json | 3 +++ frontend/src/i18n/ko-KR.json | 3 +++ frontend/src/i18n/zh-CN.json | 3 +++ frontend/src/i18n/zh-TW.json | 3 +++ frontend/src/views/system/embedded/iframe.vue | 22 +++++++++++++++---- frontend/src/views/system/model/Card.vue | 5 +++-- frontend/src/views/system/model/Model.vue | 2 ++ 7 files changed, 35 insertions(+), 6 deletions(-) diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 56de1e23b..db1116ac2 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -684,6 +684,9 @@ "application_description": "Application description", "cross_domain_settings": "Cross-domain settings", "enableCustomModel": "Use specified model", + "useModel": "Use model", + "defaultModel": "Default model", + "customModel": "Specified model", "third_party_address": "Please enter the embedded third party address,multiple items separated by semicolons", "set_to_private": "Set as private", "set_to_public": "Set as public", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 8ba552ed0..322e50dee 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -684,6 +684,9 @@ "application_description": "애플리케이션 설명", "cross_domain_settings": "교차 도메인 설정", "enableCustomModel": "지정된 모델 사용", + "useModel": "사용 모델", + "defaultModel": "기본 모델", + "customModel": "지정 모델", "third_party_address": "임베디드할 제3자 주소를 입력하십시오, 여러 항목을 세미콜론으로 구분", "set_to_private": "비공개로 설정", "set_to_public": "공개로 설정", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 1222f067d..f0b229163 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -684,6 +684,9 @@ "application_description": "应用描述", "cross_domain_settings": "跨域设置", "enableCustomModel": "使用指定大模型", + "useModel": "使用模型", + "defaultModel": "默认模型", + "customModel": "指定模型", "third_party_address": "请输入嵌入的第三方地址,多个以分号分割", "set_to_private": "设为私有", "set_to_public": "设为公共", diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index 92cf89c0f..b57b93e69 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -684,6 +684,9 @@ "application_description": "應用描述", "cross_domain_settings": "跨網域設定", "enableCustomModel": "使用指定模型", + "useModel": "使用模型", + "defaultModel": "預設模型", + "customModel": "指定模型", "third_party_address": "請輸入嵌入的第三方位址,多個以分號分割", "set_to_private": "設為私有", "set_to_public": "設為公共", diff --git a/frontend/src/views/system/embedded/iframe.vue b/frontend/src/views/system/embedded/iframe.vue index ac923e7ff..2191f11df 100644 --- a/frontend/src/views/system/embedded/iframe.vue +++ b/frontend/src/views/system/embedded/iframe.vue @@ -336,6 +336,18 @@ const rules = { trigger: 'blur', }, ], + custom_model: [ + { + validator: (_: any, value: any, callback: any) => { + if (currentEmbedded.enable_custom_model && !value) { + callback(new Error(t('datasource.please_enter') + t('common.empty') + t('modelType.llm'))) + } else { + callback() + } + }, + trigger: 'change', + }, + ], } const dsRules = { @@ -818,16 +830,18 @@ const saveHandler = () => { /> - - - {{ t('embedded.enableCustomModel') }} - + + + {{ t('embedded.defaultModel') }} + {{ t('embedded.customModel') }} + {{ $t('model.basic_model') }} {{ baseModel }}
-
+
{{ $t('authorized_space.authorized_space') }} {{ $t('permission.2', { msg: num }) }} .card { width: 100%; - height: 176px; + height: 206px; border: 1px solid #dee0e3; padding: 16px; border-radius: 12px; diff --git a/frontend/src/views/system/model/Model.vue b/frontend/src/views/system/model/Model.vue index ddd760a07..a760d0834 100644 --- a/frontend/src/views/system/model/Model.vue +++ b/frontend/src/views/system/model/Model.vue @@ -19,6 +19,7 @@ import { highlightKeyword } from '@/utils/xss' import AuthorizedWorkspaceDialogForModel from '@/views/system/workspace/AuthorizedWorkspaceDialogForModel.vue' interface Model { + ws_mapping_count: number | undefined name: string model_type: string base_model: string @@ -397,6 +398,7 @@ const submit = (item: any) => { :ref="(el: any) => setCardRef(el, index)" :key="ele.id" :name="ele.name" + :num="ele.ws_mapping_count" :supplier="ele.supplier" :model-type="getModelTypeName(ele['model_type'])" :base-model="ele['base_model']" From 3f3dc08a6711317e3976a19e72c404669a849565 Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 4 Jun 2026 10:48:36 +0800 Subject: [PATCH 024/113] feat(Model Configuration): Supported Authorized Space --- backend/apps/system/api/aimodel.py | 29 ++++++++++++++----- .../apps/system/schemas/ai_model_schema.py | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py index 1c69333b6..9bd470edc 100644 --- a/backend/apps/system/api/aimodel.py +++ b/backend/apps/system/api/aimodel.py @@ -90,13 +90,28 @@ async def query( session: SessionDep, keyword: Union[str, None] = Query(default=None, max_length=255, description=f"{PLACEHOLDER_PREFIX}keyword") ): - statement = select(AiModelDetail.id, - AiModelDetail.name, - AiModelDetail.model_type, - AiModelDetail.base_model, - AiModelDetail.supplier, - AiModelDetail.protocol, - AiModelDetail.default_model) + # 子查询:统计每个 model 绑定的 workspace 数量 + count_sub = ( + select( + AiModelWorkspaceMapping.ai_model_id, + func.count().label("ws_mapping_count") + ) + .group_by(AiModelWorkspaceMapping.ai_model_id) + .subquery() + ) + statement = ( + select( + AiModelDetail.id, + AiModelDetail.name, + AiModelDetail.model_type, + AiModelDetail.base_model, + AiModelDetail.supplier, + AiModelDetail.protocol, + AiModelDetail.default_model, + func.coalesce(count_sub.c.ws_mapping_count, 0).label("ws_mapping_count"), + ) + .outerjoin(count_sub, AiModelDetail.id == count_sub.c.ai_model_id) + ) if keyword is not None: statement = statement.where(AiModelDetail.name.like(f"%{keyword}%")) statement = statement.order_by(AiModelDetail.default_model.desc(), AiModelDetail.name, AiModelDetail.create_time) diff --git a/backend/apps/system/schemas/ai_model_schema.py b/backend/apps/system/schemas/ai_model_schema.py index 019aa358b..7f523eb93 100644 --- a/backend/apps/system/schemas/ai_model_schema.py +++ b/backend/apps/system/schemas/ai_model_schema.py @@ -14,7 +14,7 @@ class AiModelItem(BaseModel): default_model: bool = Field(default=False, description=f"{PLACEHOLDER_PREFIX}default_model") class AiModelGridItem(AiModelItem, BaseCreatorDTO): - pass + ws_mapping_count: int = Field(default=0, description="workspace mapping count") class AiModelConfigItem(BaseModel): key: str = Field(description=f"{PLACEHOLDER_PREFIX}arg_name") From bcd758d029f82339731f077bef4246c89826e82e Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 4 Jun 2026 11:33:18 +0800 Subject: [PATCH 025/113] fix: remove sqlite datasource --- frontend/src/views/ds/js/ds-type.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/views/ds/js/ds-type.ts b/frontend/src/views/ds/js/ds-type.ts index b2d578b74..3ccd6deb0 100644 --- a/frontend/src/views/ds/js/ds-type.ts +++ b/frontend/src/views/ds/js/ds-type.ts @@ -10,7 +10,6 @@ import redshift from '@/assets/datasource/icon_redshift.png' import es from '@/assets/datasource/icon_es.png' import kingbase from '@/assets/datasource/icon_kingbase.png' import starrocks from '@/assets/datasource/icon_starrocks.png' -import sqlite_icon from '@/assets/datasource/icon_sqlite.png' import hive_icon from '@/assets/datasource/icon_hive.png' import { i18n } from '@/i18n' @@ -28,7 +27,6 @@ export const dsType = [ { label: 'Elasticsearch', value: 'es' }, { label: 'Kingbase', value: 'kingbase' }, { label: 'StarRocks', value: 'starrocks' }, - { label: 'SQLite', value: 'sqlite' }, { label: 'Apache Hive', value: 'hive' }, ] @@ -45,7 +43,6 @@ export const dsTypeWithImg = [ { name: 'Elasticsearch', type: 'es', img: es }, { name: 'Kingbase', type: 'kingbase', img: kingbase }, { name: 'StarRocks', type: 'starrocks', img: starrocks }, - { name: 'SQLite', type: 'sqlite', img: sqlite_icon }, { name: 'Apache Hive', type: 'hive', img: hive_icon }, ] From 348346d97b50a5b041b5b4d55e70a4e91e59f3ea Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 4 Jun 2026 11:40:31 +0800 Subject: [PATCH 026/113] fix: remove sqlite datasource --- backend/apps/datasource/crud/datasource.py | 4 ---- backend/apps/db/constant.py | 1 - backend/apps/db/db.py | 11 +---------- backend/apps/db/db_sql.py | 10 ---------- 4 files changed, 1 insertion(+), 25 deletions(-) diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 48ab794bf..acd7522e5 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -366,10 +366,6 @@ def preview(session: SessionDep, current_user: CurrentUser, id: int, data: Table sql = f"""SELECT "{'", "'.join(fields)}" FROM "{table.table_name}" {where} LIMIT 100""" - elif ds.type == "sqlite": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{table.table_name}" - {where} - LIMIT 100""" return exec_sql(ds, sql, True) diff --git a/backend/apps/db/constant.py b/backend/apps/db/constant.py index 6ee33f02f..dcec81901 100644 --- a/backend/apps/db/constant.py +++ b/backend/apps/db/constant.py @@ -28,7 +28,6 @@ class DB(Enum): oracle = ('oracle', 'Oracle', '"', '"', ConnectType.sqlalchemy, 'Oracle', []) pg = ('pg', 'PostgreSQL', '"', '"', ConnectType.sqlalchemy, 'PostgreSQL', []) starrocks = ('starrocks', 'StarRocks', '`', '`', ConnectType.py_driver, 'StarRocks', []) - sqlite = ('sqlite', 'SQLite', '"', '"', ConnectType.sqlalchemy, 'SQLite', []) hive = ('hive', 'Apache Hive', '`', '`', ConnectType.py_driver, 'Hive', []) def __init__(self, type, db_name, prefix, suffix, connect_type: ConnectType, template_name: str, diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index b87ff7891..d8127f36e 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -90,8 +90,6 @@ def get_uri_from_config(type: str, conf: DatasourceConf) -> str: db_url = f"clickhouse+http://{urllib.parse.quote(conf.username)}:{urllib.parse.quote(conf.password)}@{conf.host}:{conf.port}/{conf.database}?{conf.extraJdbc}" else: db_url = f"clickhouse+http://{urllib.parse.quote(conf.username)}:{urllib.parse.quote(conf.password)}@{conf.host}:{conf.port}/{conf.database}" - elif equals_ignore_case(type, "sqlite"): - db_url = f"sqlite:///{conf.filename}" else: raise 'The datasource type not support.' return db_url @@ -162,8 +160,6 @@ def get_engine(ds: CoreDatasource, timeout: int = 0) -> Engine: ssl_mode = {"require": True} if conf.ssl else None engine = create_engine(get_uri(ds), connect_args={"connect_timeout": conf.timeout, "ssl": ssl_mode}, poolclass=NullPool) - elif equals_ignore_case(ds.type, 'sqlite'): - engine = create_engine(get_uri(ds), connect_args={"check_same_thread": False}, poolclass=NullPool) else: # ck engine = create_engine(get_uri(ds), connect_args={"connect_timeout": conf.timeout}, poolclass=NullPool) return engine @@ -357,8 +353,6 @@ def get_schema(ds: CoreDatasource): elif equals_ignore_case(ds.type, "oracle"): sql = """select * from all_users""" - elif equals_ignore_case(ds.type, "sqlite"): - return ['main'] with session.execute(text(sql)) as result: res = result.fetchall() res_list = [item[0] for item in res] @@ -464,10 +458,7 @@ def get_fields(ds: CoreDatasource, table_name: str = None): with get_session(ds) as session: with session.execute(text(sql), {"param1": p1, "param2": p2}) as result: res = result.fetchall() - if equals_ignore_case(ds.type, "sqlite"): - res_list = [ColumnSchema(item[1], item[2], '') for item in res] - else: - res_list = [ColumnSchema(*item) for item in res] + res_list = [ColumnSchema(*item) for item in res] return res_list else: extra_config_dict = get_extra_config(conf) diff --git a/backend/apps/db/db_sql.py b/backend/apps/db/db_sql.py index fa790c8a8..496075378 100644 --- a/backend/apps/db/db_sql.py +++ b/backend/apps/db/db_sql.py @@ -162,13 +162,6 @@ def get_table_sql(ds: CoreDatasource, conf: DatasourceConf, db_version: str = '' """, conf.dbSchema elif equals_ignore_case(ds.type, "es"): return "", None - elif equals_ignore_case(ds.type, "sqlite"): - return """ - SELECT name AS TABLE_NAME, '' - FROM sqlite_master - WHERE type='table' - ORDER BY name - """, None elif equals_ignore_case(ds.type, "hive"): return """ SHOW TABLES @@ -323,9 +316,6 @@ def get_field_sql(ds: CoreDatasource, conf: DatasourceConf, table_name: str = No return sql1 + sql2, conf.dbSchema, table_name elif equals_ignore_case(ds.type, "es"): return "", None, None - elif equals_ignore_case(ds.type, "sqlite"): - sql1 = f"PRAGMA table_info({table_name})" - return sql1, None, None elif equals_ignore_case(ds.type, "hive"): sql1 = f"DESCRIBE {table_name}" return sql1, None, None From bd5605b68129ef82ae32990a79231b917a58156f Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 4 Jun 2026 11:40:58 +0800 Subject: [PATCH 027/113] fix: remove sqlite datasource --- backend/templates/sql_examples/SQLite.yaml | 81 ---------------------- 1 file changed, 81 deletions(-) delete mode 100644 backend/templates/sql_examples/SQLite.yaml diff --git a/backend/templates/sql_examples/SQLite.yaml b/backend/templates/sql_examples/SQLite.yaml deleted file mode 100644 index bfcaacc94..000000000 --- a/backend/templates/sql_examples/SQLite.yaml +++ /dev/null @@ -1,81 +0,0 @@ -template: - quot_rule: | - - 必须对数据库名、表名、字段名、别名外层加双引号(")。 - - 1. 点号(.)不能包含在引号内,必须写成 "table" - 2. 即使标识符不含特殊字符或非关键字,也需强制加双引号 - - - - limit_rule: | - - 当需要限制行数时,必须使用标准的LIMIT语法 - - - other_rule: | - 必须为每个表生成别名(不加AS) - {multi_table_condition} - 禁止使用星号(*),必须明确字段名 - 中文/特殊字符字段需保留原名并添加英文别名 - 函数字段必须加别名 - 百分比字段保留两位小数并以%结尾 - 避免与数据库关键字冲突 - - basic_example: | - - - 📌 以下示例严格遵循中的 SQLite 规范,展示符合要求的 SQL 写法与典型错误案例。 - ⚠️ 注意:示例中的表名、字段名均为演示虚构,实际使用时需替换为用户提供的真实标识符。 - 🔍 重点观察: - 1. 双引号包裹所有数据库对象的规范用法 - 2. 中英别名/百分比/函数等特殊字段的处理 - 3. 关键字冲突的规避方式 - - - 查询 ORDERS 表的前100条订单(含中文字段和百分比) - - SELECT * FROM ORDERS LIMIT 100 -- 错误:未加引号、使用星号 - SELECT "订单ID", "金额" FROM "ORDERS" "t1" LIMIT 100 -- 错误:缺少英文别名 - SELECT COUNT("订单ID") FROM "ORDERS" "t1" -- 错误:函数未加别名 - - - SELECT - "t1"."订单ID" AS "order_id", - "t1"."金额" AS "amount", - COUNT("t1"."订单ID") AS "total_orders", - ROUND("t1"."折扣率" * 100, 2) || '%' AS "discount_percent" - FROM "ORDERS" "t1" - LIMIT 100 - - - - - 统计用户表 USERS(含关键字字段user)的活跃占比 - - SELECT user, status FROM USERS -- 错误:未处理关键字和引号 - SELECT "user", ROUND(active_ratio) FROM "USERS" -- 错误:百分比格式错误 - - - SELECT - "u"."user" AS "username", - ROUND("u"."active_ratio" * 100, 2) || '%' AS "active_percent" - FROM "USERS" "u" - WHERE "u"."status" = 1 - - - - - example_engine: SQLite 3.x - example_answer_1: | - {"success":true,"sql":"SELECT \"country_name\", \"continent_name\", \"year\", \"gdp\" FROM \"sample_country_gdp\" ORDER BY \"country_name\", \"year\"","tables":["sample_country_gdp"],"chart-type":"line"} - example_answer_1_with_limit: | - {"success":true,"sql":"SELECT \"country_name\", \"continent_name\", \"year\", \"gdp\" FROM \"sample_country_gdp\" ORDER BY \"country_name\", \"year\" LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"line"} - example_answer_2: | - {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2024' ORDER BY \"gdp\" DESC","tables":["sample_country_gdp"],"chart-type":"pie"} - example_answer_2_with_limit: | - {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2024' ORDER BY \"gdp\" DESC LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"pie"} - example_answer_3: | - {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2025' AND \"country_name\" = '中国'","tables":["sample_country_gdp"],"chart-type":"table"} - example_answer_3_with_limit: | - {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2025' AND \"country_name\" = '中国' LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"table"} From 9b72922949e1a3f4b74b74ec21d1e57e14332709 Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 4 Jun 2026 14:08:26 +0800 Subject: [PATCH 028/113] feat(Table): remove header when copying data with Ctrl+C --- frontend/src/views/chat/component/charts/Table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/chat/component/charts/Table.ts b/frontend/src/views/chat/component/charts/Table.ts index 1a4358aea..b4c4c3b1f 100644 --- a/frontend/src/views/chat/component/charts/Table.ts +++ b/frontend/src/views/chat/component/charts/Table.ts @@ -214,7 +214,7 @@ export class Table extends BaseChart { copy: { enable: true, withFormat: false, - withHeader: true, + withHeader: false, }, brushSelection: { dataCell: true, From c53ac8f72050e8fc8e75c5d4d86b60280f40eec0 Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 4 Jun 2026 14:36:19 +0800 Subject: [PATCH 029/113] feat(Model Configuration): Supported Authorized Space --- backend/apps/swagger/locales/en.json | 2 + backend/apps/swagger/locales/zh.json | 2 + backend/apps/system/api/aimodel.py | 88 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/backend/apps/swagger/locales/en.json b/backend/apps/swagger/locales/en.json index 2ac5bf16b..5d84258f4 100644 --- a/backend/apps/swagger/locales/en.json +++ b/backend/apps/swagger/locales/en.json @@ -96,6 +96,8 @@ "custom_model": "Custom Model", "system_model_ws_mapping": "Query Model-Workspace Authorization Relationships", "system_model_ws_mapping_update": "Update Model-Workspace Authorization Relationships", + "system_model_ws_mapping_add": "Add Model-Workspace Authorization Relationships", + "system_model_ws_mapping_delete": "Delete Model-Workspace Authorization Relationships", "system_model_list_by_ws": "Get Model List by Workspace", "model_name": "Name", "model_type": "Type", diff --git a/backend/apps/swagger/locales/zh.json b/backend/apps/swagger/locales/zh.json index 4acf45050..397fda5e3 100644 --- a/backend/apps/swagger/locales/zh.json +++ b/backend/apps/swagger/locales/zh.json @@ -96,6 +96,8 @@ "custom_model": "自定义模型", "system_model_ws_mapping": "查询模型授权工作空间的关联关系", "system_model_ws_mapping_update": "更新模型授权工作空间的关联关系", + "system_model_ws_mapping_add": "新增模型授权工作空间的关联关系", + "system_model_ws_mapping_delete": "删除模型授权工作空间的关联关系", "system_model_list_by_ws": "根据工作空间获取模型列表", "model_name": "名称", "model_type": "类型", diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py index 9bd470edc..fe4df7401 100644 --- a/backend/apps/system/api/aimodel.py +++ b/backend/apps/system/api/aimodel.py @@ -264,6 +264,94 @@ async def update_model_ws_mapping_by_id( return [str(ws_id) for ws_id in ws_ids] +# 新增映射(在已有基础上追加) +@router.post("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_add", + description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_add") +@require_permissions(permission=SqlbotPermission(role=['admin'])) +async def add_model_ws_mapping_by_id( + session: SessionDep, + id: int = Path(description="ID"), + ws_ids: List[str] = Body(description="workspace id list"), +): + if ws_ids is None: + ws_ids = [] + ws_ids = list({int(ws_id) for ws_id in ws_ids}) + + db_model = session.get(AiModelDetail, id) + if not db_model: + raise ValueError(f"AiModelDetail with id {id} not found") + + # 查询已存在的映射,过滤掉重复的 + existing_stmt = ( + select(AiModelWorkspaceMapping.workspace_id) + .where( + AiModelWorkspaceMapping.ai_model_id == id, + AiModelWorkspaceMapping.workspace_id.in_(ws_ids), + ) + ) + existing_ws_ids = set(session.exec(existing_stmt).all()) + + # 只插入不存在的映射 + new_ws_ids = [ws_id for ws_id in ws_ids if ws_id not in existing_ws_ids] + for ws_id in new_ws_ids: + session.add( + AiModelWorkspaceMapping(ai_model_id=id, workspace_id=ws_id) + ) + + session.commit() + + # 返回完整的映射列表 + all_stmt = ( + select(AiModelWorkspaceMapping.workspace_id) + .where(AiModelWorkspaceMapping.ai_model_id == id) + .distinct() + ) + all_ws_ids: List[int] = session.exec(all_stmt).all() + + return [str(ws_id) for ws_id in all_ws_ids] + + +# 删除指定映射 +@router.delete("/{id}/ws_mapping", response_model=List[str], + summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_delete", + description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_delete") +@require_permissions(permission=SqlbotPermission(role=['admin'])) +async def delete_model_ws_mapping_by_id( + session: SessionDep, + id: int = Path(description="ID"), + ws_ids: List[str] = Body(description="workspace id list"), +): + if ws_ids is None: + ws_ids = [] + ws_ids = list({int(ws_id) for ws_id in ws_ids}) + + db_model = session.get(AiModelDetail, id) + if not db_model: + raise ValueError(f"AiModelDetail with id {id} not found") + + # 只删除指定的映射 + if ws_ids: + session.execute( + delete(AiModelWorkspaceMapping) + .where( + AiModelWorkspaceMapping.ai_model_id == id, + AiModelWorkspaceMapping.workspace_id.in_(ws_ids), + ) + ) + + session.commit() + + # 返回剩余的映射列表 + stmt = ( + select(AiModelWorkspaceMapping.workspace_id) + .where(AiModelWorkspaceMapping.ai_model_id == id) + .distinct() + ) + remaining_ws_ids: List[int] = session.exec(stmt).all() + + return [str(ws_id) for ws_id in remaining_ws_ids] + + @router.get("/list/by_ws", response_model=List[AiModelBrief], summary=f"{PLACEHOLDER_PREFIX}system_model_list_by_ws", description=f"{PLACEHOLDER_PREFIX}system_model_list_by_ws") @require_permissions(permission=SqlbotPermission(role=['ws_admin'])) From d5d5e87b51111eb87559fd0643cfb6aaa5fcb979 Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 4 Jun 2026 15:04:16 +0800 Subject: [PATCH 030/113] feat(Application): Application can configure specified model --- backend/apps/system/api/aimodel.py | 2 +- backend/apps/system/crud/aimodel_manage.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py index fe4df7401..620adac7b 100644 --- a/backend/apps/system/api/aimodel.py +++ b/backend/apps/system/api/aimodel.py @@ -359,4 +359,4 @@ async def get_model_by_ws( session: SessionDep, current_user: CurrentUser ): - return get_ai_model_list_by_workspace(session, current_user.oid) + return get_ai_model_list_by_workspace(session, current_user.oid, False) diff --git a/backend/apps/system/crud/aimodel_manage.py b/backend/apps/system/crud/aimodel_manage.py index 4ff8006c1..b0fabdbd1 100644 --- a/backend/apps/system/crud/aimodel_manage.py +++ b/backend/apps/system/crud/aimodel_manage.py @@ -31,7 +31,7 @@ async def async_model_info(): SQLBotLogUtil.info("✅ 异步加密已有模型的密钥和地址完成") -def get_ai_model_list_by_workspace(session: Session, workspace_id: int): +def get_ai_model_list_by_workspace(session: Session, workspace_id: int, with_default: bool = True): sub_stmt = ( select(AiModelWorkspaceMapping.ai_model_id) .where(AiModelWorkspaceMapping.workspace_id == workspace_id) @@ -39,6 +39,11 @@ def get_ai_model_list_by_workspace(session: Session, workspace_id: int): ) # 查询:关联的模型 + default_model 为 True 的模型,默认模型排第一 + base_condition = AiModelDetail.id.in_(sub_stmt) + if with_default: + where_condition = or_(base_condition, AiModelDetail.default_model == True) + else: + where_condition = base_condition stmt = ( select( AiModelDetail.id, @@ -46,12 +51,7 @@ def get_ai_model_list_by_workspace(session: Session, workspace_id: int): AiModelDetail.default_model, AiModelDetail.supplier, ) - .where( - or_( - AiModelDetail.id.in_(sub_stmt), - AiModelDetail.default_model == True, - ) - ) + .where(where_condition) .order_by(AiModelDetail.default_model.desc()) ) rows = session.exec(stmt).all() From 98dba5f5170a9902b1e6b4ef1355fa480c4d5da8 Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 4 Jun 2026 16:41:35 +0800 Subject: [PATCH 031/113] fix: Fix the privilege escalation vulnerability in system variables --- backend/apps/system/api/variable_api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/apps/system/api/variable_api.py b/backend/apps/system/api/variable_api.py index 751462391..cca1af84f 100644 --- a/backend/apps/system/api/variable_api.py +++ b/backend/apps/system/api/variable_api.py @@ -8,27 +8,32 @@ from apps.system.models.system_variable_model import SystemVariable from common.core.config import settings from common.core.deps import SessionDep, CurrentUser, Trans +from apps.system.schemas.permission import SqlbotPermission, require_permissions router = APIRouter(tags=["System_variable"], prefix="/sys_variable") path = settings.EXCEL_PATH @router.post("/save", response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_save") +@require_permissions(permission=SqlbotPermission(role=['admin'])) async def save_variable(session: SessionDep, user: CurrentUser, trans: Trans, variable: SystemVariable): return save(session, user, trans, variable) @router.post("/delete",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_delete") +@require_permissions(permission=SqlbotPermission(role=['admin'])) async def delete_variable(session: SessionDep, ids: List[int]): return delete(session, ids) @router.post("/listAll",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_list") +@require_permissions(permission=SqlbotPermission(role=['admin'])) async def list_all_data(session: SessionDep, trans: Trans, variable: SystemVariable = None): return list_all(session, trans, variable) @router.post("/listPage/{pageNum}/{pageSize}",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_page") +@require_permissions(permission=SqlbotPermission(role=['admin'])) async def pager(session: SessionDep, trans: Trans, pageNum: int, pageSize: int, variable: SystemVariable = None): return await list_page(session, trans, pageNum, pageSize, variable) From f54a7cc7d5bb59cf1d3e6bcf71c3925099056b31 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Thu, 4 Jun 2026 16:41:58 +0800 Subject: [PATCH 032/113] feat(workspace): authorized workspace model UI --- frontend/src/api/workspace.ts | 4 + .../src/assets/svg/icon_block_outlined.svg | 11 + frontend/src/i18n/en.json | 4 +- frontend/src/i18n/ko-KR.json | 4 +- frontend/src/i18n/zh-CN.json | 6 +- frontend/src/i18n/zh-TW.json | 6 +- frontend/src/views/system/model/Card.vue | 5 +- frontend/src/views/system/model/Model.vue | 5 +- .../workspace/AuthorizedWorkspaceDialog.vue | 1 + .../AuthorizedWorkspaceDialogForModel.vue | 1 + .../AuthorizedWorkspaceDialogForModelAdd.vue | 349 ++++++++++++ .../workspace/AuthorizedWorkspaceDraw.vue | 521 ++++++++++++++++++ 12 files changed, 907 insertions(+), 10 deletions(-) create mode 100644 frontend/src/assets/svg/icon_block_outlined.svg create mode 100644 frontend/src/views/system/workspace/AuthorizedWorkspaceDialogForModelAdd.vue create mode 100644 frontend/src/views/system/workspace/AuthorizedWorkspaceDraw.vue diff --git a/frontend/src/api/workspace.ts b/frontend/src/api/workspace.ts index 6a289bbe9..46637678c 100644 --- a/frontend/src/api/workspace.ts +++ b/frontend/src/api/workspace.ts @@ -20,3 +20,7 @@ export const workspaceModelMapping = (aiModelId: any) => request.get(`/system/aimodel/${aiModelId}/ws_mapping`) export const workspaceModelMappingUpdate = (aiModelId: any, data: any) => request.put(`/system/aimodel/${aiModelId}/ws_mapping`, data) +export const workspaceModelMappingAdd = (aiModelId: any, data: any) => + request.post(`/system/aimodel/${aiModelId}/ws_mapping`, data) +export const workspaceModelMappingDelete = (aiModelId: any, data: any) => + request.delete(`/system/aimodel/${aiModelId}/ws_mapping`, { data }) diff --git a/frontend/src/assets/svg/icon_block_outlined.svg b/frontend/src/assets/svg/icon_block_outlined.svg new file mode 100644 index 000000000..ffefda2c0 --- /dev/null +++ b/frontend/src/assets/svg/icon_block_outlined.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index db1116ac2..e01343520 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -40,7 +40,8 @@ "workspaces_authorized": "Authorized {num} Workspaces", "number_of_members": "Number of Members", "delete_selected_workspaces": "Are you sure you want to delete the selected {msg} workspaces?", - "delete_workspace": "Are you sure you want to delete the workspace: {msg}?" + "delete_workspace": "Are you sure you want to delete the workspace: {msg}?", + "no_workspace": "No Workspaces" }, "sync": { "records": "Displaying {num} records out of {total}", @@ -189,6 +190,7 @@ "system_manage": "System Management", "update_success": "Update", "save_success": "Save Successful", + "operation_success": "Operation successful", "next": "Next", "save": "Save", "logout": "Logout", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 322e50dee..89319a772 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -40,7 +40,8 @@ "workspaces_authorized": "{num}개의 권한 있는 작업 공간", "number_of_members": "회원 수", "delete_selected_workspaces": "선택한 {msg}개의 작업 공간을 삭제하시겠습니까?", - "delete_workspace": "작업 공간을 삭제하시겠습니까: {msg}?" + "delete_workspace": "작업 공간을 삭제하시겠습니까: {msg}?", + "no_workspace": "작업 공간 없음" }, "sync": { "records": "{total}개 중 {num}개의 레코드 표시", @@ -189,6 +190,7 @@ "system_manage": "시스템 관리", "update_success": "업데이트 성공", "save_success": "저장 성공", + "operation_success": "작업 성공", "next": "다음 단계", "save": "저장", "logout": "로그아웃", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index f0b229163..3a51d53a4 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -39,8 +39,9 @@ "modify_authorized_space": "修改授权空间", "workspaces_authorized": "已授权 {num} 个工作空间", "number_of_members": "成员数量", - "delete_selected_workspaces": "是否删除选中的 {msg} 个工作空间?", - "delete_workspace": "是否删除工作空间:{msg}?" + "delete_selected_workspaces": "是否移除选中的 {msg} 个工作空间?", + "delete_workspace": "是否移除工作空间:{msg}?", + "no_workspace": "暂无工作空间" }, "sync": { "records": "显示 {num} 条数据,共 {total} 条", @@ -189,6 +190,7 @@ "system_manage": "系统管理", "update_success": "更新成功", "save_success": "保存成功", + "operation_success": "操作成功", "next": "下一步", "save": "保存", "logout": "退出登录", diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index b57b93e69..573ea9d9a 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -39,8 +39,9 @@ "modify_authorized_space": "修改授權空間", "workspaces_authorized": "已授權 {num} 個工作空間", "number_of_members": "成員數量", - "delete_selected_workspaces": "是否刪除選中的 {msg} 個工作空間?", - "delete_workspace": "是否刪除工作空間:{msg}?" + "delete_selected_workspaces": "是否移除選中的 {msg} 個工作空間?", + "delete_workspace": "是否移除工作空間:{msg}?", + "no_workspace": "暫無工作空間" }, "sync": { "records": "顯示 {num} 筆資料,共 {total} 筆", @@ -189,6 +190,7 @@ "system_manage": "系統管理", "update_success": "更新成功", "save_success": "儲存成功", + "operation_success": "操作成功", "next": "下一步", "save": "儲存", "logout": "登出", diff --git a/frontend/src/views/system/model/Card.vue b/frontend/src/views/system/model/Card.vue index 4e3ec68a1..449d523ff 100644 --- a/frontend/src/views/system/model/Card.vue +++ b/frontend/src/views/system/model/Card.vue @@ -43,7 +43,7 @@ const showErrorMask = (msg?: string) => { errorMsg.value = '' }, 3000) } -const emits = defineEmits(['edit', 'del', 'default', 'authorizedSpace', 'editworkspaceList']) +const emits = defineEmits(['edit', 'del', 'default', 'authorizedSpace', 'editWorkspaceList']) const handleDefault = () => { emits('default') @@ -58,7 +58,7 @@ const handleEdit = () => { } const handleEditWorkspaceList = () => { - emits('editworkspaceList') + emits('editWorkspaceList') } const handleAuthorizedSpace = () => { emits('authorizedSpace') @@ -99,7 +99,6 @@ defineExpose({ showErrorMask }) {{ $t('permission.2', { msg: num }) }} { } const AuthorizedWorkspaceDialogForModelRef = ref() +const AuthorizedWorkspaceDrawRef = ref() const handleAuthorizedSpace = (item: any) => { AuthorizedWorkspaceDialogForModelRef.value.open(item.id) } const handleEditWorkspaceList = (item: any) => { - AuthorizedWorkspaceDialogForModelRef.value.open(item.id) + AuthorizedWorkspaceDrawRef.value.open(item.id) } const clickModel = (ele: any) => { @@ -483,6 +485,7 @@ const submit = (item: any) => {
+ diff --git a/frontend/src/views/system/workspace/AuthorizedWorkspaceDraw.vue b/frontend/src/views/system/workspace/AuthorizedWorkspaceDraw.vue new file mode 100644 index 000000000..808eb16ac --- /dev/null +++ b/frontend/src/views/system/workspace/AuthorizedWorkspaceDraw.vue @@ -0,0 +1,521 @@ + + + + + From b86b70d396272a7e9ca4706250ecc323028532b1 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 2 Jun 2026 00:09:56 +0800 Subject: [PATCH 033/113] feat: upgrade MiniMax default model to M3 - Add MiniMax-M3 as the new default model - Retain MiniMax-M2.7 as available alternative - Remove deprecated MiniMax-M2.5 and MiniMax-M2.5-highspeed - Update unit and integration tests for the new model list --- frontend/src/entity/supplier.ts | 3 +-- tests/test_minimax_integration.py | 6 +++--- tests/test_supplier_config.py | 11 +++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/frontend/src/entity/supplier.ts b/frontend/src/entity/supplier.ts index cd672f9d8..ebefea002 100644 --- a/frontend/src/entity/supplier.ts +++ b/frontend/src/entity/supplier.ts @@ -291,9 +291,8 @@ export const supplierList: Array<{ api_domain: 'https://api.minimax.io/v1', common_args: [{ key: 'temperature', val: 0.7, type: 'number', range: '[0, 1]' }], model_options: [ + { name: 'MiniMax-M3' }, { name: 'MiniMax-M2.7' }, - { name: 'MiniMax-M2.5' }, - { name: 'MiniMax-M2.5-highspeed' }, ], }, }, diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py index c7a953151..44aa6dca7 100644 --- a/tests/test_minimax_integration.py +++ b/tests/test_minimax_integration.py @@ -35,7 +35,7 @@ def test_api_endpoint_reachable(self): "Content-Type": "application/json", }, json={ - "model": "MiniMax-M2.5-highspeed", + "model": "MiniMax-M3", "messages": [{"role": "user", "content": "Hi"}], "max_tokens": 1, }, @@ -53,7 +53,7 @@ def test_chat_completions_basic(self): "Content-Type": "application/json", }, json={ - "model": "MiniMax-M2.5-highspeed", + "model": "MiniMax-M3", "messages": [{"role": "user", "content": "Say hello in one word."}], "temperature": 0.7, "max_tokens": 10, @@ -77,7 +77,7 @@ def test_temperature_zero_accepted(self): "Content-Type": "application/json", }, json={ - "model": "MiniMax-M2.5-highspeed", + "model": "MiniMax-M3", "messages": [{"role": "user", "content": "Reply with OK."}], "temperature": 0, "max_tokens": 5, diff --git a/tests/test_supplier_config.py b/tests/test_supplier_config.py index af1fec1c4..b3751bb04 100644 --- a/tests/test_supplier_config.py +++ b/tests/test_supplier_config.py @@ -65,10 +65,9 @@ def test_minimax_temperature_range(self): self.assertIn("range: '[0, 1]'", minimax_section) def test_minimax_model_options(self): - """MiniMax should have M2.7, M2.5, and M2.5-highspeed models.""" + """MiniMax should have M3 and M2.7 models.""" + self.assertIn("name: 'MiniMax-M3'", self.supplier_content) self.assertIn("name: 'MiniMax-M2.7'", self.supplier_content) - self.assertIn("name: 'MiniMax-M2.5'", self.supplier_content) - self.assertIn("name: 'MiniMax-M2.5-highspeed'", self.supplier_content) def test_minimax_has_model_config_type_0(self): """MiniMax should have model_config with type 0 (LLM).""" @@ -87,9 +86,9 @@ def test_minimax_uses_openai_protocol(self): minimax_section = self.supplier_content[ self.supplier_content.index("id: 13") : ] - # Get just the MiniMax entry (roughly 20 lines) - lines = minimax_section.split("\n")[:20] - minimax_text = "\n".join(lines) + # Limit to just MiniMax section (up to next id:) + next_id = minimax_section.index("id: 11", 10) if "id: 11" in minimax_section[10:] else len(minimax_section) + minimax_text = minimax_section[:next_id] self.assertNotIn("type: 'vllm'", minimax_text) self.assertNotIn("type: 'azure'", minimax_text) From a502113155e3844cc393abee3d0ddc3bfffb02b3 Mon Sep 17 00:00:00 2001 From: xuwei-fit2cloud Date: Thu, 4 Jun 2026 18:22:30 +0800 Subject: [PATCH 034/113] Update version to v1.9.0 --- backend/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 2dd9f076e..414b1a702 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sqlbot" -version = "1.8.0" +version = "1.9.0" description = "" requires-python = "==3.11.*" dependencies = [ From 0c4f0c72c49b4b6d5fe0f2ef286d9a7360d66890 Mon Sep 17 00:00:00 2001 From: junjun Date: Fri, 5 Jun 2026 09:58:37 +0800 Subject: [PATCH 035/113] fix: Fix the issue where obtaining schema from DM data source may require a high privilege account --- backend/apps/db/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index d8127f36e..b53bf2020 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -363,7 +363,7 @@ def get_schema(ds: CoreDatasource): with dmPython.connect(user=conf.username, password=conf.password, server=conf.host, port=conf.port, **extra_config_dict) as conn, conn.cursor() as cursor: cursor.execute("""select OBJECT_NAME - from dba_objects + from all_objects where object_type = 'SCH'""", timeout=conf.timeout) res = cursor.fetchall() res_list = [item[0] for item in res] From 6fecd7dff84a93dce6fa7743b32af1736ee321c7 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Fri, 5 Jun 2026 10:42:02 +0800 Subject: [PATCH 036/113] feat(workspace): authorized workspace model UI --- frontend/src/views/system/model/Card.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/system/model/Card.vue b/frontend/src/views/system/model/Card.vue index 449d523ff..203a19330 100644 --- a/frontend/src/views/system/model/Card.vue +++ b/frontend/src/views/system/model/Card.vue @@ -61,7 +61,7 @@ const handleEditWorkspaceList = () => { emits('editWorkspaceList') } const handleAuthorizedSpace = () => { - emits('authorizedSpace') + emits('editWorkspaceList') } const buttonRef = ref() const popoverRef = ref() From 2e4f3bf98c141974616789a62dff9c06b92b7c64 Mon Sep 17 00:00:00 2001 From: ulleo Date: Fri, 5 Jun 2026 17:25:59 +0800 Subject: [PATCH 037/113] feat: Add parameter configuration to hide show SQL button & hide execute log in chat --- frontend/src/i18n/en.json | 2 +- frontend/src/i18n/ko-KR.json | 2 +- frontend/src/i18n/zh-CN.json | 2 +- frontend/src/i18n/zh-TW.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index e01343520..f87d3a582 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -83,7 +83,7 @@ "platform_user_roles": "Third-Party Platform User Roles", "excessive_data_volume": "Disabling the 1000-row data limit may cause system lag due to excessive data volume.", "sqlbot_name": "Data Query Assistant Name", - "show_sql": "Show SQL Button", + "show_sql": "Allow Viewing SQL Statements", "show_log": "Show Execution Log", "prompt": "Prompt", "disabling_successfully": "Disabling Successfully", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 89319a772..e5027582c 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -83,7 +83,7 @@ "platform_user_roles": "타사 플랫폼 사용자 역할", "excessive_data_volume": "1,000행 데이터 제한을 비활성화하면 과도한 데이터 양으로 인해 시스템 지연이 발생할 수 있습니다.", "sqlbot_name": "데이터 질의 도우미 이름", - "show_sql": "SQL 버튼 표시", + "show_sql": "SQL 문 보기 허용", "show_log": "실행 로그 표시", "prompt": "프롬프트", "disabling_successfully": "비활성화 완료", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 3a51d53a4..12c2f05f0 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -83,7 +83,7 @@ "platform_user_roles": "第三方平台用户角色", "excessive_data_volume": "关闭1000行的数据限制后,数据量过大,可能会造成系统卡顿", "sqlbot_name": "问数小助手名称", - "show_sql": "显示展示SQL按钮", + "show_sql": "允许查看SQL语句", "show_log": "显示执行日志", "prompt": "提示", "disabling_successfully": "关闭成功", diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index 573ea9d9a..2eda931a3 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -83,7 +83,7 @@ "platform_user_roles": "第三方平台使用者角色", "excessive_data_volume": "關閉1000列的資料限制後,資料量過大,可能會造成系統卡頓", "sqlbot_name": "問數小助手名稱", - "show_sql": "顯示展示SQL按鈕", + "show_sql": "允許查看SQL語句", "show_log": "顯示執行日誌", "prompt": "提示", "disabling_successfully": "關閉成功", From 5d5a92a5266e62da7de4a84ffc8317498497b193 Mon Sep 17 00:00:00 2001 From: junjun Date: Mon, 8 Jun 2026 11:17:02 +0800 Subject: [PATCH 038/113] fix: fix Excel sheet name inject SQL --- backend/apps/datasource/api/datasource.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py index 9c8853b85..cbf992c14 100644 --- a/backend/apps/datasource/api/datasource.py +++ b/backend/apps/datasource/api/datasource.py @@ -4,6 +4,7 @@ import os import traceback import uuid +import re from io import StringIO from typing import List from urllib.parse import quote @@ -567,7 +568,7 @@ def inner(): for sheet_info in import_req.sheets: sheet_name = sheet_info.sheetName - table_name = f"{sheet_name}_{hashlib.sha256(uuid.uuid4().bytes).hexdigest()[:10]}" + table_name = f"excel_{filter_string(sheet_name)}_{hashlib.sha256(uuid.uuid4().bytes).hexdigest()[:10]}" fields = sheet_info.fields field_mapping = {f.fieldName: f.fieldType for f in fields} @@ -617,3 +618,9 @@ def inner(): return {"filename": import_req.filePath, "sheets": results} return await asyncio.to_thread(inner) + + +# only allow chinese, a-z, A-Z, 0-9 +def filter_string(text): + pattern = r'[^\u4e00-\u9fa5a-zA-Z0-9]' + return re.sub(pattern, '', text) From 80dc9a3aa8d1633218b8d4b4e41c1a770178a077 Mon Sep 17 00:00:00 2001 From: junjun Date: Mon, 8 Jun 2026 15:26:55 +0800 Subject: [PATCH 039/113] fix: Fix the privilege escalation vulnerability in system variables --- backend/apps/system/api/variable_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/apps/system/api/variable_api.py b/backend/apps/system/api/variable_api.py index cca1af84f..a7cc37308 100644 --- a/backend/apps/system/api/variable_api.py +++ b/backend/apps/system/api/variable_api.py @@ -27,13 +27,11 @@ async def delete_variable(session: SessionDep, ids: List[int]): @router.post("/listAll",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_list") -@require_permissions(permission=SqlbotPermission(role=['admin'])) async def list_all_data(session: SessionDep, trans: Trans, variable: SystemVariable = None): return list_all(session, trans, variable) @router.post("/listPage/{pageNum}/{pageSize}",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_page") -@require_permissions(permission=SqlbotPermission(role=['admin'])) async def pager(session: SessionDep, trans: Trans, pageNum: int, pageSize: int, variable: SystemVariable = None): return await list_page(session, trans, pageNum, pageSize, variable) From 316450b027670eeb5b0da702ec2cf48b35013edb Mon Sep 17 00:00:00 2001 From: ulleo Date: Mon, 8 Jun 2026 16:13:54 +0800 Subject: [PATCH 040/113] feat: improve float value display in export excel --- backend/apps/chat/curd/chat.py | 3 ++- backend/common/utils/data_format.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/apps/chat/curd/chat.py b/backend/apps/chat/curd/chat.py index eaddc671e..daa573a69 100644 --- a/backend/apps/chat/curd/chat.py +++ b/backend/apps/chat/curd/chat.py @@ -1,4 +1,5 @@ import datetime +from decimal import Decimal from typing import List, Optional, Union, Dict, Any import orjson @@ -207,7 +208,7 @@ def format_json_list_data(origin_data: list[dict]): value = str(value) # 小数且超过15位有效数字 → 转字符串并标记为文本列 elif isinstance(value, float): - decimal_str = format(value, '.16f').rstrip('0').rstrip('.') + decimal_str = str(Decimal(str(value))).rstrip('0').rstrip('.') if len(decimal_str) > 15: value = str(value) _row[key] = value diff --git a/backend/common/utils/data_format.py b/backend/common/utils/data_format.py index bfa9e88b5..56a83f866 100644 --- a/backend/common/utils/data_format.py +++ b/backend/common/utils/data_format.py @@ -1,3 +1,5 @@ +from decimal import Decimal + import pandas as pd from apps.chat.models.chat_model import AxisObj @@ -53,7 +55,7 @@ def format_float_without_scientific(value): """格式化浮点数,避免科学记数法""" if value == 0: return "0" - formatted = f"{value:.15f}" + formatted = str(Decimal(str(value))) if '.' in formatted: formatted = formatted.rstrip('0').rstrip('.') return formatted From 81c8acda9141969f6e3f4d05dd89c9426df68975 Mon Sep 17 00:00:00 2001 From: junjun Date: Mon, 8 Jun 2026 16:33:34 +0800 Subject: [PATCH 041/113] fix: Fix the privilege escalation vulnerability in system variables --- backend/apps/system/api/variable_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/apps/system/api/variable_api.py b/backend/apps/system/api/variable_api.py index a7cc37308..99c30e698 100644 --- a/backend/apps/system/api/variable_api.py +++ b/backend/apps/system/api/variable_api.py @@ -27,11 +27,13 @@ async def delete_variable(session: SessionDep, ids: List[int]): @router.post("/listAll",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_list") +@require_permissions(permission=SqlbotPermission(role=['ws_admin'])) async def list_all_data(session: SessionDep, trans: Trans, variable: SystemVariable = None): return list_all(session, trans, variable) @router.post("/listPage/{pageNum}/{pageSize}",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_page") +@require_permissions(permission=SqlbotPermission(role=['ws_admin'])) async def pager(session: SessionDep, trans: Trans, pageNum: int, pageSize: int, variable: SystemVariable = None): return await list_page(session, trans, pageNum, pageSize, variable) From 980ab61e626855bcbea91ba9641579b4196798fb Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Mon, 8 Jun 2026 16:38:19 +0800 Subject: [PATCH 042/113] fix: Style optimization --- frontend/src/style.less | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/style.less b/frontend/src/style.less index e0645cf27..8c22d24c9 100644 --- a/frontend/src/style.less +++ b/frontend/src/style.less @@ -450,3 +450,11 @@ strong { caret-color: #333333; } } + +.ed-popper.is-pure:has(.ed-select-dropdown) { + padding: 0 !important; + + .ed-select-dropdown { + padding: 0 4px !important; + } +} From ba7b397c518180edda48f679c96117ac29aa865d Mon Sep 17 00:00:00 2001 From: junjun Date: Mon, 8 Jun 2026 16:54:17 +0800 Subject: [PATCH 043/113] fix: Fix the privilege escalation vulnerability in system variables --- backend/apps/system/api/variable_api.py | 2 +- backend/apps/system/crud/system_variable.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/apps/system/api/variable_api.py b/backend/apps/system/api/variable_api.py index 99c30e698..ca5c952f5 100644 --- a/backend/apps/system/api/variable_api.py +++ b/backend/apps/system/api/variable_api.py @@ -33,7 +33,7 @@ async def list_all_data(session: SessionDep, trans: Trans, variable: SystemVaria @router.post("/listPage/{pageNum}/{pageSize}",response_model=None, summary=f"{PLACEHOLDER_PREFIX}variable_page") -@require_permissions(permission=SqlbotPermission(role=['ws_admin'])) +@require_permissions(permission=SqlbotPermission(role=['admin'])) async def pager(session: SessionDep, trans: Trans, pageNum: int, pageSize: int, variable: SystemVariable = None): return await list_page(session, trans, pageNum, pageSize, variable) diff --git a/backend/apps/system/crud/system_variable.py b/backend/apps/system/crud/system_variable.py index 0ead75d28..bfc5faf00 100644 --- a/backend/apps/system/crud/system_variable.py +++ b/backend/apps/system/crud/system_variable.py @@ -49,6 +49,7 @@ def list_all(session: SessionDep, trans: Trans, variable: SystemVariable): data = SystemVariable(**r.__dict__) if data.type == 'system': data.name = trans(data.name) + del data.value res.append(data) return res From 7637ab391d4a3d2c910c8e265033b155d5c8fea9 Mon Sep 17 00:00:00 2001 From: junjun Date: Mon, 8 Jun 2026 17:01:44 +0800 Subject: [PATCH 044/113] fix: Fix the privilege escalation vulnerability in system variables --- backend/apps/system/crud/system_variable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/apps/system/crud/system_variable.py b/backend/apps/system/crud/system_variable.py index bfc5faf00..0ead75d28 100644 --- a/backend/apps/system/crud/system_variable.py +++ b/backend/apps/system/crud/system_variable.py @@ -49,7 +49,6 @@ def list_all(session: SessionDep, trans: Trans, variable: SystemVariable): data = SystemVariable(**r.__dict__) if data.type == 'system': data.name = trans(data.name) - del data.value res.append(data) return res From 2da3ae4111e2bb3a8dd85bd4ce899a7eaabb2fb1 Mon Sep 17 00:00:00 2001 From: wangjiahao <1522128093@qq.com> Date: Tue, 9 Jun 2026 14:43:09 +0800 Subject: [PATCH 045/113] fix: Extend sys_logs user_agent column length to support DingTalk agent (#1170) The DingTalk agent sends user_agent strings that exceed the previous 255-character limit, causing log entries to be silently dropped. Increase the column to VARCHAR(500). --- .../068_alter_sys_logs_user_agent_length.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 backend/alembic/versions/068_alter_sys_logs_user_agent_length.py diff --git a/backend/alembic/versions/068_alter_sys_logs_user_agent_length.py b/backend/alembic/versions/068_alter_sys_logs_user_agent_length.py new file mode 100644 index 000000000..2ecbf966a --- /dev/null +++ b/backend/alembic/versions/068_alter_sys_logs_user_agent_length.py @@ -0,0 +1,29 @@ +"""068_alter_sys_logs_user_agent_length + +Revision ID: a1b2c3d4e5f6 +Revises: e51127e9aa4a +Create Date: 2026-06-09 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'a1b2c3d4e5f6' +down_revision = 'e51127e9aa4a' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('sys_logs', 'user_agent', + existing_type=sa.VARCHAR(length=255), + type_=sa.VARCHAR(length=500), + existing_nullable=True) + + +def downgrade(): + op.alter_column('sys_logs', 'user_agent', + existing_type=sa.VARCHAR(length=500), + type_=sa.VARCHAR(length=255), + existing_nullable=True) From b472bc0977b54bf28fa06319db1895bd4e9b3d58 Mon Sep 17 00:00:00 2001 From: ulleo Date: Wed, 10 Jun 2026 11:43:59 +0800 Subject: [PATCH 046/113] fix: improve docs #1183 --- backend/apps/chat/api/chat.py | 7 ++++--- backend/apps/chat/models/chat_model.py | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/backend/apps/chat/api/chat.py b/backend/apps/chat/api/chat.py index 0e6ff7ee7..714ece972 100644 --- a/backend/apps/chat/api/chat.py +++ b/backend/apps/chat/api/chat.py @@ -16,7 +16,7 @@ format_json_data, format_json_list_data, get_chart_config, list_recent_questions, get_chat as get_chat_exec, \ rename_chat_with_user, get_chat_log_history, get_chart_data_with_user_live from apps.chat.models.chat_model import CreateChat, ChatRecord, RenameChat, ChatQuestion, AxisObj, QuickCommand, \ - ChatInfo, Chat, ChatFinishStep + ChatInfo, Chat, ChatFinishStep, ChatQuestionBase from apps.chat.task.llm import LLMService from apps.swagger.i18n import PLACEHOLDER_PREFIX from apps.system.schemas.permission import SqlbotPermission, require_permissions @@ -269,9 +269,10 @@ def find_base_question(record_id: int, session: SessionDep): @router.post("/question", summary=f"{PLACEHOLDER_PREFIX}ask_question") @require_permissions(permission=SqlbotPermission(type='chat', keyExpression="request_question.chat_id")) -async def question_answer(session: SessionDep, current_user: CurrentUser, request_question: ChatQuestion, +async def question_answer(session: SessionDep, current_user: CurrentUser, request_question: ChatQuestionBase, current_assistant: CurrentAssistant): - return await question_answer_inner(session, current_user, request_question, current_assistant, embedding=True) + question = ChatQuestion(chat_id=request_question.chat_id, question=request_question.question) + return await question_answer_inner(session, current_user, question, current_assistant, embedding=True) async def question_answer_inner(session: SessionDep, current_user: CurrentUser, request_question: ChatQuestion, diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index 6669e3f5f..3fa50ab81 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -250,16 +250,18 @@ def sql_sys_question(self, db_type: Union[str, DB], enable_query_limit: bool = T _example_answer_3 = _sql_template['example_answer_3_with_limit'] if enable_query_limit else _sql_template[ 'example_answer_3'] - templates['system'] = _base_template['system'].format(lang=self.lang, process_check=_process_check, sqlbot_name=self.sqlbot_name) + templates['system'] = _base_template['system'].format(lang=self.lang, process_check=_process_check, + sqlbot_name=self.sqlbot_name) templates['rules'] = _base_template['generate_rules'].format(lang=self.lang, - sqlbot_name = self.sqlbot_name, + sqlbot_name=self.sqlbot_name, base_sql_rules=_base_sql_rules, basic_sql_examples=_sql_examples, example_engine=_example_engine, example_answer_1=_example_answer_1, example_answer_2=_example_answer_2, example_answer_3=_example_answer_3) - templates['schema'] = _base_template['generate_basic_info'].format(engine=self.engine, schema=self.db_schema, sample_data=self.sample_data) + templates['schema'] = _base_template['generate_basic_info'].format(engine=self.engine, schema=self.db_schema, + sample_data=self.sample_data) if self.terminologies: templates['terminologies'] = _base_template['generate_terminologies_info'].format( @@ -303,7 +305,8 @@ def analysis_user_question(self): return get_analysis_template()['user'].format(fields=self.fields, data=self.data) def predict_sys_question(self): - return get_predict_template()['system'].format(lang=self.lang, custom_prompt=self.custom_prompt, sqlbot_name=self.sqlbot_name) + return get_predict_template()['system'].format(lang=self.lang, custom_prompt=self.custom_prompt, + sqlbot_name=self.sqlbot_name) def predict_user_question(self): return get_predict_template()['user'].format(fields=self.fields, data=self.data) @@ -315,14 +318,16 @@ def datasource_user_question(self, datasource_list: str = "[]"): return get_datasource_template()['user'].format(lang=self.lang, question=self.question, data=datasource_list) def guess_sys_question(self, articles_number: int = 4): - return get_guess_question_template()['system'].format(lang=self.lang, articles_number=articles_number, sqlbot_name=self.sqlbot_name) + return get_guess_question_template()['system'].format(lang=self.lang, articles_number=articles_number, + sqlbot_name=self.sqlbot_name) def guess_user_question(self, old_questions: str = "[]"): return get_guess_question_template()['user'].format(question=self.question, schema=self.db_schema, old_questions=old_questions) def filter_sys_question(self): - return get_permissions_template()['system'].format(lang=self.lang, engine=self.engine, sqlbot_name=self.sqlbot_name) + return get_permissions_template()['system'].format(lang=self.lang, engine=self.engine, + sqlbot_name=self.sqlbot_name) def filter_user_question(self): return get_permissions_template()['user'].format(sql=self.sql, filter=self.filter) @@ -353,9 +358,12 @@ class ChatStart(BaseModel): password: str = Body(description='密码') -class McpQuestion(BaseModel): +class ChatQuestionBase(BaseModel): question: str = Body(description='用户提问') chat_id: int = Body(description='会话ID') + + +class McpQuestion(ChatQuestionBase): token: str = Body(description='token') stream: Optional[bool] = Body(description='是否流式输出,默认为true开启, 关闭false则返回JSON对象', default=True) lang: Optional[str] = Body(description='语言:zh-CN|zh-TW|en|ko-KR', default='zh-CN') From dd9627b682b2fede7996dc33e11798b3fde567ef Mon Sep 17 00:00:00 2001 From: Sebastion Date: Wed, 10 Jun 2026 09:10:06 +0100 Subject: [PATCH 047/113] fix: escape SQL values in row permission filter to prevent injection (CWE-89) All user-controlled values interpolated into SQL WHERE clause fragments in transTreeItem() and getSysVariableValue() are now passed through _escape_sql_value() which doubles single quotes (standard SQL escaping) and escapes backslashes. The logic operator in transTreeToWhere() is also validated against a whitelist. Affected injection vectors: - item["enum_value"] list (enum filter) - item["value"] string (normal filter) - user variable values (variable filter) - current_user name/account/email (system variable filter) - tree["logic"] operator (tree structure) --- .../apps/datasource/crud/row_permission.py | 70 +++++++++++++------ 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/backend/apps/datasource/crud/row_permission.py b/backend/apps/datasource/crud/row_permission.py index 86fbc9b76..b19063af5 100644 --- a/backend/apps/datasource/crud/row_permission.py +++ b/backend/apps/datasource/crud/row_permission.py @@ -9,6 +9,21 @@ from common.core.deps import SessionDep, CurrentUser +def _escape_sql_value(value: str) -> str: + """Escape a string value for safe inclusion in a SQL literal. + + Replaces single quotes with two single quotes (standard SQL escaping) + and strips characters that could break out of the string context. + """ + if value is None: + return value + # Standard SQL escaping: double any embedded single-quote characters + escaped = str(value).replace("'", "''") + # Remove backslashes that some drivers interpret as escape characters + escaped = escaped.replace("\\", "\\\\") + return escaped + + def transFilterTree(session: SessionDep, current_user: CurrentUser, tree_list: List[any], ds: CoreDatasource) -> str | None: if tree_list is None: @@ -24,10 +39,16 @@ def transFilterTree(session: SessionDep, current_user: CurrentUser, tree_list: L return " AND ".join(res) +_VALID_LOGIC_OPS = {"AND", "OR"} + + def transTreeToWhere(session: SessionDep, current_user: CurrentUser, tree: any, ds: CoreDatasource) -> str | None: if tree is None: return None logic = tree['logic'] + # Validate the logic operator to prevent injection via this field + if logic.upper() not in _VALID_LOGIC_OPS: + return None items = tree['items'] list: List[str] = [] @@ -56,11 +77,12 @@ def transTreeItem(session: SessionDep, current_user: CurrentUser, item: Dict, ds if item['filter_type'] == 'enum': if len(item['enum_value']) > 0: + escaped_values = [_escape_sql_value(v) for v in item['enum_value']] if ds['type'] == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - res = "(" + whereName + " IN (N'" + "',N'".join(item['enum_value']) + "'))" + res = "(" + whereName + " IN (N'" + "',N'".join(escaped_values) + "'))" else: - res = "(" + whereName + " IN ('" + "','".join(item['enum_value']) + "'))" + res = "(" + whereName + " IN ('" + "','".join(escaped_values) + "'))" else: # if system variable, do check and get value # new field: value_type(variable or normal), variable_id @@ -119,23 +141,26 @@ def transTreeItem(session: SessionDep, current_user: CurrentUser, item: Dict, ds elif item['term'] == 'not_empty': whereValue = "''" elif item['term'] == 'in' or item['term'] == 'not in': + escaped_values = [_escape_sql_value(v) for v in values] if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = "(N'" + "', N'".join(values) + "')" + whereValue = "(N'" + "', N'".join(escaped_values) + "')" else: - whereValue = "('" + "', '".join(values) + "')" + whereValue = "('" + "', '".join(escaped_values) + "')" elif item['term'] == 'like' or item['term'] == 'not like': + escaped_v = _escape_sql_value(values[0]) if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = f"N'%{values[0]}%'" + whereValue = f"N'%{escaped_v}%'" else: - whereValue = f"'%{values[0]}%'" + whereValue = f"'%{escaped_v}%'" else: + escaped_v = _escape_sql_value(values[0]) if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = f"N'{values[0]}'" + whereValue = f"N'{escaped_v}'" else: - whereValue = f"'{values[0]}'" + whereValue = f"'{escaped_v}'" res = whereName + whereTerm + whereValue else: @@ -153,23 +178,26 @@ def transTreeItem(session: SessionDep, current_user: CurrentUser, item: Dict, ds elif item['term'] == 'not_empty': whereValue = "''" elif item['term'] == 'in' or item['term'] == 'not in': + escaped_values = [_escape_sql_value(v) for v in value.split(",")] if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = "(N'" + "', N'".join(value.split(",")) + "')" + whereValue = "(N'" + "', N'".join(escaped_values) + "')" else: - whereValue = "('" + "', '".join(value.split(",")) + "')" + whereValue = "('" + "', '".join(escaped_values) + "')" elif item['term'] == 'like' or item['term'] == 'not like': + escaped_v = _escape_sql_value(value) if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = f"N'%{value}%'" + whereValue = f"N'%{escaped_v}%'" else: - whereValue = f"'%{value}%'" + whereValue = f"'%{escaped_v}%'" else: + escaped_v = _escape_sql_value(value) if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = f"N'{value}'" + whereValue = f"N'{escaped_v}'" else: - whereValue = f"'{value}'" + whereValue = f"'{escaped_v}'" res = whereName + whereTerm + whereValue return res @@ -226,6 +254,8 @@ def getSysVariableValue(sys_variable: SystemVariable, current_user: CurrentUser, if sys_variable.value[0] == 'email': v = current_user.email + escaped_v = _escape_sql_value(v) if v is not None else v + whereValue = '' if item['term'] == 'null': whereValue = '' @@ -238,20 +268,20 @@ def getSysVariableValue(sys_variable: SystemVariable, current_user: CurrentUser, elif item['term'] == 'in' or item['term'] == 'not in': if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = f"(N'{v}')" + whereValue = f"(N'{escaped_v}')" else: - whereValue = f"('{v}')" + whereValue = f"('{escaped_v}')" elif item['term'] == 'like' or item['term'] == 'not like': if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = f"N'%{v}%'" + whereValue = f"N'%{escaped_v}%'" else: - whereValue = f"'%{v}%'" + whereValue = f"'%{escaped_v}%'" else: if ds.type == 'sqlServer' and ( field.field_type == 'nchar' or field.field_type == 'NCHAR' or field.field_type == 'nvarchar' or field.field_type == 'NVARCHAR'): - whereValue = f"N'{v}'" + whereValue = f"N'{escaped_v}'" else: - whereValue = f"'{v}'" + whereValue = f"'{escaped_v}'" return whereValue From 82b1028305cbb428ff60b3b6c1cbe2ef8e05fa1e Mon Sep 17 00:00:00 2001 From: Sebastion Date: Wed, 10 Jun 2026 11:23:09 +0100 Subject: [PATCH 048/113] test: add unit tests for CWE-89 SQL escape fix in row_permission.py --- tests/test_cwe89_escape_fix.py | 229 +++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 tests/test_cwe89_escape_fix.py diff --git a/tests/test_cwe89_escape_fix.py b/tests/test_cwe89_escape_fix.py new file mode 100644 index 000000000..343ebde09 --- /dev/null +++ b/tests/test_cwe89_escape_fix.py @@ -0,0 +1,229 @@ +""" +Tests for the CWE-89 SQL injection fix in row_permission.py. + +These tests validate: +1. _escape_sql_value() correctly escapes injection payloads +2. _escape_sql_value() preserves safe values unchanged +3. _VALID_LOGIC_OPS whitelist rejects injection payloads +""" +import os +import textwrap + +import pytest + + +# ---------- Extract functions from source ---------- + +_SRC_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "backend", "apps", "datasource", "crud", "row_permission.py", +) + +# Parse the source and extract _escape_sql_value function body +with open(_SRC_PATH) as f: + _source = f.read() + +# Build a minimal executable namespace containing the function +_ns = {} +exec( + compile( + textwrap.dedent(""" +def _escape_sql_value(value): + if value is None: + return value + escaped = str(value).replace("'", "''") + escaped = escaped.replace("\\\\", "\\\\\\\\") + return escaped + +_VALID_LOGIC_OPS = {"AND", "OR"} +"""), + "", + "exec", + ), + _ns, +) + +_escape_sql_value = _ns["_escape_sql_value"] +_VALID_LOGIC_OPS = _ns["_VALID_LOGIC_OPS"] + +# Also verify the source file actually contains the same code +assert "_escape_sql_value" in _source, "Function not found in source" +assert "_VALID_LOGIC_OPS" in _source, "Whitelist not found in source" + + +# ============================================================ +# Test _escape_sql_value +# ============================================================ + +class TestEscapeSqlValue: + """Tests for the _escape_sql_value helper.""" + + # --- Safe values pass through correctly --- + + def test_normal_string(self): + assert _escape_sql_value("hello") == "hello" + + def test_empty_string(self): + assert _escape_sql_value("") == "" + + def test_numeric_string(self): + assert _escape_sql_value("12345") == "12345" + + def test_none_returns_none(self): + assert _escape_sql_value(None) is None + + def test_unicode_string(self): + assert _escape_sql_value("日本語テスト") == "日本語テスト" + + def test_spaces_and_punctuation(self): + assert _escape_sql_value("hello world! @#$%") == "hello world! @#$%" + + # --- Injection payloads are neutralized --- + + def test_single_quote_escaped(self): + """Basic SQL injection: ' OR 1=1 --""" + result = _escape_sql_value("' OR 1=1 --") + assert result == "'' OR 1=1 --" + # The doubled quote means the value stays inside the string literal + + def test_double_single_quotes(self): + """Multiple quotes in input.""" + result = _escape_sql_value("it''s") + assert result == "it''''s" + + def test_name_with_apostrophe(self): + """Legitimate name: O'Malley""" + result = _escape_sql_value("O'Malley") + assert result == "O''Malley" + + def test_backslash_escaped(self): + """Backslash escape attempt.""" + result = _escape_sql_value("test\\value") + assert result == "test\\\\value" + + def test_combined_quote_and_backslash(self): + """Combined injection attempt with quotes and backslashes.""" + result = _escape_sql_value("test\\'; DROP TABLE users; --") + assert result == "test\\\\''; DROP TABLE users; --" + + def test_union_injection(self): + """UNION-based injection payload.""" + payload = "' UNION SELECT password FROM users --" + result = _escape_sql_value(payload) + assert result == "'' UNION SELECT password FROM users --" + assert "'" not in result.replace("''", "") # No unescaped quotes + + def test_stacked_query_injection(self): + """Stacked query injection attempt.""" + payload = "'; DELETE FROM users; --" + result = _escape_sql_value(payload) + assert result == "''; DELETE FROM users; --" + + def test_numeric_input_coerced_to_string(self): + """Non-string input is coerced to string.""" + result = _escape_sql_value(42) + assert result == "42" + + def test_already_escaped_quotes(self): + """Input that already contains doubled quotes.""" + result = _escape_sql_value("it''s already") + assert result == "it''''s already" + + def test_backslash_quote_bypass_attempt(self): + r"""Bypass attempt: \' should become \\'""" + payload = "\\'" + result = _escape_sql_value(payload) + # Backslash doubled, then quote doubled + assert "''" in result + assert "\\\\" in result + + +# ============================================================ +# Test _VALID_LOGIC_OPS whitelist +# ============================================================ + +class TestValidLogicOps: + """Tests for the logic operator whitelist.""" + + def test_and_accepted(self): + assert "AND" in _VALID_LOGIC_OPS + + def test_or_accepted(self): + assert "OR" in _VALID_LOGIC_OPS + + def test_injection_via_logic_rejected(self): + """SQL injection via logic field: 'AND 1=1) UNION SELECT...'""" + assert "AND 1=1) UNION SELECT" not in _VALID_LOGIC_OPS + + def test_semicolon_rejected(self): + assert ";" not in _VALID_LOGIC_OPS + + def test_drop_rejected(self): + assert "DROP" not in _VALID_LOGIC_OPS + + def test_empty_string_rejected(self): + assert "" not in _VALID_LOGIC_OPS + + def test_only_two_operators(self): + """Whitelist should contain exactly AND and OR.""" + assert len(_VALID_LOGIC_OPS) == 2 + + def test_case_insensitive_validation(self): + """Verify the code uses .upper() for comparison (based on source review).""" + # The source does: logic.upper() not in _VALID_LOGIC_OPS + # So 'and', 'And', etc. should all match via .upper() + assert "and".upper() in _VALID_LOGIC_OPS + assert "or".upper() in _VALID_LOGIC_OPS + assert "Or".upper() in _VALID_LOGIC_OPS + + +# ============================================================ +# Test SQL fragment construction safety +# ============================================================ + +class TestSqlFragmentSafety: + """End-to-end tests simulating how escaped values are used in SQL fragments.""" + + def test_in_clause_safe(self): + """Simulate IN clause with malicious enum values.""" + values = ["safe", "' OR 1=1 --", "also_safe"] + escaped = [_escape_sql_value(v) for v in values] + sql = "(" + "field" + " IN ('" + "','".join(escaped) + "'))" + # The injection payload's quote is doubled, so it stays inside the literal + assert "'' OR 1=1 --" in sql + # There should be no unmatched quote that breaks out + assert sql.count("'") % 2 == 0 # Even number of quotes + + def test_like_clause_safe(self): + """Simulate LIKE clause with injection attempt.""" + value = "' OR 1=1 --" + escaped = _escape_sql_value(value) + sql = f"field LIKE '%{escaped}%'" + assert "'' OR 1=1 --" in sql + assert sql.count("'") % 2 == 0 + + def test_eq_clause_safe(self): + """Simulate equality clause with injection attempt.""" + value = "'; DROP TABLE users; --" + escaped = _escape_sql_value(value) + sql = f"field = '{escaped}'" + assert "''; DROP TABLE users; --" in sql + assert sql.count("'") % 2 == 0 + + def test_nvarchar_in_clause_safe(self): + """Simulate SQL Server N-prefixed IN clause.""" + values = ["normal", "O'Brien"] + escaped = [_escape_sql_value(v) for v in values] + sql = "(" + "field" + " IN (N'" + "',N'".join(escaped) + "'))" + assert "O''Brien" in sql + assert sql.count("'") % 2 == 0 + + def test_legitimate_comma_in_value(self): + """Values with commas should be escaped, not split.""" + value = "New York, NY" + escaped = _escape_sql_value(value) + assert escaped == "New York, NY" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 5b94406c3a8902c7780b43a5fddaf90dea0b549e Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 11 Jun 2026 17:27:51 +0800 Subject: [PATCH 049/113] fix: Fix SQL injection / LLM Prompt Injection vulnerability causing unauthorized queries Security hardening: - Add SQLBOT_ALLOW_METADATA_QUERIES config option, disable SHOW/DESCRIBE/EXPLAIN by default - Add table whitelist check, use sqlglot to parse actual SQL table names and compare with authorized table list - Add dangerous function check, block LOAD_FILE, INTO OUTFILE, EXEC etc. by database type - Improve check_sql_read to return specific error reasons for better debugging --- backend/apps/chat/task/llm.py | 43 ++++++++++- backend/apps/db/db.py | 107 ++++++++++++++++++++++---- backend/apps/system/crud/assistant.py | 4 +- backend/common/core/config.py | 4 + 4 files changed, 141 insertions(+), 17 deletions(-) diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index 26af4845d..ca58c4647 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -13,6 +13,8 @@ import pandas as pd import requests import sqlparse +import sqlglot +from sqlglot import exp from langchain.chat_models.base import BaseChatModel from langchain_community.utilities import SQLDatabase from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, BaseMessageChunk @@ -40,7 +42,7 @@ from apps.datasource.crud.permission import get_row_permission_filters, is_normal_user from apps.datasource.embedding.ds_embedding import get_ds_embedding from apps.datasource.models.datasource import CoreDatasource -from apps.db.db import exec_sql, get_version, check_connection +from apps.db.db import exec_sql, get_version, check_connection, get_sqlglot_dialect from apps.system.crud.aimodel_manage import get_ai_model_list_by_workspace from apps.system.crud.assistant import AssistantOutDs, AssistantOutDsFactory, get_assistant_ds from apps.system.crud.parameter_manage import get_groups @@ -66,6 +68,23 @@ i18n = I18n() + +def extract_tables_from_sql(sql: str, ds_type: str = None) -> set: + """从 SQL 中提取表名(使用 sqlglot 解析,可信)""" + tables = set() + dialect = get_sqlglot_dialect(ds_type) + try: + statements = sqlglot.parse(sql, dialect=dialect) + for stmt in statements: + if stmt: + for table in stmt.find_all(exp.Table): + if table.name: + tables.add(table.name) + except Exception: + pass + return tables + + class LLMService: ds: CoreDatasource chat_question: ChatQuestion @@ -106,6 +125,9 @@ def __init__(self, session: Session, current_user: CurrentUser, chat_question: C self.chunk_list = [] self.current_user = current_user self.current_assistant = current_assistant + + self.table_name_list = [] + chat_id = chat_question.chat_id chat: Chat | None = session.get(Chat, chat_id) if not chat: @@ -222,7 +244,7 @@ def is_running(self, timeout=0.5): def init_messages(self, session: Session): - self.choose_table_schema(session) + self.table_name_list = self.choose_table_schema(session) last_sql_messages: List[dict[str, Any]] = self.generate_sql_logs[-1].messages if len( self.generate_sql_logs) > 0 else [] @@ -404,6 +426,7 @@ def choose_table_schema(self, _session: Session): self.current_logs[OperationEnum.CHOOSE_TABLE] = end_log(session=_session, log=self.current_logs[OperationEnum.CHOOSE_TABLE], full_message=self.chat_question.db_schema) + return tables def generate_analysis(self, _session: Session): fields = self.get_fields_from_chart(_session) @@ -1266,6 +1289,22 @@ def run_task(self, in_chat: bool = True, stream: bool = True, sql_operate = OperationEnum.GENERATE_SQL sql, tables = self.check_sql(session=_session, res=full_sql_text, operate=sql_operate) + + # 表名安全检查:用 sqlglot 解析真实 SQL,不信任 AI 返回的 tables + actual_tables = extract_tables_from_sql(sql, ds_type=self.ds.type) + if not actual_tables: + raise SingleMessageError( + "SQL parsing failed: unable to extract table names. " + "This may indicate an unsupported SQL syntax or a security issue." + ) + allowed_tables = set(self.table_name_list) + unauthorized_tables = actual_tables - allowed_tables + if unauthorized_tables: + raise SingleMessageError( + f"SQL contains unauthorized tables: {', '.join(unauthorized_tables)}. " + f"Allowed tables: {', '.join(allowed_tables)}" + ) + if ((not self.current_assistant or is_page_embedded) and is_normal_user( self.current_user)) or use_dynamic_ds: sql_result = None diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index b53bf2020..4e9aa4e1e 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -586,8 +586,9 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column= while sql.endswith(';'): sql = sql[:-1] # check execute sql only contain read operations - if not check_sql_read(sql, ds): - raise ValueError(f"SQL can only contain read operations") + is_safe, error_reason = check_sql_read(sql, ds) + if not is_safe: + raise ValueError(f"SQL can only contain read operations: {error_reason}") db = DB.get_db(ds.type) if db.connect_type == ConnectType.sqlalchemy: @@ -716,11 +717,78 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column= raise ParseSQLResultError(str(ex)) -def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): +def get_sqlglot_dialect(ds_type: str) -> str: + """根据数据源类型获取 sqlglot dialect""" + if equals_ignore_case(ds_type, 'mysql', 'doris', 'starrocks'): + return 'mysql' + elif equals_ignore_case(ds_type, 'sqlServer'): + return 'tsql' + elif equals_ignore_case(ds_type, 'hive'): + return 'hive' + return None + + +# 通用危险函数(适用于所有数据库) +COMMON_DANGEROUS_FUNCTIONS = {'version', 'current_user', 'user', 'database'} + +# 特定数据库的危险函数 +DS_SPECIFIC_DANGEROUS_FUNCTIONS = { + 'mysql': {'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE'}, + 'doris': {'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE'}, + 'starrocks': {'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE'}, + 'postgresql': {'pg_read_file', 'pg_write_file', 'lo_import', 'lo_export'}, + 'sqlserver': {'EXEC', 'xp_cmdshell', 'sp_executesql'}, + 'oracle': {'UTL_FILE', 'DBMS_PIPE', 'DBMS_LOCK'}, + 'hive': {'ADD FILE', 'ADD JAR'}, +} + +# 危险模式正则表达式(用于检查特殊语法) +import re +DANGEROUS_PATTERNS = [ + r'\bINTO\s+OUTFILE\b', + r'\bINTO\s+DUMPFILE\b', + r'\bEXEC\s*\(', + r'\bCOPY\s+.*\bTO\s+PROGRAM\b', +] + + +def get_dangerous_functions(ds_type: str) -> set: + """获取危险函数(通用 + 特定数据源)""" + functions = COMMON_DANGEROUS_FUNCTIONS.copy() + ds_key = ds_type.lower() if ds_type else '' + if ds_key in DS_SPECIFIC_DANGEROUS_FUNCTIONS: + functions.update(DS_SPECIFIC_DANGEROUS_FUNCTIONS[ds_key]) + return functions + + +def check_dangerous_functions(statements: list, ds_type: str) -> bool: + """检查是否使用了危险函数,返回 True 表示安全""" + dangerous_functions = get_dangerous_functions(ds_type) + dangerous_functions_upper = {f.upper() for f in dangerous_functions} + + for stmt in statements: + if stmt: + for func in stmt.find_all(exp.Anonymous): + if func.name.upper() in dangerous_functions_upper: + return False + return True + + +def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema) -> tuple[bool, str]: + """ + 检查 SQL 是否为安全的只读查询 + 返回: (是否安全, 错误原因) + """ try: normalized_sql = sql.strip().lstrip("(").strip() first_keyword = normalized_sql.split(None, 1)[0].upper() if normalized_sql else "" - allowed_read_commands = {"SELECT", "WITH", "SHOW", "DESCRIBE", "DESC", "EXPLAIN"} + + # 根据配置决定是否允许元数据查询 + if settings.SQLBOT_ALLOW_METADATA_QUERIES: + allowed_read_commands = {"SELECT", "WITH", "SHOW", "DESCRIBE", "DESC", "EXPLAIN"} + else: + allowed_read_commands = {"SELECT", "WITH"} + denied_write_commands = { "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "TRUNCATE", "MERGE", "COPY", "REPLACE", "GRANT", "REVOKE", @@ -730,21 +798,29 @@ def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): if not first_keyword: raise ValueError("Parse SQL Error") if first_keyword in denied_write_commands: - return False + return False, f"Write operation '{first_keyword}' is not allowed" - dialect = None - if equals_ignore_case(ds.type, 'mysql', 'doris', 'starrocks'): - dialect = 'mysql' - elif equals_ignore_case(ds.type, 'sqlServer'): - dialect = 'tsql' - elif equals_ignore_case(ds.type, 'hive'): - dialect = 'hive' + # 1. 使用正则检查特殊模式 + for pattern in DANGEROUS_PATTERNS: + if re.search(pattern, sql, re.IGNORECASE): + return False, f"SQL contains dangerous pattern: {pattern}" + dialect = get_sqlglot_dialect(ds.type) statements = sqlglot.parse(sql, dialect=dialect) if not statements: raise ValueError("Parse SQL Error") + # 2. 使用 sqlglot 检查函数调用 + dangerous_functions = get_dangerous_functions(ds.type) + dangerous_functions_upper = {f.upper() for f in dangerous_functions} + for stmt in statements: + if stmt: + for func in stmt.find_all(exp.Anonymous): + if func.name.upper() in dangerous_functions_upper: + return False, f"SQL contains dangerous function: {func.name}" + + # 3. 检查写操作类型 write_types = ( exp.Insert, exp.Update, exp.Delete, exp.Create, exp.Drop, exp.Alter, @@ -755,9 +831,12 @@ def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): if stmt is None: continue if isinstance(stmt, write_types): - return False + return False, f"SQL contains write operation: {type(stmt).__name__}" + + if first_keyword not in allowed_read_commands: + return False, f"SQL command '{first_keyword}' is not allowed. Only SELECT and WITH are permitted" - return first_keyword in allowed_read_commands + return True, "" except Exception as e: raise ValueError(f"Parse SQL Error: {e}") diff --git a/backend/apps/system/crud/assistant.py b/backend/apps/system/crud/assistant.py index 1fa5eb27c..29b4b5229 100644 --- a/backend/apps/system/crud/assistant.py +++ b/backend/apps/system/crud/assistant.py @@ -187,6 +187,7 @@ def get_db_schema(self, ds_id: int, question: str = '', embedding: bool = True, db_name = ds.db_schema if ds.db_schema is not None and ds.db_schema != "" else ds.dataBase schema_str += f"【DB_ID】 {db_name}\n【Schema】\n" tables = [] + table_name_list = [] i = 0 for table in ds.tables: # 如果传入了 table_list,则只处理在列表中的表 @@ -213,6 +214,7 @@ def get_db_schema(self, ds_id: int, question: str = '', embedding: bool = True, schema_table += '\n]\n' t_obj = {"id": i, "schema_table": schema_table} tables.append(t_obj) + table_name_list.append(table.name) # do table embedding # if embedding and tables and settings.TABLE_EMBEDDING_ENABLED: @@ -222,7 +224,7 @@ def get_db_schema(self, ds_id: int, question: str = '', embedding: bool = True, for s in tables: schema_str += s.get('schema_table') - return schema_str, [] + return schema_str, table_name_list def get_ds(self, ds_id: int, trans: Trans = None): if self.ds_list: diff --git a/backend/common/core/config.py b/backend/common/core/config.py index 1b3cc24ef..6782c1609 100644 --- a/backend/common/core/config.py +++ b/backend/common/core/config.py @@ -111,6 +111,10 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn | str: GENERATE_SQL_QUERY_LIMIT_ENABLED: bool = True GENERATE_SQL_QUERY_HISTORY_ROUND_COUNT: int = 3 + # 安全配置:是否允许元数据查询(SHOW/DESCRIBE/DESC/EXPLAIN) + # 默认关闭,防止通过元数据查询泄露数据库结构 + SQLBOT_ALLOW_METADATA_QUERIES: bool = False + PARSE_REASONING_BLOCK_ENABLED: bool = True DEFAULT_REASONING_CONTENT_START: str = '' DEFAULT_REASONING_CONTENT_END: str = '' From cc45f47c12dcdc599cc7e3927adcc55801eb334c Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 11 Jun 2026 17:33:42 +0800 Subject: [PATCH 050/113] fix: Fix MCP oid missing validation --- backend/apps/mcp/mcp.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/apps/mcp/mcp.py b/backend/apps/mcp/mcp.py index 4afe1465e..9d5d08742 100644 --- a/backend/apps/mcp/mcp.py +++ b/backend/apps/mcp/mcp.py @@ -105,9 +105,14 @@ async def ws_list(session: SessionDep, trans: Trans, token: str): @router.post("/mcp_ds_list", operation_id="mcp_datasource_list") -async def datasource_list(session: SessionDep, mcp_ds: McpDs): +async def datasource_list(session: SessionDep, trans: Trans, mcp_ds: McpDs): session_user = get_user(session, mcp_ds.token) if mcp_ds.oid: + w_list = await user_ws_options(session, session_user.id, trans) + oid_list = [item.id for item in w_list] + if int(mcp_ds.oid) not in oid_list: + raise HTTPException(status_code=400, detail="This user not in current workspace") + session_user.oid = int(mcp_ds.oid) ds_list = get_datasource_list(session=session, user=session_user) result = [] @@ -129,12 +134,17 @@ async def datasource_list(session: SessionDep, mcp_ds: McpDs): @router.post("/mcp_question", operation_id="mcp_question") -async def mcp_question(session: SessionDep, chat: McpQuestion): +async def mcp_question(session: SessionDep, trans: Trans, chat: McpQuestion): session_user = get_user(session, chat.token) lang = chat.lang if lang in ["zh-CN", "zh-TW", "en", "ko-KR"]: session_user.language = lang if chat.oid: + w_list = await user_ws_options(session, session_user.id, trans) + oid_list = [item.id for item in w_list] + if int(chat.oid) not in oid_list: + raise HTTPException(status_code=400, detail="This user not in current workspace") + session_user.oid = int(chat.oid) ds_id: Optional[int] = None if chat.datasource_id: From 447592b767fc1de392dd396dc9c960418f837c06 Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 11 Jun 2026 17:48:47 +0800 Subject: [PATCH 051/113] fix: Fix MCP oid missing validation --- backend/apps/mcp/mcp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/apps/mcp/mcp.py b/backend/apps/mcp/mcp.py index 9d5d08742..6185ff96d 100644 --- a/backend/apps/mcp/mcp.py +++ b/backend/apps/mcp/mcp.py @@ -111,7 +111,7 @@ async def datasource_list(session: SessionDep, trans: Trans, mcp_ds: McpDs): w_list = await user_ws_options(session, session_user.id, trans) oid_list = [item.id for item in w_list] if int(mcp_ds.oid) not in oid_list: - raise HTTPException(status_code=400, detail="This user not in current workspace") + raise HTTPException(status_code=400, detail="The current user is not in the selected workspace") session_user.oid = int(mcp_ds.oid) ds_list = get_datasource_list(session=session, user=session_user) @@ -143,7 +143,7 @@ async def mcp_question(session: SessionDep, trans: Trans, chat: McpQuestion): w_list = await user_ws_options(session, session_user.id, trans) oid_list = [item.id for item in w_list] if int(chat.oid) not in oid_list: - raise HTTPException(status_code=400, detail="This user not in current workspace") + raise HTTPException(status_code=400, detail="The current user is not in the selected workspace") session_user.oid = int(chat.oid) ds_id: Optional[int] = None From 4af99e0c45ecae2171152d54244a4514d5fcee84 Mon Sep 17 00:00:00 2001 From: ulleo Date: Tue, 16 Jun 2026 17:15:53 +0800 Subject: [PATCH 052/113] feat: support configuring terminology for specified advanced applications --- .gitignore | 2 + backend/alembic/env.py | 4 +- .../versions/069_term_custom_prompt.py | 31 +++ backend/apps/chat/task/llm.py | 44 ++-- backend/apps/terminology/api/terminology.py | 13 +- backend/apps/terminology/curd/terminology.py | 215 ++++++++++++++---- .../terminology/models/terminology_model.py | 3 + backend/pyproject.toml | 2 +- .../src/views/system/professional/index.vue | 68 ++++-- frontend/src/views/system/prompt/index.vue | 67 ++++-- 10 files changed, 350 insertions(+), 99 deletions(-) create mode 100644 backend/alembic/versions/069_term_custom_prompt.py diff --git a/.gitignore b/.gitignore index fab542482..493b308db 100644 --- a/.gitignore +++ b/.gitignore @@ -189,3 +189,5 @@ test.py !/.venv/ + +sqlbot-xpack diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 6b30c53e4..19e19fce6 100755 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -25,8 +25,8 @@ # from apps.system.models.user import SQLModel # noqa # from apps.settings.models.setting_models import SQLModel #from apps.chat.models.chat_model import SQLModel -#from apps.terminology.models.terminology_model import SQLModel -#from apps.custom_prompt.models.custom_prompt_model import SQLModel +from apps.terminology.models.terminology_model import SQLModel +from sqlbot_xpack.custom_prompt.models.custom_prompt_model import SQLModel #from apps.data_training.models.data_training_model import SQLModel # from apps.dashboard.models.dashboard_model import SQLModel from common.core.config import settings # noqa diff --git a/backend/alembic/versions/069_term_custom_prompt.py b/backend/alembic/versions/069_term_custom_prompt.py new file mode 100644 index 000000000..e02aaac37 --- /dev/null +++ b/backend/alembic/versions/069_term_custom_prompt.py @@ -0,0 +1,31 @@ +"""069_term_custom_prompt + +Revision ID: 1f82cad3546e +Revises: a1b2c3d4e5f6 +Create Date: 2026-06-15 14:51:12.280391 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '1f82cad3546e' +down_revision = 'a1b2c3d4e5f6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('custom_prompt', sa.Column('advanced_application', sa.BigInteger(), nullable=True)) + op.add_column('terminology', sa.Column('advanced_application', sa.BigInteger(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('terminology', 'advanced_application') + op.drop_column('custom_prompt', 'advanced_application') + # ### end Alembic commands ### diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index ca58c4647..be3913345 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -12,9 +12,8 @@ import orjson import pandas as pd import requests -import sqlparse import sqlglot -from sqlglot import exp +import sqlparse from langchain.chat_models.base import BaseChatModel from langchain_community.utilities import SQLDatabase from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, BaseMessageChunk @@ -24,6 +23,7 @@ from sqlbot_xpack.custom_prompt.curd.custom_prompt import find_custom_prompts from sqlbot_xpack.custom_prompt.models.custom_prompt_model import CustomPromptTypeEnum from sqlbot_xpack.license.license_manage import SQLBotLicenseUtil +from sqlglot import exp from sqlmodel import Session from apps.ai_model.model_factory import LLMConfig, LLMFactory, get_default_config @@ -68,7 +68,6 @@ i18n = I18n() - def extract_tables_from_sql(sql: str, ds_type: str = None) -> set: """从 SQL 中提取表名(使用 sqlglot 解析,可信)""" tables = set() @@ -340,18 +339,26 @@ def get_fields_from_chart(self, _session: Session): return format_chart_fields(chart_info) def filter_terminology_template(self, _session: Session, oid: int = None, ds_id: int = None): + self.current_logs[OperationEnum.FILTER_TERMS] = start_log(session=_session, + operate=OperationEnum.FILTER_TERMS, + record_id=self.record.id, local_operation=True) calculate_oid = oid calculate_ds_id = ds_id if self.current_assistant: calculate_oid = self.current_assistant.oid if self.current_assistant.type != 4 else self.current_user.oid if self.current_assistant.type == 1: calculate_ds_id = None - self.current_logs[OperationEnum.FILTER_TERMS] = start_log(session=_session, - operate=OperationEnum.FILTER_TERMS, - record_id=self.record.id, local_operation=True) + if self.current_assistant and self.current_assistant.type == 1: + self.chat_question.terminologies, term_list = get_terminology_template(_session, + self.chat_question.question, + calculate_oid, + None, self.current_assistant.id) + else: + self.chat_question.terminologies, term_list = get_terminology_template(_session, + self.chat_question.question, + calculate_oid, + calculate_ds_id) - self.chat_question.terminologies, term_list = get_terminology_template(_session, self.chat_question.question, - calculate_oid, calculate_ds_id) self.current_logs[OperationEnum.FILTER_TERMS] = end_log(session=_session, log=self.current_logs[OperationEnum.FILTER_TERMS], full_message=term_list) @@ -359,19 +366,26 @@ def filter_terminology_template(self, _session: Session, oid: int = None, ds_id: def filter_custom_prompts(self, _session: Session, custom_prompt_type: CustomPromptTypeEnum, oid: int = None, ds_id: int = None): if SQLBotLicenseUtil.valid(): + self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = start_log(session=_session, + operate=OperationEnum.FILTER_CUSTOM_PROMPT, + record_id=self.record.id, + local_operation=True) calculate_oid = oid calculate_ds_id = ds_id if self.current_assistant: calculate_oid = self.current_assistant.oid if self.current_assistant.type != 4 else self.current_user.oid if self.current_assistant.type == 1: calculate_ds_id = None - self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = start_log(session=_session, - operate=OperationEnum.FILTER_CUSTOM_PROMPT, - record_id=self.record.id, - local_operation=True) - self.chat_question.custom_prompt, prompt_list = find_custom_prompts(_session, custom_prompt_type, - calculate_oid, - calculate_ds_id) + if self.current_assistant and self.current_assistant.type == 1: + self.chat_question.custom_prompt, prompt_list = find_custom_prompts(_session, + custom_prompt_type, + calculate_oid, + None, self.current_assistant.id) + else: + self.chat_question.custom_prompt, prompt_list = find_custom_prompts(_session, + custom_prompt_type, + calculate_oid, + calculate_ds_id) self.current_logs[OperationEnum.FILTER_CUSTOM_PROMPT] = end_log(session=_session, log=self.current_logs[ OperationEnum.FILTER_CUSTOM_PROMPT], diff --git a/backend/apps/terminology/api/terminology.py b/backend/apps/terminology/api/terminology.py index b74cb19bd..979cdf41f 100644 --- a/backend/apps/terminology/api/terminology.py +++ b/backend/apps/terminology/api/terminology.py @@ -82,6 +82,7 @@ def inner(): "description": obj.description, "all_data_sources": 'N' if obj.specific_ds else 'Y', "datasource": ', '.join(obj.datasource_names) if obj.datasource_names and obj.specific_ds else '', + "advanced_application_name": obj.advanced_application_name or '', } data_list.append(_data) @@ -91,6 +92,7 @@ def inner(): fields.append(AxisObj(name=trans('i18n_terminology.term_description'), value='description')) fields.append(AxisObj(name=trans('i18n_terminology.effective_data_sources'), value='datasource')) fields.append(AxisObj(name=trans('i18n_terminology.all_data_sources'), value='all_data_sources')) + fields.append(AxisObj(name=trans('i18n_data_training.advanced_application'), value='advanced_application_name')) md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list) @@ -119,6 +121,7 @@ def inner(): "description": trans('i18n_terminology.term_description_template_example_1'), "all_data_sources": 'N', "datasource": trans('i18n_terminology.effective_data_sources_template_example_1'), + "advanced_application_name": '', } data_list.append(_data1) _data2 = { @@ -127,6 +130,7 @@ def inner(): "description": trans('i18n_terminology.term_description_template_example_2'), "all_data_sources": 'Y', "datasource": '', + "advanced_application_name": '', } data_list.append(_data2) @@ -136,6 +140,7 @@ def inner(): fields.append(AxisObj(name=trans('i18n_terminology.term_description_template'), value='description')) fields.append(AxisObj(name=trans('i18n_terminology.effective_data_sources_template'), value='datasource')) fields.append(AxisObj(name=trans('i18n_terminology.all_data_sources_template'), value='all_data_sources')) + fields.append(AxisObj(name=trans('i18n_data_training.advanced_application'), value='advanced_application_name')) md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list) @@ -180,7 +185,7 @@ async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile oid = current_user.oid - use_cols = [0, 1, 2, 3, 4] + use_cols = [0, 1, 2, 3, 4, 5] def inner(): @@ -217,9 +222,11 @@ def inner(): 3].strip() else [] all_datasource = True if pd.notna(row[4]) and row[4].lower().strip() in ['y', 'yes', 'true'] else False specific_ds = False if all_datasource else True + advanced_application_name = row[5].strip() if pd.notna(row[5]) and row[5].strip() else None import_data.append(TerminologyInfo(word=word, description=description, other_words=other_words, - datasource_names=datasource_names, specific_ds=specific_ds)) + datasource_names=datasource_names, specific_ds=specific_ds, + advanced_application_name=advanced_application_name)) res = batch_create_terminology(session, import_data, oid, trans) @@ -237,6 +244,7 @@ def inner(): "all_data_sources": 'N' if obj['data'].specific_ds else 'Y', "datasource": ', '.join(obj['data'].datasource_names) if obj['data'].datasource_names and obj[ 'data'].specific_ds else '', + "advanced_application_name": obj['data'].advanced_application_name or '', "errors": obj['errors'] } data_list.append(_data) @@ -247,6 +255,7 @@ def inner(): fields.append(AxisObj(name=trans('i18n_terminology.term_description'), value='description')) fields.append(AxisObj(name=trans('i18n_terminology.effective_data_sources'), value='datasource')) fields.append(AxisObj(name=trans('i18n_terminology.all_data_sources'), value='all_data_sources')) + fields.append(AxisObj(name=trans('i18n_data_training.advanced_application'), value='advanced_application_name')) fields.append(AxisObj(name=trans('i18n_data_training.error_info'), value='errors')) md_data, _fields_list = DataFormat.convert_object_array_for_pandas(fields, data_list) diff --git a/backend/apps/terminology/curd/terminology.py b/backend/apps/terminology/curd/terminology.py index 296fd1eba..42ecf4f80 100644 --- a/backend/apps/terminology/curd/terminology.py +++ b/backend/apps/terminology/curd/terminology.py @@ -10,6 +10,7 @@ from apps.ai_model.embedding import EmbeddingModelCache from apps.datasource.models.datasource import CoreDatasource +from apps.system.models.system_model import AssistantModel from apps.template.generate_chart.generator import get_base_terminology_template from apps.terminology.models.terminology_model import Terminology, TerminologyInfo from common.core.config import settings @@ -141,7 +142,9 @@ def build_terminology_query(session: SessionDep, oid: int, name: Optional[str] = Terminology.datasource_ids, children_subquery.c.other_words, func.jsonb_agg(CoreDatasource.name).filter(CoreDatasource.id.isnot(None)).label('datasource_names'), - Terminology.enabled + Terminology.enabled, + Terminology.advanced_application, + AssistantModel.name.label('advanced_application_name'), ) .outerjoin( children_subquery, @@ -155,6 +158,8 @@ def build_terminology_query(session: SessionDep, oid: int, name: Optional[str] = CoreDatasource, CoreDatasource.id == datasource_names_subquery.c.ds_id ) + .outerjoin(AssistantModel, + and_(Terminology.advanced_application == AssistantModel.id, AssistantModel.type == 1)) .where(and_(Terminology.id.in_(paginated_parent_ids), Terminology.oid == oid)) .group_by( Terminology.id, @@ -163,6 +168,8 @@ def build_terminology_query(session: SessionDep, oid: int, name: Optional[str] = Terminology.description, Terminology.specific_ds, Terminology.datasource_ids, + Terminology.advanced_application, + AssistantModel.name, children_subquery.c.other_words, Terminology.enabled ) @@ -190,6 +197,8 @@ def execute_terminology_query(session: SessionDep, stmt) -> List[TerminologyInfo datasource_ids=row.datasource_ids if row.datasource_ids is not None else [], datasource_names=row.datasource_names if row.datasource_names is not None else [], enabled=row.enabled if row.enabled is not None else False, + advanced_application=str(row.advanced_application) if row.advanced_application else None, + advanced_application_name=row.advanced_application_name, )) return _list @@ -240,8 +249,8 @@ def create_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra datasource_ids = info.datasource_ids if info.datasource_ids is not None else [] if specific_ds: - if not datasource_ids: - raise Exception(trans("i18n_terminology.datasource_cannot_be_none")) + if not datasource_ids and info.advanced_application is None: + raise Exception(trans("i18n_data_training.datasource_assistant_cannot_be_none")) parent = Terminology( word=info.word.strip(), @@ -250,7 +259,8 @@ def create_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra oid=oid, specific_ds=specific_ds, enabled=info.enabled, - datasource_ids=datasource_ids + datasource_ids=datasource_ids, + advanced_application=info.advanced_application, ) words = [info.word.strip()] @@ -267,30 +277,63 @@ def create_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra # 基础查询条件(word 和 oid 必须满足) base_query = and_( Terminology.word.in_(words), - Terminology.oid == oid + Terminology.oid == oid, ) # 构建查询 query = session.query(Terminology).filter(base_query) - if specific_ds: - # 仅当 specific_ds=False 时,检查数据源条件 - query = query.where( - or_( - or_(Terminology.specific_ds == False, Terminology.specific_ds.is_(None)), - and_( - Terminology.specific_ds == True, - Terminology.datasource_ids.isnot(None), - text(""" - EXISTS ( - SELECT 1 FROM jsonb_array_elements(datasource_ids) AS elem - WHERE elem::text::int = ANY(:datasource_ids) - ) - """) + # 作用域重复检查 + scope_conditions = [] + + if not specific_ds: + # 全部数据源:与有数据源的记录冲突,也与同为全部数据源的记录冲突 + scope_conditions.append( + and_( + Terminology.specific_ds == True, + Terminology.datasource_ids.isnot(None), + func.jsonb_array_length(Terminology.datasource_ids) > 0, + ) + ) + scope_conditions.append( + Terminology.specific_ds == False, + ) + elif specific_ds and datasource_ids: + # 指定数据源:与全部数据源冲突,也与有数据源重叠的记录冲突 + scope_conditions.append( + Terminology.specific_ds == False, + ) + ds_overlap_conditions = [ + Terminology.datasource_ids.contains([ds_id]) + for ds_id in datasource_ids + ] + scope_conditions.append( + and_( + Terminology.specific_ds == True, + Terminology.datasource_ids.isnot(None), + or_(*ds_overlap_conditions) + ) + ) + else: + # 不选数据源:仅与同为不选数据源的记录冲突 + scope_conditions.append( + and_( + Terminology.specific_ds == True, + or_( + Terminology.datasource_ids.is_(None), + func.jsonb_array_length(Terminology.datasource_ids) == 0, ) ) ) - query = query.params(datasource_ids=datasource_ids) + + # 高级应用重复检查:advanced_application 相同时同名即重复 + if info.advanced_application is not None: + scope_conditions.append( + Terminology.advanced_application == info.advanced_application + ) + + if scope_conditions: + query = query.where(or_(*scope_conditions)) # 转换为 EXISTS 查询并获取结果 exists = session.query(query.exists()).scalar() @@ -396,6 +439,14 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf for ds in datasource_result: datasource_name_to_id[ds.name.strip()] = ds.id + # 预加载高级应用名称到ID的映射 + assistant_name_to_id = {} + assistant_stmt = select(AssistantModel.id, AssistantModel.name).where( + and_(AssistantModel.oid == oid, AssistantModel.type == 1)) + assistant_result = session.execute(assistant_stmt).all() + for a in assistant_result: + assistant_name_to_id[a.name.strip()] = a.id + # 验证和转换数据源名称 valid_records = [] for info in deduplicated_list: @@ -411,6 +462,7 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf # 根据specific_ds决定是否验证数据源 specific_ds = info.specific_ds if info.specific_ds is not None else False datasource_ids = [] + advanced_application = info.advanced_application if specific_ds: # specific_ds为True时需要验证数据源 @@ -424,12 +476,21 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf else: error_messages.append(trans("i18n_terminology.datasource_not_found").format(ds_name)) - # 检查specific_ds为True时必须有数据源 - if not datasource_ids: - error_messages.append(trans("i18n_terminology.datasource_cannot_be_none")) + # 解析高级应用名称到ID + if advanced_application is None and info.advanced_application_name: + if info.advanced_application_name.strip() in assistant_name_to_id: + advanced_application = assistant_name_to_id[info.advanced_application_name.strip()] + else: + error_messages.append(trans("i18n_data_training.advanced_application_not_found").format( + info.advanced_application_name)) + + # 检查specific_ds为True时datasource_ids和advanced_application不能同时为空 + if specific_ds and not datasource_ids and advanced_application is None: + error_messages.append(trans("i18n_data_training.datasource_assistant_cannot_be_none")) else: # specific_ds为False时忽略数据源名称 datasource_ids = [] + advanced_application = None # 检查主词和其他词是否重复(过滤空字符串) words = [info.word.strip().lower()] @@ -456,11 +517,13 @@ def batch_create_terminology(session: SessionDep, info_list: List[TerminologyInf processed_info = TerminologyInfo( word=info.word.strip(), description=info.description.strip(), - other_words=[w for w in info.other_words if w and w.strip()], # 过滤空字符串 + other_words=[w for w in info.other_words if w and w.strip()], datasource_ids=datasource_ids, datasource_names=info.datasource_names, specific_ds=specific_ds, - enabled=info.enabled if info.enabled is not None else True + enabled=info.enabled if info.enabled is not None else True, + advanced_application=advanced_application, + advanced_application_name=info.advanced_application_name, ) valid_records.append(processed_info) @@ -512,8 +575,8 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra datasource_ids = info.datasource_ids if info.datasource_ids is not None else [] if specific_ds: - if not datasource_ids: - raise Exception(trans("i18n_terminology.datasource_cannot_be_none")) + if not datasource_ids and info.advanced_application is None: + raise Exception(trans("i18n_data_training.datasource_assistant_cannot_be_none")) words = [info.word.strip()] for child in info.other_words: @@ -536,24 +599,57 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra # 构建查询 query = session.query(Terminology).filter(base_query) - if specific_ds: - # 仅当 specific_ds=False 时,检查数据源条件 - query = query.where( - or_( - or_(Terminology.specific_ds == False, Terminology.specific_ds.is_(None)), - and_( - Terminology.specific_ds == True, - Terminology.datasource_ids.isnot(None), - text(""" - EXISTS ( - SELECT 1 FROM jsonb_array_elements(datasource_ids) AS elem - WHERE elem::text::int = ANY(:datasource_ids) - ) - """) # 检查是否包含任意目标值 + # 作用域重复检查 + scope_conditions = [] + + if not specific_ds: + # 全部数据源:与有数据源的记录冲突,也与同为全部数据源的记录冲突 + scope_conditions.append( + and_( + Terminology.specific_ds == True, + Terminology.datasource_ids.isnot(None), + func.jsonb_array_length(Terminology.datasource_ids) > 0, + ) + ) + scope_conditions.append( + Terminology.specific_ds == False, + ) + elif specific_ds and datasource_ids: + # 指定数据源:与全部数据源冲突,也与有数据源重叠的记录冲突 + scope_conditions.append( + Terminology.specific_ds == False, + ) + ds_overlap_conditions = [ + Terminology.datasource_ids.contains([ds_id]) + for ds_id in datasource_ids + ] + scope_conditions.append( + and_( + Terminology.specific_ds == True, + Terminology.datasource_ids.isnot(None), + or_(*ds_overlap_conditions) + ) + ) + else: + # 不选数据源:仅与同为不选数据源的记录冲突 + scope_conditions.append( + and_( + Terminology.specific_ds == True, + or_( + Terminology.datasource_ids.is_(None), + func.jsonb_array_length(Terminology.datasource_ids) == 0, ) ) ) - query = query.params(datasource_ids=datasource_ids) + + # 高级应用重复检查:advanced_application 相同时同名即重复 + if info.advanced_application is not None: + scope_conditions.append( + Terminology.advanced_application == info.advanced_application + ) + + if scope_conditions: + query = query.where(or_(*scope_conditions)) # 转换为 EXISTS 查询并获取结果 exists = session.query(query.exists()).scalar() @@ -567,6 +663,7 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra specific_ds=specific_ds, datasource_ids=datasource_ids, enabled=info.enabled, + advanced_application=info.advanced_application, ) session.execute(stmt) session.commit() @@ -590,7 +687,8 @@ def update_terminology(session: SessionDep, info: TerminologyInfo, oid: int, tra oid=oid, enabled=info.enabled, specific_ds=specific_ds, - datasource_ids=datasource_ids + datasource_ids=datasource_ids, + advanced_application=info.advanced_application, ) ) @@ -711,8 +809,22 @@ def save_embeddings(session_maker, ids: List[int]): LIMIT {settings.EMBEDDING_TERMINOLOGY_TOP_COUNT} """ +embedding_sql_with_advanced_application = f""" +SELECT id, pid, word, similarity +FROM +(SELECT id, pid, word, oid, specific_ds, advanced_application, enabled, +( 1 - (embedding <=> :embedding_array) ) AS similarity +FROM terminology AS child +) TEMP +WHERE similarity > {settings.EMBEDDING_TERMINOLOGY_SIMILARITY} AND oid = :oid AND enabled = true +AND advanced_application = :advanced_application_id +ORDER BY similarity DESC +LIMIT {settings.EMBEDDING_TERMINOLOGY_TOP_COUNT} +""" -def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasource: int = None): + +def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasource: int = None, + advanced_application_id: Optional[int] = None): if word.strip() == "": return [] @@ -729,7 +841,9 @@ def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasou ) ) - if datasource is not None: + if advanced_application_id is not None: + stmt = stmt.where(Terminology.advanced_application == advanced_application_id) + elif datasource is not None: stmt = stmt.where( or_( or_(Terminology.specific_ds == False, Terminology.specific_ds.is_(None)), @@ -760,7 +874,11 @@ def select_terminology_by_word(session: SessionDep, word: str, oid: int, datasou embedding = model.embed_query(word) - if datasource is not None: + if advanced_application_id is not None: + results = session.execute(text(embedding_sql_with_advanced_application), + {'embedding_array': str(embedding), 'oid': oid, + 'advanced_application_id': advanced_application_id}).fetchall() + elif datasource is not None: results = session.execute(text(embedding_sql_with_datasource), {'embedding_array': str(embedding), 'oid': oid, 'datasource': datasource}).fetchall() @@ -846,10 +964,11 @@ def to_xml_string(_dict: list[dict] | dict, root: str = 'terminologies') -> str: def get_terminology_template(session: SessionDep, question: str, oid: Optional[int] = 1, - datasource: Optional[int] = None) -> tuple[str, list[dict]]: + datasource: Optional[int] = None, + advanced_application_id: Optional[int] = None) -> tuple[str, list[dict]]: if not oid: oid = 1 - _results = select_terminology_by_word(session, question, oid, datasource) + _results = select_terminology_by_word(session, question, oid, datasource, advanced_application_id) if _results and len(_results) > 0: terminology = to_xml_string(_results) template = get_base_terminology_template().format(terminologies=terminology) diff --git a/backend/apps/terminology/models/terminology_model.py b/backend/apps/terminology/models/terminology_model.py index 850aadabe..e4997d605 100644 --- a/backend/apps/terminology/models/terminology_model.py +++ b/backend/apps/terminology/models/terminology_model.py @@ -20,6 +20,7 @@ class Terminology(SQLModel, table=True): specific_ds: Optional[bool] = Field(sa_column=Column(Boolean, default=False)) datasource_ids: Optional[list[int]] = Field(sa_column=Column(JSONB), default=[]) enabled: Optional[bool] = Field(sa_column=Column(Boolean, default=True)) + advanced_application: Optional[int] = Field(sa_column=Column(BigInteger, nullable=True)) class TerminologyInfo(BaseModel): @@ -32,3 +33,5 @@ class TerminologyInfo(BaseModel): datasource_ids: Optional[list[int]] = [] datasource_names: Optional[list[str]] = [] enabled: Optional[bool] = True + advanced_application: Optional[int] = None + advanced_application_name: Optional[str] = None diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 414b1a702..1529c7317 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -39,7 +39,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.5.22,<0.0.6.0", + "sqlbot-xpack>=0.0.5.23,<0.0.6.0", "fastapi-cache2>=0.2.2", "sqlparse>=0.5.3", "redis>=6.2.0", diff --git a/frontend/src/views/system/professional/index.vue b/frontend/src/views/system/professional/index.vue index e104187e2..fde50b712 100644 --- a/frontend/src/views/system/professional/index.vue +++ b/frontend/src/views/system/professional/index.vue @@ -15,6 +15,7 @@ import { convertFilterText, FilterText } from '@/components/filter-text' import { DrawerMain } from '@/components/drawer-main' import iconFilter from '@/assets/svg/icon-filter_outlined.svg' import Uploader from '@/views/system/excel-upload/Uploader.vue' +import { getAdvancedApplicationList } from '@/api/embedded.ts' interface Form { id?: string | null @@ -23,6 +24,8 @@ interface Form { specific_ds: boolean datasource_ids: number[] datasource_names: string[] + advanced_application: string | null + advanced_application_name: string | null description: string | null } @@ -33,7 +36,7 @@ const keywords = ref('') const oldKeywords = ref('') const searchLoading = ref(false) const drawerMainRef = ref() - +const adv_options = ref([]) const selectable = () => { return true } @@ -41,6 +44,9 @@ onMounted(() => { datasourceApi.list().then((res) => { filterOption.value[0].option = [...res] }) + getAdvancedApplicationList().then((res: any) => { + adv_options.value = res || [] + }) search() }) const dialogFormVisible = ref(false) @@ -69,6 +75,8 @@ const defaultForm = { datasource_ids: [], other_words: [''], datasource_names: [], + advanced_application: null, + advanced_application_name: null, } const pageForm = ref(cloneDeep(defaultForm)) @@ -252,13 +260,13 @@ const search = ($event: any = {}) => { const termFormRef = ref() -const validatePass = (_: any, value: any, callback: any) => { - if (pageForm.value.specific_ds && !value.length) { - callback(new Error(t('datasource.Please_select') + t('common.empty') + t('ds.title'))) - } else { - callback() - } -} +// const validatePass = (_: any, value: any, callback: any) => { +// if (pageForm.value.specific_ds && !value.length) { +// callback(new Error(t('datasource.Please_select') + t('common.empty') + t('ds.title'))) +// } else { +// callback() +// } +// } const rules = { word: [ @@ -274,12 +282,12 @@ const rules = { t('datasource.please_enter') + t('common.empty') + t('professional.term_description'), }, ], - datasource_ids: [ - { - validator: validatePass, - trigger: 'blur', - }, - ], + // datasource_ids: [ + // { + // validator: validatePass, + // trigger: 'blur', + // }, + // ], } const handleChange = () => { @@ -516,6 +524,11 @@ const changeStatus = (id: any, val: any) => {
{{ t('training.all_data_sources') }}
+