Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-07-28 11:37:31 +08:00
54 changed files with 1300 additions and 573 deletions
+9
View File
@@ -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)
+12 -2
View File
@@ -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
+16 -2
View File
@@ -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}")
+12 -9
View File
@@ -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:
+14 -17
View File
@@ -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
+25 -4
View File
@@ -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()
+6 -4
View File
@@ -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(
+47 -14
View File
@@ -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()
+14 -11
View File
@@ -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:
+12 -9
View File
@@ -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
+17 -2
View File
@@ -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))
+8 -2
View File
@@ -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,
+33 -10
View File
@@ -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
+3
View File
@@ -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)]
+5 -16
View File
@@ -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": "*"}
)
+16 -5
View File
@@ -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:
+30 -1
View File
@@ -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}]!"
}
}
+31 -1
View File
@@ -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}]!"
}
}
+1 -1
View File
@@ -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",
+122 -8
View File
@@ -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) => `
<div class="sqlbot-assistant-chat-button">
<img style="height:100%;width:100%;" src="${data.float_icon}">
<img style="height:100%;width:100%;display:none;" src="${data.float_icon}">
<svg style="display:none;margin-right: 20px;" data-v-39a51454="" xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="none">
<path fill="#149CC5" d="M28.333 13.669h-.231c.138.758.207 1.527.206 2.298.005.946-.107 1.89-.335 2.808h.65a1.05 1.05 0 0 0 1.05-1.05V15.01a1.343 1.343 0 0 0-1.34-1.341"></path>
<path fill="#69CAA4" d="M1.692 15.967c0-.77.068-1.54.206-2.298h-.23a1.34 1.34 0 0 0-1.342 1.34v2.716a1.05 1.05 0 0 0 1.05 1.05h.651a11.4 11.4 0 0 1-.335-2.808"></path>
<path fill="url(#a)" d="M15 3.795c-6.89 0-12.474 5.282-12.474 12.171 0 6.89 5.585 10.239 12.474 10.239s12.474-3.35 12.474-10.238c0-6.89-5.585-12.172-12.474-12.172m3.026 17.33h-6.052a6 6 0 0 1-2.484-.535c-.564-.256-2.208.282-2.663-.127-.551-.495.117-1.969-.221-2.636a6.014 6.014 0 0 1 5.368-8.73h6.052a6.014 6.014 0 0 1 0 12.029"></path>
<path fill="url(#b)" d="M15 3.795c-6.89 0-12.474 5.282-12.474 12.171 0 6.89 5.585 10.239 12.474 10.239s12.474-3.35 12.474-10.238c0-6.89-5.585-12.172-12.474-12.172m3.026 17.33h-6.052a6 6 0 0 1-2.484-.535c-.564-.256-2.208.282-2.663-.127-.551-.495.117-1.969-.221-2.636a6.014 6.014 0 0 1 5.368-8.73h6.052a6.014 6.014 0 0 1 0 12.029"></path>
<path fill="#75CCCC" d="M10.546 14.915h-1.78v3.88h1.78zM14.12 13.193h-1.78v5.602h1.78zM17.694 15.438h-1.78v3.357h1.78zM21.233 12.452h-1.779v6.343h1.78z"></path>
<defs>
<linearGradient id="a" x1="6.65" x2="25.099" y1="4.441" y2="29.249" gradientUnits="userSpaceOnUse">
<stop stop-color="#B2F288"></stop>
<stop offset=".181" stop-color="#80D79C"></stop>
<stop offset=".371" stop-color="#52BEAD"></stop>
<stop offset=".554" stop-color="#2FAABB"></stop>
<stop offset=".724" stop-color="#159CC5"></stop>
<stop offset=".878" stop-color="#0594CB"></stop>
<stop offset="1" stop-color="#0091CD"></stop>
</linearGradient>
<linearGradient id="b" x1="6.65" x2="25.099" y1="4.441" y2="29.249" gradientUnits="userSpaceOnUse">
<stop stop-color="#B2F288"></stop>
<stop offset=".181" stop-color="#80D79C"></stop>
<stop offset=".371" stop-color="#52BEAD"></stop>
<stop offset=".554" stop-color="#2FAABB"></stop>
<stop offset=".724" stop-color="#159CC5"></stop>
<stop offset=".878" stop-color="#0594CB"></stop>
<stop offset="1" stop-color="#0091CD"></stop>
</linearGradient>
</defs>
</svg>
</div>`
const getChatContainerHtml = (data) => {
return `
<div id="sqlbot-assistant-chat-container">
<iframe id="sqlbot-assistant-chat-iframe-${data.id}" allow="microphone" src="${data.domain_url}/#/assistant?id=${data.id}"></iframe>
<div class="sqlbot-assistant-operate">
<iframe id="sqlbot-assistant-chat-iframe-${data.id}" allow="microphone" src="${data.domain_url}/#/assistant?id=${data.id}&online=${!!data.online}"></iframe>
<div class="sqlbot-assistant-operate">
<div class="sqlbot-assistant-closeviewport sqlbot-assistant-viewportnone">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.507 11.6645C7.73712 11.6645 7.94545 11.7578 8.09625 11.9086C8.24706 12.0594 8.34033 12.2677 8.34033 12.4978V16.7976C8.34033 17.0277 8.15378 17.2143 7.92366 17.2143H7.09033C6.86021 17.2143 6.67366 17.0277 6.67366 16.7976V14.5812L3.41075 17.843C3.24803 18.0057 2.98421 18.0057 2.82149 17.843L2.23224 17.2537C2.06952 17.091 2.06952 16.8272 2.23224 16.6645L5.56668 13.3311H3.19634C2.96622 13.3311 2.77967 13.1446 2.77967 12.9145V12.0811C2.77967 11.851 2.96622 11.6645 3.19634 11.6645H7.507ZM16.5991 2.1572C16.7619 1.99448 17.0257 1.99448 17.1884 2.1572L17.7777 2.74645C17.9404 2.90917 17.9404 3.17299 17.7777 3.33571L14.4432 6.66904H16.8136C17.0437 6.66904 17.2302 6.85559 17.2302 7.08571V7.91904C17.2302 8.14916 17.0437 8.33571 16.8136 8.33571H12.5029C12.2728 8.33571 12.0644 8.24243 11.9136 8.09163C11.7628 7.94082 11.6696 7.73249 11.6696 7.50237V3.20257C11.6696 2.97245 11.8561 2.7859 12.0862 2.7859H12.9196C13.1497 2.7859 13.3362 2.97245 13.3362 3.20257V5.419L16.5991 2.1572Z" fill="${data.header_font_color}"/>
@@ -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)
})()
+5
View File
@@ -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<ChatRecord>
@@ -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<ChatRecord> = []
@@ -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)
+1
View File
@@ -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}`)
+2
View File
@@ -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 } }),
}
+1
View File
@@ -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),
}
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33333 5.3335V13.3335H10V5.3335H3.33333ZM11.3333 4.66683V14.0742C11.3333 14.4015 11.0548 14.6668 10.7111 14.6668H2.62222C2.27858 14.6668 2 14.4015 2 14.0742V4.59276C2 4.26548 2.27858 4.00016 2.62222 4.00016H10.6667C11.0349 4.00016 11.3333 4.29864 11.3333 4.66683ZM13.8047 1.52876C13.9254 1.6494 14 1.81607 14 2.00016V10.3335C14 10.5176 13.8508 10.6668 13.6667 10.6668H13C12.8159 10.6668 12.6667 10.5176 12.6667 10.3335V2.66683H6.33333C6.14924 2.66683 6 2.51759 6 2.3335V1.66683C6 1.48273 6.14924 1.3335 6.33333 1.3335H13.3333C13.5174 1.3335 13.6841 1.40812 13.8047 1.52876Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 706 B

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23Z"/>
<path d="M11.5 7C11.2239 7 11 7.22386 11 7.5V13.5C11 13.7761 11.2239 14 11.5 14H12.5C12.7761 14 13 13.7761 13 13.5V7.5C13 7.22386 12.7761 7 12.5 7H11.5Z" fill="white"/>
<path d="M11.5 15C11.2239 15 11 15.2239 11 15.5V16.5C11 16.7761 11.2239 17 11.5 17H12.5C12.7761 17 13 16.7761 13 16.5V15.5C13 15.2239 12.7761 15 12.5 15H11.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

