From 63a1d1fe7d1b19461492393d2a5642ea6eec7360 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Wed, 23 Jul 2025 15:59:09 +0800 Subject: [PATCH 1/2] feat: Log file --- .env.example | 6 + backend/alembic/env.py | 2 - backend/apps/chat/task/llm.py | 37 +++--- backend/apps/datasource/crud/datasource.py | 6 +- backend/apps/system/crud/user.py | 4 +- backend/apps/system/middleware/auth.py | 6 +- backend/common/core/config.py | 5 + backend/common/core/response_middleware.py | 8 +- backend/common/core/sqlbot_cache.py | 19 ++- backend/common/utils/utils.py | 146 ++++++++++++++++++++- backend/common/utils/whitelist.py | 3 +- backend/main.py | 7 +- 12 files changed, 200 insertions(+), 49 deletions(-) diff --git a/.env.example b/.env.example index e60c0f21..10f5bd88 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 1c821a4f..db6e9413 100755 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -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 diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index b4786662..c1d718ce 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -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() diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index cde525f5..c48c954f 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -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 diff --git a/backend/apps/system/crud/user.py b/backend/apps/system/crud/user.py index c0549f63..ffad38a8 100644 --- a/backend/apps/system/crud/user.py +++ b/backend/apps/system/crud/user.py @@ -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") diff --git a/backend/apps/system/middleware/auth.py b/backend/apps/system/middleware/auth.py index e298d3a7..365dfff1 100644 --- a/backend/apps/system/middleware/auth.py +++ b/backend/apps/system/middleware/auth.py @@ -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 \ No newline at end of file diff --git a/backend/common/core/config.py b/backend/common/core/config.py index 99c7fbc7..f9026b09 100644 --- a/backend/common/core/config.py +++ b/backend/common/core/config.py @@ -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 diff --git a/backend/common/core/response_middleware.py b/backend/common/core/response_middleware.py index 880117d3..59590ef8 100644 --- a/backend/common/core/response_middleware.py +++ b/backend/common/core/response_middleware.py @@ -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={ diff --git a/backend/common/core/sqlbot_cache.py b/backend/common/core/sqlbot_cache.py index 196a7275..ea83cdec 100644 --- a/backend/common/core/sqlbot_cache.py +++ b/backend/common/core/sqlbot_cache.py @@ -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) diff --git a/backend/common/utils/utils.py b/backend/common/utils/utils.py index f690b27f..bbc4c2a0 100644 --- a/backend/common/utils/utils.py +++ b/backend/common/utils/utils.py @@ -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) + + + diff --git a/backend/common/utils/whitelist.py b/backend/common/utils/whitelist.py index 10fa0c02..225d94e1 100644 --- a/backend/common/utils/whitelist.py +++ b/backend/common/utils/whitelist.py @@ -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 diff --git a/backend/main.py b/backend/main.py index 19ef6f33..54410660 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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 "" From 4ee583030a8cc34482fdfd29e22407b691c9c7ac Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Wed, 23 Jul 2025 16:19:15 +0800 Subject: [PATCH 2/2] feat(assistant): assistant --- frontend/src/assets/embedded/LOGO.png | Bin 0 -> 2602 bytes frontend/src/assets/embedded/answer.png | Bin 0 -> 1251 bytes frontend/src/assets/embedded/disable.png | Bin 0 -> 1654 bytes .../embedded/icon_expand-down_filled.svg | 3 + .../embedded/icon_new-chat_outlined.svg | 4 + .../embedded/icon_new-chat_outlined_1.svg | 4 + .../assets/embedded/icon_rename_outlined.svg | 4 + .../assets/embedded/icon_sidebar_outlined.svg | 6 + frontend/src/assets/embedded/loading.png | Bin 0 -> 1588 bytes frontend/src/assets/embedded/send.png | Bin 0 -> 1706 bytes frontend/src/i18n/en.json | 12 +- frontend/src/i18n/zh-CN.json | 10 +- frontend/src/views/chat/ChatList.vue | 156 +++++-- frontend/src/views/embedded/assistant.vue | 438 ++++++++---------- frontend/src/views/embedded/index.vue | 264 +++++++++++ frontend/src/views/system/model/ModelForm.vue | 2 +- 16 files changed, 621 insertions(+), 282 deletions(-) create mode 100644 frontend/src/assets/embedded/LOGO.png create mode 100644 frontend/src/assets/embedded/answer.png create mode 100644 frontend/src/assets/embedded/disable.png create mode 100644 frontend/src/assets/embedded/icon_expand-down_filled.svg create mode 100644 frontend/src/assets/embedded/icon_new-chat_outlined.svg create mode 100644 frontend/src/assets/embedded/icon_new-chat_outlined_1.svg create mode 100644 frontend/src/assets/embedded/icon_rename_outlined.svg create mode 100644 frontend/src/assets/embedded/icon_sidebar_outlined.svg create mode 100644 frontend/src/assets/embedded/loading.png create mode 100644 frontend/src/assets/embedded/send.png create mode 100644 frontend/src/views/embedded/index.vue diff --git a/frontend/src/assets/embedded/LOGO.png b/frontend/src/assets/embedded/LOGO.png new file mode 100644 index 0000000000000000000000000000000000000000..6ca459b611fe5aa7f06f5799e13c73c444533d68 GIT binary patch literal 2602 zcmV+_3f1+AP);^TyXAT<$o@R8REl90N1ZnmzC|9GN1!;im z$xQeF5pVwW+uvCm2FFlA7W99BEuN)h%j|D8#!h;JR@6ZbZj%iRSla~I-DOmuIvL)Om&4ftD5%m$No(ifF z#r>-DQ9-w(3R>wBVF3o7CM^&KY$u(a7kuwB1K$~Oaidr5H2p$GtHwq zJyOHB_HG5D!1wx0*qW+GFN7}T%Jt?8^$<=`Ub;iTv9L)XOXw0=io__+vQJHaRGkY4 z^>d_#g)bUhxa7nf|5Gp6Xuj}j4bDLR{Ibff%9Y#iw(0eY2rS_Q$KNC1GCtP_^)pbz ztE64e!#!?dW}Gk>l0pGn%`d-Ng>#Uv-+r${04r{Y;)#}fY^pH5Tb;{wts^omQIYwt z$$(TFZHx_p`8VO5q`;q4iA#DyBq=J!p^wH`j2?^SfzJ-Qz|mi_`D-SL;D9TyU;c3j zr!2qR*$GVhB0#TME-S4`CjRQ?-J3L?Rzh6bv}KH^9RKQ|K65oJ&6zY>ij*1C##x*n zD=mkwU;arOPFW;a&iS6%9z{_~Yxk?)Y|H526c4N)lTs?dW~Z3fZ?1;_$ES!1G09!s zl@P!-j3B?bf4{;!Z_yn4p11Prayf)C2mxCs@@i;5CQU;Um!}@V0^QQiKBE9H?`#$i zqDMbp76+0(eFw zc(YT|gq5y@m^?dAQh-sQniX>Ro~b&xOF+31M^xZh5$>04Wd)oS6eyBa)@m>rNt-m& z7v;?=(J+aJDaa|d_7~w95yCcaiw7dTMkcMtT)+kc2A_Ri?x@NbrhM0wafHrdzE5jZ zIA;l3_W~N@LVrOZ1}{KG98&xh^9+Tko_BHTfD#Fe)ekCIM-jen%s4V>CEC=I2ticf zoP{&qNEP(JDF~=K7sUAj2Js_3&u6=ZwQ{{jL{#>?sAf8&1^upDBuna8`UOm6-p<%A zZEbTX!;5L75Fuq-_8JqyUZdFEn{u9$_YS&7ViTUvwf;d>7_7mZdu7&?Jdb z3ftA4GMNf*7YbfmW?H#aR_nBic;VA~rlgWC(8+;$b6YP~yKoM2sr8}E^LO;>$fT4y zldv(EVsr1cna}1upBt9ibM@X~#5QCsPQD~CTPKd-eB{y}KUhQ*9oLjCSPfK%D1d?a z{=fo&@Yt5Ea@az9tZA-&{j4Nt?G!JyK3FC5ZBwKu+696oEJafK9Bm%Qfra@ai+JDd zyd9TP(q&TRH{f;S08bT7D8>~{`ko6LA5~xO!WckCuwSB-b)I5rP3OxAF{Mm6wTJe{ z*JnnrX-SbKQmQRWe<_+uVX8c83gwV|9T4&AigxmAzDi(po-2LdPulSSxiHMC69%0% zVL*KZCO=L569kU5{6LamSluI&G8ws{nup+2>|bZ47Nqiiu5pyz;rzRw`0s3tH0 zW&s$`E;#7dq|Tg-C1?u(%feuvH`-xmkDI@&@pnL7wvWRfSF2#hCh1r6=nGEMMY2V)( zAhW_@_LyUaebF_42drET(Pz?8z)CsMe>`S3?*z1JYSF+eDc_4VAX1vmG2uMoN=h@a zzb3w#n6B=F?&(eeuOG$6V+Cxa^w!-Pl}Q%40-gq)@?pr3R<5bXCh9OTy3CBK*`>zH z{*Bo#oKVK{KdsAe-7N|3EmIu5N}3DI$1UrB2=TX{H0uh-T;14eW2_#cEVgd|@~x)< zYGmwxJ^aEOoeK3z2|l~ZDPf|8Mk!%w#|jWqX6~4BNQb+~#ruL*VdDVl%9;GRRIYFc z4wZhfNM=kG2w&#oN+d|eyf7-r;{3&4uRdDUav~F%$V4VGkx#JvA2V=eGwKWF7ytkO M07*qoM6N<$f~IuYMF0Q* literal 0 HcmV?d00001 diff --git a/frontend/src/assets/embedded/answer.png b/frontend/src/assets/embedded/answer.png new file mode 100644 index 0000000000000000000000000000000000000000..7f27a90889e229033a90a44e264d8406c13fc351 GIT binary patch literal 1251 zcmV<91RVQ`P)kR|q%PN_UZX{V$^yG8-BOYA>j=^v=1y+!P`UD7G79l%1S zZdR8T?NCs)oV7xS0EtAt+@m5zvZ)`Gj*3L)1K6bLGp+aTd-v|gJ3_={o>Vd&6Ip}l z1{=tLkzpWB>Q}?%)H6ptYlGmxwG^5P{nd3^j+cgRVE;1vH2!J>{> z$N`t1)(XsAT9)DG=_}$iXYXT9=5m0l^-Q-zkJN1zD}Xq{O!4kKYA_HIsdcwYCs+~V z1r~uB{tH)R&$StakebK(882kF4*kzQk%`X2^uE^HUYOo$1yTqNWU4lUEqQP zwhk+KUT_AtqGW>xN+V6cm%h|~JRKlNQ@E;{Lc9N9L;-46cR$9)3+e8w(UI&x0dH#m zL{@7MZ$?xwP=MP(HUY*9BPy`oKBqls!v6Rs9$wzTht>zs!T;Tt`0@E4DF5{gS|BQ@ z?F!wa>H}FUY&Zq4u?#&_r5nmK)yz#B?8;_~xVQh>C|-cAyQ$ zJ-PJzfGjkjv*x3GH-YZT)sWsl=AkbTv03&k4A;dr5{m0@*d*8iN(I{n8FT|lMQzg= zYSx!QQX#rxBxLj#h?)na4N71H=p&IJ^u|csc<%rt9O(odBox_HHT<-cgrX|HZ4f0_ z0bp*bfe<&5TpH2`G>}lpR1QeFs3W169pwP$OWR3eB#;Oow)d9Az&}G1vVa*Cn%`ZQ zZ@qyK;W0Gvee(oS@ze7^p$kd8l62UddgHp*W$_=H4Se^{Z_t6r z|1?*;D))nZ0Tm8wYJRL`Z=m$|GyiLK;XxABU@l-?LNy_y!nQdS`34SX2hGwWS|2?CUI&Co_kUHea0?{{p;t|#ULs$m&fG@3R821H-4?`6Y1MNUnUGb zILq1jNoC$AOJfG)MQV_AfHgsCeiq|}nU}@S?kFkoI&_R&nBH0G@RgCmwj^UoJ`ztkJ}+B!>~fP0G-S_Wo{lXHuK~S)+k6jVWgq|m N002ovPDHLkV1oM5Lec;L literal 0 HcmV?d00001 diff --git a/frontend/src/assets/embedded/disable.png b/frontend/src/assets/embedded/disable.png new file mode 100644 index 0000000000000000000000000000000000000000..355a68a9879b8ab933f73b51eab6c7567cb2cebf GIT binary patch literal 1654 zcmV-+28sEJP)9Ud}Ulc(=B1| zhb@D$S>xK-X7@Bq0k-xs{5*#qpah)$R3G-$;%fl*^X%%<8sH?rau%5*e&Yn+6eyu& z^pJRxD-Yc2s58K2<7#3LGbxkz8z|MJfE)Vseoq>JNGh!Dmof9QccLZ&ELh_`ES}_E zAX^g!l?dom?`N}FY}x!OcJJDWm6a9T|KlMdQWFJP@9Y|^yy_6~6uoEH4(!;z%@NP{ zWTEgJG2lhPr1M?gSq(M^J*m;({|a_)f5Q>IHTQbm_1kyuiuWI2-`?HM`zVo+ff`5I z08R?Hb9l_xCrv@8?^;;o+Z;9P#fQJb(xC`WHDD4O{0NqZ0uNnlygN z``^oFirynR?S*zTJn?J;D4r}X{)MQB+7ohc4IttZd)T2!dYA&L^ErrOu_#`?gGVBk z|NGvdw=374t~^6jG^pcpIHk(dTXzv7LHQg6#a}mXN~`}&K4%-=KYXZiZE5*${2aZOn|-L7`i7uQ@+>vv|7gA5|rupY!gagEiM_fVkKsJzrMF{E@?-o*u07 zQoF3KK8Jbp*NXTLh7Q-I%+m5x#6=0#kbJr}2&CBox={Re^C#!X6!H_#=!i1{#Q6i7 zyIvQK$I?)-rEZrn;z^w1iHLto<3c53!0WDB#ZP>rF~Q@-Cx{bj*Ww)e(+I7M(3tCC z#giv7M*y{l>y%sI_MHXiq2<89n+?Sl!^_ ztE;O{EB=p&p%FZq8OtMX?|UR-01q}=K@Ar}f8X9c2qNzM_h2JfK#GJBA9wfXLG7Ab zmXaP@LW~-h5(DbY+|Qr#a>N;#?T z7(lv%i%5xPgQcYe!{4^886F)2aCS7!G~h!NhsoL-1R5oY?vLItL7Vg5R}A2;mD%T!BJSkKP3JM0 z^8`u2NFS>zl&F~wPCE(+jj&MoJu&ZF$WB+Et1&=%#WZx0VfUe?#@3a|_|^!`8uMHy z%%g{(_eb^N{p-*Bu87Er39BqqO@(-|mrKa#eph@AP!5wGN`?B+_h`5#Hlh!oQz`cf zabUro8fj1?NCInxFMs{bv{KqQx)AO&8r46VXm(G}7E4Q`XO6~EEKOK-?L9nn1Fq+T z#gowM;-eokla2g%iMcVvLr$_Z;E$wJ&Yts~WgJw`N zI;E^mnB&-Gtc^`mk+#jTu2?L8COt#Y>#Aut7epij5JrPML`~RG;~QrPC*04kVDq`p z^dphdF|q+X+_bRrNk)XbW7M9BLwMC^#t#cGB_bx|!v_&@{7~K=E_U?h=|(Uz z`mi>{Vk`x1&<@x>uVkr?OIFgznq0FOOL+VI|4+-tN-N<><^TWy07*qoM6N<$f>|68 AEC2ui literal 0 HcmV?d00001 diff --git a/frontend/src/assets/embedded/icon_expand-down_filled.svg b/frontend/src/assets/embedded/icon_expand-down_filled.svg new file mode 100644 index 00000000..959fa80c --- /dev/null +++ b/frontend/src/assets/embedded/icon_expand-down_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/embedded/icon_new-chat_outlined.svg b/frontend/src/assets/embedded/icon_new-chat_outlined.svg new file mode 100644 index 00000000..fcc09f84 --- /dev/null +++ b/frontend/src/assets/embedded/icon_new-chat_outlined.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/embedded/icon_new-chat_outlined_1.svg b/frontend/src/assets/embedded/icon_new-chat_outlined_1.svg new file mode 100644 index 00000000..b060b027 --- /dev/null +++ b/frontend/src/assets/embedded/icon_new-chat_outlined_1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/embedded/icon_rename_outlined.svg b/frontend/src/assets/embedded/icon_rename_outlined.svg new file mode 100644 index 00000000..fd43aabf --- /dev/null +++ b/frontend/src/assets/embedded/icon_rename_outlined.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/embedded/icon_sidebar_outlined.svg b/frontend/src/assets/embedded/icon_sidebar_outlined.svg new file mode 100644 index 00000000..1257b82c --- /dev/null +++ b/frontend/src/assets/embedded/icon_sidebar_outlined.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/embedded/loading.png b/frontend/src/assets/embedded/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..08576ddaeae622bdcbb63a0e523ac69cc1ec429b GIT binary patch literal 1588 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 -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) {
-
{{ group.key }}
+
+ + + + {{ group.key }} +
@@ -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; + } + } +} + + + diff --git a/frontend/src/views/embedded/assistant.vue b/frontend/src/views/embedded/assistant.vue index ba6bd644..d6dbe568 100644 --- a/frontend/src/views/embedded/assistant.vue +++ b/frontend/src/views/embedded/assistant.vue @@ -1,264 +1,198 @@ - - + + diff --git a/frontend/src/views/embedded/index.vue b/frontend/src/views/embedded/index.vue new file mode 100644 index 00000000..e9951c67 --- /dev/null +++ b/frontend/src/views/embedded/index.vue @@ -0,0 +1,264 @@ + + + + diff --git a/frontend/src/views/system/model/ModelForm.vue b/frontend/src/views/system/model/ModelForm.vue index bb7b44cc..57f294ca 100644 --- a/frontend/src/views/system/model/ModelForm.vue +++ b/frontend/src/views/system/model/ModelForm.vue @@ -291,10 +291,10 @@ defineExpose({