mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}]",
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
},
|
||||
"i18n_permission": {
|
||||
"only_admin": "仅支持管理员调用!",
|
||||
"no_permission": "无权调用{url}{msg}"
|
||||
"no_permission": "无权调用{url}{msg}",
|
||||
"authenticate_invalid": "认证无效【{msg}】"
|
||||
|
||||
},
|
||||
"i18n_llm": {
|
||||
|
||||
@@ -54,13 +54,13 @@ template:
|
||||
]
|
||||
```
|
||||
|
||||
### 提供表结构如下:
|
||||
{schema}
|
||||
|
||||
### 响应, 请直接返回JSON结果:
|
||||
```json
|
||||
|
||||
user: |
|
||||
### 提供表结构如下:
|
||||
{schema}
|
||||
|
||||
### 问题:
|
||||
{question}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ class HttpService {
|
||||
setTimeout(() => {
|
||||
wsCache.delete('user.token')
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
}, 2000)
|
||||
return
|
||||
// break
|
||||
case 403:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'"
|
||||
>
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user