+2 -2
View File
@@ -73,9 +73,9 @@ const logout = () => {
<template>
<el-popover trigger="click" popper-class="system-person" :placement="collapse ? 'right' : 'top'">
<template #reference>
<button class="person" :class="collapse && 'collapse'">
<button class="person" :title="name" :class="collapse && 'collapse'">
<img class="default-avatar" :src="Default_avatar" width="32px" height="32px" />
<span v-if="!collapse" class="name ellipsis" :title="name">{{ name }}</span>
<span v-if="!collapse" class="name ellipsis">{{ name }}</span>
</button></template
>
<div class="popover">
+26 -22
View File
@@ -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,
})
</script>
+21 -18
View File
@@ -103,25 +103,28 @@ onMounted(async () => {
</template>
</el-input>
<div class="popover-content">
<div
v-for="ele in defaultWorkspaceListWithSearch"
:key="ele.name"
class="popover-item"
:class="currentWorkspace.id === ele.id && 'isActive'"
@click="handleDefaultWorkspaceChange(ele)"
>
<el-icon size="16">
<icon_moments_categories_outlined></icon_moments_categories_outlined>
</el-icon>
<el-scrollbar max-height="400px">
<div
:title="ele.name"
class="datasource-name ellipsis"
v-html="formatKeywords(ele.name)"
></div>
<el-icon size="16" class="done">
<icon_done_outlined></icon_done_outlined>
</el-icon>
</div>
v-for="ele in defaultWorkspaceListWithSearch"
:key="ele.name"
class="popover-item"
:class="currentWorkspace.id === ele.id && 'isActive'"
@click="handleDefaultWorkspaceChange(ele)"
>
<el-icon size="16">
<icon_moments_categories_outlined></icon_moments_categories_outlined>
</el-icon>
<div
:title="ele.name"
class="datasource-name ellipsis"
v-html="formatKeywords(ele.name)"
></div>
<el-icon size="16" class="done">
<icon_done_outlined></icon_done_outlined>
</el-icon>
</div>
</el-scrollbar>
<div v-if="!defaultWorkspaceListWithSearch.length" class="popover-item empty">
{{ $t('model.relevant_results_found') }}
</div>
+14 -2
View File
@@ -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?"
}
}
}
+20 -5
View File
@@ -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": "确定还原至社区版?"
}
}
}
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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
}
+8
View File
@@ -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
+21 -6
View File
@@ -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
@@ -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) {
@@ -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'))
}
}
</script>
<template>
@@ -209,11 +217,11 @@ function addToDashboard() {
<div class="chart-select-container">
<el-tooltip effect="dark" :content="t('chat.type')" placement="top">
<ChartPopover
:chartTypeList="chartTypeList"
:chartType="chartType"
:title="t('chat.type')"
@typeChange="onTypeChange"
v-if="chartTypeList.length > 0"
:chart-type-list="chartTypeList"
:chart-type="chartType"
:title="t('chat.type')"
@type-change="onTypeChange"
></ChartPopover>
</el-tooltip>
@@ -318,12 +326,17 @@ function addToDashboard() {
direction="rtl"
body-class="chart-sql-drawer-body"
>
<div>
<div class="sql-block">
<SQLComponent
v-if="message.record?.sql"
:sql="message.record?.sql"
style="margin-top: 12px"
/>
<el-button v-if="message.record?.sql" circle class="input-icon" @click="copy">
<el-icon size="16">
<icon_copy_outlined />
</el-icon>
</el-button>
</div>
</el-drawer>
</div>
@@ -490,4 +503,18 @@ function addToDashboard() {
margin-top: 16px;
}
}
.sql-block {
position: relative;
.input-icon {
min-width: unset;
position: absolute;
top: 12px;
right: 12px;
border-color: #dee0e3;
box-shadow: 0px 4px 8px 0px #1f23291a;
}
}
</style>
@@ -17,9 +17,22 @@ const props = defineProps({
},
})
const currentIcon = computed(() => {
return props.chartTypeList.find((ele) => ele.value === props.chartType || ele.value === 'table')
.icon
if (props.chartType === 'table') {
const [ele] = props.chartTypeList || []
if (ele.icon) {
return ele.icon
}
return null
}
return props.chartTypeList.find((ele) => ele.value === props.chartType).icon
})
const firstItem = () => {
if (props.chartType === 'table') {
const [ele] = props.chartTypeList || []
handleDefaultChatChange(ele || {})
}
}
const emits = defineEmits(['typeChange'])
const handleDefaultChatChange = (val: any) => {
emits('typeChange', val.value)
@@ -29,7 +42,11 @@ const handleDefaultChatChange = (val: any) => {
<template>
<el-popover trigger="click" popper-class="chat-type_select" placement="bottom">
<template #reference>
<div class="chat-select_type" :class="chartType && chartType !== 'table' && 'active'">
<div
class="chat-select_type"
:class="chartType && chartType !== 'table' && 'active'"
@click="firstItem"
>
<component :is="currentIcon" />
<el-icon style="transform: scale(0.75)" class="expand" size="16">
<icon_expand_down_filled></icon_expand_down_filled>
@@ -106,7 +106,13 @@ export class Bar extends BaseG2Chart {
},
labels: [
{
text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
text: (data: any) => {
const value = data[y[0].value]
if (value === undefined || value === null) {
return ''
}
return `${value}${_data.isPercent ? '%' : ''}`
},
transform: [
{ type: 'contrastReverse' },
{ type: 'exceedAdjust' },
@@ -95,7 +95,13 @@ export class Column extends BaseG2Chart {
},
labels: [
{
text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
text: (data: any) => {
const value = data[y[0].value]
if (value === undefined || value === null) {
return ''
}
return `${value}${_data.isPercent ? '%' : ''}`
},
position: 'top',
transform: [
{ type: 'contrastReverse' },
@@ -61,7 +61,13 @@ export class Line extends BaseG2Chart {
},
labels: [
{
text: (data: any) => `${data[y[0].value]}${_data.isPercent ? '%' : ''}`,
text: (data: any) => {
const value = data[y[0].value]
if (value === undefined || value === null) {
return ''
}
return `${value}${_data.isPercent ? '%' : ''}`
},
style: {
dx: -10,
dy: -12,
+81 -59
View File
@@ -15,15 +15,16 @@
@on-click-side-bar-btn="hideSideBar"
/>
</el-aside>
<div v-if="!isAssistant && !chatListSideBarShow" class="hidden-sidebar-btn">
<div
v-if="isAssistant || !chatListSideBarShow"
class="hidden-sidebar-btn"
:class="{ 'assistant-popover-sidebar': isAssistant }"
>
<el-popover
:visible="isAssistant ? floatPopoverVisible : null"
:width="280"
placement="bottom-start"
popper-style="padding: 0;
height: 654px;
border: 1px solid rgba(222, 224, 227, 1);
border-radius: 6px;
"
:popper-style="{ ...defaultFloatPopoverStyle }"
>
<template #reference>
<el-button link type="primary" class="icon-btn" @click="showSideBar">
@@ -33,6 +34,7 @@
</el-button>
</template>
<ChatListContainer
ref="floatPopoverRef"
v-model:chat-list="chatList"
v-model:current-chat-id="currentChatId"
v-model:current-chat="currentChat"
@@ -57,7 +59,10 @@
<el-container :loading="loading">
<el-main
class="chat-record-list"
:class="{ 'hide-sidebar': !isAssistant && !chatListSideBarShow }"
:class="{
'hide-sidebar': !isAssistant && !chatListSideBarShow,
'assistant-chat-main': isAssistant,
}"
>
<div v-if="computedMessages.length == 0 && !loading" class="welcome-content-block">
<div class="welcome-content">
@@ -65,11 +70,16 @@
<el-icon size="32">
<logo_fold />
</el-icon>
{{ t('qa.greeting') }}
{{ isAssistant ? t('embedded.i_am_sqlbot') : t('qa.greeting') }}
</div>
<div class="sub">
{{ isAssistant ? t('embedded.predict_data_etc') : t('qa.hint_description') }}
</div>
<div v-if="isAssistant" class="sub assistant-sub">
{{ t('embedded.intelligent_data_query') }}
</div>
<div class="sub">{{ t('qa.hint_description') }}</div>
<el-button
v-if="currentChatId === undefined"
v-if="!isAssistant && currentChatId === undefined"
size="large"
type="primary"
class="greeting-btn"
@@ -255,15 +265,13 @@
</div>
</el-scrollbar>
</el-main>
<el-footer
v-if="computedMessages.length > 0 || (isAssistant && currentChatId)"
class="chat-footer"
>
<el-footer v-if="computedMessages.length > 0 || isAssistant" class="chat-footer">
<div class="input-wrapper" @click="clickInput">
<div class="datasource">
<div v-if="!isAssistant" class="datasource">
<template v-if="currentChat.datasource && currentChat.datasource_name">
{{ t('qa.selected_datasource') }}:
<img
v-if="currentChatEngineType"
style="margin-left: 4px; margin-right: 4px"
:src="currentChatEngineType"
width="16px"
@@ -332,14 +340,21 @@ import icon_start_outlined from '@/assets/svg/icon_start_outlined.svg'
import logo_fold from '@/assets/LOGO-fold.svg'
import logo from '@/assets/LOGO.svg'
import icon_send_filled from '@/assets/svg/icon_send_filled.svg'
import { useAssistantStore } from '@/stores/assistant'
import { onClickOutside } from '@vueuse/core'
const props = defineProps<{
startChatDsId?: number
}>()
const floatPopoverRef = ref()
const floatPopoverVisible = ref(false)
const assistantStore = useAssistantStore()
const defaultFloatPopoverStyle = ref({
padding: '0',
height: '654px',
border: '1px solid rgba(222, 224, 227, 1)',
borderRadius: '6px',
})
const isAssistant = computed(() => assistantStore.getAssistant)
@@ -410,10 +425,8 @@ const createNewChatSimple = async () => {
const createNewChat = async () => {
goEmpty()
if (isAssistant.value) {
const assistantChat = await assistantStore.setChat()
if (assistantChat) {
onChatCreatedQuick(assistantChat as any)
}
currentChat.value = new ChatInfo()
currentChatId.value = undefined
return
}
chatCreatorRef.value?.showDs()
@@ -440,32 +453,9 @@ function onClickHistory(chat: Chat) {
}
const currentChatEngineType = computed(() => {
return (
dsTypeWithImg.find((ele) => currentChat.value.ds_type === ele.type || ele.type === 'excel') ||
{}
).img
return (dsTypeWithImg.find((ele) => currentChat.value.ds_type === ele.type) || {}).img
})
function toAssistantHistory(chat: Chat) {
currentChat.value = new ChatInfo(chat)
if (chat !== undefined && chat.id !== undefined && !loading.value) {
currentChatId.value = chat.id
loading.value = true
chatApi
.get(chat.id)
.then((res) => {
const info = chatApi.toChatInfo(res)
if (info) {
currentChat.value = info
onClickHistory(info)
}
})
.finally(() => {
loading.value = false
})
}
}
function onChatDeleted(id: number) {
console.log('deleted', id)
}
@@ -476,6 +466,10 @@ function onChatRenamed(chat: Chat) {
const chatListSideBarShow = ref<boolean>(true)
function hideSideBar() {
if (isAssistant.value) {
floatPopoverVisible.value = false
return
}
chatListSideBarShow.value = false
}
@@ -542,13 +536,23 @@ function onChatStop() {
isTyping.value = false
console.debug('onChatStop')
}
const assistantPrepareSend = async () => {
if (
isAssistant.value &&
(currentChatId.value == null || typeof currentChatId.value == 'undefined')
) {
const assistantChat = await assistantStore.setChat()
if (assistantChat) {
onChatCreatedQuick(assistantChat as any)
}
}
}
const sendMessage = async () => {
if (!inputMessage.value.trim()) return
loading.value = true
isTyping.value = true
await assistantPrepareSend()
const currentRecord = new ChatRecord()
currentRecord.create_time = new Date()
currentRecord.chat_id = currentChatId.value
@@ -716,13 +720,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
})
}
const getHistoryList = () => {
return chatList.value
}
const getCurrentChatId = () => {
return currentChatId.value
}
const inputRef = ref()
function clickInput() {
@@ -770,12 +767,29 @@ function stop(func?: (...p: any[]) => void, ...param: any[]) {
func(...param)
}
}
const showFloatPopover = () => {
if (isAssistant.value && !floatPopoverVisible.value) {
floatPopoverVisible.value = true
}
}
const assistantPrepareInit = () => {
if (!isAssistant.value) {
return
}
Object.assign(defaultFloatPopoverStyle.value, {
height: '100% !important',
inset: '0px auto auto 0px',
})
goEmpty()
onClickOutside(floatPopoverRef, () => {
if (floatPopoverVisible.value) {
floatPopoverVisible.value = false
}
})
}
defineExpose({
getHistoryList,
toAssistantHistory,
getCurrentChatId,
createNewChat,
showFloatPopover,
})
const hiddenChatCreatorRef = ref()
@@ -793,6 +807,7 @@ function jumpCreatChat() {
onMounted(() => {
getChatList(jumpCreatChat)
assistantPrepareInit()
})
</script>
@@ -802,7 +817,11 @@ onMounted(() => {
position: relative;
border-radius: 12px;
.assistant-popover-sidebar {
button {
display: none;
}
}
.hidden-sidebar-btn {
z-index: 1;
position: absolute;
@@ -841,6 +860,9 @@ onMounted(() => {
border-radius: 12px;
}
}
.assistant-chat-main {
padding: 0 0 20px 0;
}
.chat-scroll {
width: 100%;
+110 -39
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, reactive } from 'vue'
import { datasourceApi } from '@/api/datasource'
import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg'
import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg'
@@ -67,11 +67,32 @@ const currentTable = ref<any>({})
const ds = ref<any>({})
const btnSelect = ref('d')
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const handleSizeChange = (val: number) => {
pageInfo.currentPage = 1
pageInfo.pageSize = val
}
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
}
const fieldListComputed = computed(() => {
const { currentPage, pageSize } = pageInfo
return fieldList.value.slice((currentPage - 1) * pageSize, currentPage * pageSize)
})
const init = () => {
initLoading.value = true
datasourceApi.getDs(props.info.id).then((res) => {
ds.value = res
fieldList.value = []
pageInfo.total = 0
pageInfo.currentPage = 1
datasourceApi.tableList(props.info.id).then((res) => {
tableList.value = res
initLoading.value = false
@@ -100,10 +121,15 @@ const handleSelectTableList = () => {
const clickTable = (table: any) => {
loading.value = true
currentTable.value = table
fieldList.value = []
pageInfo.total = 0
previewData.value = []
datasourceApi
.fieldList(table.id)
.then((res) => {
fieldList.value = res
pageInfo.total = res.length
pageInfo.currentPage = 1
datasourceApi.previewData(props.info.id, buildData()).then((res) => {
previewData.value = res
})
@@ -191,6 +217,8 @@ const btnSelectClick = (val: any) => {
.fieldList(currentTable.value.id)
.then((res) => {
fieldList.value = res
pageInfo.total = res.length
pageInfo.currentPage = 1
})
.finally(() => {
loading.value = false
@@ -307,43 +335,72 @@ const btnSelectClick = (val: any) => {
</el-button>
</div>
<div class="preview-or-schema">
<el-table v-if="btnSelect === 'd'" :data="fieldList" style="width: 100%">
<el-table-column prop="field_name" :label="t('datasource.field_name')" width="180" />
<el-table-column prop="field_type" :label="t('datasource.field_type')" width="180" />
<el-table-column prop="field_comment" :label="t('datasource.field_original_notes')" />
<el-table-column :label="t('datasource.field_notes_1')">
<template #default="scope">
<div class="field-comment">
<span :title="scope.row.custom_comment" class="notes-in_table">{{
scope.row.custom_comment
}}</span>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editField(scope.row)">
<edit></edit>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column :label="t('datasource.enabled_status')" width="180">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-switch
v-model="scope.row.checked"
size="small"
@change="changeStatus(scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
<template v-else>
<div
v-if="!loading"
class="preview-or-schema"
:class="btnSelect === 'q' && 'overflow-preview'"
>
<div v-if="btnSelect === 'd'" class="table-content_preview">
<el-table :data="fieldListComputed" style="width: 100%">
<el-table-column
prop="field_name"
:label="t('datasource.field_name')"
width="180"
/>
<el-table-column
prop="field_type"
:label="t('datasource.field_type')"
width="180"
/>
<el-table-column
prop="field_comment"
:label="t('datasource.field_original_notes')"
/>
<el-table-column :label="t('datasource.field_notes_1')">
<template #default="scope">
<div class="field-comment">
<span :title="scope.row.custom_comment" class="notes-in_table">{{
scope.row.custom_comment
}}</span>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editField(scope.row)">
<edit></edit>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column :label="t('datasource.enabled_status')" width="180">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-switch
v-model="scope.row.checked"
size="small"
@change="changeStatus(scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="fieldList.length && btnSelect === 'd'" class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<template v-if="btnSelect === 'q'">
<div class="preview-num">
{{ t('ds.pieces_in_total', { msg: total, ms: showNum }) }}
</div>
@@ -588,7 +645,21 @@ const btnSelectClick = (val: any) => {
.preview-or-schema {
margin-top: 16px;
height: calc(100% - 50px);
overflow-y: auto;
&.overflow-preview {
overflow-y: auto;
}
.table-content_preview {
max-height: calc(100% - 50px);
overflow-y: auto;
margin-bottom: 16px;
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
.field-comment {
display: flex;
+9 -6
View File
@@ -233,7 +233,6 @@ const save = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
saveLoading.value = true
const list = tableList.value
.filter((ele: any) => {
return checkList.value.includes(ele.tableName)
@@ -242,6 +241,12 @@ const save = async (formEl: FormInstance | undefined) => {
return { table_name: ele.tableName, table_comment: ele.tableComment }
})
if (checkList.value.length > 30) {
ElMessage.error(t('common.limited_to_30'))
return
}
saveLoading.value = true
const requestObj = buildConf()
if (form.value.id) {
if (!isEditTable.value) {
@@ -278,8 +283,6 @@ const save = async (formEl: FormInstance | undefined) => {
.finally(() => {
saveLoading.value = false
})
console.log(a, 'datasourceApi')
}
}
})
@@ -510,12 +513,12 @@ defineExpose({
<div class="name">{{ form.filename }}</div>
<div class="size">{{ form.filename.split('.')[1] }} - {{ fileSize }}</div>
</div>
<el-icon class="action-btn" size="16" @click="clearFile">
<el-icon v-if="!form.id" class="action-btn" size="16" @click="clearFile">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</div>
<el-upload
v-if="form.filename"
v-if="form.filename && !form.id"
class="upload-user"
accept=".xlsx,.xls,.csv"
:headers="headers"
@@ -531,7 +534,7 @@ defineExpose({
</el-button>
</el-upload>
<el-upload
v-else
v-else-if="!form.id"
class="upload-user"
accept=".xlsx,.xls,.csv"
:headers="headers"
+92 -166
View File
@@ -1,69 +1,30 @@
<template>
<div class="sqlbot-assistant-container">
<div class="head">
<div class="flex align-center">
<div class="head-img-div">
<img :src="AssistantGif" />
</div>
<h4>SQLBot 小助手</h4>
</div>
<div class="header">
<el-icon size="20" @click="openHistory">
<icon_sidebar_outlined></icon_sidebar_outlined>
</el-icon>
<img :src="LOGO" class="logo" width="30px" height="30px" alt="" />
<span class="tite">{{ $t('embedded.intelligent_customer_service') }}</span>
<el-tooltip effect="dark" :content="$t('embedded.new_conversation')" placement="top">
<el-icon class="new-chat" size="20" @click="createChat">
<icon_new_chat_outlined></icon_new_chat_outlined>
</el-icon>
</el-tooltip>
</div>
<chat-component v-if="!loading" ref="chatRef" class="sqlbot-chat-container" />
</div>
<div class="sqlbot-top-btn">
<el-icon style="cursor: pointer" @click="createChat">
<icon_new_chat_outlined />
</el-icon>
<el-icon style="cursor: pointer" @click="openHistory">
<history></history>
</el-icon>
</div>
<div class="sqlbot-history-container">
<el-drawer
v-model="drawer"
:append-to-body="false"
class="sqlbot-history-drawer"
modal-class="sqlbot-history-modal"
title=""
direction="ttb"
:with-header="false"
>
<div class="sqlbot-history-title">
<span>历史记录</span>
</div>
<div class="sqlbot-history-content">
<div v-if="!chatList.length" class="history-empty">
<span>暂无历史记录</span>
</div>
<div v-else class="sqlbot-chat-list-inner">
<el-scrollbar max-height="350px" class="sqlbot-chat-scroller">
<template v-for="chat in chatList" :key="chat.id">
<div
class="chat-list-item"
:class="{ 'is-active-chat': currentId === chat.id }"
@click="onClickHistory(chat)"
>
<span class="title">{{ chat.brief ?? 'Untitled' }}</span>
<!-- <div class="history-operate">
<el-icon @click="EditPen(chat)"><IconOpeEdit /></el-icon>
<el-icon @click="Delete(chat)"><IconOpeDelete /></el-icon>
</div> -->
</div>
</template>
</el-scrollbar>
</div>
</div>
</el-drawer>
<div class="sqlbot-chat-container">
<chat-component v-if="!loading" ref="chatRef" />
</div>
<div class="drawer-assistant" @click="openHistory"></div>
</div>
</template>
<script setup lang="ts">
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import ChatComponent from '@/views/chat/index.vue'
import AssistantGif from '@/assets/img/assistant.gif'
import history from '@/assets/svg/chart/history.svg'
import LOGO from '@/assets/embedded/LOGO.png'
import icon_sidebar_outlined from '@/assets/embedded/icon_sidebar_outlined.svg'
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
/* import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg' */
import { useRoute } from 'vue-router'
import { assistantApi } from '@/api/assistant'
import { useAssistantStore } from '@/stores/assistant'
@@ -72,28 +33,14 @@ const assistantStore = useAssistantStore()
const route = useRoute()
const chatRef = ref()
const chatList = ref<Array<any>>([])
const drawer = ref(false)
const currentId = ref()
const createChat = () => {
chatRef.value?.createNewChat()
}
const openHistory = () => {
chatList.value = chatRef.value?.getHistoryList()
currentId.value = chatRef.value?.getCurrentChatId()
drawer.value = true
}
const onClickHistory = (chat: any) => {
chatRef.value?.toAssistantHistory(chat)
chatRef.value?.showFloatPopover()
}
/* const EditPen = (chat: any) => {
chatRef.value?.onChatRenamed(chat)
}
const Delete = (chat: any) => {
chatRef.value?.onChatDeleted(chat.id)
} */
const validator = ref({
id: '',
valid: false,
@@ -111,14 +58,31 @@ const communicationCb = async (event: any) => {
const certificate = event.data['certificate']
assistantStore.setType(1)
assistantStore.setCertificate(certificate)
// store certificate to pinia
}
if (event.data?.busi == 'setOnline') {
setFormatOnline(event.data.online)
}
}
}
const setFormatOnline = (text?: any) => {
if (text === null || typeof text === 'undefined') {
assistantStore.setOnline(false)
return
}
if (typeof text === 'boolean') {
assistantStore.setOnline(text)
return
}
if (typeof text === 'string') {
assistantStore.setOnline(text.toLowerCase() === 'true')
return
}
assistantStore.setOnline(false)
}
onBeforeMount(async () => {
const assistantId = route.query.id
const online = route.query.online
console.log(online)
setFormatOnline(online)
const now = Date.now()
assistantStore.setFlag(now)
const param = {
@@ -151,31 +115,58 @@ onBeforeUnmount(() => {
height: 100%;
width: 100%;
color: rgb(31, 35, 41);
.flex {
display: flex;
}
.head {
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);
position: fixed;
.header {
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: 56px;
line-height: 56px;
box-sizing: border-box;
border-bottom: 1px solid #dee0e3;
.align-center {
align-items: center;
.head-img-div {
display: flex;
margin-left: 24px;
img {
width: 32px;
height: 32px;
background: #1cba9014;
height: 56px;
padding: 0 16px;
display: flex;
align-items: center;
position: fixed;
.logo {
margin: 0 8px 0 16px;
}
.title {
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.ed-icon {
position: relative;
cursor: pointer;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 28px;
height: 28px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
.new-chat {
position: absolute;
right: 88px;
top: 17px;
}
}
.sqlbot-chat-container {
padding-top: 56px;
@@ -183,82 +174,17 @@ onBeforeUnmount(() => {
display: none;
}
}
}
.sqlbot-top-btn {
right: 85px;
z-index: 2009;
position: fixed;
top: 16px;
display: flex;
column-gap: 16px;
// right: 16px;
font-size: 22px;
i:first-child {
color: var(--primary-color);
}
}
.sqlbot-history-container {
width: 100%;
:deep(.sqlbot-history-modal) {
top: 56px !important;
}
:deep(.sqlbot-history-title) {
border-bottom: 1px solid #dee0e3;
padding: 16px 24px;
font-size: 14px;
font-weight: 500;
color: #1f2329;
}
:deep(.ed-drawer__body) {
padding: 0 !important;
}
:deep(.sqlbot-history-drawer) {
height: fit-content !important;
.sqlbot-history-content {
padding: 16px;
.history-empty {
height: 60px;
color: #8f959e !important;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.is-active-chat {
background-color: #ebf1ff;
color: #3370ff;
font-weight: 500;
}
.chat-list-item {
display: flex;
align-items: center;
padding: 10px 8px;
font-size: 14px;
color: #1f2329;
justify-content: space-between;
.history-operate {
display: none;
}
&:hover {
cursor: pointer;
border-radius: 4px;
background: rgba(31, 35, 41, 0.1);
.history-operate {
padding-right: 16px;
display: flex;
column-gap: 16px;
align-items: center;
font-size: 18px;
i {
color: #8f959e;
&:hover {
color: #8f959e99;
}
}
}
}
}
}
.drawer-assistant {
height: 100%;
position: absolute;
left: 0;
top: 0;
width: 50%;
background: #f5f6f7;
box-shadow: 0px 6px 24px 0px #1f232914;
padding: 16px;
border-right: 1px solid #dee0e3;
display: none;
}
}
</style>
+28 -9
View File
@@ -7,21 +7,26 @@
<div class="login-right">
<div class="login-form">
<h2 class="title">Login</h2>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" @keyup.enter="submitForm">
<el-form
ref="loginFormRef"
class="form-content_error"
:model="loginForm"
:rules="rules"
@keyup.enter="submitForm"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="Account"
:prefix-icon="User"
:placeholder="$t('common.your_account_email_address')"
size="large"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
placeholder="Password"
:placeholder="$t('common.enter_your_password')"
type="password"
:prefix-icon="Lock"
show-password
size="large"
></el-input>
</el-form-item>
@@ -39,10 +44,11 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { User, Lock } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const userStore = useUserStore()
const { t } = useI18n()
const loginForm = ref({
username: '',
@@ -50,8 +56,8 @@ const loginForm = ref({
})
const rules = {
username: [{ required: true, message: 'Please input account', trigger: 'blur' }],
password: [{ required: true, message: 'Please input password', trigger: 'blur' }],
username: [{ required: true, message: t('common.your_account_email_address'), trigger: 'blur' }],
password: [{ required: true, message: t('common.the_correct_password'), trigger: 'blur' }],
}
const loginFormRef = ref()
@@ -108,10 +114,24 @@ const submitForm = () => {
padding: 40px;
display: flex;
align-items: center;
position: relative;
height: 361px;
.login-form {
width: 100%;
padding: 0 40px;
position: absolute;
top: 40px;
left: 0;
.form-content_error {
.ed-form-item--default {
margin-bottom: 24px;
&.is-error {
margin-bottom: 48px;
}
}
}
.title {
font-size: 28px;
@@ -125,7 +145,6 @@ const submitForm = () => {
height: 45px;
font-size: 16px;
border-radius: 4px;
margin-top: 20px;
}
.agreement {
+2 -1
View File
@@ -38,7 +38,7 @@ const handleEmbedded = () => {
<div class="card">
<div class="name-icon">
<img :src="Lock" width="32px" height="32px" />
<span class="name">{{ name }}</span>
<span class="name ellipsis" :title="name">{{ name }}</span>
<span class="default" :class="isBase && 'is-base'">{{
isBase ? $t('embedded.basic_application') : $t('embedded.advanced_application')
}}</span>
@@ -88,6 +88,7 @@ const handleEmbedded = () => {
font-weight: 500;
font-size: 16px;
line-height: 24px;
max-width: 200px;
}
.default {
+49 -11
View File
@@ -14,8 +14,7 @@ import icon_copy_outlined from '@/assets/embedded/icon_copy_outlined.svg'
import Card from './Card.vue'
import { workspaceList } from '@/api/workspace'
import DsCard from './DsCard.vue'
import { datasourceApi } from '@/api/datasource'
import { getList, updateAssistant, saveAssistant, delOne } from '@/api/embedded'
import { getList, updateAssistant, saveAssistant, delOne, dsApi } from '@/api/embedded'
import { useI18n } from 'vue-i18n'
import { cloneDeep } from 'lodash-es'
@@ -49,7 +48,7 @@ const currentEmbedded = reactive<any>(cloneDeep(defaultEmbedded))
const isCreate = ref(false)
const defaultForm = {
oid: '',
oid: 1,
private_list: [] as any,
}
const dsForm = reactive(cloneDeep(defaultForm))
@@ -107,9 +106,13 @@ const handleAddEmbedded = (val: any) => {
handleAdvancedEmbedded(null)
}
}
const wsChanged = (val: any) => {
dsForm.private_list = []
dsForm.oid = val
getDsList()
}
const getDsList = () => {
datasourceApi.list().then((res: any) => {
dsApi(dsForm.oid).then((res: any) => {
dsListOptions.value = res || []
if (!currentEmbedded.id) {
dsForm.private_list = dsListOptions.value.map((ele) => ele.id)
@@ -119,10 +122,10 @@ const getDsList = () => {
const handleBaseEmbedded = (row: any) => {
advancedApplication.value = false
initWorkspace()
getDsList()
if (row) {
Object.assign(dsForm, JSON.parse(row.configuration))
}
getDsList()
ruleConfigvVisible.value = true
dialogTitle.value = row?.id
? t('embedded.edit_basic_applications')
@@ -350,16 +353,36 @@ const btnSelect = ref('d')
const dialogVisible = ref(false)
const scriptElement = ref('')
const jsCodeElement = ref('')
const handleEmbedded = (row: any) => {
dialogVisible.value = true
const { origin, pathname } = window.location
scriptElement.value = `g-#script
async
defer
id="${row.id}"
id="sqlbot-assistant-float-script-${row.id}"
src="${origin + pathname}assistant.js?id=${row.id}"k-*g-#/scriptk-*`
.replaceAll('g-#', '<')
.replaceAll('k-*', '>')
jsCodeElement.value = `(function(){
const script = document.createElement('script');
script.defer = true;
script.async = true;
script.src = "${origin + pathname}assistant.js?id=${row.id}";
script.id = "sqlbot-assistant-float-script-${row.id}";
document.head.appendChild(script);
})()`
}
const copyJsCode = () => {
navigator.clipboard
.writeText(jsCodeElement.value)
.then(function () {
ElMessage.success(t('embedded.copy_successful'))
})
.catch(function () {
ElMessage.error(t('embedded.copy_successful'))
})
}
const copyCode = () => {
navigator.clipboard
@@ -417,15 +440,15 @@ const saveHandler = () => {
</script>
<template>
<div v-loading="searchLoading" class="permission">
<div v-loading="searchLoading" class="embedded">
<div class="tool-left">
<div class="btn-select">
<el-button :class="[btnSelect === 'd' && 'is-active']" text @click="btnSelect = 'd'">
{{ t('embedded.embedded_assistant') }}
</el-button>
<el-button :class="[btnSelect === 'q' && 'is-active']" text @click="btnSelect = 'q'">
<!-- <el-button :class="[btnSelect === 'q' && 'is-active']" text @click="btnSelect = 'q'">
{{ t('embedded.embedded_page') }}
</el-button>
</el-button> -->
</div>
<div>
<el-input
@@ -697,6 +720,7 @@ const saveHandler = () => {
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.workspace')
"
@change="wsChanged"
>
<el-option
v-for="item in workspaces"
@@ -770,6 +794,19 @@ const saveHandler = () => {
{{ scriptElement }}
</div>
</div>
<div class="code">
<div class="copy">
{{ $t('common.or') }}
<el-icon size="16" @click="copyJsCode">
<icon_copy_outlined></icon_copy_outlined>
</el-icon>
</div>
<div class="script">
{{ jsCodeElement }}
</div>
</div>
</div>
</el-dialog>
<el-drawer
@@ -857,7 +894,8 @@ const saveHandler = () => {
</template>
<style lang="less" scoped>
.permission {
.embedded {
height: 100%;
.ed-empty {
padding-top: 200px;
padding-bottom: 0;
+43 -19
View File
@@ -69,6 +69,48 @@ const defaultModelListWithSearch = computed(() => {
})
})
const modelCheckHandler = (item: any) => {
setTimeout(() => {
modelApi.check(item).catch((err: any) => {
if (err.response?.data?.msg) {
ElMessage.error(t('model.check_failed', { msg: err.response.data.msg || '' }))
} else {
ElMessage.error(t('model.check_failed', { msg: err.message || '' }))
}
})
}, 1000)
}
const duplicateName = async (item: any) => {
const res = await modelApi.queryAll()
const names = res.filter((ele: any) => ele.id !== item.id).map((ele: any) => ele.name)
if (names.includes(item.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
if (!item.id) {
modelApi.add(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('workspace.add_successfully'),
})
modelCheckHandler(item)
})
return
}
modelApi.edit(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
modelCheckHandler(item)
})
}
const handleDefaultModelChange = (item: any) => {
const current_default_node = modelList.value.find((ele: Model) => ele.default_model)
if (current_default_node?.id === item.id) {
@@ -185,25 +227,7 @@ const search = () => {
search()
const submit = (item: any) => {
if (!item.id) {
modelApi.add(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('workspace.add_successfully'),
})
})
return
}
modelApi.edit(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
duplicateName(item)
}
</script>
+28 -4
View File
@@ -76,7 +76,7 @@ const advancedSettingPagination = computed(() => {
return advancedSetting.value.slice(currentPage.value * 5 - 5, currentPage.value * 5)
})
const handelCurrentChange = (val: any) => {
const handleCurrentChange = (val: any) => {
currentPage.value = val
}
@@ -116,9 +116,22 @@ const addParams = () => {
paramsFormRef.value.submit()
}
const submit = (item: any) => {
const duplicateName = async (item: any) => {
const arr = advancedSetting.value.filter((ele: any) => ele.id !== item.id)
const names = arr.map((ele: any) => ele.name)
const keys = arr.map((ele: any) => ele.key)
if (names.includes(item.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
if (keys.includes(item.key)) {
ElMessage.error(t('embedded.repeating_parameters'))
return
}
if (isCreate.value) {
advancedSetting.value.push({ ...item })
advancedSetting.value.push({ ...item, id: +new Date() })
beforeClose()
return
}
@@ -132,6 +145,10 @@ const submit = (item: any) => {
beforeClose()
}
const submit = (item: any) => {
duplicateName(item)
}
const beforeClose = () => {
paramsFormRef.value.close()
paramsFormDrawer.value = false
@@ -142,6 +159,7 @@ const supplierChang = (supplier: any) => {
modelForm.api_domain = config.api_domain
modelForm.base_model = ''
}
let curId = +new Date()
const initForm = (item?: any) => {
modelForm.id = ''
modelRef.value.clearValidate()
@@ -149,6 +167,12 @@ const initForm = (item?: any) => {
Object.assign(modelForm, { ...item })
if (item?.config_list?.length) {
advancedSetting.value = item.config_list
advancedSetting.value.forEach((ele: any) => {
if (!ele.id) {
ele.id = curId
curId += 1
}
})
}
}
}
@@ -294,7 +318,7 @@ defineExpose({
:default-page-size="5"
layout="prev, pager, next"
:total="advancedSetting.length"
@current-change="handelCurrentChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
@@ -153,7 +153,7 @@ const emits = defineEmits(['update:item', 'del'])
</el-option>
</el-select>
<el-select
v-if="['null', 'empty'].includes(item.term)"
v-if="['null', 'not_null', 'empty', 'not_empty'].includes(item.term)"
v-model="item.term"
style="max-width: 280px; margin-left: 8px"
:placeholder="t('datasource.Please_select')"
+10 -1
View File
@@ -219,7 +219,9 @@ const handleInitDsIdChange = (val: any) => {
activeTable.value = null
fieldListOptions.value = []
columnForm.permissions = []
authTreeRef.value.init({})
if (authTreeRef.value) {
authTreeRef.value.init({})
}
})
}
@@ -375,6 +377,13 @@ const closeForm = () => {
const authTreeRef = ref()
const saveHandler = () => {
columnFormRef.value.validate((res: any) => {
const names = currentPermission.permissions
.filter((ele: any) => ele.id !== columnForm.id)
.map((ele: any) => ele.name)
if (names.includes(columnForm.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
if (res) {
if (columnForm.type === 'row') {
authTreeRef.value.submit()
+188 -56
View File
@@ -23,12 +23,12 @@
</template>
{{ $t('user.filter') }}
</el-button>
<el-button secondary @click="handleUserImport">
<!-- <el-button secondary @click="handleUserImport">
<template #icon>
<ccmUpload></ccmUpload>
</template>
{{ $t('user.batch_import') }}
</el-button>
</el-button> -->
<el-button type="primary" @click="editHandler(null)">
<template #icon>
<icon_add_outlined></icon_add_outlined>
@@ -110,10 +110,57 @@
:content="$t('common.reset_password')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="handleEditPassword(scope.row.id)">
<el-icon
:ref="
(el: any) => {
setButtonRef(el, scope.row)
}
"
v-click-outside="() => onClickOutside(scope.row)"
class="action-btn"
size="16"
>
<IconLock></IconLock>
</el-icon>
</el-tooltip>
<el-popover
:ref="
(el: any) => {
setPopoverRef(el, scope.row)
}
"
placement="right"
virtual-triggering
:width="300"
:virtual-ref="scope.row.buttonRef"
trigger="click"
show-arrow
>
<div class="reset-pwd-confirm">
<div class="confirm-header">
<span class="icon-span">
<el-icon size="24">
<icon_warning_filled class="svg-icon" />
</el-icon>
</span>
<span class="header-span">{{ t('datasource.the_original_one') }}</span>
</div>
<div class="confirm-content">
<span>SQLBot@123456</span>
<el-button style="margin-left: 4px" text @click="copyText">{{
t('datasource.copy')
}}</el-button>
</div>
<div class="confirm-foot">
<el-button secondary @click="closeResetInfo(scope.row)">{{
t('common.cancel')
}}</el-button>
<el-button type="primary" @click="handleEditPassword(scope.row.id)">
{{ t('datasource.confirm') }}
</el-button>
</div>
</div>
</el-popover>
<el-tooltip
:offset="14"
@@ -227,7 +274,11 @@
multiple
:placeholder="$t('datasource.Please_select') + $t('common.empty') + $t('user.workspace')"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id">
<div class="ellipsis" :title="item.name" style="max-width: 500px; padding-right: 30px">
{{ item.name }}
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('user.user_status')">
@@ -295,7 +346,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, unref, reactive, onMounted, nextTick } from 'vue'
import UserImport from './UserImport.vue'
import SuccessFilled from '@/assets/svg/gou_icon.svg'
import CircleCloseFilled from '@/assets/svg/icon_ban_filled.svg'
@@ -308,11 +359,13 @@ import IconLock from '@/assets/svg/icon-key_outlined.svg'
import IconOpeEdit from '@/assets/svg/icon_edit_outlined.svg'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import iconFilter from '@/assets/svg/icon-filter_outlined.svg'
import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg'
// import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { userApi } from '@/api/user'
import { workspaceList } from '@/api/workspace'
import { formatTimestamp } from '@/utils/date'
import { ClickOutside as vClickOutside } from 'element-plus-secondary'
import icon_warning_filled from '@/assets/svg/icon_warning_filled.svg'
const { t } = useI18n()
const keyword = ref('')
@@ -404,6 +457,12 @@ const rules = {
message: t('datasource.please_enter') + t('common.empty') + t('user.email'),
trigger: 'blur',
},
{
required: true,
pattern: /^[a-zA-Z0-9_._-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
message: t('datasource.incorrect_email_format'),
trigger: 'blur',
},
],
}
@@ -424,6 +483,34 @@ const passwordRules = {
],
}
const closeResetInfo = (row: any) => {
row.popoverRef?.hide()
row.resetInfoShow = false
}
const setPopoverRef = (el: any, row: any) => {
row.popoverRef = el
}
const copyText = () => {
navigator.clipboard
.writeText('SQLBot@123456')
.then(function () {
ElMessage.success(t('embedded.copy_successful'))
})
.catch(function () {
ElMessage.error(t('embedded.copy_successful'))
})
}
const setButtonRef = (el: any, row: any) => {
row.buttonRef = el
}
const onClickOutside = (row: any) => {
if (row.popoverRef) {
unref(row.popoverRef).popperRef?.delayHide?.()
}
}
const multipleTableRef = ref()
const multipleSelectionAll = ref<any[]>([])
const dialogTitle = ref('')
@@ -447,9 +534,9 @@ const handleEditPassword = (id: any) => {
})
}
const handleUserImport = () => {
userImportRef.value.showDialog()
}
// const handleUserImport = () => {
// userImportRef.value.showDialog()
// }
const handleConfirmPassword = () => {
passwordRef.value.validate((val: any) => {
@@ -533,8 +620,13 @@ const editHandler = (row: any) => {
}
const statusHandler = (row: any) => {
state.form = { ...row }
editTerm()
/* state.form = { ...row }
editTerm() */
const param = {
id: row.id,
status: row.status,
}
userApi.status(param)
}
const cancelDelete = () => {
@@ -569,30 +661,14 @@ const deleteHandler = (row: any) => {
customClass: 'confirm-no_icon',
autofocus: false,
}).then(() => {
multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => ele.id !== row.id)
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
userApi.delete(row.id).then(() => {
multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => ele.id !== row.id)
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
search()
})
search()
// userApi.delete(row.id).then(() => {
// multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => ele.id !== row.id)
// ElMessage({
// type: 'success',
// message: t('dashboard.delete_success'),
// })
// search()
// })
})
}
/* const openDialog = () => {
dialogFormVisible.value = true
} */
const resetForm = () => {
if (!termFormRef.value) return
Object.keys(state.form).forEach((key) => {
state.form[key as keyof typeof state.form] = ''
})
}
@@ -600,7 +676,7 @@ const closeForm = () => {
dialogFormVisible.value = false
}
const onFormClose = () => {
resetForm()
state.form = { ...defaultForm }
dialogFormVisible.value = false
}
@@ -639,22 +715,8 @@ const search = () => {
})
}
const addTerm = () => {
userApi
.add(state.form)
.then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
.finally(() => {
state.form = { ...defaultForm }
})
}
const editTerm = () => {
userApi.edit(state.form).then(() => {
const { account, email, name, oid, status } = state.form
userApi.add({ account, email, name, oid, status }).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
@@ -663,14 +725,33 @@ const editTerm = () => {
})
})
}
const editTerm = () => {
const { account, id, create_time, email, language, name, oid, oid_list, origin, status } =
state.form
userApi
.edit({ account, id, create_time, email, language, name, oid, oid_list, origin, status })
.then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
}
const duplicateName = () => {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
}
const saveHandler = () => {
termFormRef.value.validate((res: any) => {
if (res) {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
duplicateName()
}
})
}
@@ -839,3 +920,54 @@ onMounted(() => {
}
}
</style>
<style lang="less">
.reset-pwd-confirm {
padding: 5px 15px;
.confirm-header {
width: 100%;
min-height: 40px;
line-height: 40px;
display: flex;
flex-direction: row;
.icon-span {
color: var(--ed-color-warning);
font-size: 22px;
i {
top: 3px;
}
}
.header-span {
font-size: 16px;
font-weight: bold;
margin-left: 10px;
white-space: pre-wrap;
word-break: keep-all;
}
}
.confirm-foot {
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
.ed-button {
min-width: 48px;
height: 28px;
line-height: 28px;
font-size: 12px;
}
}
.confirm-warning {
font-size: 12px;
color: var(--ed-color-danger);
margin-left: 33px;
}
.confirm-content {
margin-left: 33px;
display: flex;
align-items: center;
}
}
</style>
+21 -9
View File
@@ -97,6 +97,26 @@ const init = () => {
}
})
}
const duplicateName = async () => {
const res = await workspaceList()
const names = res.filter((ele: any) => ele.id !== workspaceForm.id).map((ele: any) => ele.name)
if (names.includes(workspaceForm.name)) {
ElMessage.error(t('embedded.duplicate_name'))
return
}
const req = workspaceForm.id ? workspaceUpdate : workspaceCreate
req(workspaceForm).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
init()
fieldDialog.value = false
})
}
onMounted(() => {
init()
})
@@ -224,15 +244,7 @@ const closeField = () => {
const saveField = () => {
workspaceFormRef.value.validate((res: any) => {
if (res) {
const req = workspaceForm.id ? workspaceUpdate : workspaceCreate
req(workspaceForm).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
init()
fieldDialog.value = false
})
duplicateName()
}
})
}