From 526fb1bddcd05eefda684c9b1dfa47530fb752e3 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Mon, 8 Dec 2025 10:08:11 +0800 Subject: [PATCH 001/521] feat(Data Source): Data source table structure supports field retrieval #447 --- frontend/src/api/datasource.ts | 3 ++- frontend/src/i18n/en.json | 10 +++++----- frontend/src/views/ds/DataTable.vue | 23 ++++++++++++++++++++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/frontend/src/api/datasource.ts b/frontend/src/api/datasource.ts index 4a733185f..802f92692 100644 --- a/frontend/src/api/datasource.ts +++ b/frontend/src/api/datasource.ts @@ -17,7 +17,8 @@ export const datasourceApi = { request.post(`/datasource/execSql/${id}`, { sql: sql }), chooseTables: (id: number, data: any) => request.post(`/datasource/chooseTables/${id}`, data), tableList: (id: number) => request.post(`/datasource/tableList/${id}`), - fieldList: (id: number) => request.post(`/datasource/fieldList/${id}`), + fieldList: (id: number, data = { fieldName: '' }) => + request.post(`/datasource/fieldList/${id}`, data), edit: (data: any) => request.post('/datasource/editLocalComment', data), previewData: (id: number, data: any) => request.post(`/datasource/previewData/${id}`, data), saveTable: (data: any) => request.post('/datasource/editTable', data), diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 455202620..32928006c 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -4,11 +4,11 @@ "Data Q&A": "Data Q&A", "Data Connections": "Data Sources", "Dashboard": "Dashboard", - "AI Model Configuration": "AI Model Configuration", + "AI Model Configuration": "AI Model Config", "Details": "Details" }, "parameter": { - "parameter_configuration": "Parameter Configuration", + "parameter_configuration": "Parameter Config", "question_count_settings": "Question Count Settings", "model_thinking_process": "Expand Model Thinking Process", "rows_of_data": "Limit 1000 Rows of Data", @@ -72,7 +72,7 @@ "code_for_debugging": "Copy the following code for debugging", "full_screen_mode": "Full screen mode", "editing_terminology": "Editing Terminology", - "professional_terminology": "Terminology Configuration", + "professional_terminology": "Terminology Config", "term_name": "Term Name", "term_description": "Term Description", "search_term": "Search Term", @@ -388,7 +388,7 @@ "set_successfully": "Set successfully", "operate_with_caution": "After the system default model is replaced, the result of intelligent question will be affected, please operate with caution.", "system_default_model": "Do you want to set {msg} as the system default model?", - "ai_model_configuration": "AI model configuration", + "ai_model_configuration": "AI model config", "system_default_model_de": "System default model", "relevant_results_found": "No relevant results found", "add_model": "Add model", @@ -508,7 +508,7 @@ "find_user": "Find user", "add_successfully": "Add successfully", "member_management": "Member management", - "permission_configuration": "Permission configuration", + "permission_configuration": "Permission Config", "set": "Settings", "operate_with_caution": "After deletion, the users under the workspace will be removed and all resources will be deleted. Please operate with caution." }, diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index 6c3d73358..82bf3ffa1 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -244,13 +244,17 @@ const renderHeader = ({ column }: any) => { return column.label } +const fieldNameSearch = () => { + btnSelectClick(btnSelect.value) +} +const fieldName = ref('') const btnSelectClick = (val: any) => { btnSelect.value = val loading.value = true if (val === 'd') { datasourceApi - .fieldList(currentTable.value.id) + .fieldList(currentTable.value.id, { fieldName: fieldName.value }) .then((res) => { fieldList.value = res pageInfo.total = res.length @@ -403,6 +407,15 @@ const btnSelectClick = (val: any) => { {{ t('ds.preview') }} +
+ +
{ .table-content { padding: 16px 24px; height: calc(100% - 80px); + position: relative; + + .field-name { + position: absolute; + right: 16px; + top: 16px; + width: 240px; + } .btn-select { height: 32px; From 71e9f1fe385ed12d7c1c6987fe64c7dfc4feb03e Mon Sep 17 00:00:00 2001 From: junjun Date: Mon, 8 Dec 2025 10:07:36 +0800 Subject: [PATCH 002/521] feat: Datasource table structure supports field retrieval #447 --- backend/apps/datasource/api/datasource.py | 6 +++--- backend/apps/datasource/crud/datasource.py | 10 ++++++++-- backend/apps/datasource/crud/field.py | 14 +++++++++++--- backend/apps/datasource/models/datasource.py | 6 ++++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py index cb8fceeac..6849da369 100644 --- a/backend/apps/datasource/api/datasource.py +++ b/backend/apps/datasource/api/datasource.py @@ -20,7 +20,7 @@ check_status_by_id from ..crud.field import get_fields_by_table_id from ..crud.table import get_tables_by_ds_id -from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable, CoreField +from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable, CoreField, FieldObj router = APIRouter(tags=["datasource"], prefix="/datasource") path = settings.EXCEL_PATH @@ -164,8 +164,8 @@ async def table_list(session: SessionDep, id: int): @router.post("/fieldList/{id}") -async def field_list(session: SessionDep, id: int): - return get_fields_by_table_id(session, id) +async def field_list(session: SessionDep, id: int, field: FieldObj): + return get_fields_by_table_id(session, id, field) @router.post("/editLocalComment") diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index e3a9a8335..10e68e7a1 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -269,11 +269,17 @@ def preview(session: SessionDep, current_user: CurrentUser, id: int, data: Table ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first() # check_status(session, ds, True) - if data.fields is None or len(data.fields) == 0: + # ignore data's fields param, query fields from database + if not data.table.id: + return {"fields": [], "data": [], "sql": ''} + + fields = session.query(CoreField).filter(CoreField.table_id == data.table.id).all() + + if fields is None or len(fields) == 0: return {"fields": [], "data": [], "sql": ''} where = '' - f_list = [f for f in data.fields if f.checked] + f_list = [f for f in fields if f.checked] if is_normal_user(current_user): # column is checked, and, column permission for data.fields contain_rules = session.query(DsRules).all() diff --git a/backend/apps/datasource/crud/field.py b/backend/apps/datasource/crud/field.py index d7a2de7c6..371b86e78 100644 --- a/backend/apps/datasource/crud/field.py +++ b/backend/apps/datasource/crud/field.py @@ -1,5 +1,6 @@ from common.core.deps import SessionDep -from ..models.datasource import CoreField +from ..models.datasource import CoreField, FieldObj +from sqlalchemy import or_, and_ def delete_field_by_ds_id(session: SessionDep, id: int): @@ -7,8 +8,15 @@ def delete_field_by_ds_id(session: SessionDep, id: int): session.commit() -def get_fields_by_table_id(session: SessionDep, id: int): - return session.query(CoreField).filter(CoreField.table_id == id).order_by(CoreField.field_index.asc()).all() +def get_fields_by_table_id(session: SessionDep, id: int, field: FieldObj): + if field and field.fieldName: + return session.query(CoreField).filter( + and_(CoreField.table_id == id, or_(CoreField.field_name.like(f'%{field.fieldName}%'), + CoreField.field_name.like(f'%{field.fieldName.lower()}%'), + CoreField.field_name.like(f'%{field.fieldName.upper()}%')))).order_by( + CoreField.field_index.asc()).all() + else: + return session.query(CoreField).filter(CoreField.table_id == id).order_by(CoreField.field_index.asc()).all() def update_field(session: SessionDep, item: CoreField): diff --git a/backend/apps/datasource/models/datasource.py b/backend/apps/datasource/models/datasource.py index ff0c8b6d3..c814bda01 100644 --- a/backend/apps/datasource/models/datasource.py +++ b/backend/apps/datasource/models/datasource.py @@ -35,6 +35,7 @@ class CoreTable(SQLModel, table=True): custom_comment: str = Field(sa_column=Column(Text)) embedding: str = Field(sa_column=Column(Text, nullable=True)) + class DsRecommendedProblem(SQLModel, table=True): __tablename__ = "ds_recommended_problem" id: int = Field(sa_column=Column(BigInteger, Identity(always=True), nullable=False, primary_key=True)) @@ -84,6 +85,7 @@ def __init__(self, datasource_id,recommended_config,questions): questions: str = None + class RecommendedProblemBase(BaseModel): datasource_id: int = None recommended_config: int = None @@ -164,3 +166,7 @@ def __init__(self, schema, table, fields): schema: str table: CoreTable fields: List[CoreField] + + +class FieldObj(BaseModel): + fieldName: str | None From 1ba4a7c56144cc4719f89b5e286f84cd6faab9db Mon Sep 17 00:00:00 2001 From: junjun Date: Mon, 8 Dec 2025 10:56:34 +0800 Subject: [PATCH 003/521] fix: Unauthenticated Arbitrary File Upload in SQLBot uploadExcel Endpoint --- backend/common/utils/whitelist.py | 1 - frontend/src/views/ds/DatasourceForm.vue | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/common/utils/whitelist.py b/backend/common/utils/whitelist.py index c880fe53e..b6565ac09 100644 --- a/backend/common/utils/whitelist.py +++ b/backend/common/utils/whitelist.py @@ -33,7 +33,6 @@ "/system/assistant/info/*", "/system/assistant/app/*", "/system/assistant/picture/*", - "/datasource/uploadExcel", "/system/authentication/platform/status", "/system/authentication/login/*", "/system/authentication/sso/*", diff --git a/frontend/src/views/ds/DatasourceForm.vue b/frontend/src/views/ds/DatasourceForm.vue index a4c1d430b..f4b5f7f65 100644 --- a/frontend/src/views/ds/DatasourceForm.vue +++ b/frontend/src/views/ds/DatasourceForm.vue @@ -16,6 +16,7 @@ import { setSize } from '@/utils/utils' import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue' import icon_fileExcel_colorful from '@/assets/datasource/icon_excel.png' import IconOpeDelete from '@/assets/svg/icon_delete.svg' +import { useCache } from '@/utils/useCache' const props = withDefaults( defineProps<{ @@ -128,6 +129,10 @@ const close = () => { saveLoading.value = false } +const { wsCache } = useCache() +const token = wsCache.get('user.token') +const headers = ref({ 'X-SQLBOT-TOKEN': `Bearer ${token}` }) + const initForm = (item: any, editTable: boolean = false) => { isEditTable.value = false keywords.value = '' @@ -539,6 +544,7 @@ defineExpose({ v-if="form.filename && !form.id" class="upload-user" accept=".xlsx,.xls,.csv" + :headers="headers" :action="getUploadURL" :before-upload="beforeUpload" :on-error="onError" @@ -554,6 +560,7 @@ defineExpose({ v-else-if="!form.id" class="upload-user" accept=".xlsx,.xls,.csv" + :headers="headers" :action="getUploadURL" :before-upload="beforeUpload" :on-success="onSuccess" From a611289bfa364e070784dda2912f998893a389d0 Mon Sep 17 00:00:00 2001 From: junjun Date: Tue, 9 Dec 2025 10:40:05 +0800 Subject: [PATCH 004/521] feat: Add table field update functionality --- backend/apps/datasource/api/datasource.py | 7 +++- backend/apps/datasource/crud/datasource.py | 17 +++++++++- frontend/src/api/datasource.ts | 1 + frontend/src/i18n/en.json | 3 +- frontend/src/i18n/ko-KR.json | 3 +- frontend/src/i18n/zh-CN.json | 3 +- frontend/src/views/ds/DataTable.vue | 39 +++++++++++++++++----- 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py index 6849da369..1fbca788c 100644 --- a/backend/apps/datasource/api/datasource.py +++ b/backend/apps/datasource/api/datasource.py @@ -17,7 +17,7 @@ from common.utils.utils import SQLBotLogUtil from ..crud.datasource import get_datasource_list, check_status, create_ds, update_ds, delete_ds, getTables, getFields, \ execSql, update_table_and_fields, getTablesByDs, chooseTables, preview, updateTable, updateField, get_ds, fieldEnum, \ - check_status_by_id + check_status_by_id, sync_single_fields from ..crud.field import get_fields_by_table_id from ..crud.table import get_tables_by_ds_id from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, CoreTable, CoreField, FieldObj @@ -134,6 +134,11 @@ async def get_fields(session: SessionDep, id: int, table_name: str): return getFields(session, id, table_name) +@router.post("/syncFields/{id}") +async def sync_fields(session: SessionDep, id: int): + return sync_single_fields(session, id) + + from pydantic import BaseModel diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 10e68e7a1..153e50882 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -109,12 +109,14 @@ def update_ds(session: SessionDep, trans: Trans, user: CurrentUser, ds: CoreData run_save_ds_embeddings([ds.id]) return ds -def update_ds_recommended_config(session: SessionDep,datasource_id: int, recommended_config:int): + +def update_ds_recommended_config(session: SessionDep, datasource_id: int, recommended_config: int): record = session.exec(select(CoreDatasource).where(CoreDatasource.id == datasource_id)).first() record.recommended_config = recommended_config session.add(record) session.commit() + def delete_ds(session: SessionDep, id: int): term = session.exec(select(CoreDatasource).where(CoreDatasource.id == id)).first() if term.type == "excel": @@ -163,6 +165,19 @@ def execSql(session: SessionDep, id: int, sql: str): return exec_sql(ds, sql, True) +def sync_single_fields(session: SessionDep, id: int): + table = session.query(CoreTable).filter(CoreTable.id == id).first() + ds = session.query(CoreDatasource).filter(CoreDatasource.id == table.ds_id).first() + + # sync field + fields = getFieldsByDs(session, ds, table.table_name) + sync_fields(session, ds, table, fields) + + # do table embedding + run_save_table_embeddings([table.id]) + run_save_ds_embeddings([ds.id]) + + def sync_table(session: SessionDep, ds: CoreDatasource, tables: List[CoreTable]): id_list = [] for item in tables: diff --git a/frontend/src/api/datasource.ts b/frontend/src/api/datasource.ts index 802f92692..5a66e985b 100644 --- a/frontend/src/api/datasource.ts +++ b/frontend/src/api/datasource.ts @@ -26,4 +26,5 @@ export const datasourceApi = { getDs: (id: number) => request.post(`/datasource/get/${id}`), cancelRequests: () => request.cancelRequests(), getSchema: (data: any) => request.post('/datasource/getSchemaByConf', data), + syncFields: (id: number) => request.post(`/datasource/syncFields/${id}`), } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 32928006c..b9cf2c990 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -335,7 +335,8 @@ }, "timeout": "Timeout(second)", "address": "Address" - } + }, + "sync_fields": "Sync Fields" }, "datasource": { "recommended_repetitive_tips": "Duplicate questions exist", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 00724c6dd..ab0e1c48d 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -335,7 +335,8 @@ }, "timeout": "쿼리 시간 초과(초)", "address": "주소" - } + }, + "sync_fields": "동기화된 테이블 구조" }, "datasource": { "recommended_repetitive_tips": "중복된 문제가 존재합니다", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 0be8b96bd..86990ecf6 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -336,7 +336,8 @@ }, "timeout": "查询超时(秒)", "address": "地址" - } + }, + "sync_fields": "同步表结构" }, "datasource": { "recommended_repetitive_tips": "存在重复问题", diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index 82bf3ffa1..cf528aa37 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -10,6 +10,7 @@ import { useI18n } from 'vue-i18n' import ParamsForm from './ParamsForm.vue' import TableRelationship from '@/views/ds/TableRelationship.vue' import icon_mindnote_outlined from '@/assets/svg/icon_mindnote_outlined.svg' +import { Refresh } from '@element-plus/icons-vue' interface Table { name: string host: string @@ -223,6 +224,19 @@ const changeStatus = (row: any) => { }) } +const syncFields = () => { + loading.value = true + datasourceApi + .syncFields(currentTable.value.id) + .then(() => { + btnSelectClick('d') + loading.value = false + }) + .catch(() => { + loading.value = false + }) +} + const emits = defineEmits(['back', 'refresh']) const back = () => { emits('back') @@ -316,13 +330,13 @@ const btnSelectClick = (val: any) => { :key="ele.table_name" :draggable="activeRelationship && !tableName.includes(ele.id)" class="model" - @dragstart="($event: any) => singleDragStartD($event, ele)" - @dragend="singleDragEnd" :class="[ currentTable.table_name === ele.table_name && 'isActive', tableName.includes(ele.id) && activeRelationship && 'disabled-table', ]" :title="ele.table_name" + @dragstart="($event: any) => singleDragStartD($event, ele)" + @dragend="singleDragEnd" @click="clickTable(ele)" > @@ -349,7 +363,7 @@ const btnSelectClick = (val: any) => {
-
+
@@ -362,9 +376,9 @@ const btnSelectClick = (val: any) => {
{{ t('training.table_relationship_management') }}
@@ -409,12 +423,20 @@ const btnSelectClick = (val: any) => {
+ + {{ t('ds.sync_fields') }} +
{ position: absolute; right: 16px; top: 16px; - width: 240px; + width: 360px; + display: flex; } .btn-select { From 9deca7e6b641e140b0d71da6ab05dbcbee872530 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Tue, 9 Dec 2025 14:16:07 +0800 Subject: [PATCH 005/521] feat(Data Source): Add minimum width to table columns in the data preview page. --- frontend/src/views/ds/DataTable.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index cf528aa37..480b46d7c 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -518,6 +518,7 @@ const btnSelectClick = (val: any) => { :key="index" :prop="c" :label="c" + min-width="150" :render-header="renderHeader" /> From e47e0e8ab8973e79300995d501957923010e50e2 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Tue, 9 Dec 2025 15:02:42 +0800 Subject: [PATCH 006/521] feat: Add parameter configuration functionality --- backend/apps/api.py | 3 +- backend/apps/system/api/parameter.py | 17 ++ backend/apps/system/crud/parameter_manage.py | 41 ++++ frontend/src/i18n/en.json | 8 +- frontend/src/i18n/ko-KR.json | 8 +- frontend/src/i18n/zh-CN.json | 8 +- frontend/src/router/index.ts | 2 + frontend/src/utils/utils.ts | 19 ++ frontend/src/views/system/parameter/index.vue | 136 +++++------- .../system/parameter/xpack/PlatformParam.vue | 210 ++++++++++++++++++ 10 files changed, 363 insertions(+), 89 deletions(-) create mode 100644 backend/apps/system/api/parameter.py create mode 100644 backend/apps/system/crud/parameter_manage.py create mode 100644 frontend/src/views/system/parameter/xpack/PlatformParam.vue diff --git a/backend/apps/api.py b/backend/apps/api.py index 9cdb83159..f53f7fa48 100644 --- a/backend/apps/api.py +++ b/backend/apps/api.py @@ -5,7 +5,7 @@ from apps.data_training.api import data_training from apps.datasource.api import datasource, table_relation, recommended_problem from apps.mcp import mcp -from apps.system.api import login, user, aimodel, workspace, assistant +from apps.system.api import login, user, aimodel, workspace, assistant, parameter from apps.terminology.api import terminology from apps.settings.api import base @@ -23,5 +23,6 @@ api_router.include_router(dashboard_api.router) api_router.include_router(mcp.router) api_router.include_router(table_relation.router) +api_router.include_router(parameter.router) api_router.include_router(recommended_problem.router) diff --git a/backend/apps/system/api/parameter.py b/backend/apps/system/api/parameter.py new file mode 100644 index 000000000..dc335281b --- /dev/null +++ b/backend/apps/system/api/parameter.py @@ -0,0 +1,17 @@ + +from fastapi import APIRouter, Request +from sqlbot_xpack.config.model import SysArgModel + + +from apps.system.crud.parameter_manage import get_parameter_args, save_parameter_args +from common.core.deps import SessionDep + +router = APIRouter(tags=["system/parameter"], prefix="/system/parameter") + +@router.get("") +async def get_args(session: SessionDep) -> list[SysArgModel]: + return await get_parameter_args(session) + +@router.post("", ) +async def save_args(session: SessionDep, request: Request): + return await save_parameter_args(session = session, request = request) diff --git a/backend/apps/system/crud/parameter_manage.py b/backend/apps/system/crud/parameter_manage.py new file mode 100644 index 000000000..118dad51d --- /dev/null +++ b/backend/apps/system/crud/parameter_manage.py @@ -0,0 +1,41 @@ +from fastapi import Request +from sqlbot_xpack.config.arg_manage import get_group_args, save_group_args +from sqlbot_xpack.config.model import SysArgModel +import json +from common.core.deps import SessionDep +from sqlbot_xpack.file_utils import SQLBotFileUtils + +async def get_parameter_args(session: SessionDep) -> list[SysArgModel]: + group_args = await get_group_args(session=session) + return [x for x in group_args if not x.pkey.startswith('appearance.')] + +async def save_parameter_args(session: SessionDep, request: Request): + allow_file_mapping = { + """ "test_logo": { "types": [".jpg", ".jpeg", ".png", ".svg"], "size": 5 * 1024 * 1024 } """ + } + form_data = await request.form() + files = form_data.getlist("files") + json_text = form_data.get("data") + sys_args = [ + SysArgModel(**{**item, "pkey": f"{item['pkey']}"}) + for item in json.loads(json_text) + if "pkey" in item + ] + if not sys_args: + return + file_mapping = None + if files: + file_mapping = {} + for file in files: + origin_file_name = file.filename + file_name, flag_name = SQLBotFileUtils.split_filename_and_flag(origin_file_name) + file.filename = file_name + allow_limit_obj = allow_file_mapping.get(flag_name) + if allow_limit_obj: + SQLBotFileUtils.check_file(file=file, file_types=allow_limit_obj.get("types"), limit_file_size=allow_limit_obj.get("size")) + else: + raise Exception(f'The file [{file_name}] is not allowed to be uploaded!') + file_id = await SQLBotFileUtils.upload(file) + file_mapping[f"{flag_name}"] = file_id + + await save_group_args(session=session, sys_args=sys_args, file_mapping=file_mapping) \ No newline at end of file diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index b9cf2c990..f3fe10f4c 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -14,13 +14,15 @@ "rows_of_data": "Limit 1000 Rows of Data", "third_party_platform_settings": "Third-Party Platform Settings", "by_third_party_platform": "Automatic User Creation by Third-Party Platform", - "platform_user_organization": "Third-Party Platform User Organization", + "platform_user_organization": "Third-Party Platform Workspace", "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.", "prompt": "Prompt", "disabling_successfully": "Disabling Successfully", "closed_by_default": "In the Question Count window, control whether the model thinking process is expanded or closed by default.", - "and_platform_integration": "Scope includes authentication settings and platform integration." + "and_platform_integration": "Scope includes authentication settings and platform integration.", + "login_settings": "Login Settings", + "default_login": "Default Login Method" }, "prompt": { "default_password": "Default password:{msg}", @@ -816,4 +818,4 @@ "modelType": { "llm": "Large Language Model" } -} \ No newline at end of file +} diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index ab0e1c48d..92b2d63d0 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -14,13 +14,15 @@ "rows_of_data": "데이터 1,000행 제한", "third_party_platform_settings": "타사 플랫폼 설정", "by_third_party_platform": "타사 플랫폼에 의한 자동 사용자 생성", - "platform_user_organization": "타사 플랫폼 사용자 구성", + "platform_user_organization": "제3자 플랫폼 작업 공간", "platform_user_roles": "타사 플랫폼 사용자 역할", "excessive_data_volume": "1,000행 데이터 제한을 비활성화하면 과도한 데이터 양으로 인해 시스템 지연이 발생할 수 있습니다.", "prompt": "프롬프트", "disabling_successfully": "비활성화 완료", "closed_by_default": "질문 수 창에서 모델 사고 프로세스를 기본적으로 확장할지 또는 닫을지 여부를 제어합니다.", - "and_platform_integration": "범위에는 인증 설정 및 플랫폼 통합이 포함됩니다." + "and_platform_integration": "범위에는 인증 설정 및 플랫폼 통합이 포함됩니다.", + "login_settings": "로그인 설정", + "default_login": "기본 로그인 방식" }, "prompt": { "default_password:": "기본 비밀번호:{msg}", @@ -816,4 +818,4 @@ "modelType": { "llm": "대형 언어 모델" } -} \ No newline at end of file +} diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 86990ecf6..02a803dc8 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -14,13 +14,15 @@ "rows_of_data": "限制 1000 行数据", "third_party_platform_settings": "第三方平台设置", "by_third_party_platform": "第三方自动创建用户", - "platform_user_organization": "第三方平台用户组织", + "platform_user_organization": "第三方平台工作空间", "platform_user_roles": "第三方平台用户角色", "excessive_data_volume": "关闭1000行的数据限制后,数据量过大,可能会造成系统卡顿", "prompt": "提示", "disabling_successfully": "关闭成功", "closed_by_default": "在问数窗口中,控制模型思考过程默认展开或者关闭", - "and_platform_integration": "作用域包括认证设置和平台对接" + "and_platform_integration": "作用域包括认证设置和平台对接", + "login_settings": "登录设置", + "default_login": "默认登录方式" }, "prompt": { "default_password": "默认密码:{msg}", @@ -817,4 +819,4 @@ "modelType": { "llm": "大语言模型" } -} \ No newline at end of file +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 8ba1f21f0..275d33a48 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -95,6 +95,7 @@ export const routes = [ }, { path: '/set', + name: 'set', component: LayoutDsl, redirect: '/set/member', meta: { title: t('workspace.set'), iconActive: 'set', iconDeActive: 'noSet' }, @@ -145,6 +146,7 @@ export const routes = [ }, { path: '/system', + name: 'system', component: LayoutDsl, redirect: '/system/user', meta: { hidden: true }, diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index f31e25d66..1373ada5a 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -263,3 +263,22 @@ export const getSQLBotAddr = (portEnd?: boolean) => { } return addr.substring(0, addr.length - 1) } + +export const formatArg = (text: string) => { + if (!text) { + return false + } + const mappingArray = ['true', 'false', '1', '0'] + const match = mappingArray.some((item: string) => { + return item === text.toLocaleLowerCase() + }) + if (!match) { + return text + } + try { + return JSON.parse(text) + } catch (e: any) { + console.warn(e) + return text + } +} diff --git a/frontend/src/views/system/parameter/index.vue b/frontend/src/views/system/parameter/index.vue index 273b7f9c7..dd87dacca 100644 --- a/frontend/src/views/system/parameter/index.vue +++ b/frontend/src/views/system/parameter/index.vue @@ -1,23 +1,63 @@ diff --git a/frontend/src/views/system/parameter/xpack/PlatformParam.vue b/frontend/src/views/system/parameter/xpack/PlatformParam.vue new file mode 100644 index 000000000..0b08b2b3c --- /dev/null +++ b/frontend/src/views/system/parameter/xpack/PlatformParam.vue @@ -0,0 +1,210 @@ + + + + + From 57d71416755a64e552eb132606c19b296ff615f0 Mon Sep 17 00:00:00 2001 From: junjun Date: Tue, 9 Dec 2025 15:05:59 +0800 Subject: [PATCH 007/521] feat: Add table field update functionality --- frontend/src/views/ds/DataTable.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index 480b46d7c..bc46f253d 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -424,6 +424,7 @@ const btnSelectClick = (val: any) => {
{ position: absolute; right: 16px; top: 16px; - width: 360px; display: flex; } From 2f4042edd6f666f9832b81b0e9193ee45decf8db Mon Sep 17 00:00:00 2001 From: Matt Lei <366664088@qq.com> Date: Wed, 10 Dec 2025 15:02:15 +0800 Subject: [PATCH 008/521] fix: correct syntax, format issues in template.yaml --- backend/templates/template.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/templates/template.yaml b/backend/templates/template.yaml index 44bd4c7bb..2862a11db 100644 --- a/backend/templates/template.yaml +++ b/backend/templates/template.yaml @@ -217,8 +217,8 @@ template: 使用饼图展示去年各个国家的GDP - {example_answer_2} + {example_answer_2} @@ -230,8 +230,8 @@ template: 查询今年中国大陆的GDP - {example_answer_3} + {example_answer_3} @@ -388,7 +388,7 @@ template: - 推测的问题不能与当前用户问题重复 - 推测的问题必须与给出的表结构相关 - 若有以往用户提问列表,则根据以往用户提问列表,推测用户最频繁提问的问题,加入到你生成的推测问题中 - - 忽略“重新生成”想关的问题 + - 忽略“重新生成”相关的问题 - 如果用户没有提问且没有以往用户提问,则仅根据提供的表结构推测问题 - 生成的推测问题使用JSON格式返回: ["推测问题1", "推测问题2", "推测问题3", "推测问题4"] From 1669ba3ed1a32ebb2bcd46dd10f03628ba09343c Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Wed, 10 Dec 2025 16:42:37 +0800 Subject: [PATCH 009/521] fix: Error occurs when re-uploading after the license expires --- backend/common/core/sqlbot_cache.py | 2 ++ backend/main.py | 1 + 2 files changed, 3 insertions(+) diff --git a/backend/common/core/sqlbot_cache.py b/backend/common/core/sqlbot_cache.py index 150180197..dda597b18 100644 --- a/backend/common/core/sqlbot_cache.py +++ b/backend/common/core/sqlbot_cache.py @@ -37,6 +37,8 @@ def custom_key_builder( # 支持属性路径格式 parts = keyExpression.split('.') + if not bound_args.arguments.get(parts[0]): + return f"{base_key}{parts[0]}" value = bound_args.arguments[parts[0]] for part in parts[1:]: value = getattr(value, part) diff --git a/backend/main.py b/backend/main.py index 48e124f79..dddc3e89a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -51,6 +51,7 @@ async def lifespan(app: FastAPI): SQLBotLogUtil.info("✅ SQLBot 初始化完成") await sqlbot_xpack.core.clean_xpack_cache() await async_model_info() # 异步加密已有模型的密钥和地址 + await sqlbot_xpack.core.monitor_app(app) yield SQLBotLogUtil.info("SQLBot 应用关闭") From c88a587817e639ff316d01473e3fd2d18e8ebe99 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Wed, 10 Dec 2025 17:20:03 +0800 Subject: [PATCH 010/521] chore: Upgrade xpack dependency --- backend/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index b1e7bb2ea..83681ccd9 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sqlbot" -version = "1.4.0" +version = "1.5.0" description = "" requires-python = "==3.11.*" dependencies = [ @@ -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.3.59,<0.0.4.0", + "sqlbot-xpack>=0.0.4.0,<0.0.5.0", "fastapi-cache2>=0.2.2", "sqlparse>=0.5.3", "redis>=6.2.0", From 966af42e819e3f0713da294bcc40b4b0537c24bf Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Thu, 11 Dec 2025 09:05:52 +0800 Subject: [PATCH 011/521] feat(X-Pack): Supports third-party platforms as the default login method --- backend/apps/system/api/parameter.py | 6 +- backend/apps/system/crud/parameter_manage.py | 4 + backend/common/utils/whitelist.py | 1 + frontend/src/router/index.ts | 5 ++ frontend/src/router/watch.ts | 4 +- frontend/src/views/login/xpack/Handler.vue | 84 +++++++++---------- frontend/src/views/login/xpack/QrcodeLdap.vue | 3 + 7 files changed, 61 insertions(+), 46 deletions(-) diff --git a/backend/apps/system/api/parameter.py b/backend/apps/system/api/parameter.py index dc335281b..af254d71f 100644 --- a/backend/apps/system/api/parameter.py +++ b/backend/apps/system/api/parameter.py @@ -3,11 +3,15 @@ from sqlbot_xpack.config.model import SysArgModel -from apps.system.crud.parameter_manage import get_parameter_args, save_parameter_args +from apps.system.crud.parameter_manage import get_groups, get_parameter_args, save_parameter_args from common.core.deps import SessionDep router = APIRouter(tags=["system/parameter"], prefix="/system/parameter") +@router.get("/login") +async def get_login_args(session: SessionDep) -> list[SysArgModel]: + return await get_groups(session, "login") + @router.get("") async def get_args(session: SessionDep) -> list[SysArgModel]: return await get_parameter_args(session) diff --git a/backend/apps/system/crud/parameter_manage.py b/backend/apps/system/crud/parameter_manage.py index 118dad51d..f80edd29c 100644 --- a/backend/apps/system/crud/parameter_manage.py +++ b/backend/apps/system/crud/parameter_manage.py @@ -9,6 +9,10 @@ async def get_parameter_args(session: SessionDep) -> list[SysArgModel]: group_args = await get_group_args(session=session) return [x for x in group_args if not x.pkey.startswith('appearance.')] +async def get_groups(session: SessionDep, flag: str) -> list[SysArgModel]: + group_args = await get_group_args(session=session, flag=flag) + return group_args + async def save_parameter_args(session: SessionDep, request: Request): allow_file_mapping = { """ "test_logo": { "types": [".jpg", ".jpeg", ".png", ".svg"], "size": 5 * 1024 * 1024 } """ diff --git a/backend/common/utils/whitelist.py b/backend/common/utils/whitelist.py index b6565ac09..d9df569be 100644 --- a/backend/common/utils/whitelist.py +++ b/backend/common/utils/whitelist.py @@ -36,6 +36,7 @@ "/system/authentication/platform/status", "/system/authentication/login/*", "/system/authentication/sso/*", + "/system/parameter/login" ] class WhitelistChecker: diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 275d33a48..51f8aba45 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -231,6 +231,11 @@ export const routes = [ name: 'assistantTest', component: assistantTest, }, + { + path: '/admin-login', + name: 'admin-login', + component: login, + }, { path: '/401', name: '401', diff --git a/frontend/src/router/watch.ts b/frontend/src/router/watch.ts index 7c63f1314..432840b32 100644 --- a/frontend/src/router/watch.ts +++ b/frontend/src/router/watch.ts @@ -8,7 +8,7 @@ import type { Router } from 'vue-router' const appearanceStore = useAppearanceStoreWithOut() const userStore = useUserStore() const { wsCache } = useCache() -const whiteList = ['/login'] +const whiteList = ['/login', '/admin-login'] const assistantWhiteList = ['/assistant', '/embeddedPage', '/401'] export const watchRouter = (router: Router) => { router.beforeEach(async (to: any, from: any, next: any) => { @@ -40,7 +40,7 @@ export const watchRouter = (router: Router) => { next('/chat') return } - if (to.path === '/login') { + if (to.path === '/login' || to.path === '/admin-login') { console.info(from) next('/chat') } else { diff --git a/frontend/src/views/login/xpack/Handler.vue b/frontend/src/views/login/xpack/Handler.vue index 90d8b5cef..e564583c0 100644 --- a/frontend/src/views/login/xpack/Handler.vue +++ b/frontend/src/views/login/xpack/Handler.vue @@ -51,7 +51,7 @@ diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index da20796b3..ffc28f45a 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -410,7 +410,7 @@ type="primary" class="input-icon" :disabled="isTyping" - @click.stop="sendMessage" + @click.stop="($event: any) => sendMessage(undefined, $event)" > @@ -458,6 +458,7 @@ import { debounce } from 'lodash-es' import { isMobile } from '@/utils/utils' import router from '@/router' import QuickQuestion from '@/views/chat/QuickQuestion.vue' +import { useChatConfigStore } from '@/stores/chatConfig.ts' const userStore = useUserStore() const props = defineProps<{ startChatDsId?: number @@ -486,6 +487,9 @@ const customName = computed(() => { return '' }) const { t } = useI18n() + +const chatConfig = useChatConfigStore() + const isPhone = computed(() => { return isMobile() }) @@ -1062,6 +1066,7 @@ function jumpCreatChat() { } onMounted(() => { + chatConfig.fetchGlobalConfig() if (isPhone.value) { chatListSideBarShow.value = false if (props.pageEmbedded) { diff --git a/frontend/src/views/system/parameter/index.vue b/frontend/src/views/system/parameter/index.vue index ab9bf5c19..2c1e47b12 100644 --- a/frontend/src/views/system/parameter/index.vue +++ b/frontend/src/views/system/parameter/index.vue @@ -9,8 +9,8 @@ const { t } = useI18n() const state = reactive({ parameterForm: reactive({ - 'chat.enable_model_thinking': false, - 'chat.rows_of_data': false, + 'chat.expand_thinking_block': false, + 'chat.limit_rows': false, }), }) provide('parameterForm', state.parameterForm) @@ -99,7 +99,7 @@ onMounted(() => {
- +
@@ -118,7 +118,7 @@ onMounted(() => {
From c6403fae78e8722707eb107c270f608333f4c22d Mon Sep 17 00:00:00 2001 From: ulleo Date: Tue, 16 Dec 2025 18:54:35 +0800 Subject: [PATCH 030/521] feat: improve SQL template --- backend/apps/chat/models/chat_model.py | 4 ++-- .../templates/sql_examples/AWS_Redshift.yaml | 1 + backend/templates/sql_examples/ClickHouse.yaml | 1 + backend/templates/sql_examples/DM.yaml | 1 + backend/templates/sql_examples/Doris.yaml | 1 + .../templates/sql_examples/Elasticsearch.yaml | 14 ++++++++++++++ backend/templates/sql_examples/Kingbase.yaml | 1 + .../sql_examples/Microsoft_SQL_Server.yaml | 1 + backend/templates/sql_examples/MySQL.yaml | 1 + backend/templates/sql_examples/Oracle.yaml | 1 + backend/templates/sql_examples/PostgreSQL.yaml | 1 + backend/templates/sql_examples/StarRocks.yaml | 1 + backend/templates/template.yaml | 18 ++++++++++++++++++ 13 files changed, 44 insertions(+), 2 deletions(-) diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index dc19b7e8f..338720053 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -200,8 +200,8 @@ def sql_sys_question(self, db_type: Union[str, DB], enable_query_limit: bool = T _process_check = _sql_template.get('process_check') if _sql_template.get('process_check') else _base_template[ 'process_check'] _query_limit = _base_template['query_limit'] if enable_query_limit else _base_template['no_query_limit'] - _base_sql_rules = _sql_template['quot_rule'] + _query_limit + _sql_template['limit_rule'] + _sql_template[ - 'other_rule'] + _other_rule = _sql_template['other_rule'].format(multi_table_condition=_base_template['multi_table_condition']) + _base_sql_rules = _sql_template['quot_rule'] + _query_limit + _sql_template['limit_rule'] + _other_rule _sql_examples = _sql_template['basic_example'] _example_engine = _sql_template['example_engine'] _example_answer_1 = _sql_template['example_answer_1_with_limit'] if enable_query_limit else _sql_template[ diff --git a/backend/templates/sql_examples/AWS_Redshift.yaml b/backend/templates/sql_examples/AWS_Redshift.yaml index 5036941e5..40036b975 100644 --- a/backend/templates/sql_examples/AWS_Redshift.yaml +++ b/backend/templates/sql_examples/AWS_Redshift.yaml @@ -20,6 +20,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/ClickHouse.yaml b/backend/templates/sql_examples/ClickHouse.yaml index f278b62b9..c2b56f2dd 100644 --- a/backend/templates/sql_examples/ClickHouse.yaml +++ b/backend/templates/sql_examples/ClickHouse.yaml @@ -21,6 +21,7 @@ template: other_rule: | 必须为每个表生成简短别名(如t1/t2) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 JSON字段需用点号语法访问:`"column.field"` 函数字段必须加别名 diff --git a/backend/templates/sql_examples/DM.yaml b/backend/templates/sql_examples/DM.yaml index bb04396dd..29e65d82c 100644 --- a/backend/templates/sql_examples/DM.yaml +++ b/backend/templates/sql_examples/DM.yaml @@ -21,6 +21,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/Doris.yaml b/backend/templates/sql_examples/Doris.yaml index 5b2ea3602..b973c529d 100644 --- a/backend/templates/sql_examples/Doris.yaml +++ b/backend/templates/sql_examples/Doris.yaml @@ -21,6 +21,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/Elasticsearch.yaml b/backend/templates/sql_examples/Elasticsearch.yaml index cddc4577b..14188c2f6 100644 --- a/backend/templates/sql_examples/Elasticsearch.yaml +++ b/backend/templates/sql_examples/Elasticsearch.yaml @@ -21,6 +21,20 @@ template: other_rule: | 必须为每个索引生成别名(不加AS) + + Elasticsearch SQL多表查询字段限定规则 + + 当使用JOIN涉及多个索引时,所有字段引用必须明确限定索引别名 + 适用于SELECT、WHERE、GROUP BY、HAVING、ORDER BY、ON等子句中的所有字段引用 + 单索引查询不需要表名前缀 + + + Elasticsearch的JOIN性能有限,仅适合小数据量关联 + 标识符引用使用双引号(") + 大多数Elasticsearch查询是单索引查询,不需要字段限定 + 嵌套字段通过点号访问,如 "customer.name",不需要表名前缀 + + 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/Kingbase.yaml b/backend/templates/sql_examples/Kingbase.yaml index 568b4201b..e3fa4caa4 100644 --- a/backend/templates/sql_examples/Kingbase.yaml +++ b/backend/templates/sql_examples/Kingbase.yaml @@ -21,6 +21,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/Microsoft_SQL_Server.yaml b/backend/templates/sql_examples/Microsoft_SQL_Server.yaml index 83f560e26..856ac2a6a 100644 --- a/backend/templates/sql_examples/Microsoft_SQL_Server.yaml +++ b/backend/templates/sql_examples/Microsoft_SQL_Server.yaml @@ -21,6 +21,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/MySQL.yaml b/backend/templates/sql_examples/MySQL.yaml index 1cbfca44c..1335ec9e4 100644 --- a/backend/templates/sql_examples/MySQL.yaml +++ b/backend/templates/sql_examples/MySQL.yaml @@ -20,6 +20,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/Oracle.yaml b/backend/templates/sql_examples/Oracle.yaml index ba3015b27..44e7ebedc 100644 --- a/backend/templates/sql_examples/Oracle.yaml +++ b/backend/templates/sql_examples/Oracle.yaml @@ -83,6 +83,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/PostgreSQL.yaml b/backend/templates/sql_examples/PostgreSQL.yaml index ca3bcd892..42a9cfe41 100644 --- a/backend/templates/sql_examples/PostgreSQL.yaml +++ b/backend/templates/sql_examples/PostgreSQL.yaml @@ -15,6 +15,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/sql_examples/StarRocks.yaml b/backend/templates/sql_examples/StarRocks.yaml index d0dc10fe2..212677fc4 100644 --- a/backend/templates/sql_examples/StarRocks.yaml +++ b/backend/templates/sql_examples/StarRocks.yaml @@ -21,6 +21,7 @@ template: other_rule: | 必须为每个表生成别名(不加AS) + {multi_table_condition} 禁止使用星号(*),必须明确字段名 中文/特殊字符字段需保留原名并添加英文别名 函数字段必须加别名 diff --git a/backend/templates/template.yaml b/backend/templates/template.yaml index 82c990ec8..a6f50da1b 100644 --- a/backend/templates/template.yaml +++ b/backend/templates/template.yaml @@ -49,6 +49,20 @@ template: 不要拒绝查询所有数据的情况 + multi_table_condition: | + + 多表查询字段限定规则(必须严格遵守) + + 当SQL涉及多个表/索引(通过FROM/JOIN/子查询等)时,所有字段引用必须明确限定表名/索引名或表别名/索引别名 + 适用于SELECT、WHERE、GROUP BY、HAVING、ORDER BY、ON等子句中的所有字段引用 + 即使字段名在所有表/索引中是唯一的,也必须明确限定以确保清晰性 + + + 生成SQL后必须检查是否涉及多表查询 + 如果是多表查询,验证所有字段引用是否有表名/表别名限定 + 如果发现未限定的字段,必须重新生成SQL + + system: | 你是"SQLBOT",智能问数小助手,可以根据用户提问,专业生成SQL,查询数据并进行图表展示。 @@ -68,6 +82,7 @@ template: 请使用语言:{lang} 回答,若有深度思考过程,则思考过程也需要使用 {lang} 输出 + 即使示例中显示了中文消息,在实际生成时也必须翻译成英文 你只能生成查询用的SQL语句,不得生成增删改相关或操作数据库以及操作数据库数据的SQL @@ -120,6 +135,9 @@ template: 若需关联多表,优先使用中标记为"Primary key"/"ID"/"主键"的字段作为关联条件。 + + 若涉及多表查询,则生成的SQL内,不论查询的表字段是否有重名,表字段前必须加上对应的表名 + 我们目前的情况适用于单指标、多分类的场景(展示table除外) From ee1836d1d2084279c87e158045803fd47640ba85 Mon Sep 17 00:00:00 2001 From: junjun Date: Wed, 17 Dec 2025 10:46:18 +0800 Subject: [PATCH 031/521] feat: Add Api Docs --- backend/apps/datasource/api/table_relation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/apps/datasource/api/table_relation.py b/backend/apps/datasource/api/table_relation.py index af7f2245f..7b76f9584 100644 --- a/backend/apps/datasource/api/table_relation.py +++ b/backend/apps/datasource/api/table_relation.py @@ -11,7 +11,7 @@ router = APIRouter(tags=["Table Relation"], prefix="/table_relation") -@router.post("/save/{ds_id}", response_model=List[dict], summary=f"{PLACEHOLDER_PREFIX}tr_save") +@router.post("/save/{ds_id}", response_model=None, summary=f"{PLACEHOLDER_PREFIX}tr_save") async def save_relation(session: SessionDep, relation: List[dict], ds_id: int = Path(..., description=f"{PLACEHOLDER_PREFIX}ds_id")): ds = session.get(CoreDatasource, ds_id) @@ -23,8 +23,8 @@ async def save_relation(session: SessionDep, relation: List[dict], return True -@router.post("/get/{ds_id}", response_model=List[dict], summary=f"{PLACEHOLDER_PREFIX}tr_get") -async def save_relation(session: SessionDep, ds_id: int = Path(..., description=f"{PLACEHOLDER_PREFIX}ds_id")): +@router.post("/get/{ds_id}", response_model=List, summary=f"{PLACEHOLDER_PREFIX}tr_get") +async def get_relation(session: SessionDep, ds_id: int = Path(..., description=f"{PLACEHOLDER_PREFIX}ds_id")): ds = session.get(CoreDatasource, ds_id) if ds: return ds.table_relation if ds.table_relation else [] From 7668a6f8514778d215549daf6e89a5061195ae3c Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Wed, 17 Dec 2025 14:36:21 +0800 Subject: [PATCH 032/521] fix(Embedded Management): English Overbounded Optimization --- frontend/src/views/chat/QuickQuestion.vue | 6 +++--- frontend/src/views/chat/index.vue | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/chat/QuickQuestion.vue b/frontend/src/views/chat/QuickQuestion.vue index e5e1296d0..fd273ba28 100644 --- a/frontend/src/views/chat/QuickQuestion.vue +++ b/frontend/src/views/chat/QuickQuestion.vue @@ -136,7 +136,7 @@ const props = withDefaults( .quick_question_popover { padding: 4px !important; .quick_question_title { - width: 40px; + min-width: 40px; height: 24px; border-radius: 6px; opacity: 1; @@ -149,8 +149,8 @@ const props = withDefaults( cursor: pointer; margin-left: 8px; &:hover { - color: rgba(24, 158, 122, 0.5); - background: rgba(28, 186, 144, 0.1); + color: #189e7a; + background: #1f23291a; } } .title-active { diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index ffc28f45a..a5ddcfad1 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -1197,7 +1197,7 @@ onMounted(() => { } .quick_question { - width: 100px; + min-width: 100px; position: absolute; margin-left: 1px; margin-top: 1px; From d2556a423178e99571116d50f013e0ea771002cb Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Thu, 18 Dec 2025 14:49:02 +0800 Subject: [PATCH 033/521] fix: Assistant asks about the process of mathematical thinking internationalization error --- backend/apps/system/middleware/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py index ecc7b4162..2dd201873 100644 --- a/backend/apps/system/middleware/auth.py +++ b/backend/apps/system/middleware/auth.py @@ -39,6 +39,8 @@ async def dispatch(self, request, call_next): validator: tuple[any] = await self.validateAssistant(assistantToken, trans) if validator[0]: request.state.current_user = validator[1] + if request.state.current_user and trans.lang: + request.state.current_user.language = trans.lang request.state.assistant = validator[2] origin = request.headers.get("X-SQLBOT-HOST-ORIGIN") or get_origin_from_referer(request) if origin and validator[2]: From 417c59456dd6e0a62fcfbae1926cddfcb46dc7b4 Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 18 Dec 2025 15:23:19 +0800 Subject: [PATCH 034/521] feat: Support for batch uploading data table notes #557 --- backend/apps/datasource/api/datasource.py | 134 +++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py index 5724f92a5..084d76470 100644 --- a/backend/apps/datasource/api/datasource.py +++ b/backend/apps/datasource/api/datasource.py @@ -1,14 +1,18 @@ import asyncio import hashlib +import io import os import traceback import uuid from io import StringIO from typing import List +from urllib.parse import quote import orjson import pandas as pd from fastapi import APIRouter, File, UploadFile, HTTPException, Path +from fastapi.responses import StreamingResponse +from sqlalchemy import and_ from apps.db.db import get_schema from apps.db.engine import get_engine_conn @@ -81,7 +85,6 @@ def inner(): await asyncio.to_thread(inner) - @router.post("/update", response_model=CoreDatasource, summary=f"{PLACEHOLDER_PREFIX}ds_update") @require_permissions(permission=SqlbotPermission(type='ds', keyExpression="ds.id")) async def update(session: SessionDep, trans: Trans, user: CurrentUser, ds: CoreDatasource): @@ -360,3 +363,132 @@ def insert_pg(df, tableName, engine): finally: cursor.close() conn.close() + + +t_sheet = "数据表列表" +t_n_col = "表名" +t_c_col = "表备注" +f_n_col = "字段名" +f_c_col = "字段备注" + + +@router.get("/exportDsSchema/{id}") +async def export_ds_schema(session: SessionDep, id: int = Path(..., description=f"{PLACEHOLDER_PREFIX}ds_id")): + # { + # 'sheet':'', sheet name + # 'c1_h':'', column1 column name + # 'c2_h':'', column2 column name + # 'c1':[], column1 data + # 'c2':[], column2 data + # } + def inner(): + if id == 0: # download template + file_name = '批量上传备注' + df_list = [ + {'sheet': t_sheet, 'c1_h': t_n_col, 'c2_h': t_c_col, 'c1': ["user", "score"], + 'c2': ["用来存放用户信息的数据表", "用来存放用户课程信息的数据表"]}, + {'sheet': '数据表1', 'c1_h': f_n_col, 'c2_h': f_c_col, 'c1': ["id", "name"], + 'c2': ["用户id", "用户姓名"]}, + {'sheet': '数据表2', 'c1_h': f_n_col, 'c2_h': f_c_col, 'c1': ["course", "user_id", "score"], + 'c2': ["课程名称", "用户ID", "课程得分"]}, + ] + else: + ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first() + file_name = ds.name + tables = session.query(CoreTable).filter(CoreTable.ds_id == id).all() + if len(tables) == 0: + raise HTTPException(400, "No tables") + + df_list = [] + df1 = {'sheet': t_sheet, 'c1_h': t_n_col, 'c2_h': t_c_col, 'c1': [], 'c2': []} + df_list.append(df1) + for table in tables: + df1['c1'].append(table.table_name) + df1['c2'].append(table.custom_comment) + + fields = session.query(CoreField).filter(CoreField.table_id == table.id).all() + df_fields = {'sheet': table.table_name, 'c1_h': f_n_col, 'c2_h': f_c_col, 'c1': [], 'c2': []} + for field in fields: + df_fields['c1'].append(field.field_name) + df_fields['c2'].append(field.custom_comment) + df_list.append(df_fields) + + # build dataframe and export + output = io.BytesIO() + + with pd.ExcelWriter(output, engine='xlsxwriter') as writer: + for df in df_list: + pd.DataFrame({df['c1_h']: df['c1'], df['c2_h']: df['c2']}).to_excel(writer, sheet_name=df['sheet'], + index=False) + + output.seek(0) + + filename = f'{file_name}.xlsx' + encoded_filename = quote(filename) + return io.BytesIO(output.getvalue()) + + # headers = { + # 'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}" + # } + + result = await asyncio.to_thread(inner) + return StreamingResponse( + result, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + + +@router.post("/uploadDsSchema/{id}") +async def upload_ds_schema(session: SessionDep, id: int = Path(..., description=f"{PLACEHOLDER_PREFIX}ds_id"), + file: UploadFile = File(...)): + ALLOWED_EXTENSIONS = {"xlsx", "xls"} + if not file.filename.lower().endswith(tuple(ALLOWED_EXTENSIONS)): + raise HTTPException(400, "Only support .xlsx/.xls") + + try: + contents = await file.read() + excel_file = io.BytesIO(contents) + + sheet_names = pd.ExcelFile(excel_file, engine="openpyxl").sheet_names + + excel_file.seek(0) + + field_sheets = [] + table_sheet = None # [] + for sheet in sheet_names: + df = pd.read_excel(excel_file, sheet_name=sheet, engine="openpyxl") + if sheet == t_sheet: + table_sheet = df.where(pd.notnull(df), None).to_dict(orient="records") + else: + field_sheets.append( + {'sheet_name': sheet, 'data': df.where(pd.notnull(df), None).to_dict(orient="records")}) + + # print(field_sheets) + + # get data and update + # update table comment + if table_sheet and len(table_sheet) > 0: + for table in table_sheet: + session.query(CoreTable).filter( + and_(CoreTable.ds_id == id, CoreTable.table_name == table[t_n_col])).update( + {'custom_comment': table[t_c_col]}) + + # update field comment + if field_sheets and len(field_sheets) > 0: + for fields in field_sheets: + if len(fields['data']) > 0: + # get table id + table = session.query(CoreTable).filter( + and_(CoreTable.ds_id == id, CoreTable.table_name == fields['sheet_name'])).first() + if table: + for field in fields['data']: + session.query(CoreField).filter( + and_(CoreField.ds_id == id, + CoreField.table_id == table.id, + CoreField.field_name == field[f_n_col])).update( + {'custom_comment': field[f_c_col]}) + session.commit() + + return True + except Exception as e: + raise HTTPException(status_code=500, detail=f"解析 Excel 失败: {str(e)}") From 30fc652f84e9a63c61e4d7a0fa646ebccf0e6d83 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Thu, 18 Dec 2025 15:37:19 +0800 Subject: [PATCH 035/521] feat(Data Source): Supports batch uploading of data tables (Note) --- frontend/src/api/datasource.ts | 5 + .../src/assets/svg/icon_import_outlined.svg | 3 + frontend/src/i18n/en.json | 1 + frontend/src/i18n/ko-KR.json | 3 +- frontend/src/i18n/zh-CN.json | 3 +- frontend/src/views/ds/DataTable.vue | 85 ++++- .../system/excel-upload/UploaderRemark.vue | 323 ++++++++++++++++++ 7 files changed, 415 insertions(+), 8 deletions(-) create mode 100644 frontend/src/assets/svg/icon_import_outlined.svg create mode 100644 frontend/src/views/system/excel-upload/UploaderRemark.vue diff --git a/frontend/src/api/datasource.ts b/frontend/src/api/datasource.ts index 5a66e985b..c349c85e8 100644 --- a/frontend/src/api/datasource.ts +++ b/frontend/src/api/datasource.ts @@ -27,4 +27,9 @@ export const datasourceApi = { cancelRequests: () => request.cancelRequests(), getSchema: (data: any) => request.post('/datasource/getSchemaByConf', data), syncFields: (id: number) => request.post(`/datasource/syncFields/${id}`), + exportDsSchema: (id: any) => + request.get(`/datasource/exportDsSchema/${id}`, { + responseType: 'blob', + requestOptions: { customError: true }, + }), } diff --git a/frontend/src/assets/svg/icon_import_outlined.svg b/frontend/src/assets/svg/icon_import_outlined.svg new file mode 100644 index 000000000..52e5c2645 --- /dev/null +++ b/frontend/src/assets/svg/icon_import_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index dca1f8359..c0f7bd7b1 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -8,6 +8,7 @@ "Details": "Details" }, "parameter": { + "memo": "Memo update rules: Match table name and field name. If a match is found, update the table memo and field memo; otherwise, leave the memo unchanged.", "parameter_configuration": "Parameter Config", "question_count_settings": "Question Count Settings", "model_thinking_process": "Expand Model Thinking Process", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index e77a7fe0c..0132d6456 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -8,6 +8,7 @@ "Details": "세부" }, "parameter": { + "memo": "메모 업데이트 규칙: 테이블 이름과 필드 이름이 일치하는지 확인합니다. 일치하는 항목이 있으면 테이블 메모와 필드 메모를 업데이트하고, 그렇지 않으면 메모를 변경하지 않고 그대로 둡니다.", "parameter_configuration": "매개변수 구성", "question_count_settings": "질문 수 설정", "model_thinking_process": "모델 사고 프로세스 확장", @@ -819,4 +820,4 @@ "modelType": { "llm": "대형 언어 모델" } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 2ec825a75..47826a2bc 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -8,6 +8,7 @@ "Details": "详情" }, "parameter": { + "memo": "备注更新规则:根据表名和字段名匹配,如果匹配则更新表备注和字段备注;如果匹配不上,备注保持不变。", "parameter_configuration": "参数配置", "question_count_settings": "问数设置", "model_thinking_process": "展开模型思考过程", @@ -140,7 +141,7 @@ "click_to_select_file": "点击选择文件", "upload_hint_first": "先", "upload_hint_download_template": "下载模板", - "upload_hint_end": ",按要求填写后上传", + "upload_hint_end": ",按要求填写后上传。", "continue_to_upload": "继续导入", "reset_password": "重置密码", "password_reset_successful": "重置密码成功", diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index 4f43f97df..89aed990a 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -3,14 +3,17 @@ import { ref, computed, onMounted, reactive } from 'vue' import { datasourceApi } from '@/api/datasource' import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg' import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg' +import icon_import_outlined from '@/assets/svg/icon_import_outlined.svg' import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg' import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue' import edit from '@/assets/svg/icon_edit_outlined.svg' import { useI18n } from 'vue-i18n' import ParamsForm from './ParamsForm.vue' +import UploaderRemark from '@/views/system/excel-upload/UploaderRemark.vue' import TableRelationship from '@/views/ds/TableRelationship.vue' import icon_mindnote_outlined from '@/assets/svg/icon_mindnote_outlined.svg' import { Refresh } from '@element-plus/icons-vue' + interface Table { name: string host: string @@ -106,17 +109,29 @@ const fieldListComputed = computed(() => { return fieldList.value.slice((currentPage - 1) * pageSize, currentPage * pageSize) }) -const init = () => { +const init = (reset = false) => { initLoading.value = true datasourceApi.getDs(props.info.id).then((res) => { ds.value = res fieldList.value = [] pageInfo.total = 0 pageInfo.currentPage = 1 - datasourceApi.tableList(props.info.id).then((res) => { - tableList.value = res - initLoading.value = false - }) + datasourceApi + .tableList(props.info.id) + .then((res) => { + tableList.value = res + + if (currentTable.value?.id && reset) { + tableList.value.forEach((ele) => { + if (ele.id === currentTable.value?.id) { + clickTable(ele) + } + }) + } + }) + .finally(() => { + initLoading.value = false + }) }) } onMounted(() => { @@ -237,6 +252,47 @@ const syncFields = () => { }) } +function downloadTemplate() { + datasourceApi + .exportDsSchema(props.info.id) + .then((res) => { + const blob = new Blob([res], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = props.info.name + '.xlsx' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + }) + .catch(async (error) => { + if (error.response) { + try { + let text = await error.response.data.text() + try { + text = JSON.parse(text) + } finally { + ElMessage({ + message: text, + type: 'error', + showClose: true, + }) + } + } catch (e) { + console.error('Error processing error response:', e) + } + } else { + console.error('Other error:', error) + ElMessage({ + message: error, + type: 'error', + showClose: true, + }) + } + }) +} + const emits = defineEmits(['back', 'refresh']) const back = () => { emits('back') @@ -298,6 +354,16 @@ const btnSelectClick = (val: any) => {
{{ info.name }}
+ + + {{ $t('professional.export') }} + +
@@ -590,8 +656,9 @@ const btnSelectClick = (val: any) => { line-height: 22px; color: #646a73; border-bottom: 1px solid #1f232926; + position: relative; - .ed-button { + .ed-button.is-text { height: 22px; line-height: 22px; color: #646a73; @@ -606,6 +673,12 @@ const btnSelectClick = (val: any) => { } } + .export-remark { + position: absolute; + right: 116px; + top: 12px; + } + .name { color: #1f2329; margin-left: 4px; diff --git a/frontend/src/views/system/excel-upload/UploaderRemark.vue b/frontend/src/views/system/excel-upload/UploaderRemark.vue new file mode 100644 index 000000000..0f3362dc9 --- /dev/null +++ b/frontend/src/views/system/excel-upload/UploaderRemark.vue @@ -0,0 +1,323 @@ + + + + + + From 8a143ed21e884940d4d3c458589b0ba7edf48740 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Thu, 18 Dec 2025 16:00:33 +0800 Subject: [PATCH 036/521] fix: Page Embedding Authentication Vulnerability --- backend/apps/system/middleware/auth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py index 2dd201873..41167624c 100644 --- a/backend/apps/system/middleware/auth.py +++ b/backend/apps/system/middleware/auth.py @@ -169,6 +169,9 @@ async def validateEmbedded(self, param: str, trans: I18n) -> tuple[any]: raise Exception(message) assistant_info = await get_assistant_info(session=session, assistant_id=embeddedId) assistant_info = AssistantModel.model_validate(assistant_info) + payload = jwt.decode( + param, assistant_info.app_secret, algorithms=[security.ALGORITHM] + ) assistant_info = AssistantHeader.model_validate(assistant_info.model_dump(exclude_unset=True)) return True, session_user, assistant_info except Exception as e: From 2dc6da82a986200b3a5709a350d46c3c143e04e5 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Thu, 18 Dec 2025 16:53:24 +0800 Subject: [PATCH 037/521] fix: Add loading prompt for embedded page #622 --- frontend/src/views/embedded/page.vue | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/embedded/page.vue b/frontend/src/views/embedded/page.vue index e8ab6f8ef..78c26fd57 100644 --- a/frontend/src/views/embedded/page.vue +++ b/frontend/src/views/embedded/page.vue @@ -1,5 +1,8 @@ diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index ae1e6d077..ef0443d6d 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -819,5 +819,11 @@ }, "modelType": { "llm": "Large Language Model" + }, + "api_key": { + "info_tips": "The API Key is your credential for accessing the SQLBot API, with full permissions for your account. Please keep it secure! Do not disclose the API Key in any public channels to avoid security risks from unauthorized use.", + "create": "Create", + "to_doc": "View API", + "trigger_limit": "Supports up to {0} API Keys" } } diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 203f2a6c9..14837c614 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -819,5 +819,11 @@ }, "modelType": { "llm": "대형 언어 모델" + }, + "api_key": { + "info_tips": "API 키는 SQLBot API에 액세스하는 비밀키로 계정의 모든 권한을 갖고 있습니다. 꼭 안전하게 보관하세요! 외부 채널에 어떠한 방식으로도 API 키를 공개하지 마시고, 타인이 악용하여 보안 위협이 발생하는 것을 방지하세요.", + "create": "생성", + "to_doc": "API 보기", + "trigger_limit": "최대 {0}개의 API 키 생성 지원" } } diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index d96421c2b..6c5ecfef9 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -820,5 +820,11 @@ }, "modelType": { "llm": "大语言模型" + }, + "api_key": { + "info_tips": "API Key 是您访问 SQLBot API 的密钥,具有账户的完全权限,请您务必妥善保管!不要以任何方式公开 API Key 到外部渠道,避免被他人利用造成安全威胁。", + "create": "创建", + "to_doc": "查看 API", + "trigger_limit": "最多支持创建 {0} 个 API Key" } } From 6606bc09517421d24ce4150169cea467b494e6c1 Mon Sep 17 00:00:00 2001 From: ulleo Date: Tue, 23 Dec 2025 14:22:00 +0800 Subject: [PATCH 052/521] fix: resolve ambiguous boolean value error in pandas Series operations --- backend/common/utils/data_format.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/common/utils/data_format.py b/backend/common/utils/data_format.py index 9dc1304a7..366e1d7b9 100644 --- a/backend/common/utils/data_format.py +++ b/backend/common/utils/data_format.py @@ -8,14 +8,12 @@ class DataFormat: def safe_convert_to_string(df): df_copy = df.copy() - def format_value(x): - if pd.isna(x): - return "" - - return "\u200b" + str(x) - for col in df_copy.columns: - df_copy[col] = df_copy[col].apply(format_value) + # 使用map避免ambiguous truth value问题 + df_copy[col] = df_copy[col].map( + # 关键:在数字字符串前添加零宽空格,阻止pandas的自动格式化 + lambda x: "" if pd.isna(x) else "\u200b" + str(x) + ) return df_copy From 87b144face07692adf5791ee25d2c5f6f46b6825 Mon Sep 17 00:00:00 2001 From: xuwei-fit2cloud Date: Tue, 23 Dec 2025 14:59:28 +0800 Subject: [PATCH 053/521] Update default OS options in xpack.yml Removed 'ubuntu-22.04-arm' from default OS options. --- .github/workflows/xpack.yml | 98 +++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/xpack.yml diff --git a/.github/workflows/xpack.yml b/.github/workflows/xpack.yml new file mode 100644 index 000000000..fe092d340 --- /dev/null +++ b/.github/workflows/xpack.yml @@ -0,0 +1,98 @@ +# .github/workflows/wheel.yml +name: Build Wheels Universal + +on: + workflow_dispatch: + inputs: + + target_os: + description: 'Select OS to run on' + required: false + default: '["ubuntu-latest","macos-latest","windows-latest","macos-15-intel"]' + type: choice + options: + - '["macos-latest"]' + - '["macos-15-intel"]' + - '["ubuntu-latest"]' + - '["windows-latest"]' + - '["ubuntu-latest","windows-latest"]' + - '["ubuntu-latest","macos-latest"]' + - '["macos-latest","windows-latest"]' + - '["ubuntu-latest","macos-latest","windows-latest"]' + - '["ubuntu-latest","macos-latest","windows-latest","macos-15-intel"]' + publish: + description: 'Publish to repository' + required: false + default: false + type: boolean + repository: + description: 'Select repository to publish (Attention: Disable pypi selection until release)' + required: false + default: 'testpypi' + type: choice + options: + - testpypi + - pypi + xpack_branch: + description: 'SQLBot X-Pack Branch' + required: true + type: choice + default: "main" + options: + - main + +jobs: + build: + runs-on: ${{ matrix.os }} + environment: sqlbot-xpack-env + defaults: + run: + working-directory: ./sqlbot-xpack + strategy: + matrix: + os: ${{ fromJSON(inputs.target_os) }} + steps: + - uses: actions/checkout@v4 + - name: Check out SQLBot xpack repo + uses: actions/checkout@v4 + with: + repository: dataease/sqlbot-xpack + ref: ${{ inputs.xpack_branch }} + path: ./sqlbot-xpack + token: ${{ secrets.GH_TOKEN }} + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install and build + working-directory: ./sqlbot-xpack/xpack_static + run: | + npm install + npm run build:prod + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install cython wheel twine + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + - name: Install dependencies + run: uv sync + - name: Build encrypted + run: python build_scripts/build.py + - name: Pre build wheel + run: python ./build_scripts/format.py encrypted + - name: Build wheel + run: python -m pip wheel --no-deps -w dist . + - name: Finish build wheek + run: python ./build_scripts/format.py + - name: Show wheel + run: unzip -l dist/* + - name: Publish to repository + if: ${{ inputs.publish }} + run: twine upload --repository-url ${{ inputs.repository == 'pypi' && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }} -u __token__ -p ${{ inputs.repository == 'pypi' && secrets.FORMAL_PYPI_API_TOKEN || secrets.PYPI_API_TOKEN }} dist/*.whl --verbose From 687c7cf04f19d77d93f9d3d70438643e852faed7 Mon Sep 17 00:00:00 2001 From: junjun Date: Tue, 23 Dec 2025 15:03:19 +0800 Subject: [PATCH 054/521] feat: Add Api Docs --- backend/apps/datasource/models/datasource.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/apps/datasource/models/datasource.py b/backend/apps/datasource/models/datasource.py index c4760d624..ddd37c7f1 100644 --- a/backend/apps/datasource/models/datasource.py +++ b/backend/apps/datasource/models/datasource.py @@ -148,8 +148,8 @@ def __init__(self, attr1, attr2): class TableSchemaResponse(BaseModel): - tableName: str = None - tableComment: str = None + tableName: str = '' + tableComment: str | None = '' class ColumnSchema: @@ -164,9 +164,9 @@ def __init__(self, attr1, attr2, attr3): class ColumnSchemaResponse(BaseModel): - fieldName: str - fieldType: str - fieldComment: str + fieldName: str | None = '' + fieldType: str | None = '' + fieldComment: str | None = '' class TableAndFields: @@ -183,7 +183,8 @@ def __init__(self, schema, table, fields): class FieldObj(BaseModel): fieldName: str | None + class PreviewResponse(BaseModel): - fields:List = [] - data:List = [] - sql:str = '' \ No newline at end of file + fields: List | None = [] + data: List | None = [] + sql: str | None = '' From cfa19b58c0dcf584aa5e5776376b97186dc5ae37 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Tue, 23 Dec 2025 15:29:49 +0800 Subject: [PATCH 055/521] fix(Data Source): Supports batch uploading of data tables (Note) --- frontend/src/i18n/en.json | 2 ++ frontend/src/i18n/ko-KR.json | 4 +++- frontend/src/i18n/zh-CN.json | 4 +++- frontend/src/views/ds/DataTable.vue | 2 +- frontend/src/views/system/excel-upload/UploaderRemark.vue | 4 ++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index ef0443d6d..7d0622465 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -8,6 +8,8 @@ "Details": "Details" }, "parameter": { + "export_notes": "Export notes", + "import_notes": "Import notes", "memo": "Memo update rules: Match table name and field name. If a match is found, update the table memo and field memo; otherwise, leave the memo unchanged.", "parameter_configuration": "Parameter Config", "question_count_settings": "Question Count Settings", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index 14837c614..ca764db42 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -8,6 +8,8 @@ "Details": "세부" }, "parameter": { + "export_notes": "수출 참고사항", + "import_notes": "수입 참고사항", "memo": "메모 업데이트 규칙: 테이블 이름과 필드 이름이 일치하는지 확인합니다. 일치하는 항목이 있으면 테이블 메모와 필드 메모를 업데이트하고, 그렇지 않으면 메모를 변경하지 않고 그대로 둡니다.", "parameter_configuration": "매개변수 구성", "question_count_settings": "질문 수 설정", @@ -826,4 +828,4 @@ "to_doc": "API 보기", "trigger_limit": "최대 {0}개의 API 키 생성 지원" } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 6c5ecfef9..91464287f 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -8,6 +8,8 @@ "Details": "详情" }, "parameter": { + "export_notes": "导出备注", + "import_notes": "导入备注", "memo": "备注更新规则:根据表名和字段名匹配,如果匹配则更新表备注和字段备注;如果匹配不上,备注保持不变。", "parameter_configuration": "参数配置", "question_count_settings": "问数设置", @@ -827,4 +829,4 @@ "to_doc": "查看 API", "trigger_limit": "最多支持创建 {0} 个 API Key" } -} +} \ No newline at end of file diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index 25c309dca..d0db7d2b7 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -364,7 +364,7 @@ const btnSelectClick = (val: any) => { - {{ $t('professional.export') }} + {{ $t('parameter.export_notes') }} { - {{ $t('user.import') }} + {{ $t('parameter.import_notes') }} Date: Tue, 23 Dec 2025 16:15:35 +0800 Subject: [PATCH 056/521] perf: Optimizing API Key Authentication --- backend/alembic/versions/056_api_key_ddl.py | 39 +++++++++++ backend/apps/api.py | 3 +- backend/apps/system/api/apikey.py | 60 +++++++++++++++++ backend/apps/system/crud/apikey_manage.py | 17 +++++ backend/apps/system/middleware/auth.py | 55 ++++++++++++++- backend/apps/system/models/system_model.py | 13 +++- backend/apps/system/schemas/auth.py | 1 + backend/apps/system/schemas/system_schema.py | 10 +++ frontend/src/components/layout/Apikey.vue | 70 ++++++++------------ 9 files changed, 223 insertions(+), 45 deletions(-) create mode 100644 backend/alembic/versions/056_api_key_ddl.py create mode 100644 backend/apps/system/api/apikey.py create mode 100644 backend/apps/system/crud/apikey_manage.py diff --git a/backend/alembic/versions/056_api_key_ddl.py b/backend/alembic/versions/056_api_key_ddl.py new file mode 100644 index 000000000..e2657927d --- /dev/null +++ b/backend/alembic/versions/056_api_key_ddl.py @@ -0,0 +1,39 @@ +"""056_api_key_ddl + +Revision ID: d9a5589fc00b +Revises: 3d4bd2d673dc +Create Date: 2025-12-23 13:41:26.705947 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'd9a5589fc00b' +down_revision = '3d4bd2d673dc' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('sys_apikey', + sa.Column('access_key', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('secret_key', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('create_time', sa.BigInteger(), nullable=False), + sa.Column('uid', sa.BigInteger(), nullable=False), + sa.Column('status', sa.Boolean(), nullable=False), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_sys_apikey_id'), 'sys_apikey', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_sys_apikey_id'), table_name='sys_apikey') + op.drop_table('sys_apikey') + # ### end Alembic commands ### diff --git a/backend/apps/api.py b/backend/apps/api.py index f53f7fa48..06dd11b59 100644 --- a/backend/apps/api.py +++ b/backend/apps/api.py @@ -5,7 +5,7 @@ from apps.data_training.api import data_training from apps.datasource.api import datasource, table_relation, recommended_problem from apps.mcp import mcp -from apps.system.api import login, user, aimodel, workspace, assistant, parameter +from apps.system.api import login, user, aimodel, workspace, assistant, parameter, apikey from apps.terminology.api import terminology from apps.settings.api import base @@ -24,5 +24,6 @@ api_router.include_router(mcp.router) api_router.include_router(table_relation.router) api_router.include_router(parameter.router) +api_router.include_router(apikey.router) api_router.include_router(recommended_problem.router) diff --git a/backend/apps/system/api/apikey.py b/backend/apps/system/api/apikey.py new file mode 100644 index 000000000..cfed5ae74 --- /dev/null +++ b/backend/apps/system/api/apikey.py @@ -0,0 +1,60 @@ + +from fastapi import APIRouter +from sqlmodel import func, select +from apps.system.crud.apikey_manage import clear_api_key_cache +from apps.system.models.system_model import ApiKeyModel +from apps.system.schemas.system_schema import ApikeyGridItem, ApikeyStatus +from common.core.deps import CurrentUser, SessionDep +from common.utils.time import get_timestamp +import secrets + +router = APIRouter(tags=["system_apikey"], prefix="/system/apikey") + +@router.get("") +async def grid(session: SessionDep, current_user: CurrentUser) -> list[ApikeyGridItem]: + query = select(ApiKeyModel).where(ApiKeyModel.uid == current_user.id).order_by(ApiKeyModel.create_time.desc()) + return session.exec(query).all() + +@router.post("") +async def create(session: SessionDep, current_user: CurrentUser): + count = session.exec(select(func.count()).select_from(ApiKeyModel)).one() + if count >= 5: + raise ValueError("Maximum of 5 API keys allowed") + access_key = secrets.token_urlsafe(16) + secret_key = secrets.token_urlsafe(32) + api_key = ApiKeyModel( + access_key=access_key, + secret_key=secret_key, + create_time=get_timestamp(), + uid=current_user.id, + status=True + ) + session.add(api_key) + session.commit() + return api_key.id + +@router.put("/status") +async def status(session: SessionDep, current_user: CurrentUser, dto: ApikeyStatus): + api_key = session.get(ApiKeyModel, dto.id) + if not api_key: + raise ValueError("API Key not found") + if api_key.uid != current_user.id: + raise PermissionError("No permission to modify this API Key") + if dto.status == api_key.status: + return + api_key.status = dto.status + await clear_api_key_cache(api_key.access_key) + session.add(api_key) + session.commit() + +@router.delete("/{id}") +async def delete(session: SessionDep, current_user: CurrentUser, id: int): + api_key = session.get(ApiKeyModel, id) + if not api_key: + raise ValueError("API Key not found") + if api_key.uid != current_user.id: + raise PermissionError("No permission to delete this API Key") + await clear_api_key_cache(api_key.access_key) + session.delete(api_key) + session.commit() + \ No newline at end of file diff --git a/backend/apps/system/crud/apikey_manage.py b/backend/apps/system/crud/apikey_manage.py new file mode 100644 index 000000000..ce6b43fcc --- /dev/null +++ b/backend/apps/system/crud/apikey_manage.py @@ -0,0 +1,17 @@ + +from sqlmodel import select + +from apps.system.models.system_model import ApiKeyModel +from apps.system.schemas.auth import CacheName, CacheNamespace +from common.core.deps import SessionDep +from common.core.sqlbot_cache import cache, clear_cache +from common.utils.utils import SQLBotLogUtil + +@cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.ASK_INFO, keyExpression="access_key") +async def get_api_key(session: SessionDep, access_key: str) -> ApiKeyModel | None: + query = select(ApiKeyModel).where(ApiKeyModel.access_key == access_key) + return session.exec(query).first() + +@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.ASK_INFO, keyExpression="access_key") +async def clear_api_key_cache(access_key: str): + SQLBotLogUtil.info(f"Api key cache for [{access_key}] has been cleaned") \ No newline at end of file diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py index 41167624c..bdf887118 100644 --- a/backend/apps/system/middleware/auth.py +++ b/backend/apps/system/middleware/auth.py @@ -7,7 +7,8 @@ import jwt from sqlmodel import Session from starlette.middleware.base import BaseHTTPMiddleware -from apps.system.models.system_model import AssistantModel +from apps.system.crud.apikey_manage import get_api_key +from apps.system.models.system_model import ApiKeyModel, AssistantModel from common.core.db import engine from apps.system.crud.assistant import get_assistant_info, get_assistant_user from apps.system.crud.user import get_user_by_account, get_user_info @@ -33,7 +34,15 @@ async def dispatch(self, request, call_next): return await call_next(request) assistantTokenKey = settings.ASSISTANT_TOKEN_KEY assistantToken = request.headers.get(assistantTokenKey) + askToken = request.headers.get("X-SQLBOT-ASK-TOKEN") trans = await get_i18n(request) + if askToken: + validate_pass, data = await self.validateAskToken(askToken, trans) + if validate_pass: + request.state.current_user = data + return await call_next(request) + message = trans('i18n_permission.authenticate_invalid', msg = data) + return JSONResponse(message, status_code=401, headers={"Access-Control-Allow-Origin": "*"}) #if assistantToken and assistantToken.lower().startswith("assistant "): if assistantToken: validator: tuple[any] = await self.validateAssistant(assistantToken, trans) @@ -62,6 +71,50 @@ async def dispatch(self, request, call_next): def is_options(self, request: Request): return request.method == "OPTIONS" + async def validateAskToken(self, askToken: Optional[str], trans: I18n): + if not askToken: + return False, f"Miss Token[X-SQLBOT-ASK-TOKEN]!" + schema, param = get_authorization_scheme_param(askToken) + if schema.lower() != "sk": + return False, f"Token schema error!" + try: + payload = jwt.decode( + param, options={"verify_signature": False, "verify_exp": False}, algorithms=[security.ALGORITHM] + ) + access_key = payload.get('access_key', None) + + if not access_key: + return False, f"Miss access_key payload error!" + with Session(engine) as session: + api_key_model = await get_api_key(session, access_key) + api_key_model = ApiKeyModel.model_validate(api_key_model) if api_key_model else None + if not api_key_model: + return False, f"Invalid access_key!" + if not api_key_model.status: + return False, f"Disabled access_key!" + payload = jwt.decode( + param, api_key_model.secret_key, algorithms=[security.ALGORITHM] + ) + uid = api_key_model.uid + session_user = await get_user_info(session = session, user_id = uid) + if not session_user: + message = trans('i18n_not_exist', msg = trans('i18n_user.account')) + raise Exception(message) + session_user = UserInfoDTO.model_validate(session_user) + if session_user.status != 1: + message = trans('i18n_login.user_disable', msg = trans('i18n_concat_admin')) + raise Exception(message) + if not session_user.oid or session_user.oid == 0: + message = trans('i18n_login.no_associated_ws', msg = trans('i18n_concat_admin')) + raise Exception(message) + return True, session_user + except Exception as e: + msg = str(e) + SQLBotLogUtil.exception(f"Token validation error: {msg}") + if 'expired' in msg: + return False, jwt.ExpiredSignatureError(trans('i18n_permission.token_expired')) + return False, e + async def validateToken(self, token: Optional[str], trans: I18n): if not token: return False, f"Miss Token[{settings.TOKEN_KEY}]!" diff --git a/backend/apps/system/models/system_model.py b/backend/apps/system/models/system_model.py index 281b737c7..681c6c73d 100644 --- a/backend/apps/system/models/system_model.py +++ b/backend/apps/system/models/system_model.py @@ -67,4 +67,15 @@ 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) \ No newline at end of file + 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()) + status: bool = Field(default=True, nullable=False) + +class ApiKeyModel(SnowflakeBase, ApiKeyBaseModel, table=True): + __tablename__ = "sys_apikey" \ No newline at end of file diff --git a/backend/apps/system/schemas/auth.py b/backend/apps/system/schemas/auth.py index 3b3816ab0..14db17d97 100644 --- a/backend/apps/system/schemas/auth.py +++ b/backend/apps/system/schemas/auth.py @@ -16,6 +16,7 @@ class CacheName(Enum): USER_INFO = "user:info" ASSISTANT_INFO = "assistant:info" ASSISTANT_DS = "assistant:ds" + ASK_INFO = "ask:info" def __str__(self): return self.value diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index 67d66e1da..9c67f92f0 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -207,3 +207,13 @@ class AssistantUiSchema(BaseCreatorDTO): name: Optional[str] = None welcome: Optional[str] = None welcome_desc: Optional[str] = None + +class ApikeyStatus(BaseModel): + id: int = Field(description=f"{PLACEHOLDER_PREFIX}id") + status: bool = Field(description=f"{PLACEHOLDER_PREFIX}status") + +class ApikeyGridItem(BaseCreatorDTO): + access_key: str = Field(description=f"Access Key") + secret_key: str = Field(description=f"Secret Key") + status: bool = Field(description=f"{PLACEHOLDER_PREFIX}status") + create_time: int = Field(description=f"{PLACEHOLDER_PREFIX}create_time") \ No newline at end of file diff --git a/frontend/src/components/layout/Apikey.vue b/frontend/src/components/layout/Apikey.vue index 36382e73f..e56d9d5a3 100644 --- a/frontend/src/components/layout/Apikey.vue +++ b/frontend/src/components/layout/Apikey.vue @@ -1,5 +1,5 @@