diff --git a/README.md b/README.md index daf72d41..fc7d4b30 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,15 @@ TBD - Backend:[Python / Django](https://www.djangoproject.com/) - Database:[PostgreSQL + pgvector](https://www.postgresql.org/) +## 飞致云的其他明星项目 + +- [DataEase](https://github.com/dataease/dataease/) - 人人可用的开源 BI 工具 +- [1Panel](https://github.com/1panel-dev/1panel/) - 现代化、开源的 Linux 服务器运维管理面板 +- [MaxKB](https://github.com/1panel-dev/MaxKB/) - 基于 LLM 大语言模型的开源知识库问答系统 +- [JumpServer](https://github.com/jumpserver/jumpserver/) - 广受欢迎的开源堡垒机 +- [Halo](https://github.com/halo-dev/halo/) - 强大易用的开源建站工具 +- [MeterSphere](https://github.com/metersphere/metersphere/) - 新一代的开源持续测试工具 + ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/SQLBot&type=Date)](https://star-history.com/#1Panel-dev/SQLBot&Date) diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index fe0dd7bd..f501a63c 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -536,8 +536,8 @@ class LLMService: ).first() if obj is not None: res.append(transRecord2DTO(self.session, permission)) - wheres = transFilterTree(self.session, res, self.ds) - filters.append({"table": table.table_name, "filter": wheres}) + where_str = transFilterTree(self.session, res, self.ds) + filters.append({"table": table.table_name, "filter": where_str}) filter = json.dumps(filters, ensure_ascii=False) @@ -662,6 +662,16 @@ class LLMService: if data['type'] and data['type'] != 'error': # todo type check chart = data + if chart.get('columns'): + for v in chart.get('columns'): + v['value'] = v.get('value').lower() + if chart.get('axis'): + if chart.get('axis').get('x'): + chart.get('axis').get('x')['value'] = chart.get('axis').get('x').get('value').lower() + if chart.get('axis').get('y'): + chart.get('axis').get('y')['value'] = chart.get('axis').get('y').get('value').lower() + if chart.get('axis').get('series'): + chart.get('axis').get('series')['value'] = chart.get('axis').get('series').get('value').lower() elif data['type'] == 'error': message = data['reason'] error = True diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py index a52ffc8b..7642c221 100644 --- a/backend/apps/datasource/api/datasource.py +++ b/backend/apps/datasource/api/datasource.py @@ -8,6 +8,7 @@ from fastapi import APIRouter, File, UploadFile, HTTPException from apps.db.engine import create_table, get_data_engine, insert_data from common.core.deps import SessionDep, CurrentUser, Trans +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 from ..crud.field import get_fields_by_table_id @@ -17,6 +18,11 @@ from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, Core router = APIRouter(tags=["datasource"], prefix="/datasource") path = "/opt/sqlbot/data/excel" +@router.get("/ws/{oid}", include_in_schema=False) +async def query_by_oid(session: SessionDep, user: CurrentUser, oid: int) -> List[CoreDatasource]: + if not user.isAdmin: + raise Exception("no permission to execute") + return get_datasource_list(session=session, user=user, oid=oid) @router.get("/list") async def datasource_list(session: SessionDep, user: CurrentUser): @@ -60,7 +66,11 @@ async def get_tables(session: SessionDep, id: int): @router.post("/getTablesByConf") async def get_tables_by_conf(session: SessionDep, ds: CoreDatasource): - return getTablesByDs(session, ds) + try: + return getTablesByDs(session, ds) + except Exception as e: + SQLBotLogUtil.error(f"get table failed: {e}") + raise HTTPException(status_code=500, detail=f'Get table Failed: {e.args}') @router.post("/getFields/{id}/{table_name}") @@ -100,7 +110,11 @@ async def edit_field(session: SessionDep, field: CoreField): @router.post("/previewData/{id}") async def edit_local(session: SessionDep, current_user: CurrentUser, id: int, data: TableObj): - return preview(session, current_user, id, data) + try: + return preview(session, current_user, id, data) + except Exception as e: + SQLBotLogUtil.error(f"Preview failed: {e}") + raise HTTPException(status_code=500, detail=f'Preview Failed: {e.args}') @router.post("/fieldEnum/{id}") diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 2e461291..52381c6f 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -1,6 +1,6 @@ import datetime import json -from typing import List +from typing import List, Optional from fastapi import HTTPException from sqlalchemy import and_, text, cast, or_, func @@ -25,10 +25,13 @@ from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, Cor DatasourceConf, TableAndFields -def get_datasource_list(session: SessionDep, user: CurrentUser): - oid = user.oid if user.oid is not None else 1 - return session.query(CoreDatasource).filter(CoreDatasource.oid == oid).order_by( - func.convert_to(CoreDatasource.name, 'gbk')).all() +def get_datasource_list(session: SessionDep, user: CurrentUser, oid: Optional[int] = None) -> List[CoreDatasource]: + current_oid = user.oid if user.oid is not None else 1 + if user.isAdmin and oid: + current_oid = oid + return session.exec(select(CoreDatasource).where(CoreDatasource.oid == current_oid).order_by( + func.convert_to(CoreDatasource.name, 'gbk'))).all() + def get_ds(session: SessionDep, id: int): @@ -134,7 +137,7 @@ def getTables(session: SessionDep, id: int): def getTablesByDs(session: SessionDep, ds: CoreDatasource): - check_status(session, ds, True) + # check_status(session, ds, True) tables = get_tables(ds) return tables @@ -241,7 +244,7 @@ def updateField(session: SessionDep, field: CoreField): def preview(session: SessionDep, current_user: CurrentUser, id: int, data: TableObj): ds = session.query(CoreDatasource).filter(CoreDatasource.id == id).first() - check_status(session, ds, True) + # check_status(session, ds, True) if data.fields is None or len(data.fields) == 0: return {"fields": [], "data": [], "sql": ''} @@ -278,8 +281,8 @@ def preview(session: SessionDep, current_user: CurrentUser, id: int, data: Table ).first() if obj is not None: res.append(transRecord2DTO(session, permission)) - wheres = transFilterTree(session, res, ds) - where = (' where ' + wheres) if wheres is not None and wheres != '' else '' + where_str = transFilterTree(session, res, ds) + where = (' where ' + where_str) if where_str is not None and where_str != '' else '' fields = [f.field_name for f in f_list] if fields is None or len(fields) == 0: diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index 2c00bcaf..b051d787 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -254,20 +254,17 @@ def get_fields(ds: CoreDatasource, table_name: str = None): def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str): - session = get_session(ds) - result = session.execute(text(sql)) - try: - columns = result.keys()._keys - res = result.fetchall() - result_list = [ - {columns[i]: float(value) if isinstance(value, Decimal) else value for i, value in enumerate(tuple_item)} - for tuple_item in res - ] - return {"fields": columns, "data": result_list, "sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))} - except Exception as ex: - raise ex - finally: - if result is not None: - result.close() - if session is not None: - session.close() + with get_session(ds) as session: + with session.execute(text(sql)) as result: + try: + columns = result.keys()._keys + res = result.fetchall() + result_list = [ + {columns[i]: float(value) if isinstance(value, Decimal) else value for i, value in + enumerate(tuple_item)} + for tuple_item in res + ] + return {"fields": columns, "data": result_list, + "sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))} + except Exception as ex: + raise ex diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py index 781d1889..eece4367 100644 --- a/backend/apps/system/api/aimodel.py +++ b/backend/apps/system/api/aimodel.py @@ -1,15 +1,35 @@ import json from typing import List, Union +from apps.ai_model.model_factory import LLMConfig, LLMFactory from apps.system.schemas.ai_model_schema import AiModelConfigItem, AiModelCreator, AiModelEditor, AiModelGridItem from fastapi import APIRouter, Query from sqlmodel import func, select, update from apps.system.models.system_model import AiModelDetail -from common.core.deps import SessionDep +from common.core.deps import SessionDep, Trans from common.utils.time import get_timestamp +from common.utils.utils import SQLBotLogUtil router = APIRouter(tags=["system/aimodel"], prefix="/system/aimodel") +@router.post("/status") +async def check_llm(info: AiModelCreator, trans: Trans): + try: + additional_params = {item.key: item.val for item in info.config_list} + config = LLMConfig( + model_type="openai", + model_name=info.base_model, + api_key=info.api_key, + api_base_url=info.api_domain, + additional_params=additional_params, + ) + llm_instance = LLMFactory.create_llm(config) + result = llm_instance.llm.invoke("who are you?") + SQLBotLogUtil.info(f"check_llm result: {result}") + except Exception as e: + SQLBotLogUtil.error(f"Error checking LLM: {e}") + raise Exception(trans('i18n_llm.validate_error', msg = str(e))) + @router.get("", response_model=list[AiModelGridItem]) async def query( session: SessionDep, @@ -74,19 +94,20 @@ async def update_model( data["config"] = json.dumps([item.model_dump(exclude_unset=True) for item in editor.config_list]) data.pop("config_list", None) db_model = session.get(AiModelDetail, id) - update_data = AiModelDetail.model_validate(data) - db_model.sqlmodel_update(update_data) + #update_data = AiModelDetail.model_validate(data) + db_model.sqlmodel_update(data) session.add(db_model) session.commit() @router.delete("/{id}") async def delete_model( session: SessionDep, + trans: Trans, id: int ): item = session.get(AiModelDetail, id) if item.default_model: - raise RuntimeError(f"Can not delete [${item.name}], because it is default model!") + raise Exception(trans('i18n_llm.delete_default_error', key = item.name)) session.delete(item) session.commit() diff --git a/backend/apps/system/api/login.py b/backend/apps/system/api/login.py index 0aca2934..8e360e08 100644 --- a/backend/apps/system/api/login.py +++ b/backend/apps/system/api/login.py @@ -2,7 +2,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from fastapi.security import OAuth2PasswordRequestForm from apps.system.schemas.system_schema import BaseUserDTO -from common.core.deps import SessionDep +from common.core.deps import SessionDep, Trans from ..crud.user import authenticate from common.core.security import create_access_token from datetime import timedelta @@ -13,14 +13,16 @@ router = APIRouter(tags=["login"], prefix="/login") @router.post("/access-token") def local_login( session: SessionDep, + trans: Trans, form_data: Annotated[OAuth2PasswordRequestForm, Depends()] ) -> Token: user: BaseUserDTO = authenticate(session=session, account=form_data.username, password=form_data.password) if not user: - raise HTTPException(status_code=400, detail="Incorrect account or password") - + raise HTTPException(status_code=400, detail=trans('i18n_login.account_pwd_error')) if not user.oid or user.oid == 0: - raise HTTPException(status_code=400, detail="No associated workspace, Please contact the administrator") + raise HTTPException(status_code=400, detail=trans('i18n_login.no_associated_ws', msg = trans('i18n_concat_admin'))) + if user.status != 1: + raise HTTPException(status_code=400, detail=trans('i18n_login.user_disable', msg = trans('i18n_concat_admin'))) access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) user_dict = user.to_dict() return Token(access_token=create_access_token( diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py index cb4e9fdb..9d9644cd 100644 --- a/backend/apps/system/api/user.py +++ b/backend/apps/system/api/user.py @@ -1,11 +1,11 @@ from typing import Optional from fastapi import APIRouter, HTTPException, Query from sqlmodel import func, or_, select, delete as sqlmodel_delete -from apps.system.crud.user import get_db_user, single_delete, user_ws_options -from apps.system.models.system_model import UserWsModel +from apps.system.crud.user import check_account_exists, check_email_exists, check_email_format, check_pwd_format, get_db_user, single_delete, user_ws_options +from apps.system.models.system_model import UserWsModel, WorkspaceModel from apps.system.models.user import UserModel from apps.system.schemas.auth import CacheName, CacheNamespace -from apps.system.schemas.system_schema import PwdEditor, UserCreator, UserEditor, UserGrid, UserLanguage, UserWs +from apps.system.schemas.system_schema import PwdEditor, UserCreator, UserEditor, UserGrid, UserLanguage, UserStatus, UserWs from common.core.deps import CurrentUser, SessionDep, Trans from common.core.pagination import Paginator from common.core.schemas import PaginatedResponse, PaginationParams @@ -96,10 +96,13 @@ async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Tran @router.put("/ws/{oid}") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") -async def ws_change(session: SessionDep, current_user: CurrentUser, oid: int): +async def ws_change(session: SessionDep, current_user: CurrentUser, trans:Trans, oid: int): ws_list: list[UserWs] = await user_ws_options(session, current_user.id) if not any(x.id == oid for x in ws_list): - raise HTTPException(f"oid [{oid}] is invalid!") + db_ws = session.get(WorkspaceModel, oid) + if db_ws: + raise Exception(trans('i18n_user.ws_miss', ws = db_ws.name)) + raise Exception(trans('i18n_not_exist', msg = f"{trans('i18n_ws.title')}[{oid}]")) user_model: UserModel = get_db_user(session = session, user_id = current_user.id) user_model.oid = oid session.add(user_model) @@ -115,7 +118,13 @@ async def query(session: SessionDep, trans: Trans, id: int) -> UserEditor: return result @router.post("") -async def create(session: SessionDep, creator: UserCreator): +async def create(session: SessionDep, creator: UserCreator, trans: Trans): + if check_account_exists(session=session, account=creator.account): + raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.account')} [{creator.account}]")) + if check_email_exists(session=session, email=creator.email): + raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.email')} [{creator.email}]")) + if not check_email_format(creator.email): + raise Exception(trans('i18n_format_invalid', key = f"{trans('i18n_user.email')} [{creator.email}]")) data = creator.model_dump(exclude_unset=True) user_model = UserModel.model_validate(data) #user_model.create_time = get_timestamp() @@ -138,8 +147,16 @@ async def create(session: SessionDep, creator: UserCreator): @router.put("") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="editor.id") -async def update(session: SessionDep, editor: UserEditor): +async def update(session: SessionDep, editor: UserEditor, trans: Trans): user_model: UserModel = get_db_user(session = session, user_id = editor.id) + if not user_model: + raise Exception(f"User with id [{editor.id}] not found!") + if editor.account != user_model.account: + raise Exception(f"account cannot be changed!") + if editor.email != user_model.email and check_email_exists(session=session, account=editor.email): + raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.email')} [{editor.email}]")) + if not check_email_format(editor.email): + raise Exception(trans('i18n_format_invalid', key = f"{trans('i18n_user.email')} [{editor.email}]")) origin_oid: int = user_model.oid del_stmt = sqlmodel_delete(UserWsModel).where(UserWsModel.uid == editor.id) session.exec(del_stmt) @@ -174,10 +191,10 @@ async def batch_del(session: SessionDep, id_list: list[int]): @router.put("/language") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") -async def langChange(session: SessionDep, current_user: CurrentUser, language: UserLanguage): +async def langChange(session: SessionDep, current_user: CurrentUser, trans: Trans, language: UserLanguage): lang = language.language if lang not in ["zh-CN", "en"]: - return {"message": "Language not supported"} + raise Exception(trans('i18n_user.language_not_support', key = lang)) db_user: UserModel = get_db_user(session=session, user_id=current_user.id) db_user.language = lang session.add(db_user) @@ -185,9 +202,9 @@ async def langChange(session: SessionDep, current_user: CurrentUser, language: U @router.patch("/pwd/{id}") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id") -async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int): +async def pwdReset(session: SessionDep, current_user: CurrentUser, trans: Trans, id: int): if not current_user.isAdmin: - raise HTTPException('only for admin') + raise Exception(trans('i18n_permission.no_permission', url = " patch[/user/pwd/id],", msg = trans('i18n_permission.only_admin'))) db_user: UserModel = get_db_user(session=session, user_id=id) db_user.password = default_md5_pwd() session.add(db_user) @@ -195,10 +212,26 @@ async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int): @router.put("/pwd") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") -async def pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdEditor): +async def pwdUpdate(session: SessionDep, current_user: CurrentUser, trans: Trans, editor: PwdEditor): + new_pwd = editor.new_pwd + if not check_pwd_format(new_pwd): + raise Exception(trans('i18n_format_invalid', key = trans('i18n_user.password'))) db_user: UserModel = get_db_user(session=session, user_id=current_user.id) if not verify_md5pwd(editor.pwd, db_user.password): - raise HTTPException("pwd error") - db_user.password = md5pwd(editor.new_pwd) + raise Exception(trans('i18n_error', key = trans('i18n_user.password'))) + db_user.password = md5pwd(new_pwd) + session.add(db_user) + session.commit() + +@router.patch("/status") +@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="statusDto.id") +async def langChange(session: SessionDep, current_user: CurrentUser, trans: Trans, statusDto: UserStatus): + if not current_user.isAdmin: + raise Exception(trans('i18n_permission.no_permission', url = ", ", msg = trans('i18n_permission.only_admin'))) + status = statusDto.status + if status not in [0, 1]: + return {"message": "status not supported"} + db_user: UserModel = get_db_user(session=session, user_id=statusDto.id) + db_user.status = status session.add(db_user) session.commit() \ No newline at end of file diff --git a/backend/apps/system/api/workspace.py b/backend/apps/system/api/workspace.py index 5201200d..893333ca 100644 --- a/backend/apps/system/api/workspace.py +++ b/backend/apps/system/api/workspace.py @@ -17,15 +17,16 @@ router = APIRouter(tags=["system/workspace"], prefix="/system/workspace") async def option_pager( session: SessionDep, current_user: CurrentUser, + trans: Trans, pageNum: int, pageSize: int, oid: int = Query(description="空间ID"), keyword: Optional[str] = Query(None, description="搜索关键字(可选)"), ): if not current_user.isAdmin: - raise RuntimeError('only for admin') + raise Exception(trans('i18n_permission.no_permission', url = ", ", msg = trans('i18n_permission.only_admin'))) if not oid: - raise RuntimeError('oid miss error') + raise Exception(trans('i18n_miss_args', key = '[oid]')) pagination = PaginationParams(page=pageNum, size=pageSize) paginator = Paginator(session) stmt = select(UserModel.id, UserModel.account, UserModel.name).where( @@ -50,12 +51,13 @@ async def option_pager( async def option_user( session: SessionDep, current_user: CurrentUser, + trans: Trans, keyword: str = Query(description="搜索关键字") ): if not keyword: - raise HTTPException("keyword is required") + raise Exception(trans('i18n_miss_args', key = '[keyword]')) if (not current_user.isAdmin) and current_user.weight == 0: - raise RuntimeError("no permission to execute this api") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) oid = current_user.oid stmt = select(UserModel.id, UserModel.account, UserModel.name).where( @@ -76,13 +78,14 @@ async def option_user( async def pager( session: SessionDep, current_user: CurrentUser, + trans: Trans, pageNum: int, pageSize: int, keyword: Optional[str] = Query(None, description="搜索关键字(可选)"), oid: Optional[int] = Query(None, description="空间ID(仅admin用户生效)"), ): if not current_user.isAdmin and current_user.weight == 0: - raise HTTPException("no permission to execute") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) if current_user.isAdmin: workspace_id = oid if oid else current_user.oid else: @@ -112,9 +115,9 @@ async def pager( @router.post("/uws") -async def create(session: SessionDep, current_user: CurrentUser, creator: UserWsDTO): +async def create(session: SessionDep, current_user: CurrentUser, trans: Trans, creator: UserWsDTO): if not current_user.isAdmin and current_user.weight == 0: - raise HTTPException("no permission to execute") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) oid: int = creator.oid if (current_user.isAdmin and creator.oid) else current_user.oid weight = creator.weight if (current_user.isAdmin and creator.weight) else 0 # 判断uid_list以及oid合法性 @@ -134,9 +137,9 @@ async def create(session: SessionDep, current_user: CurrentUser, creator: UserWs session.commit() @router.put("/uws") -async def edit(session: SessionDep, editor: UserWsEditor): +async def edit(session: SessionDep, trans: Trans, editor: UserWsEditor): if not editor.oid or not editor.uid: - raise HTTPException("param [oid, uid] miss") + raise Exception(trans('i18n_miss_args', key = '[oid, uid]')) db_model = session.exec(select(UserWsModel).where(UserWsModel.uid == editor.uid, UserWsModel.oid == editor.oid)).first() if not db_model: raise HTTPException("uws not exist") @@ -150,9 +153,9 @@ async def edit(session: SessionDep, editor: UserWsEditor): session.commit() @router.delete("/uws") -async def delete(session: SessionDep, current_user: CurrentUser, dto: UserWsBase): +async def delete(session: SessionDep, current_user: CurrentUser, trans: Trans, dto: UserWsBase): if not current_user.isAdmin and current_user.weight == 0: - raise HTTPException("no permission to execute") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) oid: int = dto.oid if (current_user.isAdmin and dto.oid) else current_user.oid db_model_list: list[UserWsModel] = session.exec(select(UserWsModel).where(UserWsModel.uid.in_(dto.uid_list), UserWsModel.oid == oid)).all() if not db_model_list: diff --git a/backend/apps/system/crud/assistant.py b/backend/apps/system/crud/assistant.py index fddf2247..76c8c3bf 100644 --- a/backend/apps/system/crud/assistant.py +++ b/backend/apps/system/crud/assistant.py @@ -33,11 +33,12 @@ def get_assistant_ds(llm_service) -> list[dict]: configuration = assistant.configuration if configuration: config: dict[any] = json.loads(configuration) - oid: str = config['oid'] + oid: int = int(config['oid']) stmt = select(CoreDatasource.id, CoreDatasource.name, CoreDatasource.description).where(CoreDatasource.oid == oid) - private_list:list[int] = config.get('private_list') or None - if private_list: - stmt.where(~CoreDatasource.id.in_(private_list)) + if not assistant.online: + private_list:list[int] = config.get('private_list') or None + if private_list: + stmt = stmt.where(~CoreDatasource.id.in_(private_list)) db_ds_list = session.exec(stmt) result_list = [ @@ -131,7 +132,7 @@ class AssistantOutDs: def get_db_schema(self, ds_id: int) -> str: ds = self.get_ds(ds_id) schema_str = "" - db_name = ds.schema + db_name = ds.db_schema schema_str += f"【DB_ID】 {db_name}\n【Schema】\n" for table in ds.tables: schema_str += f"# Table: {db_name}.{table.name}" @@ -159,7 +160,9 @@ class AssistantOutDs: if attr in ds_dict: id_marker += str(ds_dict.get(attr, '')) + '--sqlbot--' id = string_to_numeric_hash(id_marker) - return AssistantOutDsSchema(**{**ds_dict, "id": id}) + db_schema = ds_dict.get('schema', ds_dict.get('db_schema', '')) + ds_dict.pop("schema", None) + return AssistantOutDsSchema(**{**ds_dict, "id": id, "db_schema": db_schema}) class AssistantOutDsFactory: @staticmethod @@ -177,12 +180,12 @@ def get_ds_engine(ds: AssistantOutDsSchema) -> Engine: database=ds.dataBase, driver='', extraJdbc=ds.extraParams, - dbSchema=ds.schema or '' + dbSchema=ds.db_schema or '' ) from apps.db.db import get_uri_from_config uri = get_uri_from_config(ds.type, conf) - if ds.type == "pg" and ds.schema: - connect_args.update({"options": f"-c search_path={ds.schema}"}) + if ds.type == "pg" and ds.db_schema: + connect_args.update({"options": f"-c search_path={ds.db_schema}"}) engine = create_engine(uri, connect_args=connect_args, pool_timeout=timeout, pool_size=20, max_overflow=10) return engine \ No newline at end of file diff --git a/backend/apps/system/crud/user.py b/backend/apps/system/crud/user.py index fa0166e8..d721ef50 100644 --- a/backend/apps/system/crud/user.py +++ b/backend/apps/system/crud/user.py @@ -1,15 +1,16 @@ from typing import Optional -from sqlmodel import Session, select, delete as sqlmodel_delete +from sqlmodel import Session, func, select, delete as sqlmodel_delete from apps.system.models.system_model import UserWsModel, WorkspaceModel from apps.system.schemas.auth import CacheName, CacheNamespace -from apps.system.schemas.system_schema import BaseUserDTO, UserInfoDTO, UserWs +from apps.system.schemas.system_schema import EMAIL_REGEX, PWD_REGEX, BaseUserDTO, UserInfoDTO, UserWs from common.core.deps import SessionDep from common.core.sqlbot_cache import cache, clear_cache from common.utils.locale import I18n from common.utils.utils import SQLBotLogUtil from ..models.user import UserModel from common.core.security import verify_md5pwd +import re def get_db_user(*, session: Session, user_id: int) -> UserModel: db_user = session.get(UserModel, user_id) @@ -69,3 +70,17 @@ async def single_delete(session: SessionDep, id: int): @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id") async def clean_user_cache(id: int): SQLBotLogUtil.info(f"User cache for [{id}] has been cleaned") + + +def check_account_exists(*, session: Session, account: str) -> bool: + return session.exec(select(func.count()).select_from(UserModel).where(UserModel.account == account)).one() > 0 +def check_email_exists(*, session: Session, email: str) -> bool: + return session.exec(select(func.count()).select_from(UserModel).where(UserModel.email == email)).one() > 0 + + + +def check_email_format(email: str) -> bool: + return bool(EMAIL_REGEX.fullmatch(email)) + +def check_pwd_format(pwd: str) -> bool: + return bool(PWD_REGEX.fullmatch(pwd)) diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py index 23bdd512..4f4d8706 100644 --- a/backend/apps/system/middleware/auth.py +++ b/backend/apps/system/middleware/auth.py @@ -36,7 +36,7 @@ class TokenMiddleware(BaseHTTPMiddleware): request.state.current_user = validator[1] request.state.assistant = validator[2] return await call_next(request) - return JSONResponse({"msg": f"Unauthorized:[{validator[1]}]"}, status_code=401, headers={"Access-Control-Allow-Origin": "*"}) + return JSONResponse(f"Unauthorized:[{validator[1]}]", status_code=401, headers={"Access-Control-Allow-Origin": "*"}) #validate pass tokenkey = settings.TOKEN_KEY token = request.headers.get(tokenkey) @@ -44,7 +44,7 @@ class TokenMiddleware(BaseHTTPMiddleware): if validate_pass: request.state.current_user = data return await call_next(request) - return JSONResponse({"msg": f"Unauthorized:[{data}]"}, status_code=401, headers={"Access-Control-Allow-Origin": "*"}) + return JSONResponse(f"Unauthorized:[{data}]", status_code=401, headers={"Access-Control-Allow-Origin": "*"}) def is_options(self, request: Request): return request.method == "OPTIONS" @@ -62,7 +62,13 @@ class TokenMiddleware(BaseHTTPMiddleware): try: 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}") session_user = UserInfoDTO.model_validate(session_user) + if session_user.status != 1: + raise Exception(f"User is not active!") + 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, diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index 3d2ab410..7ea29e67 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -1,11 +1,22 @@ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field, field_validator from common.core.schemas import BaseCreatorDTO +import re -class model_status(BaseModel): - status: bool - ids: list[int] +EMAIL_REGEX = re.compile( + r"^[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*@" + r"([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+" + r"[a-zA-Z]{2,}$" +) +PWD_REGEX = re.compile( + r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)" + r"(?=.*[~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./])" + r"[A-Za-z\d~!@#$%^&*()_+\-={}|:\"<>?`\[\];',./]{8,20}$" +) +class UserStatus(BaseCreatorDTO): + status: int = 1 + class UserLanguage(BaseModel): @@ -13,24 +24,36 @@ class UserLanguage(BaseModel): class BaseUser(BaseModel): - account: str + account: str = Field(min_length=1, max_length=100, description="用户账号") oid: int - + class BaseUserDTO(BaseUser, BaseCreatorDTO): - language: str + language: str = Field(pattern=r"^(zh-CN|en)$", default="zh-CN", description="用户语言") password: str + status: int = 1 def to_dict(self): return { "id": self.id, "account": self.account, "oid": self.oid } + @field_validator("language") + def validate_language(cls, lang: str) -> str: + if not re.fullmatch(r"^(zh-CN|en)$", lang): + raise ValueError("Language must be 'zh-CN' or 'en'") + return lang class UserCreator(BaseUser): - name: str - email: str + name: str = Field(min_length=1, max_length=100, description="用户名") + email: str = Field(min_length=1, max_length=100, description="用户邮箱") status: int = 1 oid_list: Optional[list[int]] = None + + """ @field_validator("email") + def validate_email(cls, lang: str) -> str: + if not re.fullmatch(EMAIL_REGEX, lang): + raise ValueError("Email format is invalid!") + return lang """ class UserEditor(UserCreator, BaseCreatorDTO): pass @@ -139,7 +162,7 @@ class AssistantOutDsSchema(AssistantOutDsBase): dataBase: Optional[str] = None user: Optional[str] = None password: Optional[str] = None - schema: Optional[str] = None + db_schema: Optional[str] = None extraParams: Optional[str] = None tables: Optional[list[AssistantTableSchema]] = None \ No newline at end of file diff --git a/backend/common/core/deps.py b/backend/common/core/deps.py index afc2dc36..0dcef442 100644 --- a/backend/common/core/deps.py +++ b/backend/common/core/deps.py @@ -25,6 +25,9 @@ async def get_current_assistant(request: Request) -> AssistantHeader | None: if request.headers.get("X-SQLBOT-ASSISTANT-CERTIFICATE"): entry_certificate = request.headers['X-SQLBOT-ASSISTANT-CERTIFICATE'] base_assistant.certificate = unquote(base64.b64decode(entry_certificate).decode('utf-8')) + if request.headers.get("X-SQLBOT-ASSISTANT-ONLINE"): + entry_online = request.headers['X-SQLBOT-ASSISTANT-ONLINE'] + base_assistant.online = str(entry_online).lower() == 'true' if entry_online else False return base_assistant CurrentAssistant = Annotated[AssistantHeader, Depends(get_current_assistant)] diff --git a/backend/common/core/response_middleware.py b/backend/common/core/response_middleware.py index e1dbab9b..6842893e 100644 --- a/backend/common/core/response_middleware.py +++ b/backend/common/core/response_middleware.py @@ -14,7 +14,8 @@ class ResponseMiddleware(BaseHTTPMiddleware): if isinstance(response, JSONResponse) or request.url.path == f"{settings.API_V1_STR}/openapi.json": return response - + if response.status_code != 200: + return response if response.headers.get("content-type") == "application/json": try: body = b"" @@ -51,11 +52,7 @@ class ResponseMiddleware(BaseHTTPMiddleware): SQLBotLogUtil.error(f"Response processing error: {str(e)}", exc_info=True) return JSONResponse( status_code=500, - content={ - "code": 500, - "data": None, - "msg": str(e) - }, + content=str(e), headers={ k: v for k, v in response.headers.items() if k.lower() not in ("content-length", "content-type") @@ -71,11 +68,7 @@ class exception_handler(): SQLBotLogUtil.error(f"HTTP Exception: {exc.detail}", exc_info=True) return JSONResponse( status_code=exc.status_code, - content={ - "code": exc.status_code, - "msg": exc.detail, - "data": None - }, + content=exc.detail, headers={"Access-Control-Allow-Origin": "*"} ) @@ -85,11 +78,7 @@ class exception_handler(): SQLBotLogUtil.error(f"Unhandled Exception: {str(exc)}", exc_info=True) return JSONResponse( status_code=500, - content={ - "code": 500, - "msg": str(exc), - "data": None - }, + content=str(exc), headers={"Access-Control-Allow-Origin": "*"} ) diff --git a/backend/common/utils/locale.py b/backend/common/utils/locale.py index 32d9009d..67a0d97c 100644 --- a/backend/common/utils/locale.py +++ b/backend/common/utils/locale.py @@ -1,16 +1,15 @@ from pathlib import Path import json -from typing import Dict, Optional +from typing import Dict, Optional, Any from fastapi import Request class I18n: def __init__(self, locale_dir: str = "locales"): self.locale_dir = Path(locale_dir) - self.translations: Dict[str, Dict[str, str]] = {} + self.translations: Dict[str, Dict[str, Any]] = {} self.load_translations() def load_translations(self): - if not self.locale_dir.exists(): self.locale_dir.mkdir() return @@ -34,9 +33,21 @@ class I18nHelper: self.request = request self.lang = i18n.get_language(request) - def __call__(self, key: str, **kwargs) -> str: + def _get_nested_translation(self, data: Dict[str, Any], key_path: str) -> str: + keys = key_path.split('.') + current = data + + for key in keys: + if isinstance(current, dict) and key in current: + current = current[key] + else: + return key_path # 如果找不到,返回原键 + + return current if isinstance(current, str) else key_path + + def __call__(self, arg_key: str, **kwargs) -> str: lang_data = self.i18n.translations.get(self.lang, {}) - text = lang_data.get(key, key) # 找不到则返回原key + text = self._get_nested_translation(lang_data, arg_key) if kwargs: try: diff --git a/backend/locales/en.json b/backend/locales/en.json index 1bb123f7..7b775419 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -1,4 +1,33 @@ { "i18n_default_workspace": "Default workspace", - "i18n_ds_name_exist": "Name exist" + "i18n_ds_name_exist": "Name already exists", + "i18n_concat_admin": "Please contact administrator!", + "i18n_exist": "{msg} already exists!", + "i18n_not_exist": "{msg} not exists", + "i18n_error": "{key} error!", + "i18n_miss_args": "Missing {key} parameter!", + "i18n_format_invalid": "{key} format is incorrect!", + "i18n_login": { + "account_pwd_error": "Account or password error!", + "no_associated_ws": "No associated workspace, {msg}", + "user_disable": "Account disabled, {msg}" + }, + "i18n_user": { + "account": "Account", + "email": "Email", + "password": "Password", + "language_not_support": "System does not support [{key}] language!", + "ws_miss": "The current user is not in the workspace [{ws}]!" + }, + "i18n_ws": { + "title": "Workspace" + }, + "i18n_permission": { + "only_admin": "Only administrators can call this!", + "no_permission": "No permission to access {url}{msg}" + }, + "i18n_llm": { + "validate_error": "Validation failed [{msg}]", + "delete_default_error": "Cannot delete default model [{key}]!" + } } \ No newline at end of file diff --git a/backend/locales/zh-CN.json b/backend/locales/zh-CN.json index 8a9f8c2a..abadd609 100644 --- a/backend/locales/zh-CN.json +++ b/backend/locales/zh-CN.json @@ -1,4 +1,34 @@ { "i18n_default_workspace": "默认工作空间", - "i18n_ds_name_exist": "名称已存在" + "i18n_ds_name_exist": "名称已存在", + "i18n_concat_admin": "请联系管理员!", + "i18n_exist": "{msg}已存在!", + "i18n_not_exist": "{msg}不存在!", + "i18n_error": "{key}错误!", + "i18n_miss_args": "缺失{key}参数!", + "i18n_format_invalid": "{key}格式不正确!", + "i18n_login": { + "account_pwd_error": "账号或密码错误!", + "no_associated_ws": "没有关联的工作空间,{msg}", + "user_disable": "账号已禁用,{msg}" + }, + "i18n_user": { + "account": "账号", + "email": "邮箱", + "password": "密码", + "language_not_support": "系统不支持[{key}]语言!", + "ws_miss": "当前用户不在工作空间[{ws}]中!" + }, + "i18n_ws": { + "title": "工作空间" + }, + "i18n_permission": { + "only_admin": "仅支持管理员调用!", + "no_permission": "无权调用{url}{msg}" + + }, + "i18n_llm": { + "validate_error": "校验失败[{msg}]", + "delete_default_error": "无法删除默认模型[{key}]!" + } } \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1de57be8..3e4a7c71 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -36,7 +36,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.7", + "sqlbot-xpack==0.0.3.8", "fastapi-cache2>=0.2.2", "sqlparse>=0.5.3", "redis>=6.2.0", diff --git a/frontend/public/assistant.js b/frontend/public/assistant.js index c7677d65..3485b3d4 100644 --- a/frontend/public/assistant.js +++ b/frontend/public/assistant.js @@ -1,4 +1,5 @@ ;(function () { + window.sqlbot_assistant_handler = window.sqlbot_assistant_handler || {} const defaultData = { id: '1', show_guide: false, @@ -34,14 +35,41 @@ const chatButtonHtml = (data) => `
- + + + + + + + + + + + + + + + + + + + + + + + + + + + +
` const getChatContainerHtml = (data) => { return `
- -
+ +
@@ -83,7 +111,11 @@ root.insertAdjacentHTML('beforeend', getChatContainerHtml(data)) // 按钮元素 const chat_button = root.querySelector('.sqlbot-assistant-chat-button') - const chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > img') + let chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > svg') + if (data.float_icon) { + chat_button_img = root.querySelector('.sqlbot-assistant-chat-button > img') + } + chat_button_img.style.display = 'block' // 对话框元素 const chat_container = root.querySelector('#sqlbot-assistant-chat-container') // 引导层 @@ -260,8 +292,8 @@ } #sqlbot-assistant-chat-container { - width: 450px; - height: 600px; + width: 460px; + height: 640px; display:none; } @media only screen and (max-width: 768px) { @@ -282,7 +314,7 @@ #sqlbot-assistant #sqlbot-assistant-chat-container{ z-index:10000;position: relative; border-radius: 8px; - border: 1px solid #ffffff; + //border: 1px solid #ffffff; background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1; box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.10); position: fixed;bottom: 16px;right: 16px;overflow: hidden; @@ -409,6 +441,7 @@ } function loadScript(src, id) { const domain_url = getDomain(src) + const online = getParam(src, 'online') let url = `${domain_url}/api/v1/system/assistant/info/${id}` if (domain_url.includes('5173')) { url = url.replace('5173', '8000') @@ -424,6 +457,7 @@ Object.assign(tempData, config) tempData = Object.assign(tempData, config) } + tempData['online'] = online && online.toString().toLowerCase() == 'true' initsqlbot_assistant(tempData) if (data.type == 1) { registerMessageEvent(id, tempData) @@ -440,13 +474,93 @@ const src_list = scriptsArray.map((script) => script.src) src_list.forEach((src) => { const id = getParam(src, 'id') + window.sqlbot_assistant_handler[id] = window.sqlbot_assistant_handler[id] || {} + window.sqlbot_assistant_handler[id]['id'] = id const propName = script_id_prefix + id + '-state' if (window[propName]) { return true } window[propName] = true loadScript(src, id) + expposeGlobalMethods(id) }) } - window.addEventListener('load', init) + function updateParam(target_url, key, newValue) { + try { + const url = new URL(target_url) + const [hashPath, hashQuery] = url.hash.split('?') + let searchParams + if (hashQuery) { + searchParams = new URLSearchParams(hashQuery) + } else { + searchParams = url.searchParams + } + searchParams.set(key, newValue) + if (hashQuery) { + url.hash = `${hashPath}?${searchParams.toString()}` + } else { + url.search = searchParams.toString() + } + return url.toString() + } catch (e) { + console.error('Invalid URL:', target_url) + return target_url + } + } + function expposeGlobalMethods(id) { + window.sqlbot_assistant_handler[id]['setOnline'] = (online) => { + if (online != null && typeof online != 'boolean') { + throw new Error('The parameter can only be of type boolean') + } + const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`) + if (iframe) { + const url = iframe.src + const eventName = 'sqlbot_assistant_event' + const params = { + busi: 'setOnline', + online, + eventName, + messageId: id, + } + const contentWindow = iframe.contentWindow + contentWindow.postMessage(params, url) + } + } + window.sqlbot_assistant_handler[id]['refresh'] = (online) => { + if (online != null && typeof online != 'boolean') { + throw new Error('The parameter can only be of type boolean') + } + const iframe = document.getElementById(`sqlbot-assistant-chat-iframe-${id}`) + if (iframe) { + const url = iframe.src + let new_url = updateParam(url, 't', Date.now()) + if (online != null) { + new_url = updateParam(new_url, 'online', online) + } + iframe.src = 'about:blank' + setTimeout(() => { + iframe.src = new_url + }, 500) + } + } + } + // window.addEventListener('load', init) + const executeWhenReady = (fn) => { + if ( + document.readyState === 'complete' || + (document.readyState !== 'loading' && !document.documentElement.doScroll) + ) { + setTimeout(fn, 0) + } else { + const onReady = () => { + document.removeEventListener('DOMContentLoaded', onReady) + window.removeEventListener('load', onReady) + fn() + } + document.addEventListener('DOMContentLoaded', onReady) + window.addEventListener('load', onReady) + } + } + + executeWhenReady(init) })() diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts index c27e8616..e28a5bf3 100644 --- a/frontend/src/api/chat.ts +++ b/frontend/src/api/chat.ts @@ -180,6 +180,7 @@ export class ChatInfo extends Chat { chat_type: string, datasource: number, engine_type: string, + ds_type: string, datasource_name: string, datasource_exists: boolean, records: Array @@ -192,6 +193,7 @@ export class ChatInfo extends Chat { chat_type?: string, datasource?: number, engine_type?: string, + ds_type?: string, datasource_name?: string, datasource_exists: boolean = true, records: Array = [] @@ -206,6 +208,7 @@ export class ChatInfo extends Chat { this.chat_type = param1.chat_type this.datasource = param1.datasource this.engine_type = param1.engine_type + this.ds_type = param1.ds_type } else { this.id = param1 this.create_time = getDate(create_time) @@ -214,6 +217,7 @@ export class ChatInfo extends Chat { this.chat_type = chat_type this.datasource = datasource this.engine_type = engine_type + this.ds_type = ds_type } } this.datasource_name = datasource_name @@ -275,6 +279,7 @@ export const chatApi = { data.chat_type, data.datasource, data.engine_type, + data.ds_type, data.datasource_name, data.datasource_exists, toChatRecordList(data.records) diff --git a/frontend/src/api/embedded.ts b/frontend/src/api/embedded.ts index fccad9d7..6002ca90 100644 --- a/frontend/src/api/embedded.ts +++ b/frontend/src/api/embedded.ts @@ -5,3 +5,4 @@ export const updateAssistant = (data: any) => request.put('/system/assistant', d export const saveAssistant = (data: any) => request.post('/system/assistant', data) export const getOne = (id: any) => request.get(`/system/assistant/${id}`) export const delOne = (id: any) => request.delete(`/system/assistant/${id}`) +export const dsApi = (id: any) => request.get(`/datasource/ws/${id}`) diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index 153c985f..ee62cb57 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -8,4 +8,6 @@ export const modelApi = { delete: (id: number) => request.delete(`/system/aimodel/${id}`), query: (id: number) => request.get(`/system/aimodel/${id}`), setDefault: (id: number) => request.put(`/system/aimodel/default/${id}`), + check: (data: any) => + request.post('/system/aimodel/status', data, { requestOptions: { silent: true } }), } diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 041cf605..ed2e32c0 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -25,4 +25,5 @@ export const userApi = { deleteBatch: (data: any) => request.delete(`/user`, { data }), get: (key: string) => request.get(`/user/${key}`), pwd: (id: any) => request.patch(`/user/pwd/${id}`), + status: (data: any) => request.patch('/user/status', data), } diff --git a/frontend/src/assets/svg/icon_copy_outlined.svg b/frontend/src/assets/svg/icon_copy_outlined.svg new file mode 100644 index 00000000..acdd1f66 --- /dev/null +++ b/frontend/src/assets/svg/icon_copy_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/svg/icon_warning_filled.svg b/frontend/src/assets/svg/icon_warning_filled.svg new file mode 100644 index 00000000..045912f3 --- /dev/null +++ b/frontend/src/assets/svg/icon_warning_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/components/layout/Person.vue b/frontend/src/components/layout/Person.vue index 223724ca..57f11ae1 100644 --- a/frontend/src/components/layout/Person.vue +++ b/frontend/src/components/layout/Person.vue @@ -73,9 +73,9 @@ const logout = () => { - {{ name }} + {{ name }}
diff --git a/frontend/src/components/layout/PwdForm.vue b/frontend/src/components/layout/PwdForm.vue index abe92bfc..4d46f7b8 100644 --- a/frontend/src/components/layout/PwdForm.vue +++ b/frontend/src/components/layout/PwdForm.vue @@ -10,6 +10,30 @@ const pwdForm = reactive({ confirm_pwd: '', }) +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const validatePass = (rule: any, value: any, callback: any) => { + if (value === '') { + callback(new Error(t('common.please_input', { msg: t('user.upgrade_pwd.new_pwd') }))) + } else { + if (pwdForm.confirm_pwd !== '') { + if (!pwdRef.value) return + pwdRef.value.validateField('confirm_pwd') + } + callback() + } +} +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const validatePass2 = (rule: any, value: any, callback: any) => { + if (value === '') { + callback(new Error(t('common.please_input', { msg: t('user.upgrade_pwd.confirm_pwd') }))) + } else if (value !== pwdForm.new_pwd) { + callback(new Error(t('user.upgrade_pwd.two_pwd_not_match'))) + } else { + callback() + } +} const rules = { pwd: [ { @@ -18,27 +42,8 @@ const rules = { trigger: 'blur', }, ], - new_pwd: [ - { - required: true, - message: t('common.please_input', { msg: t('user.upgrade_pwd.new_pwd') }), - trigger: 'blur', - }, - ], - confirm_pwd: [ - { - required: true, - message: t('common.please_input', { msg: t('user.upgrade_pwd.confirm_pwd') }), - trigger: 'blur', - }, - ], -} - -const initForm = (item: any) => { - if (item) { - Object.assign(pwdForm, { ...item }) - } - pwdRef.value.clearValidate() + new_pwd: [{ validator: validatePass, trigger: 'blur' }], + confirm_pwd: [{ validator: validatePass2, trigger: 'blur' }], } const emits = defineEmits(['pwdSaved']) @@ -61,7 +66,6 @@ const submit = () => { }) } defineExpose({ - initForm, submit, }) diff --git a/frontend/src/components/layout/Workspace.vue b/frontend/src/components/layout/Workspace.vue index 579f6163..11255947 100644 --- a/frontend/src/components/layout/Workspace.vue +++ b/frontend/src/components/layout/Workspace.vue @@ -103,25 +103,28 @@ onMounted(async () => {
-
- - - +
- - - -
+ v-for="ele in defaultWorkspaceListWithSearch" + :key="ele.name" + class="popover-item" + :class="currentWorkspace.id === ele.id && 'isActive'" + @click="handleDefaultWorkspaceChange(ele)" + > + + + +
+ + + +
+ +
{{ $t('model.relevant_results_found') }}
diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index c61b1cce..1298b7e2 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -6,6 +6,10 @@ "AI Model Configuration": "AI Model Configuration" }, "common": { + "limited_to_30": "The number of data tables is limited to 30", + "enter_your_password": "Please enter your password", + "your_account_email_address": "Please enter your account/email address", + "the_correct_password": "Please enter the correct password", "input_content": "Please Input Content", "empty": " ", "may_not_exist": "No results yet, the user may not exist", @@ -118,7 +122,8 @@ "earlier": "Earlier", "no_time": "No Time", "rename_conversation_title": "Rename conversation title", - "conversation_title": "Conversation title" + "conversation_title": "Conversation title", + "copied": "Copied" }, "ds": { "title": "Data Source", @@ -224,6 +229,9 @@ "custom_notes": "Custom notes", "field_type": "Field type", "field_name": "Field name", + "copy": "Copy", + "incorrect_email_format": "Incorrect email format", + "the_original_one": "Are you sure you want to restore the password to the original one?", "relevant_content_found": "No relevant content found", "please_enter": "Please enter", "Please_select": "Please select", @@ -458,6 +466,10 @@ "add_interface_credentials": "Add interface credentials", "edit_interface_credentials": "Edit interface credentials", "duplicate_name": "Duplicate name", + "duplicate_name_": "Duplicate name", + "duplicate_account": "Duplicate account", + "repeating_parameters": "Repeating Parameters", + "duplicate_email": "Duplicate email", "interface_credentials": "Interface credentials", "no_credentials_yet": "No credentials yet", "intelligent_customer_service": "SQLBot Intelligent Customer Service" @@ -506,4 +518,4 @@ "back_community": "Restore to Community Edition", "confirm_tips": "Are you sure to restore to Community Edition?" } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index aea50c1b..ce571212 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -6,6 +6,10 @@ "AI Model Configuration": "模型配置" }, "common": { + "limited_to_30": "数据表数量限制30", + "enter_your_password": "请输入密码", + "your_account_email_address": "请输入账号/邮箱", + "the_correct_password": "请输入正确的密码", "input_content": "请输入内容", "may_not_exist": "暂无结果,用户可能不存在", "empty": "", @@ -31,7 +35,8 @@ "re_upload": "重新上传", "not_exceed_50mb": "支持 XLS、XLSX、CSV 格式,文件大小不超过 50MB", "reset_password": "重置密码", - "password_reset_successful": "重置密码成功" + "password_reset_successful": "重置密码成功", + "or": "或者" }, "dashboard": { "add_success": "添加成功", @@ -121,7 +126,8 @@ "earlier": "更早以前", "no_time": "没有时间", "rename_conversation_title": "重命名对话标题", - "conversation_title": "对话标题" + "conversation_title": "对话标题", + "copied": "已复制" }, "ds": { "title": "数据源", @@ -223,6 +229,9 @@ "edit": "编辑", "source_connection_failed": "数据源连接失败", "confirm": "确定", + "copy": "复制", + "the_original_one": "确认恢复为初始密码吗?", + "incorrect_email_format": "邮箱格式不对", "enabled_status": "启用状态", "custom_notes": "自定义备注", "field_type": "字段类型", @@ -275,7 +284,8 @@ "verification_successful": "校验成功", "del_default_tip": "无法删除模型: {msg}", "del_default_warn": "该模型为系统默认模型,请先设置其他模型为系统默认模型,再删除此模型。", - "del_warn_tip": "是否删除模型: {msg}?" + "del_warn_tip": "是否删除模型: {msg}?", + "check_failed": "模型无效【{msg}】" }, "user": { "workspace": "工作空间", @@ -332,7 +342,8 @@ "title": "修改密码", "old_pwd": "旧密码", "new_pwd": "新密码", - "confirm_pwd": "确认密码" + "confirm_pwd": "确认密码", + "two_pwd_not_match": "两次输入的密码不一致" } }, "workspace": { @@ -463,6 +474,10 @@ "add_interface_credentials": "添加接口凭证", "edit_interface_credentials": "编辑接口凭证", "duplicate_name": "重复名称", + "duplicate_name_": "重复姓名", + "duplicate_account": "重复账号", + "duplicate_email": "重复邮箱", + "repeating_parameters": "重复参数", "interface_credentials": "接口凭证", "no_credentials_yet": "暂无凭证", "intelligent_customer_service": "SQLBot 智能客服", @@ -518,4 +533,4 @@ "back_community": "还原至社区版", "confirm_tips": "确定还原至社区版?" } -} +} \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 260eb1d7..7e2b3e3f 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -11,7 +11,7 @@ import DashboardPreview from '@//views/dashboard/preview/SQPreviewSingle.vue' import Dashboard from '@/views/dashboard/index.vue' import Model from '@/views/system/model/Model.vue' import Embedded from '@/views/system/embedded/index.vue' -import assistant from '@/views/embedded/assistant.vue' +import assistant from '@/views/embedded/index.vue' import Member from '@/views/system/member/index.vue' import Permission from '@/views/system/permission/index.vue' import User from '@/views/system/user/User.vue' diff --git a/frontend/src/router/watch.ts b/frontend/src/router/watch.ts index b480d2ca..d91cf605 100644 --- a/frontend/src/router/watch.ts +++ b/frontend/src/router/watch.ts @@ -19,7 +19,7 @@ export const watchRouter = (router: any) => { return } if (!token) { - ElMessage.error('Please login first') + // ElMessage.error('Please login first') next('/login') return } diff --git a/frontend/src/stores/assistant.ts b/frontend/src/stores/assistant.ts index ebfe0ffa..f40f8a72 100644 --- a/frontend/src/stores/assistant.ts +++ b/frontend/src/stores/assistant.ts @@ -11,6 +11,7 @@ interface AssistantState { flag: number type: number certificate: string + online: boolean } export const AssistantStore = defineStore('assistant', { @@ -21,6 +22,7 @@ export const AssistantStore = defineStore('assistant', { flag: 0, type: 0, certificate: '', + online: false, } }, getters: { @@ -39,6 +41,9 @@ export const AssistantStore = defineStore('assistant', { getType(): number { return this.type }, + getOnline(): boolean { + return this.online + }, }, actions: { setCertificate(certificate: string) { @@ -61,6 +66,9 @@ export const AssistantStore = defineStore('assistant', { wsCache.set(flagKey, flag) } }, + setOnline(online: boolean) { + this.online = !!online + }, async setChat() { if (!this.assistant) { return null diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 9aee595d..03a7e998 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -80,6 +80,9 @@ class HttpService { encodeURIComponent(assistantStore.getCertificate) ) } + if (!assistantStore.getType) { + config.headers['X-SQLBOT-ASSISTANT-ONLINE'] = assistantStore.getOnline + } } const locale = getLocale() if (locale) { @@ -168,9 +171,21 @@ class HttpService { errorMessage = 'Invalid request parameters' break case 401: - errorMessage = 'Unauthorized, please login again' + errorMessage = error.response?.data + ? error.response.data.toString() + : 'Unauthorized, please login again' // Redirect to login page if needed - break + ElMessage({ + message: errorMessage, + type: 'error', + showClose: true, + }) + setTimeout(() => { + wsCache.delete('user.token') + window.location.reload() + }, 1000) + return + // break case 403: errorMessage = 'Access denied' break @@ -184,10 +199,7 @@ class HttpService { errorMessage = `Server responded with error: ${error.response.status}` } if (error?.response?.data) { - const msgData: any = error.response.data - if (msgData?.msg) { - errorMessage = msgData.msg - } + errorMessage = error.response.data.toString() } } else if (error.request) { errorMessage = 'No response from server' @@ -249,6 +261,9 @@ class HttpService { encodeURIComponent(assistantStore.getCertificate) ) } + if (!assistantStore.getType) { + heads['X-SQLBOT-ASSISTANT-ONLINE'] = assistantStore.getOnline + } } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/frontend/src/views/chat/ChatListContainer.vue b/frontend/src/views/chat/ChatListContainer.vue index 3618c47c..0e2c2104 100644 --- a/frontend/src/views/chat/ChatListContainer.vue +++ b/frontend/src/views/chat/ChatListContainer.vue @@ -117,14 +117,9 @@ const createNewChat = async () => { async function doCreateNewChat() { if (isAssistant.value) { - const assistantChat = await assistantStore.setChat() - if (assistantChat) { - onChatCreated(assistantChat) - } return - } else { - chatCreatorRef.value?.showDs() } + chatCreatorRef.value?.showDs() } function onClickHistory(chat: Chat) { diff --git a/frontend/src/views/chat/chat-block/ChartBlock.vue b/frontend/src/views/chat/chat-block/ChartBlock.vue index 3e1f03cb..52e59969 100644 --- a/frontend/src/views/chat/chat-block/ChartBlock.vue +++ b/frontend/src/views/chat/chat-block/ChartBlock.vue @@ -15,6 +15,7 @@ import icon_export_outlined from '@/assets/svg/icon_export_outlined.svg' import icon_into_item_outlined from '@/assets/svg/icon_into-item_outlined.svg' import icon_window_max_outlined from '@/assets/svg/icon_window-max_outlined.svg' import icon_window_mini_outlined from '@/assets/svg/icon_window-mini_outlined.svg' +import icon_copy_outlined from '@/assets/svg/icon_copy_outlined.svg' import { useI18n } from 'vue-i18n' import SQLComponent from '@/views/chat/component/SQLComponent.vue' @@ -189,6 +190,13 @@ function showSql() { function addToDashboard() { console.log('todo') } + +function copy() { + if (props.message?.record?.sql) { + navigator.clipboard.writeText(props.message.record.sql) + ElMessage.info(t('qa.copied')) + } +}