Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-07-30 17:37:44 +08:00
15 changed files with 129 additions and 52 deletions
+1 -8
View File
@@ -32,16 +32,9 @@ TBD: 交流群二维码
## UI 展示
<table style="border-collapse: collapse; border: 1px solid black;">
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "TBD" alt="SQLBot Demo1" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "TBD" alt="SQLBot Demo2" /></td>
<img alt="q&a" src="https://github.com/user-attachments/assets/55526514-52f3-4cfe-98ec-08a986259280" />
</tr>
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "TBD" alt="SQLBot Demo3" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "TBD" alt="SQLBot Demo4" /></td>
</tr>
</table>
## Star History
+25 -2
View File
@@ -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
+2
View File
@@ -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:
+16 -12
View File
@@ -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)}")
-1
View File
@@ -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:
+2 -1
View File
@@ -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}]",
+2 -1
View File
@@ -24,7 +24,8 @@
},
"i18n_permission": {
"only_admin": "仅支持管理员调用!",
"no_permission": "无权调用{url}{msg}"
"no_permission": "无权调用{url}{msg}",
"authenticate_invalid": "认证无效【{msg}】"
},
"i18n_llm": {
+3 -3
View File
@@ -54,13 +54,13 @@ template:
]
```
### 提供表结构如下:
{schema}
### 响应, 请直接返回JSON结果:
```json
user: |
### 提供表结构如下:
{schema}
### 问题:
{question}
+1 -1
View File
@@ -182,7 +182,7 @@ class HttpService {
setTimeout(() => {
wsCache.delete('user.token')
window.location.reload()
}, 1000)
}, 2000)
return
// break
case 403:
+37 -1
View File
@@ -21,7 +21,6 @@
:class="{ 'assistant-popover-sidebar': isAssistant }"
>
<el-popover
:visible="isAssistant ? floatPopoverVisible : null"
:width="280"
placement="bottom-start"
:popper-style="{ ...defaultFloatPopoverStyle }"
@@ -48,6 +47,31 @@
@on-click-side-bar-btn="hideSideBar"
/>
</el-popover>
<el-drawer
v-model="floatPopoverVisible"
:with-header="false"
direction="ltr"
size="278"
modal-class="assistant-popover_sidebar"
:before-close="hideSideBar"
>
<ChatListContainer
ref="floatPopoverRef"
v-model:chat-list="chatList"
v-model:current-chat-id="currentChatId"
v-model:current-chat="currentChat"
v-model:loading="loading"
:in-popover="false"
@go-empty="goEmpty"
@on-chat-created="onChatCreated"
@on-click-history="onClickHistory"
@on-chat-deleted="onChatDeleted"
@on-chat-renamed="onChatRenamed"
@on-click-side-bar-btn="hideSideBar"
/>
</el-drawer>
<el-tooltip effect="dark" :content="t('qa.new_chat')" placement="bottom">
<el-button link type="primary" class="icon-btn" @click="createNewChatSimple">
<el-icon>
@@ -1092,3 +1116,15 @@ onMounted(() => {
}
}
</style>
<style lang="less">
.assistant-popover_sidebar {
.ed-drawer {
height: 100% !important;
margin-top: 0 !important;
}
.ed-drawer__body {
padding: 0;
}
}
</style>
+5 -1
View File
@@ -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"
></DatasourceForm>
</el-drawer>
</div>
+29 -20
View File
@@ -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<any>([])
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({
<template>
<div
v-loading="uploadLoading || saveLoading"
v-loading="uploadLoading || saveLoading || checkLoading"
class="model-form"
:class="(!isCreate || activeStep === 2) && 'edit-form'"
>
+1
View File
@@ -88,6 +88,7 @@ onMounted(() => {
}
.ed-drawer {
margin-top: 50px;
height: calc(100vh - 50px) !important;
}`
document.querySelector('head')?.appendChild(style)
})
@@ -760,6 +760,7 @@ const saveHandler = () => {
:name="ele.name"
:type="ele.type"
:type-name="ele.type_name"
:description="ele.description"
:is-private="dsForm.private_list.includes(ele.id)"
:num="ele.num"
@active="handleActive(ele)"
+4 -1
View File
@@ -35,7 +35,7 @@ const activeName = ref('')
const activeType = ref('')
const modelFormRef = ref()
const cardRefs = ref<any[]>([])
const showCardError = ref(true) // if you don`t want card mask error, just change this to false
// const showCardError = ref(true) // if you don`t want card mask error, just change this to false
reactive({
form: {
id: '',
@@ -71,6 +71,8 @@ const defaultModelListWithSearch = computed(() => {
})
const modelCheckHandler = async (item: any) => {
console.log(item)
/**
const response = await modelApi.check(item)
const reader = response.body.getReader()
const decoder = new TextDecoder()
@@ -110,6 +112,7 @@ const modelCheckHandler = async (item: any) => {
currentRef?.showErrorMask(checkMsg)
}
})
*/
}
const duplicateName = async (item: any) => {
const res = await modelApi.queryAll()