Merge remote-tracking branch 'origin/main'
@@ -14,6 +14,12 @@ FIRST_SUPERUSER_PASSWORD=123456 # Change this to your pwd
|
||||
|
||||
TOKEN_KEY="X-SQLBOT-TOKEN"
|
||||
DEFAULT_PWD="SQLBot@123456"
|
||||
ASSISTANT_TOKEN_KEY="X-SQLBOT-ASSISTANT-TOKEN"
|
||||
|
||||
LOG_LEVEL="INFO" # DEBUG, INFO, WARNING, ERROR
|
||||
LOG_DIR="/opt/sqlbot/logs"
|
||||
LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
|
||||
SQL_DEBUG=False
|
||||
|
||||
# Postgres
|
||||
POSTGRES_SERVER=localhost
|
||||
|
||||
@@ -5,7 +5,6 @@ from os.path import abspath, dirname
|
||||
sys.path.insert(0, dirname(dirname(abspath(__file__))))
|
||||
|
||||
import os
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
@@ -16,7 +15,6 @@ config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import sqlparse
|
||||
import logging
|
||||
import traceback
|
||||
import warnings
|
||||
from typing import Any, List, Optional, Union, Dict
|
||||
@@ -35,7 +34,7 @@ from apps.system.crud.assistant import AssistantOutDs, AssistantOutDsFactory, ge
|
||||
from apps.system.schemas.system_schema import AssistantOutDsSchema
|
||||
from common.core.config import settings
|
||||
from common.core.deps import CurrentAssistant, SessionDep, CurrentUser
|
||||
from common.utils.utils import extract_nested_json
|
||||
from common.utils.utils import SQLBotLogUtil, extract_nested_json
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
@@ -223,7 +222,7 @@ class LLMService:
|
||||
res = self.llm.stream(analysis_msg)
|
||||
token_usage = {}
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
reasoning_content_chunk = ''
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
@@ -273,7 +272,7 @@ class LLMService:
|
||||
res = self.llm.stream(predict_msg)
|
||||
token_usage = {}
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
reasoning_content_chunk = ''
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
@@ -323,7 +322,7 @@ class LLMService:
|
||||
token_usage = {}
|
||||
res = self.llm.stream(guess_msg)
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
reasoning_content_chunk = ''
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
@@ -390,7 +389,7 @@ class LLMService:
|
||||
token_usage = {}
|
||||
res = self.llm.stream(datasource_msg)
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
reasoning_content_chunk = ''
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
@@ -474,7 +473,7 @@ class LLMService:
|
||||
token_usage = {}
|
||||
res = self.llm.stream(self.sql_message)
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
reasoning_content_chunk = ''
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
@@ -543,7 +542,7 @@ class LLMService:
|
||||
res = self.llm.stream(msg)
|
||||
token_usage = {}
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
reasoning_content_chunk = ''
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
@@ -567,7 +566,7 @@ class LLMService:
|
||||
# 'content': msg.content} for msg
|
||||
# in
|
||||
# analysis_msg]).decode())
|
||||
print(full_filter_text)
|
||||
SQLBotLogUtil.info(full_filter_text)
|
||||
return full_filter_text
|
||||
|
||||
def generate_chart(self):
|
||||
@@ -582,7 +581,7 @@ class LLMService:
|
||||
token_usage = {}
|
||||
res = self.llm.stream(self.chart_message)
|
||||
for chunk in res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
reasoning_content_chunk = ''
|
||||
if 'reasoning_content' in chunk.additional_kwargs:
|
||||
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
|
||||
@@ -691,7 +690,7 @@ class LLMService:
|
||||
Returns:
|
||||
Query results
|
||||
"""
|
||||
print(f"Executing SQL on ds_id {self.ds.id}: {sql}")
|
||||
SQLBotLogUtil.info(f"Executing SQL on ds_id {self.ds.id}: {sql}")
|
||||
return exec_sql(self.ds, sql)
|
||||
|
||||
|
||||
@@ -717,7 +716,7 @@ def execute_sql_with_db(db: SQLDatabase, sql: str) -> str:
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"SQL execution failed: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
SQLBotLogUtil.exception(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
|
||||
@@ -741,7 +740,7 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
|
||||
ds_res = llm_service.select_datasource()
|
||||
|
||||
for chunk in ds_res:
|
||||
print(chunk)
|
||||
SQLBotLogUtil.info(chunk)
|
||||
if in_chat:
|
||||
yield orjson.dumps(
|
||||
{'content': chunk.get('content'), 'reasoning_content': chunk.get('reasoning_content'),
|
||||
@@ -765,7 +764,7 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
|
||||
yield orjson.dumps({'type': 'info', 'msg': 'sql generated'}).decode() + '\n\n'
|
||||
|
||||
# filter sql
|
||||
print(full_sql_text)
|
||||
SQLBotLogUtil.info(full_sql_text)
|
||||
|
||||
# todo row permission
|
||||
sql_json_str = extract_nested_json(full_sql_text)
|
||||
@@ -785,11 +784,11 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
|
||||
raise Exception("SQL query is empty")
|
||||
|
||||
sql_result = llm_service.generate_filter(data.get('sql'), data.get('tables')) # maybe no sql and tables
|
||||
print(sql_result)
|
||||
SQLBotLogUtil.info(sql_result)
|
||||
sql = llm_service.check_save_sql(res=sql_result)
|
||||
# sql = llm_service.check_save_sql(res=full_sql_text)
|
||||
|
||||
print(sql)
|
||||
SQLBotLogUtil.info(sql)
|
||||
format_sql = sqlparse.format(sql, reindent=True)
|
||||
if in_chat:
|
||||
yield orjson.dumps({'content': format_sql, 'type': 'sql'}).decode() + '\n\n'
|
||||
@@ -815,9 +814,9 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
|
||||
yield orjson.dumps({'type': 'info', 'msg': 'chart generated'}).decode() + '\n\n'
|
||||
|
||||
# filter chart
|
||||
print(full_chart_text)
|
||||
SQLBotLogUtil.info(full_chart_text)
|
||||
chart = llm_service.check_save_chart(res=full_chart_text)
|
||||
print(chart)
|
||||
SQLBotLogUtil.info(chart)
|
||||
if in_chat:
|
||||
yield orjson.dumps({'content': orjson.dumps(chart).decode(), 'type': 'chart'}).decode() + '\n\n'
|
||||
else:
|
||||
@@ -856,7 +855,7 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
|
||||
if chart['type'] != 'table':
|
||||
yield '### generated chart picture\n\n'
|
||||
image_url = request_picture(llm_service.record.chat_id, llm_service.record.id, chart, result)
|
||||
print(image_url)
|
||||
SQLBotLogUtil.info(image_url)
|
||||
yield f'![{chart["type"]}]({image_url})'
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -15,7 +15,7 @@ from apps.db.db import get_engine, get_tables, get_fields, exec_sql
|
||||
from apps.db.engine import get_engine_config, get_engine_conn
|
||||
from apps.db.type import db_type_relation
|
||||
from common.core.deps import SessionDep, CurrentUser, Trans
|
||||
from common.utils.utils import deepcopy_ignore_extra
|
||||
from common.utils.utils import SQLBotLogUtil, deepcopy_ignore_extra
|
||||
from .table import get_tables_by_ds_id
|
||||
from ..crud.field import delete_field_by_ds_id, update_field
|
||||
from ..crud.table import delete_table_by_ds_id, update_table
|
||||
@@ -39,12 +39,12 @@ def check_status(session: SessionDep, ds: CoreDatasource, is_raise: bool = False
|
||||
conn = get_engine(ds)
|
||||
try:
|
||||
with conn.connect() as connection:
|
||||
print("success")
|
||||
SQLBotLogUtil.info("success")
|
||||
return True
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.error(f"Datasource {ds.id} connection failed: {e}")
|
||||
if is_raise:
|
||||
raise HTTPException(status_code=500, detail=f'Connect Failed: {e.args}')
|
||||
print("Fail:", e)
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
|
||||
from typing import Optional
|
||||
from sqlmodel import Session, select, delete as sqlmodel_delete
|
||||
import logging
|
||||
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 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
|
||||
|
||||
@@ -68,4 +68,4 @@ 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):
|
||||
logging.info(f"User cache for [{id}] has been cleaned")
|
||||
SQLBotLogUtil.info(f"User cache for [{id}] has been cleaned")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import jwt
|
||||
from sqlmodel import Session
|
||||
@@ -13,6 +13,7 @@ from apps.system.schemas.system_schema import AssistantHeader, UserInfoDTO
|
||||
from common.core import security
|
||||
from common.core.config import settings
|
||||
from common.core.schemas import TokenPayload
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
from common.utils.whitelist import whiteUtils
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
class TokenMiddleware(BaseHTTPMiddleware):
|
||||
@@ -70,6 +71,7 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
) """
|
||||
return True, session_user
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.exception(f"Token validation error: {str(e)}", exc_info=True)
|
||||
return False, e
|
||||
|
||||
|
||||
@@ -95,4 +97,6 @@ class TokenMiddleware(BaseHTTPMiddleware):
|
||||
assistant_info = AssistantHeader.model_validate(assistant_info.model_dump(exclude_unset=True))
|
||||
return True, session_user, assistant_info
|
||||
except Exception as e:
|
||||
SQLBotLogUtil.exception(f"Assistant validation error: {str(e)}", exc_info=True)
|
||||
# Return False and the exception message
|
||||
return False, e
|
||||
@@ -60,6 +60,11 @@ class Settings(BaseSettings):
|
||||
TOKEN_KEY: str = "X-SQLBOT-TOKEN"
|
||||
DEFAULT_PWD: str = "SQLBot@123456"
|
||||
ASSISTANT_TOKEN_KEY: str = "X-SQLBOT-ASSISTANT-TOKEN"
|
||||
|
||||
LOG_LEVEL: str = "INFO" # DEBUG, INFO, WARNING, ERROR
|
||||
LOG_DIR: str = "logs"
|
||||
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
|
||||
SQL_DEBUG: bool = False
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import json
|
||||
import logging
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from common.core.config import settings
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
class ResponseMiddleware(BaseHTTPMiddleware):
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
@@ -48,7 +48,7 @@ class ResponseMiddleware(BaseHTTPMiddleware):
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Response processing error: {str(e)}", exc_info=True)
|
||||
SQLBotLogUtil.error(f"Response processing error: {str(e)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
@@ -68,6 +68,7 @@ class ResponseMiddleware(BaseHTTPMiddleware):
|
||||
class exception_handler():
|
||||
@staticmethod
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
SQLBotLogUtil.exception(f"HTTP Exception: {exc.detail}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
@@ -81,8 +82,7 @@ class exception_handler():
|
||||
|
||||
@staticmethod
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
logging.error(f"Error info: {str(exc)}", exc_info=True)
|
||||
|
||||
SQLBotLogUtil.exception(f"Unhandled Exception: {str(exc)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import logging
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.decorator import cache as original_cache
|
||||
from functools import partial, wraps
|
||||
from typing import Optional, Set, Any, Dict, Tuple, Callable
|
||||
from typing import Optional, Set, Any, Dict, Tuple
|
||||
from inspect import Parameter, signature
|
||||
from contextlib import asynccontextmanager
|
||||
import asyncio
|
||||
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
|
||||
|
||||
# 锁管理
|
||||
_cache_locks = {}
|
||||
@@ -65,7 +66,7 @@ def custom_key_builder(
|
||||
base_key += f"{value}:"
|
||||
|
||||
except (IndexError, KeyError, AttributeError) as e:
|
||||
logging.warning(f"Failed to evaluate keyExpression '{keyExpression}': {str(e)}")
|
||||
SQLBotLogUtil.warning(f"Failed to evaluate keyExpression '{keyExpression}': {str(e)}")
|
||||
|
||||
return base_key
|
||||
|
||||
@@ -127,19 +128,16 @@ def cache(
|
||||
)
|
||||
|
||||
async with _get_cache_lock(cache_key):
|
||||
logging.info(f"Using cache key: {cache_key}")
|
||||
print(f"Using cache key: {cache_key}")
|
||||
SQLBotLogUtil.info(f"Using cache key: {cache_key}")
|
||||
backend = FastAPICache.get_backend()
|
||||
cached_value = await backend.get(cache_key)
|
||||
if cached_value is not None:
|
||||
logging.info(f"Cache hit for key: {cache_key}, the value is: {cached_value}")
|
||||
print(f"Cache hit for key: {cache_key}, the value is: {cached_value}")
|
||||
SQLBotLogUtil.info(f"Cache hit for key: {cache_key}, the value is: {cached_value}")
|
||||
return cached_value
|
||||
|
||||
result = await func(*args, **kwargs)
|
||||
await backend.set(cache_key, result, expire)
|
||||
logging.info(f"Cache miss for key: {cache_key}, result cached.")
|
||||
print(f"Cache miss for key: {cache_key}, result cached.")
|
||||
SQLBotLogUtil.info(f"Cache miss for key: {cache_key}, result cached.")
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
@@ -167,8 +165,7 @@ def clear_cache(
|
||||
if await FastAPICache.get_backend().get(cache_key):
|
||||
await FastAPICache.clear(key=cache_key)
|
||||
result = await func(*args, **kwargs)
|
||||
logging.info(f"Clearing cache for key: {cache_key}")
|
||||
print(f"Clearing cache for key: {cache_key}")
|
||||
SQLBotLogUtil.info(f"Clearing cache for key: {cache_key}")
|
||||
return result
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import hashlib
|
||||
import inspect
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
from common.core.config import settings
|
||||
from typing import Optional
|
||||
|
||||
import jwt
|
||||
@@ -10,9 +14,6 @@ from jwt.exceptions import InvalidTokenError
|
||||
from common.core import security
|
||||
from common.core.config import settings
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_password_reset_token(email: str) -> str:
|
||||
delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
|
||||
@@ -76,3 +77,142 @@ def extract_nested_json(text):
|
||||
def string_to_numeric_hash(text: str, bits: Optional[int] = 64) -> int:
|
||||
hash_bytes = hashlib.sha256(text.encode()).digest()
|
||||
return int.from_bytes(hash_bytes[:bits//8], byteorder='big')
|
||||
|
||||
|
||||
def setup_logging():
|
||||
# 确保日志目录存在
|
||||
log_dir = Path(settings.LOG_DIR)
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 日志格式
|
||||
formatter = logging.Formatter(
|
||||
f'{settings.LOG_FORMAT}'
|
||||
)
|
||||
|
||||
# 控制台日志
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(settings.LOG_LEVEL)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# 文件日志处理器
|
||||
file_handlers = {
|
||||
'debug': logging.DEBUG,
|
||||
'info': logging.INFO,
|
||||
'warn': logging.WARNING,
|
||||
'error': logging.ERROR
|
||||
}
|
||||
|
||||
# 主日志记录器
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG) # 设置最低级别
|
||||
|
||||
# 添加控制台处理器
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# 为每个级别创建文件处理器
|
||||
for level_name, level in file_handlers.items():
|
||||
file_path = log_dir / f"{level_name}.log"
|
||||
handler = RotatingFileHandler(
|
||||
file_path,
|
||||
maxBytes=10 * 1024 * 1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# 添加过滤器只处理特定级别日志
|
||||
if level_name == 'debug':
|
||||
handler.addFilter(lambda record: record.levelno == logging.DEBUG)
|
||||
elif level_name == 'info':
|
||||
handler.addFilter(lambda record: record.levelno == logging.INFO)
|
||||
elif level_name == 'warn':
|
||||
handler.addFilter(lambda record: record.levelno == logging.WARNING)
|
||||
elif level_name == 'error':
|
||||
handler.addFilter(lambda record: record.levelno >= logging.ERROR)
|
||||
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# SQL日志特殊处理
|
||||
if settings.LOG_LEVEL == "DEBUG" and settings.SQL_DEBUG:
|
||||
sql_logger = logging.getLogger('sqlalchemy.engine')
|
||||
sql_logger.setLevel(logging.DEBUG)
|
||||
|
||||
sql_handler = RotatingFileHandler(
|
||||
log_dir / "sql.log",
|
||||
maxBytes=10 * 1024 * 1024,
|
||||
backupCount=2,
|
||||
encoding='utf-8'
|
||||
)
|
||||
sql_handler.setFormatter(formatter)
|
||||
sql_logger.addHandler(sql_handler)
|
||||
|
||||
setup_logging()
|
||||
|
||||
|
||||
class CallerLogger(logging.Logger):
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
super().__init__(logger.name, logger.level)
|
||||
|
||||
def _log(self, level, msg, args, exc_info=None, extra=None, stacklevel=3):
|
||||
if self.logger.isEnabledFor(level):
|
||||
self.logger._log(level, msg, args, exc_info=exc_info, extra=extra, stacklevel=stacklevel)
|
||||
|
||||
class SQLBotLogUtil:
|
||||
|
||||
@staticmethod
|
||||
def _get_logger() -> logging.Logger:
|
||||
frame = inspect.currentframe()
|
||||
try:
|
||||
caller_frame = frame.f_back.f_back
|
||||
module_name = caller_frame.f_globals.get('__name__', '__main__')
|
||||
return CallerLogger(logging.getLogger(module_name))
|
||||
finally:
|
||||
del frame
|
||||
|
||||
|
||||
@staticmethod
|
||||
def debug(msg: str, *args, **kwargs):
|
||||
logger = SQLBotLogUtil._get_logger()
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger._log(logging.DEBUG, msg, args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def info(msg: str, *args, **kwargs):
|
||||
logger = SQLBotLogUtil._get_logger()
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger._log(logging.INFO, msg, args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def warning(msg: str, *args, **kwargs):
|
||||
logger = SQLBotLogUtil._get_logger()
|
||||
if logger.isEnabledFor(logging.WARNING):
|
||||
logger._log(logging.WARNING, msg, args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def error(msg: str, *args, exc_info: Optional[bool] = None, **kwargs):
|
||||
logger = SQLBotLogUtil._get_logger()
|
||||
if logger.isEnabledFor(logging.ERROR):
|
||||
logger._log(
|
||||
logging.ERROR,
|
||||
msg,
|
||||
args,
|
||||
exc_info=exc_info if exc_info is not None else True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def exception(msg: str, *args, **kwargs):
|
||||
logger = SQLBotLogUtil._get_logger()
|
||||
if logger.isEnabledFor(logging.ERROR):
|
||||
logger._log(logging.ERROR, msg, args, exc_info=True, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def critical(msg: str, *args, **kwargs):
|
||||
logger = SQLBotLogUtil._get_logger()
|
||||
if logger.isEnabledFor(logging.CRITICAL):
|
||||
logger._log(logging.CRITICAL, msg, args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import re
|
||||
from typing import List, Pattern
|
||||
from common.core.config import settings
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
wlist = [
|
||||
"/",
|
||||
"/docs",
|
||||
@@ -50,7 +51,7 @@ class WhitelistChecker:
|
||||
try:
|
||||
self._compiled_patterns.append(re.compile(regex_pattern))
|
||||
except re.error:
|
||||
print(f"Invalid regular expression pattern: {pattern}")
|
||||
SQLBotLogUtil.error(f"Invalid regex pattern: {regex_pattern}")
|
||||
|
||||
def is_whitelisted(self, path: str) -> bool:
|
||||
prefix = settings.API_V1_STR
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
from fastapi.concurrency import asynccontextmanager
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI
|
||||
@@ -18,6 +17,8 @@ import sqlbot_xpack
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.backends.inmemory import InMemoryBackend
|
||||
|
||||
from common.utils.utils import SQLBotLogUtil
|
||||
|
||||
def run_migrations():
|
||||
alembic_cfg = Config("alembic.ini")
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
@@ -27,10 +28,10 @@ def run_migrations():
|
||||
async def lifespan(app: FastAPI):
|
||||
run_migrations()
|
||||
FastAPICache.init(InMemoryBackend())
|
||||
print("✅ FastAPICache 初始化完成")
|
||||
init_dynamic_cors(app)
|
||||
SQLBotLogUtil.info("✅ SQLBot 初始化完成")
|
||||
yield
|
||||
|
||||
SQLBotLogUtil.info("SQLBot 应用关闭")
|
||||
|
||||
def custom_generate_unique_id(route: APIRoute) -> str:
|
||||
tag = route.tags[0] if route.tags and len(route.tags) > 0 else ""
|
||||
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.76027 7.79265L1.11652 3.24995C1.01715 3.1262 1.01715 2.92537 1.11652 2.80142C1.16423 2.74183 1.22902 2.7085 1.29631 2.7085H8.70444C8.84485 2.7085 8.95882 2.85058 8.95882 3.02578C8.95882 3.10995 8.93194 3.19037 8.88423 3.24995L5.24048 7.79287C5.10798 7.95808 4.89298 7.95808 4.76048 7.79287L4.76027 7.79265Z" fill="#646A73"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 440 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 1.25C14.8325 1.25 18.75 5.16751 18.75 10C18.75 14.8325 14.8325 18.75 10 18.75H2.08333C1.6231 18.75 1.25 18.3769 1.25 17.9167V10C1.25 5.16751 5.16751 1.25 10 1.25ZM2.91667 17.0833H10C13.912 17.0833 17.0833 13.912 17.0833 9.99998C17.0833 6.08796 13.912 2.91665 10 2.91665C6.08798 2.91665 2.91667 6.08796 2.91667 9.99998V17.0833Z" fill="#1F2329"/>
|
||||
<path d="M9.58333 6.25C9.35321 6.25 9.16667 6.43655 9.16667 6.66667V9.16667H6.66667C6.43655 9.16667 6.25 9.35321 6.25 9.58333V10.4167C6.25 10.6468 6.43655 10.8333 6.66667 10.8333H9.16667V13.3333C9.16667 13.5635 9.35321 13.75 9.58333 13.75H10.4167C10.6468 13.75 10.8333 13.5635 10.8333 13.3333V10.8333H13.3333C13.5635 10.8333 13.75 10.6468 13.75 10.4167V9.58333C13.75 9.35321 13.5635 9.16667 13.3333 9.16667H10.8333V6.66667C10.8333 6.43655 10.6468 6.25 10.4167 6.25H9.58333Z" fill="#1F2329"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 953 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 1.125C13.3492 1.125 16.875 4.65076 16.875 9C16.875 13.3492 13.3492 16.875 9 16.875H1.875C1.46079 16.875 1.125 16.5392 1.125 16.125V9C1.125 4.65076 4.65076 1.125 9 1.125ZM2.625 15.375H9C12.5208 15.375 15.375 12.5208 15.375 8.99998C15.375 5.47917 12.5208 2.62498 9 2.62498C5.47918 2.62498 2.625 5.47917 2.625 8.99998V15.375Z" fill="#1CBA90"/>
|
||||
<path d="M8.625 5.625C8.41789 5.625 8.25 5.79289 8.25 6V8.25H6C5.79289 8.25 5.625 8.41789 5.625 8.625V9.375C5.625 9.58211 5.79289 9.75 6 9.75H8.25V12C8.25 12.2071 8.41789 12.375 8.625 12.375H9.375C9.58211 12.375 9.75 12.2071 9.75 12V9.75H12C12.2071 9.75 12.375 9.58211 12.375 9.375V8.625C12.375 8.41789 12.2071 8.25 12 8.25H9.75V6C9.75 5.79289 9.58211 5.625 9.375 5.625H8.625Z" fill="#1CBA90"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 851 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.35433 11.2487H7.625L14.379 4.49447C14.6111 4.26241 14.6116 3.88631 14.3801 3.65363L12.3345 1.59723C12.102 1.36473 11.7251 1.36473 11.4926 1.59723L11.0727 2.01709L11.0743 2.01875L4.75898 8.38268V10.6534C4.75898 10.9822 5.02553 11.2487 5.35433 11.2487ZM11.8947 2.84338L13.1003 4.0554L12.0801 5.07666L10.8806 3.87724L11.8947 2.84338ZM11.2385 5.91901L7.12283 10.0386H7.12102L5.96904 8.88486L10.0468 4.72734L11.2385 5.91901Z" fill="#646A73"/>
|
||||
<path d="M8.76346 2.31588H2.44373C2.07839 2.31588 1.78223 2.61204 1.78223 2.97737V13.5614C1.78223 13.9267 2.07839 14.2229 2.44373 14.2229H13.0277C13.393 14.2229 13.6892 13.9267 13.6892 13.5614V7.16128L12.3579 8.5006V12.898H3.11347V3.64068H7.46273L8.76346 2.31588Z" fill="#646A73"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 835 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.4997 15.8333V17.5H2.49967V15.8333H17.4997ZM17.4997 4.16667H2.49967V17.5L2.32959 17.491C1.54496 17.4115 0.921507 16.788 0.84196 16.0034L0.833008 15.8333V4.16667C0.833008 3.24619 1.5792 2.5 2.49967 2.5H17.4997C18.4201 2.5 19.1663 3.24619 19.1663 4.16667V15.8333C19.1663 16.7538 18.4201 17.5 17.4997 17.5V4.16667Z" fill="#1F2329"/>
|
||||
<path d="M6.24967 5.83333H3.74967C3.63917 5.83333 3.53319 5.87723 3.45505 5.95537C3.37691 6.03351 3.33301 6.13949 3.33301 6.25V7.08333C3.33301 7.19384 3.37691 7.29982 3.45505 7.37796C3.53319 7.4561 3.63917 7.5 3.74967 7.5H6.24967C6.30439 7.5 6.35857 7.48922 6.40913 7.46828C6.45968 7.44734 6.50561 7.41665 6.5443 7.37796C6.58299 7.33927 6.61369 7.29334 6.63462 7.24278C6.65556 7.19223 6.66634 7.13805 6.66634 7.08333V6.25C6.66634 6.19528 6.65556 6.1411 6.63462 6.09055C6.61369 6.04 6.58299 5.99406 6.5443 5.95537C6.50561 5.91668 6.45968 5.88599 6.40913 5.86505C6.35857 5.84411 6.30439 5.83333 6.24967 5.83333Z" fill="#1F2329"/>
|
||||
<path d="M6.24967 8.33333H3.74967C3.63917 8.33333 3.53319 8.37723 3.45505 8.45537C3.37691 8.53351 3.33301 8.63949 3.33301 8.75V9.58333C3.33301 9.69384 3.37691 9.79982 3.45505 9.87796C3.53319 9.9561 3.63917 10 3.74967 10H6.24967C6.30439 10 6.35857 9.98922 6.40913 9.96828C6.45968 9.94734 6.50561 9.91665 6.5443 9.87796C6.58299 9.83927 6.61369 9.79334 6.63462 9.74278C6.65556 9.69223 6.66634 9.63805 6.66634 9.58333V8.75C6.66634 8.69528 6.65556 8.6411 6.63462 8.59055C6.61369 8.54 6.58299 8.49406 6.5443 8.45537C6.50561 8.41668 6.45968 8.38599 6.40913 8.36505C6.35857 8.34411 6.30439 8.33333 6.24967 8.33333Z" fill="#1F2329"/>
|
||||
<path d="M7.49967 4.16667H9.16634V15.8333H7.49967V4.16667Z" fill="#1F2329"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -400,7 +400,14 @@
|
||||
"filter_like": "Contains",
|
||||
"filter_not_like": "Does not contain",
|
||||
"filter_in": "Belongs to",
|
||||
"filter_not_in": "Does not belong to"
|
||||
"filter_not_in": "Does not belong to",
|
||||
"enter_a_question": "Please enter a question",
|
||||
"new_conversation": "New Conversation",
|
||||
"send": "Send",
|
||||
"stop_replying": "Stop replying",
|
||||
"i_am_sqlbot": "Hello, I am SQLBot",
|
||||
"predict_data_etc": "I can query data, generate charts, detect data anomalies, predict data, etc.",
|
||||
"intelligent_data_query": "Hurry up and start intelligent data query~"
|
||||
},
|
||||
"embedded": {
|
||||
"embedded_management": "Embedded management",
|
||||
@@ -446,7 +453,8 @@
|
||||
"edit_interface_credentials": "Edit interface credentials",
|
||||
"duplicate_name": "Duplicate name",
|
||||
"interface_credentials": "Interface credentials",
|
||||
"no_credentials_yet": "No credentials yet"
|
||||
"no_credentials_yet": "No credentials yet",
|
||||
"intelligent_customer_service": "SQLBot Intelligent Customer Service"
|
||||
},
|
||||
"chat": {
|
||||
"type": "Chart Type",
|
||||
|
||||
@@ -458,7 +458,15 @@
|
||||
"edit_interface_credentials": "编辑接口凭证",
|
||||
"duplicate_name": "重复名称",
|
||||
"interface_credentials": "接口凭证",
|
||||
"no_credentials_yet": "暂无凭证"
|
||||
"no_credentials_yet": "暂无凭证",
|
||||
"intelligent_customer_service": "SQLBot 智能客服",
|
||||
"enter_a_question": "请输入问题",
|
||||
"new_conversation": "新建对话",
|
||||
"send": "发送",
|
||||
"stop_replying": "停止回答",
|
||||
"i_am_sqlbot": "你好,我是 SQLBot",
|
||||
"predict_data_etc": "我可以查询数据、生成图表、检测数据异常、预测数据等",
|
||||
"intelligent_data_query": "赶快开启智能问数吧~"
|
||||
},
|
||||
"chat": {
|
||||
"type": "图表类型",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { Delete, EditPen, MoreFilled } from '@element-plus/icons-vue'
|
||||
import icon_more_outlined from '@/assets/svg/icon_more_outlined.svg'
|
||||
import icon_expand_down_filled from '@/assets/embedded/icon_expand-down_filled.svg'
|
||||
import rename from '@/assets/svg/icon_rename_outlined.svg'
|
||||
import delIcon from '@/assets/svg/icon_delete.svg'
|
||||
import { type Chat, chatApi } from '@/api/chat.ts'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { getDate } from '@/utils/utils.ts'
|
||||
import { groupBy } from 'lodash-es'
|
||||
@@ -48,6 +51,13 @@ const computedChatGroup = computed(() => {
|
||||
return groupBy(props.chatList, groupByDate)
|
||||
})
|
||||
|
||||
const expandMap = ref({
|
||||
[t('qa.today')]: true,
|
||||
[t('qa.week')]: true,
|
||||
[t('qa.earlier')]: true,
|
||||
[t('qa.no_time')]: true,
|
||||
})
|
||||
|
||||
const computedChatList = computed(() => {
|
||||
const _list = []
|
||||
if (computedChatGroup.value[t('qa.today')]) {
|
||||
@@ -169,27 +179,44 @@ function handleCommand(command: string | number | object, chat: Chat) {
|
||||
<el-scrollbar ref="chatListRef">
|
||||
<div class="chat-list-inner">
|
||||
<div v-for="group in computedChatList" :key="group.key" class="group">
|
||||
<div class="group-title">{{ group.key }}</div>
|
||||
<div
|
||||
class="group-title"
|
||||
style="cursor: pointer"
|
||||
@click="expandMap[group.key] = !expandMap[group.key]"
|
||||
>
|
||||
<el-icon :class="!expandMap[group.key] && 'expand'" style="margin-right: 8px" size="10">
|
||||
<icon_expand_down_filled></icon_expand_down_filled>
|
||||
</el-icon>
|
||||
{{ group.key }}
|
||||
</div>
|
||||
<template v-for="chat in group.list" :key="chat.id">
|
||||
<div
|
||||
class="chat-list-item"
|
||||
:class="{ active: currentChatId === chat.id }"
|
||||
:class="{ active: currentChatId === chat.id, hide: !expandMap[group.key] }"
|
||||
@click="onClickHistory(chat)"
|
||||
>
|
||||
<span class="title">{{ chat.brief ?? 'Untitled' }}</span>
|
||||
<el-button class="icon-more" link type="primary" @click.stop>
|
||||
<el-dropdown trigger="click" @command="(cmd: any) => handleCommand(cmd, chat)">
|
||||
<el-icon>
|
||||
<MoreFilled />
|
||||
<el-popover :teleported="false" popper-class="popover-card" placement="bottom">
|
||||
<template #reference>
|
||||
<el-icon class="more" size="16" style="margin-left: auto" @click.stop>
|
||||
<icon_more_outlined></icon_more_outlined>
|
||||
</el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :icon="EditPen" command="rename">Rename</el-dropdown-item>
|
||||
<el-dropdown-item :icon="Delete" command="delete">Delete</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="content">
|
||||
<div class="item" @click.stop="handleCommand('rename', chat)">
|
||||
<el-icon size="16">
|
||||
<rename></rename>
|
||||
</el-icon>
|
||||
{{ $t('dashboard.rename') }}
|
||||
</div>
|
||||
<div class="item" @click.stop="handleCommand('delete', chat)">
|
||||
<el-icon size="16">
|
||||
<delIcon></delIcon>
|
||||
</el-icon>
|
||||
{{ $t('dashboard.delete') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -223,6 +250,10 @@ function handleCommand(command: string | number | object, chat: Chat) {
|
||||
font-size: 12px;
|
||||
|
||||
margin-bottom: 4px;
|
||||
|
||||
.expand {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,23 +282,39 @@ function handleCommand(command: string | number | object, chat: Chat) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon-more {
|
||||
margin-left: auto;
|
||||
.more {
|
||||
display: none;
|
||||
&.ed-icon {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
|
||||
min-width: unset;
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: #1f23291a;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(31, 35, 41, 0.1);
|
||||
|
||||
.icon-more {
|
||||
display: inline-flex;
|
||||
color: rgba(100, 106, 115, 1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(31, 35, 41, 0.1);
|
||||
}
|
||||
.more {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +322,63 @@ function handleCommand(command: string | number | object, chat: Chat) {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.popover-card.popover-card.popover-card {
|
||||
box-shadow: 0px 4px 8px 0px #1f23291a;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee0e3;
|
||||
width: 120px !important;
|
||||
min-width: 120px !important;
|
||||
padding: 0;
|
||||
.content {
|
||||
position: relative;
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #dee0e3;
|
||||
}
|
||||
.item {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.ed-icon {
|
||||
margin-right: 8px;
|
||||
color: #646a73;
|
||||
}
|
||||
&:hover {
|
||||
&::after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #1f23291a;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,264 +1,198 @@
|
||||
<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>
|
||||
<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>
|
||||
</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 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'
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import icon_sidebar_outlined from '@/assets/embedded/icon_sidebar_outlined.svg'
|
||||
import icon_new_chat_outlined from '@/assets/embedded/icon_new-chat_outlined.svg'
|
||||
import LOGO from '@/assets/embedded/LOGO.png'
|
||||
import answer from '@/assets/embedded/answer.png'
|
||||
import disable_answer from '@/assets/embedded/disable.png'
|
||||
import loading_answer from '@/assets/embedded/loading.png'
|
||||
import send_answer from '@/assets/embedded/send.png'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/* const EditPen = (chat: any) => {
|
||||
chatRef.value?.onChatRenamed(chat)
|
||||
}
|
||||
const Delete = (chat: any) => {
|
||||
chatRef.value?.onChatDeleted(chat.id)
|
||||
} */
|
||||
const validator = ref({
|
||||
id: '',
|
||||
valid: false,
|
||||
id_match: false,
|
||||
token: '',
|
||||
})
|
||||
const loading = ref(true)
|
||||
const eventName = 'sqlbot_assistant_event'
|
||||
const communicationCb = async (event: any) => {
|
||||
if (event.data?.eventName === eventName) {
|
||||
if (event.data?.messageId !== route.query.id) {
|
||||
return
|
||||
}
|
||||
if (event.data?.busi == 'certificate') {
|
||||
const certificate = event.data['certificate']
|
||||
assistantStore.setType(1)
|
||||
assistantStore.setCertificate(certificate)
|
||||
// store certificate to pinia
|
||||
}
|
||||
}
|
||||
}
|
||||
onBeforeMount(async () => {
|
||||
const assistantId = route.query.id
|
||||
const online = route.query.online
|
||||
console.log(online)
|
||||
const now = Date.now()
|
||||
assistantStore.setFlag(now)
|
||||
const param = {
|
||||
id: assistantId,
|
||||
virtual: assistantStore.getFlag,
|
||||
online,
|
||||
}
|
||||
validator.value = await assistantApi.validate(param)
|
||||
assistantStore.setToken(validator.value.token)
|
||||
assistantStore.setAssistant(true)
|
||||
loading.value = false
|
||||
|
||||
window.addEventListener('message', communicationCb)
|
||||
const readyData = {
|
||||
eventName: 'sqlbot_assistant_event',
|
||||
busi: 'ready',
|
||||
ready: true,
|
||||
messageId: assistantId,
|
||||
}
|
||||
window.parent.postMessage(readyData, '*')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', communicationCb)
|
||||
})
|
||||
const textareaVal = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="assistant-window">
|
||||
<div class="header">
|
||||
<el-icon size="20">
|
||||
<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">
|
||||
<icon_new_chat_outlined></icon_new_chat_outlined>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="center">
|
||||
<img :src="LOGO" class="logo" width="30px" height="30px" alt="" />
|
||||
<div class="i-am">{{ $t('embedded.i_am_sqlbot') }}</div>
|
||||
<div class="i-can">{{ $t('embedded.predict_data_etc') }}</div>
|
||||
<div class="data-query">{{ $t('embedded.intelligent_data_query') }}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="textarea-send">
|
||||
<el-input
|
||||
v-model="textareaVal"
|
||||
:autosize="{ minRows: 1, maxRows: 8.5848 }"
|
||||
type="textarea"
|
||||
:placeholder="$t('embedded.enter_a_question')"
|
||||
/>
|
||||
<el-tooltip :offset="10" effect="dark" :content="$t('embedded.send')" placement="top">
|
||||
<img :src="disable_answer" width="32px" height="32px" alt="" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
:offset="10"
|
||||
effect="dark"
|
||||
:content="$t('embedded.stop_replying')"
|
||||
placement="top"
|
||||
>
|
||||
<img :src="answer" width="32px" height="32px" alt="" />
|
||||
</el-tooltip>
|
||||
|
||||
<img :src="loading_answer" width="32px" height="32px" alt="" />
|
||||
<img :src="send_answer" width="32px" height="32px" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-assistant"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sqlbot-assistant-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: rgb(31, 35, 41);
|
||||
.flex {
|
||||
.assistant-window {
|
||||
width: 460px;
|
||||
height: 640px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #dee0e3;
|
||||
box-shadow: 0px 6px 24px 0px #1f232914;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
.header {
|
||||
background: #1cba9014;
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.head {
|
||||
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);
|
||||
position: fixed;
|
||||
.center {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.i-am {
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.i-can {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.data-query,
|
||||
.i-can {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #646a73;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
width: calc(100% - 32px);
|
||||
|
||||
.textarea-send {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.ed-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ed-textarea__inner) {
|
||||
padding: 12px 12px 52px 12px;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
border-radius: 16px;
|
||||
|
||||
&::placeholder {
|
||||
color: #8f959e;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.drawer-assistant {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sqlbot-chat-container {
|
||||
padding-top: 56px;
|
||||
:deep(.ed-aside) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
width: 50%;
|
||||
background: #f5f6f7;
|
||||
box-shadow: 0px 6px 24px 0px #1f232914;
|
||||
padding: 16px;
|
||||
border-right: 1px solid #dee0e3;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
<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>
|
||||
<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>
|
||||
</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 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'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/* const EditPen = (chat: any) => {
|
||||
chatRef.value?.onChatRenamed(chat)
|
||||
}
|
||||
const Delete = (chat: any) => {
|
||||
chatRef.value?.onChatDeleted(chat.id)
|
||||
} */
|
||||
const validator = ref({
|
||||
id: '',
|
||||
valid: false,
|
||||
id_match: false,
|
||||
token: '',
|
||||
})
|
||||
const loading = ref(true)
|
||||
const eventName = 'sqlbot_assistant_event'
|
||||
const communicationCb = async (event: any) => {
|
||||
if (event.data?.eventName === eventName) {
|
||||
if (event.data?.messageId !== route.query.id) {
|
||||
return
|
||||
}
|
||||
if (event.data?.busi == 'certificate') {
|
||||
const certificate = event.data['certificate']
|
||||
assistantStore.setType(1)
|
||||
assistantStore.setCertificate(certificate)
|
||||
// store certificate to pinia
|
||||
}
|
||||
}
|
||||
}
|
||||
onBeforeMount(async () => {
|
||||
const assistantId = route.query.id
|
||||
const online = route.query.online
|
||||
console.log(online)
|
||||
const now = Date.now()
|
||||
assistantStore.setFlag(now)
|
||||
const param = {
|
||||
id: assistantId,
|
||||
virtual: assistantStore.getFlag,
|
||||
online,
|
||||
}
|
||||
validator.value = await assistantApi.validate(param)
|
||||
assistantStore.setToken(validator.value.token)
|
||||
assistantStore.setAssistant(true)
|
||||
loading.value = false
|
||||
|
||||
window.addEventListener('message', communicationCb)
|
||||
const readyData = {
|
||||
eventName: 'sqlbot_assistant_event',
|
||||
busi: 'ready',
|
||||
ready: true,
|
||||
messageId: assistantId,
|
||||
}
|
||||
window.parent.postMessage(readyData, '*')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', communicationCb)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sqlbot-assistant-container {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sqlbot-chat-container {
|
||||
padding-top: 56px;
|
||||
:deep(.ed-aside) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -291,10 +291,10 @@ defineExpose({
|
||||
</div>
|
||||
<div v-if="advancedSetting.length > 5 && configExpand" class="params-table_pagination">
|
||||
<el-pagination
|
||||
@current-change="handelCurrentChange"
|
||||
:default-page-size="5"
|
||||
layout="prev, pager, next"
|
||||
:total="advancedSetting.length"
|
||||
@current-change="handelCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||