diff --git a/README.md b/README.md index 11041d56..a952ba30 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,9 @@ TBD: 交流群二维码 ## UI 展示 - - - + q&a - - - - -
SQLBot Demo1SQLBot Demo2
SQLBot Demo3SQLBot Demo4
## Star History diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index e758fa93..b208f4e2 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -439,6 +439,7 @@ class LLMService: _ds = self.out_ds_instance.get_ds(data['id']) self.ds = _ds self.chat_question.engine = _ds.type + self.chat_question.db_schema = self.out_ds_instance.get_db_schema(self.ds.id) _engine_type = self.chat_question.engine _chat.engine_type = _ds.type else: @@ -448,6 +449,7 @@ class LLMService: raise Exception(f"Datasource configuration with id {_datasource} not found") self.ds = CoreDatasource(**_ds.model_dump()) self.chat_question.engine = _ds.type_name if _ds.type != 'excel' else 'PostgreSQL' + self.chat_question.db_schema = get_table_schema(session=self.session, current_user=self.current_user, ds=self.ds) _engine_type = self.chat_question.engine _chat.engine_type = _ds.type_name # save chat @@ -698,6 +700,8 @@ class LLMService: def check_save_chart(self, res: str) -> Dict[str, Any]: json_str = extract_nested_json(res) + if json_str is None: + raise Exception("Cannot parse chart config from answer") data = orjson.loads(json_str) chart: Dict[str, Any] = {} @@ -834,7 +838,8 @@ class LLMService: self.ds.id) if self.out_ds_instance else get_table_schema(session=self.session, current_user=self.current_user, ds=self.ds) - + else: + self.validate_history_ds() # generate sql sql_res = self.generate_sql() full_sql_text = '' @@ -1023,7 +1028,25 @@ class LLMService: finally: # end pass - + + def validate_history_ds(self): + _ds = self.ds + if not self.current_assistant: + current_ds = self.session.exec(CoreDatasource, _ds.id) + if not current_ds: + raise Exception('ds is invalid') + else: + try: + _ds_list: list[dict] = get_assistant_ds(session=self.session, llm_service=self) + match_ds = any(item.get("id") == _ds.id for item in _ds_list) + if not match_ds: + type = self.current_assistant.type + msg = f"ds is invalid [please check ds list and public ds list]" if type == 0 else f"ds is invalid [please check ds api]" + raise Exception(msg) + except Exception as e: + raise Exception(f"ds is invalid [{str(e)}]") + + def execute_sql_with_db(db: SQLDatabase, sql: str) -> str: """Execute SQL query using SQLDatabase diff --git a/backend/apps/system/crud/user.py b/backend/apps/system/crud/user.py index d721ef50..3dcd4ccf 100644 --- a/backend/apps/system/crud/user.py +++ b/backend/apps/system/crud/user.py @@ -26,6 +26,8 @@ def get_user_by_account(*, session: Session, account: str) -> BaseUserDTO | None @cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="user_id") async def get_user_info(*, session: Session, user_id: int) -> UserInfoDTO | None: db_user: UserModel = get_db_user(session = session, user_id = user_id) + if not db_user: + return None userInfo = UserInfoDTO.model_validate(db_user.model_dump()) userInfo.isAdmin = userInfo.id == 1 and userInfo.account == 'admin' if userInfo.isAdmin: diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py index 4f4d8706..be44d814 100644 --- a/backend/apps/system/middleware/auth.py +++ b/backend/apps/system/middleware/auth.py @@ -13,9 +13,11 @@ from apps.system.schemas.system_schema import AssistantHeader, UserInfoDTO from common.core import security from common.core.config import settings from common.core.schemas import TokenPayload +from common.utils.locale import I18n from common.utils.utils import SQLBotLogUtil from common.utils.whitelist import whiteUtils from fastapi.security.utils import get_authorization_scheme_param +from common.core.deps import get_i18n class TokenMiddleware(BaseHTTPMiddleware): @@ -29,6 +31,7 @@ class TokenMiddleware(BaseHTTPMiddleware): return await call_next(request) assistantTokenKey = settings.ASSISTANT_TOKEN_KEY assistantToken = request.headers.get(assistantTokenKey) + trans = await get_i18n(request) #if assistantToken and assistantToken.lower().startswith("assistant "): if assistantToken: validator: tuple[any] = await self.validateAssistant(assistantToken) @@ -36,20 +39,23 @@ class TokenMiddleware(BaseHTTPMiddleware): request.state.current_user = validator[1] request.state.assistant = validator[2] return await call_next(request) - return JSONResponse(f"Unauthorized:[{validator[1]}]", status_code=401, headers={"Access-Control-Allow-Origin": "*"}) + message = trans('i18n_permission.authenticate_invalid', msg = validator[1]) + return JSONResponse(message, status_code=401, headers={"Access-Control-Allow-Origin": "*"}) #validate pass tokenkey = settings.TOKEN_KEY token = request.headers.get(tokenkey) - validate_pass, data = await self.validateToken(token) + validate_pass, data = await self.validateToken(token, trans) if validate_pass: request.state.current_user = data return await call_next(request) - return JSONResponse(f"Unauthorized:[{data}]", status_code=401, headers={"Access-Control-Allow-Origin": "*"}) + + message = trans('i18n_permission.authenticate_invalid', msg = data) + return JSONResponse(message, status_code=401, headers={"Access-Control-Allow-Origin": "*"}) def is_options(self, request: Request): return request.method == "OPTIONS" - async def validateToken(self, token: Optional[str]): + async def validateToken(self, token: Optional[str], trans: I18n): if not token: return False, f"Miss Token[{settings.TOKEN_KEY}]!" schema, param = get_authorization_scheme_param(token) @@ -63,17 +69,15 @@ class TokenMiddleware(BaseHTTPMiddleware): with Session(engine) as session: session_user = await get_user_info(session = session, user_id = token_data.id) if not session_user: - raise Exception(f"User not found with id: {token_data.id}") + 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: - raise Exception(f"User is not active!") + message = trans('i18n_login.user_disable', msg = trans('i18n_concat_admin')) + raise Exception(message) if not session_user.oid or session_user.oid == 0: - raise Exception(f"User default space is not set!") - """ if token_data.oid != session_user.oid: - raise HTTPException( - status_code=401, - detail="Default space has been changed, please login again!" - ) """ + message = trans('i18n_login.no_associated_ws', msg = trans('i18n_concat_admin')) + raise Exception(message) return True, session_user except Exception as e: SQLBotLogUtil.exception(f"Token validation error: {str(e)}") diff --git a/backend/common/core/config.py b/backend/common/core/config.py index 5d16c61a..9de1279b 100644 --- a/backend/common/core/config.py +++ b/backend/common/core/config.py @@ -9,7 +9,6 @@ from pydantic import ( ) from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings, SettingsConfigDict -from typing_extensions import Self def parse_cors(v: Any) -> list[str] | str: diff --git a/backend/locales/en.json b/backend/locales/en.json index 7b775419..ffe76fd3 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -24,7 +24,8 @@ }, "i18n_permission": { "only_admin": "Only administrators can call this!", - "no_permission": "No permission to access {url}{msg}" + "no_permission": "No permission to access {url}{msg}", + "authenticate_invalid": "Authenticate invalid [{msg}]" }, "i18n_llm": { "validate_error": "Validation failed [{msg}]", diff --git a/backend/locales/zh-CN.json b/backend/locales/zh-CN.json index abadd609..2a63da7f 100644 --- a/backend/locales/zh-CN.json +++ b/backend/locales/zh-CN.json @@ -24,7 +24,8 @@ }, "i18n_permission": { "only_admin": "仅支持管理员调用!", - "no_permission": "无权调用{url}{msg}" + "no_permission": "无权调用{url}{msg}", + "authenticate_invalid": "认证无效【{msg}】" }, "i18n_llm": { diff --git a/backend/template.yaml b/backend/template.yaml index 60eb5a5b..0c86b1ce 100644 --- a/backend/template.yaml +++ b/backend/template.yaml @@ -54,13 +54,13 @@ template: ] ``` + ### 提供表结构如下: + {schema} + ### 响应, 请直接返回JSON结果: ```json user: | - ### 提供表结构如下: - {schema} - ### 问题: {question} diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 5cbdc90d..e9ae6c17 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -182,7 +182,7 @@ class HttpService { setTimeout(() => { wsCache.delete('user.token') window.location.reload() - }, 1000) + }, 2000) return // break case 403: diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index ecd89429..836f4231 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -21,7 +21,6 @@ :class="{ 'assistant-popover-sidebar': isAssistant }" > + + + + + @@ -1092,3 +1116,15 @@ onMounted(() => { } } + + diff --git a/frontend/src/views/ds/Datasource.vue b/frontend/src/views/ds/Datasource.vue index b1c0264e..4273d211 100644 --- a/frontend/src/views/ds/Datasource.vue +++ b/frontend/src/views/ds/Datasource.vue @@ -77,6 +77,10 @@ const handleDefaultDatasourceChange = (item: any) => { } } +const changeActiveStep = (val: number) => { + activeStep.value = val > 2 ? 2 : val +} + const formatKeywords = (item: string) => { if (!defaultDatasourceKeywords.value) return item return item.replaceAll( @@ -359,7 +363,7 @@ const back = () => { :active-type="activeType" @refresh="refresh" @close="beforeClose" - @change-active-step="(val: number) => (activeStep = val)" + @change-active-step="changeActiveStep" > diff --git a/frontend/src/views/ds/DatasourceForm.vue b/frontend/src/views/ds/DatasourceForm.vue index 0440699e..e81db1b8 100644 --- a/frontend/src/views/ds/DatasourceForm.vue +++ b/frontend/src/views/ds/DatasourceForm.vue @@ -9,6 +9,7 @@ import { ElMessage } from 'element-plus-secondary' import type { FormInstance, FormRules } from 'element-plus-secondary' import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg' import FixedSizeList from 'element-plus-secondary/es/components/virtual-list/src/components/fixed-size-list.mjs' +import { debounce } from 'lodash-es' import { Plus } from '@element-plus/icons-vue' import { useCache } from '@/utils/useCache' import { haveSchema } from '@/views/ds/js/ds-type' @@ -42,6 +43,7 @@ const tableList = ref([]) const excelUploadSuccess = ref(false) const tableListLoading = ref(false) const token = wsCache.get('user.token') +const checkLoading = ref(false) const request_key = computed(() => { // eslint-disable-next-line no-undef return LicenseGenerator.generate() @@ -339,7 +341,7 @@ const check = () => { onBeforeUnmount(() => (saveLoading.value = false)) -const next = async (formEl: FormInstance | undefined) => { +const next = debounce(async (formEl: FormInstance | undefined) => { if (!formEl) return await formEl.validate((valid) => { if (valid) { @@ -349,31 +351,38 @@ const next = async (formEl: FormInstance | undefined) => { emit('changeActiveStep', props.activeStep + 1) } } else { + if (checkLoading.value) return // check status if success do next const requestObj = buildConf() - datasourceApi.check(requestObj).then((res: boolean) => { - if (res) { - emit('changeActiveStep', props.activeStep + 1) - // request tables - datasourceApi.getTablesByConf(requestObj).then((res: any) => { - tableList.value = res - }) - } else { - ElMessage({ - message: t('ds.form.connect.failed'), - type: 'error', - showClose: true, - }) - } - }) + checkLoading.value = true + datasourceApi + .check(requestObj) + .then((res: boolean) => { + if (res) { + emit('changeActiveStep', props.activeStep + 1) + // request tables + datasourceApi.getTablesByConf(requestObj).then((res: any) => { + tableList.value = res + }) + } else { + ElMessage({ + message: t('ds.form.connect.failed'), + type: 'error', + showClose: true, + }) + } + }) + .finally(() => { + checkLoading.value = false + }) } } }) -} +}, 300) -const preview = () => { +const preview = debounce(() => { emit('changeActiveStep', props.activeStep - 1) -} +}, 200) const beforeUpload = (rawFile: any) => { setFile(rawFile) @@ -479,7 +488,7 @@ defineExpose({