From 4a0104c648a85bf8a64e234d0cb84951da072655 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Thu, 17 Jul 2025 09:35:29 +0800 Subject: [PATCH 01/14] feat(datasource): sort --- frontend/src/i18n/en.json | 1 + frontend/src/i18n/zh-CN.json | 1 + frontend/src/views/ds/Datasource.vue | 4 ++-- frontend/src/views/ds/DatasourceForm.vue | 21 +++++++++++++-------- frontend/src/views/ds/js/ds-type.ts | 18 ++++++------------ 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index a267e7d4..89511afe 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -84,6 +84,7 @@ "type": "Data Source Type", "status": "Status", "actions": "Actions", + "local_excelcsv": "Local Excel/CSV", "pieces_in_total": "Showing {ms} records", "test_connection": "Test Connection", "check": "check", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 6feea927..e752f045 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -86,6 +86,7 @@ }, "ds": { "title": "数据源", + "local_excelcsv": "本地 Excel/CSV", "add": "添加数据源", "delete": "删除数据源", "name": "数据源名称", diff --git a/frontend/src/views/ds/Datasource.vue b/frontend/src/views/ds/Datasource.vue index bc71b28b..82ae6a61 100644 --- a/frontend/src/views/ds/Datasource.vue +++ b/frontend/src/views/ds/Datasource.vue @@ -16,7 +16,7 @@ import { datasourceApi } from '@/api/datasource' import { useEmitt } from '@/utils/useEmitt' import Card from './Card.vue' import DelMessageBox from './DelMessageBox.vue' -import { dsTypeWithImgSort } from './js/ds-type' +import { dsTypeWithImg } from './js/ds-type' import { useI18n } from 'vue-i18n' interface Datasource { @@ -41,7 +41,7 @@ const activeType = ref('') const datasourceFormRef = ref() const datasourceList = shallowRef([] as Datasource[]) -const defaultDatasourceList = shallowRef(dsTypeWithImgSort as (Datasource & { img: string })[]) +const defaultDatasourceList = shallowRef(dsTypeWithImg as (Datasource & { img: string })[]) const currentDefaultDatasource = ref('') const datasourceListWithSearch = computed(() => { diff --git a/frontend/src/views/ds/DatasourceForm.vue b/frontend/src/views/ds/DatasourceForm.vue index 45361224..595baa13 100644 --- a/frontend/src/views/ds/DatasourceForm.vue +++ b/frontend/src/views/ds/DatasourceForm.vue @@ -460,7 +460,19 @@ defineExpose({ class="model-form" :class="(!isCreate || activeStep === 2) && 'edit-form'" > -
{{ activeName }}
+
+ {{ activeName }} + + {{ t('ds.form.support_version') }}:  + 2012+ + 12+ + 5.6+ + 9.6+ + +
- - {{ t('ds.form.support_version') }}:  - 2012+ - 12+ - 5.6+ - 9.6+ -
diff --git a/frontend/src/views/ds/js/ds-type.ts b/frontend/src/views/ds/js/ds-type.ts index fb5052ea..ab10879b 100644 --- a/frontend/src/views/ds/js/ds-type.ts +++ b/frontend/src/views/ds/js/ds-type.ts @@ -3,28 +3,22 @@ import excel from '@/assets/datasource/icon_excel.png' import oracle from '@/assets/datasource/icon_oracle.png' import pg from '@/assets/datasource/icon_PostgreSQL.png' import sqlServer from '@/assets/datasource/icon_SQL_Server.png' +import { i18n } from '@/i18n' +const t = i18n.global.t export const dsType = [ + { label: t('ds.local_excelcsv'), value: 'excel' }, { label: 'MySQL', value: 'mysql' }, - { label: 'PostgreSQL', value: 'pg' }, - { label: 'Microsoft SQL Server', value: 'sqlServer' }, { label: 'Oracle', value: 'oracle' }, - { label: 'Excel/CSV', value: 'excel' }, + { label: 'PostgreSQL', value: 'pg' }, + { label: 'SQL Server', value: 'sqlServer' }, ] export const dsTypeWithImg = [ - { name: '本地 Excel/CSV', type: 'excel', img: excel }, + { name: t('ds.local_excelcsv'), type: 'excel', img: excel }, { name: 'MySQL', type: 'mysql', img: mysql_ds }, - { name: 'SQL Server', type: 'sqlServer', img: sqlServer }, { name: 'Oracle', type: 'oracle', img: oracle }, { name: 'PostgreSQL', type: 'pg', img: pg }, -] - -export const dsTypeWithImgSort = [ - { name: '本地 Excel/CSV', type: 'excel', img: excel }, - { name: 'PostgreSQL', type: 'pg', img: pg }, - { name: 'Oracle', type: 'oracle', img: oracle }, - { name: 'MySQL', type: 'mysql', img: mysql_ds }, { name: 'SQL Server', type: 'sqlServer', img: sqlServer }, ] From e899ab3b1d4931e6799185088727dae4a4603307 Mon Sep 17 00:00:00 2001 From: junjun Date: Thu, 17 Jul 2025 15:49:22 +0800 Subject: [PATCH 02/14] feat: ds add oid --- backend/alembic/versions/028_ds_oid.py | 28 +++++++++ backend/apps/datasource/api/datasource.py | 8 +-- backend/apps/datasource/crud/datasource.py | 22 +++---- backend/apps/datasource/models/datasource.py | 2 + backend/apps/mcp/mcp.py | 63 ++++++++++++++------ 5 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 backend/alembic/versions/028_ds_oid.py diff --git a/backend/alembic/versions/028_ds_oid.py b/backend/alembic/versions/028_ds_oid.py new file mode 100644 index 00000000..83afde51 --- /dev/null +++ b/backend/alembic/versions/028_ds_oid.py @@ -0,0 +1,28 @@ +"""028_ds_oid + +Revision ID: e96b16d3daab +Revises: b049c9f8ca5b +Create Date: 2025-07-17 14:40:48.522033 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'e96b16d3daab' +down_revision = 'b049c9f8ca5b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('core_datasource', sa.Column('oid', sa.BigInteger(), nullable=True)) + op.execute('update core_datasource set oid = 1') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('core_datasource', 'oid') + # ### end Alembic commands ### diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py index 2c3d36e8..43a01862 100644 --- a/backend/apps/datasource/api/datasource.py +++ b/backend/apps/datasource/api/datasource.py @@ -19,8 +19,8 @@ path = "/opt/sqlbot/data/excel" @router.get("/list") -async def datasource_list(session: SessionDep): - return get_datasource_list(session=session) +async def datasource_list(session: SessionDep, user: CurrentUser): + return get_datasource_list(session=session, user=user) @router.post("/get/{id}") @@ -44,8 +44,8 @@ async def choose_tables(session: SessionDep, id: int, tables: List[CoreTable]): @router.post("/update", response_model=CoreDatasource) -async def update(session: SessionDep, ds: CoreDatasource): - return update_ds(session, ds) +async def update(session: SessionDep,user: CurrentUser, ds: CoreDatasource): + return update_ds(session,user, ds) @router.post("/delete/{id}", response_model=CoreDatasource) diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 5647f7c8..3f6a32d3 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -22,10 +22,10 @@ from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, Cor DatasourceConf, TableAndFields -def get_datasource_list(session: SessionDep): - statement = select(CoreDatasource).order_by(CoreDatasource.create_time.desc()) - datasource_list = session.exec(statement).fetchall() - return datasource_list +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( + CoreDatasource.create_time.desc()).all() def get_ds(session: SessionDep, id: int): @@ -45,14 +45,15 @@ def check_status(session: SessionDep, ds: CoreDatasource): return False -def check_name(session: SessionDep, ds: CoreDatasource): +def check_name(session: SessionDep, user: CurrentUser, ds: CoreDatasource): if ds.id is not None: ds_list = session.query(CoreDatasource).filter( - and_(CoreDatasource.name == ds.name, CoreDatasource.id != ds.id)).all() + and_(CoreDatasource.name == ds.name, CoreDatasource.id != ds.id, CoreDatasource.oid == user.oid)).all() if ds_list is not None and len(ds_list) > 0: raise 'Name exist' else: - ds_list = session.query(CoreDatasource).filter(CoreDatasource.name == ds.name).all() + ds_list = session.query(CoreDatasource).filter( + and_(CoreDatasource.name == ds.name, CoreDatasource.oid == user.oid)).all() if ds_list is not None and len(ds_list) > 0: raise 'Name exist' @@ -60,10 +61,11 @@ def check_name(session: SessionDep, ds: CoreDatasource): def create_ds(session: SessionDep, user: CurrentUser, create_ds: CreateDatasource): ds = CoreDatasource() deepcopy_ignore_extra(create_ds, ds) - check_name(session, ds) + check_name(session, user, ds) ds.create_time = datetime.datetime.now() # status = check_status(session, ds) ds.create_by = user.id + ds.oid = user.oid if user.oid is not None else 1 ds.status = "Success" ds.type_name = db_type_relation()[ds.type] record = CoreDatasource(**ds.model_dump()) @@ -85,9 +87,9 @@ def chooseTables(session: SessionDep, id: int, tables: List[CoreTable]): updateNum(session, ds) -def update_ds(session: SessionDep, ds: CoreDatasource): +def update_ds(session: SessionDep, user: CurrentUser, ds: CoreDatasource): ds.id = int(ds.id) - check_name(session, ds) + check_name(session, user, ds) status = check_status(session, ds) ds.status = "Success" if status is True else "Fail" record = session.exec(select(CoreDatasource).where(CoreDatasource.id == ds.id)).first() diff --git a/backend/apps/datasource/models/datasource.py b/backend/apps/datasource/models/datasource.py index ab73eaa3..4377344a 100644 --- a/backend/apps/datasource/models/datasource.py +++ b/backend/apps/datasource/models/datasource.py @@ -18,6 +18,7 @@ class CoreDatasource(SQLModel, table=True): create_by: int = Field(sa_column=Column(BigInteger())) status: str = Field(max_length=64, nullable=True) num: str = Field(max_length=256, nullable=True) + oid: int = Field(sa_column=Column(BigInteger())) class CoreTable(SQLModel, table=True): @@ -53,6 +54,7 @@ class CreateDatasource(BaseModel): create_by: int = 0 status: str = '' num: str = '' + oid: int = 1 tables: List[CoreTable] = [] diff --git a/backend/apps/mcp/mcp.py b/backend/apps/mcp/mcp.py index 3530f493..42f945e5 100644 --- a/backend/apps/mcp/mcp.py +++ b/backend/apps/mcp/mcp.py @@ -3,20 +3,29 @@ from datetime import timedelta -from fastapi import APIRouter, HTTPException +import jwt +from fastapi import HTTPException, status, APIRouter from fastapi.responses import StreamingResponse +# from fastapi.security import OAuth2PasswordBearer +from jwt.exceptions import InvalidTokenError +from pydantic import ValidationError from apps.chat.api.chat import create_chat from apps.chat.models.chat_model import ChatMcp, CreateChat, ChatStart from apps.chat.task.llm import LLMService, run_task -from apps.datasource.crud.datasource import get_datasource_list -from apps.system.crud.user import authenticate -from apps.system.models.system_model import AiModelDetail +from apps.system.crud.user import authenticate, get_user_info +from apps.system.schemas.system_schema import BaseUserDTO +from apps.system.schemas.system_schema import UserInfoDTO +from common.core import security from common.core.config import settings -from common.core.deps import SessionDep, get_current_user -from common.core.schemas import Token +from common.core.deps import SessionDep +from common.core.schemas import TokenPayload, XOAuth2PasswordBearer, Token from common.core.security import create_access_token +reusable_oauth2 = XOAuth2PasswordBearer( + tokenUrl=f"{settings.API_V1_STR}/login/access-token" +) + router = APIRouter(tags=["mcp"], prefix="/mcp") @@ -35,21 +44,24 @@ router = APIRouter(tags=["mcp"], prefix="/mcp") # )) -@router.get("/ds_list", operation_id="get_datasource_list") -async def datasource_list(session: SessionDep): - return get_datasource_list(session=session) - - -@router.get("/model_list", operation_id="get_model_list") -async def get_model_list(session: SessionDep): - return session.query(AiModelDetail).all() +# @router.get("/ds_list", operation_id="get_datasource_list") +# async def datasource_list(session: SessionDep): +# return get_datasource_list(session=session) +# +# +# @router.get("/model_list", operation_id="get_model_list") +# async def get_model_list(session: SessionDep): +# return session.query(AiModelDetail).all() @router.post("/mcp_start", operation_id="mcp_start") async def mcp_start(session: SessionDep, chat: ChatStart): - user = authenticate(session=session, account=chat.username, password=chat.password) + user: BaseUserDTO = authenticate(session=session, account=chat.username, password=chat.password) if not user: raise HTTPException(status_code=400, detail="Incorrect account or password") + + if not user.oid or user.oid == 0: + raise HTTPException(status_code=400, detail="No associated workspace, Please contact the administrator") access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) user_dict = user.to_dict() t = Token(access_token=create_access_token( @@ -61,9 +73,26 @@ async def mcp_start(session: SessionDep, chat: ChatStart): @router.post("/mcp_question", operation_id="mcp_question") async def mcp_question(session: SessionDep, chat: ChatMcp): - user = await get_current_user(session, chat.token) + try: + payload = jwt.decode( + chat.token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] + ) + token_data = TokenPayload(**payload) + except (InvalidTokenError, ValidationError): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Could not validate credentials", + ) + session_user = await get_user_info(session=session, user_id=token_data.id) + session_user = UserInfoDTO.model_validate(session_user) + if not session_user: + raise HTTPException(status_code=404, detail="User not found") - llm_service = LLMService(session, user, chat) + if session_user.status != 1: + raise HTTPException(status_code=400, detail="Inactive user") + + # ask + llm_service = LLMService(session, session_user, chat) llm_service.init_record() return StreamingResponse(run_task(llm_service, False), media_type="text/event-stream") From a4e6cad4993acafd289b5978ab68da81ded06746 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Thu, 17 Jul 2025 15:16:40 +0800 Subject: [PATCH 03/14] perf: Reload after switch workspace --- frontend/src/components/layout/Workspace.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/layout/Workspace.vue b/frontend/src/components/layout/Workspace.vue index 276f8ecf..9a39955d 100644 --- a/frontend/src/components/layout/Workspace.vue +++ b/frontend/src/components/layout/Workspace.vue @@ -43,6 +43,7 @@ const handleDefaultWorkspaceChange = (item: any) => { currentWorkspace.value = { id: item.id, name: item.name } userApi.ws_change(item.id).then(() => { ElMessage.success(t('common.switch_success')) + location.reload() }) emit('selectWorkspace', item) } From d1b5b05810f7e71f3930c97fc00e784c610bab66 Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 17 Jul 2025 17:07:52 +0800 Subject: [PATCH 04/14] feat: chat --- backend/alembic/versions/029_modify_chat.py | 54 +++++ backend/apps/chat/models/chat_model.py | 8 +- backend/template.yaml | 4 +- frontend/package.json | 1 + .../src/assets/svg/icon_replace_outlined.svg | 4 + .../src/assets/svg/icon_screen_outlined.svg | 5 + .../src/assets/svg/icon_start_outlined.svg | 3 + frontend/src/i18n/zh-CN.json | 3 +- frontend/src/utils/markdown.ts | 20 +- frontend/src/views/chat/RecommendQuestion.vue | 1 - .../src/views/chat/answer/AnalysisAnswer.vue | 1 + frontend/src/views/chat/answer/BaseAnswer.vue | 5 + .../src/views/chat/answer/PredictAnswer.vue | 1 + .../src/views/chat/component/MdComponent.vue | 22 +- frontend/src/views/chat/index.vue | 190 +++++++++++++++--- 15 files changed, 277 insertions(+), 45 deletions(-) create mode 100644 backend/alembic/versions/029_modify_chat.py create mode 100644 frontend/src/assets/svg/icon_replace_outlined.svg create mode 100644 frontend/src/assets/svg/icon_screen_outlined.svg create mode 100644 frontend/src/assets/svg/icon_start_outlined.svg diff --git a/backend/alembic/versions/029_modify_chat.py b/backend/alembic/versions/029_modify_chat.py new file mode 100644 index 00000000..62b8ee9c --- /dev/null +++ b/backend/alembic/versions/029_modify_chat.py @@ -0,0 +1,54 @@ +"""029_modify_chat + +Revision ID: 77d4c39ec22f +Revises: e96b16d3daab +Create Date: 2025-07-17 17:05:13.392973 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '77d4c39ec22f' +down_revision = 'e96b16d3daab' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('chat', sa.Column('oid', sa.BigInteger(), nullable=True)) + op.execute('update chat set oid = 1') + op.alter_column('chat', 'create_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True) + op.alter_column('chat_record', 'create_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True) + op.alter_column('chat_record', 'finish_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('chat_record', 'finish_time', + existing_type=sa.DateTime(), + type_=postgresql.TIMESTAMP(timezone=True), + existing_nullable=True) + op.alter_column('chat_record', 'create_time', + existing_type=sa.DateTime(), + type_=postgresql.TIMESTAMP(timezone=True), + existing_nullable=True) + op.alter_column('chat', 'create_time', + existing_type=sa.DateTime(), + type_=postgresql.TIMESTAMP(timezone=True), + existing_nullable=True) + op.drop_column('chat', 'oid') + # ### end Alembic commands ### diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index 3f891cbf..3d43fbcd 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -17,7 +17,8 @@ from apps.template.filter.generator import get_permissions_template class Chat(SQLModel, table=True): __tablename__ = "chat" id: Optional[int] = Field(sa_column=Column(BigInteger, Identity(always=True), primary_key=True)) - create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True)) + oid: Optional[int] = Field(sa_column=Column(BigInteger, nullable=True, default=1)) + create_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True)) create_by: int = Field(sa_column=Column(BigInteger, nullable=True)) brief: str = Field(max_length=64, nullable=True) chat_type: str = Field(max_length=20, default="chat") # chat, datasource @@ -31,8 +32,8 @@ class ChatRecord(SQLModel, table=True): chat_id: int = Field(sa_column=Column(BigInteger, nullable=False)) ai_modal_id: Optional[int] = Field(sa_column=Column(BigInteger)) first_chat: bool = Field(sa_column=Column(Boolean, nullable=True, default=False)) - create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True)) - finish_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True)) + create_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True)) + finish_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True)) create_by: int = Field(sa_column=Column(BigInteger, nullable=True)) datasource: int = Field(sa_column=Column(BigInteger, nullable=True)) engine_type: str = Field(max_length=64) @@ -66,6 +67,7 @@ class ChatRecord(SQLModel, table=True): analysis_record_id: int = Field(sa_column=Column(BigInteger, nullable=True)) predict_record_id: int = Field(sa_column=Column(BigInteger, nullable=True)) + class CreateChat(BaseModel): id: int = None question: str = None diff --git a/backend/template.yaml b/backend/template.yaml index a801d88c..355a90e8 100644 --- a/backend/template.yaml +++ b/backend/template.yaml @@ -132,8 +132,8 @@ template: 你是一个数据分析师,你的任务是根据给定的数据进行数据预测,我将以json格式给你一组数据,你帮我预测之后的数据(一段可以展示趋势的数据,至少2个周期),用json格式返回。 ```json - 无法预测或者不支持预测的数据请直接返回:"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)" - 不需要返回原有数据,直接返回预测的部份 + 无法预测或者不支持预测的数据请直接返回(不需要返回JSON格式):"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)" + 如果可以预测,则不需要返回原有数据,直接返回预测的部份 user: | ### 请使用 i18n: {lang} 对应的语言输出你的结果 diff --git a/frontend/package.json b/frontend/package.json index 677c94c8..c06d2008 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "element-plus": "^2.10.1", "element-plus-secondary": "^1.0.0", "element-resize-detector": "^1.2.4", + "github-markdown-css": "^5.8.1", "highlight.js": "^11.11.1", "html2canvas": "^1.4.1", "lodash": "^4.17.21", diff --git a/frontend/src/assets/svg/icon_replace_outlined.svg b/frontend/src/assets/svg/icon_replace_outlined.svg new file mode 100644 index 00000000..b0de84d3 --- /dev/null +++ b/frontend/src/assets/svg/icon_replace_outlined.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/svg/icon_screen_outlined.svg b/frontend/src/assets/svg/icon_screen_outlined.svg new file mode 100644 index 00000000..ed5a00ba --- /dev/null +++ b/frontend/src/assets/svg/icon_screen_outlined.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/svg/icon_start_outlined.svg b/frontend/src/assets/svg/icon_start_outlined.svg new file mode 100644 index 00000000..9ac68907 --- /dev/null +++ b/frontend/src/assets/svg/icon_start_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 66bb2feb..45e2ed8f 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -84,7 +84,8 @@ "data_predict": "数据分析", "chat_search": "搜索", "thinking": "思考中", - "thinking_step": "思考过程" + "thinking_step": "思考过程", + "ask_again": "重新生成" }, "ds": { "title": "数据源", diff --git a/frontend/src/utils/markdown.ts b/frontend/src/utils/markdown.ts index 6d9fe595..817f431d 100644 --- a/frontend/src/utils/markdown.ts +++ b/frontend/src/utils/markdown.ts @@ -2,25 +2,23 @@ import MarkdownIt from 'markdown-it' import hljs from 'highlight.js' const md = new MarkdownIt({ - html: true, - linkify: true, + html: false, + linkify: false, + breaks: true, + typographer: true, + quotes: '“”‘’', highlight: (str: any, lang: any): string => { if (lang && hljs.getLanguage(lang)) { try { - return ` -
-          
-          ${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
-          
-          
- ` + return `
+                  ${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
+                
` } catch (e) { console.error(e) - return str } } - return '
' + md.utils.escapeHtml(str) + '
' + return '
' + md.utils.escapeHtml(str) + '
' }, }) diff --git a/frontend/src/views/chat/RecommendQuestion.vue b/frontend/src/views/chat/RecommendQuestion.vue index dc705406..21f043a7 100644 --- a/frontend/src/views/chat/RecommendQuestion.vue +++ b/frontend/src/views/chat/RecommendQuestion.vue @@ -53,7 +53,6 @@ function clickQuestion(question: string): void { diff --git a/frontend/src/views/chat/answer/PredictAnswer.vue b/frontend/src/views/chat/answer/PredictAnswer.vue index bf157092..89aba684 100644 --- a/frontend/src/views/chat/answer/PredictAnswer.vue +++ b/frontend/src/views/chat/answer/PredictAnswer.vue @@ -193,6 +193,7 @@ defineExpose({ sendMessage, index: index.value, chatList: _chatList }) diff --git a/frontend/src/views/chat/component/MdComponent.vue b/frontend/src/views/chat/component/MdComponent.vue index 9f066685..ea0bdf29 100644 --- a/frontend/src/views/chat/component/MdComponent.vue +++ b/frontend/src/views/chat/component/MdComponent.vue @@ -1,6 +1,7 @@ - + diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index 8f28cb59..2f00b03a 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -117,23 +117,67 @@ @error="onChartAnswerError" >
todo
-
- - {{ t('chat.data_analysis') }} - - - {{ t('chat.data_predict') }} - +
+ {{ message.record?.error }} +
+
+
+ + + + + + + + +
+
+ {{ datetimeFormat(message.record?.create_time) }} +
@@ -233,8 +303,12 @@ import { useI18n } from 'vue-i18n' import { endsWith, find, startsWith } from 'lodash-es' import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg' import icon_sidebar_outlined from '@/assets/svg/icon_sidebar_outlined.svg' +import icon_replace_outlined from '@/assets/svg/icon_replace_outlined.svg' +import icon_screen_outlined from '@/assets/svg/icon_screen_outlined.svg' +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 { datetimeFormat } from '@/utils/utils.ts' import { useAssistantStore } from '@/stores/assistant' const assistantStore = useAssistantStore() @@ -536,6 +610,13 @@ function onAnalysisAnswerError() { isTyping.value = false } +function askAgain(message: ChatMessage) { + inputMessage.value = message.record?.question ?? '' + nextTick(() => { + sendMessage() + }) +} + async function clickAnalysis(id?: number) { const baseRecord = find(currentChat.value.records, (value) => id === value.id) if (baseRecord == undefined) { @@ -750,11 +831,72 @@ defineExpose({ } } -.analysis-container { - color: var(--ed-text-color-primary); - font-size: 12px; - line-height: 1.7692307692; - padding: 16px 22px; +.error-container { + margin-top: 12px; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: rgba(31, 35, 41, 1); +} + +.tool-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + + min-height: 22px; + + margin-top: 12px; + margin-bottom: 12px; + + .tool-times { + font-size: 14px; + font-weight: 400; + line-height: 22px; + color: rgba(100, 106, 115, 1); + } + + .tool-btns { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: wrap; + + column-gap: 16px; + row-gap: 8px; + + .tool-btn { + font-size: 14px; + font-weight: 400; + line-height: 22px; + color: rgba(100, 106, 115, 1); + + .tool-btn-inner { + display: flex; + flex-direction: row; + align-items: center; + } + + &:hover { + background: rgba(31, 35, 41, 0.1); + } + &:active { + background: rgba(31, 35, 41, 0.1); + } + } + + .btn-text { + margin-left: 4px; + } + + .divider { + width: 1px; + height: 16px; + border-left: 1px solid rgba(31, 35, 41, 0.15); + } + } } .welcome-content-block { From d1ab16c78703e8f986e920ac7ccfa65d9ecb1cb4 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Thu, 17 Jul 2025 17:34:26 +0800 Subject: [PATCH 05/14] feat(member): member manage --- frontend/src/api/workspace.ts | 1 + .../permission/icon_custom-tools_colorful.png | Bin 0 -> 788 bytes .../src/assets/svg/icon_down_outlined.svg | 4 +- .../src/assets/svg/icon_member_outlined.svg | 3 + .../src/assets/svg/icon_setting_filled.svg | 3 + frontend/src/components/layout/Menu.vue | 24 +- frontend/src/components/layout/MenuItem.vue | 9 +- frontend/src/i18n/en.json | 39 +- frontend/src/i18n/zh-CN.json | 42 +- frontend/src/router/index.ts | 37 +- frontend/src/views/ds/DatasourceForm.vue | 2 +- frontend/src/views/system/member/index.vue | 490 +++++++++++++++ frontend/src/views/system/permission/Card.vue | 121 ++++ .../src/views/system/permission/index.vue | 579 ++++++++++++++++++ frontend/src/views/system/user/User.vue | 3 +- 15 files changed, 1334 insertions(+), 23 deletions(-) create mode 100644 frontend/src/assets/permission/icon_custom-tools_colorful.png create mode 100644 frontend/src/assets/svg/icon_member_outlined.svg create mode 100644 frontend/src/assets/svg/icon_setting_filled.svg create mode 100644 frontend/src/views/system/member/index.vue create mode 100644 frontend/src/views/system/permission/Card.vue create mode 100644 frontend/src/views/system/permission/index.vue diff --git a/frontend/src/api/workspace.ts b/frontend/src/api/workspace.ts index 729d57c9..b1e8c3ec 100644 --- a/frontend/src/api/workspace.ts +++ b/frontend/src/api/workspace.ts @@ -14,3 +14,4 @@ export const workspaceUwsDelete = (data: any) => request.delete('/system/workspa export const workspaceDelete = (data: any) => request.delete('/system/workspace', { data }) export const workspaceList = () => request.get('/system/workspace') export const workspaceDetail = (id: any) => request.get(`/system/workspace/${id}`) +export const uwsOption = (params: any) => request.get('system/workspace/uws/option', { params }) diff --git a/frontend/src/assets/permission/icon_custom-tools_colorful.png b/frontend/src/assets/permission/icon_custom-tools_colorful.png new file mode 100644 index 0000000000000000000000000000000000000000..41b09345f6a573c136a7eb6c0e527ebe64d7a5fe GIT binary patch literal 788 zcmV+v1MB>WP)DEGztgL-fj5;hELFPuV0{z2{$PmBwo0f zup|ocP%W@xQW9-yh(KG%_m)!IVhgjR+uvnpXJ+TUdGF2adjfTs@1p)f0gp8)f>MJi zB|_XUXb1{OAVc5esF@QI7KB{uFtD7Nh-9kgiE6>cqV`fihDoR&EkX#yw(o``Clm-Q z##ZSOf;U*YAt%ThIgQ^2mts*5g)^bzA;R1cWO}n$5MSXvIkLhvtyI7`+JnF31iQDU zuYqOjotPnTNdKMhnONo zk~Mo9rKg3i@dirUq_S+GCfF6ktmXcVDiV$L42l zb@x@i$rlfh%6>sJ1YNDMIkr-cN8dRuO<}XUDy8+RUx^Vmd8N8bl7lA za$2SdSd@mF|C_*nJx(|{`EW}rn{GCo)1{P~RoVH;1$LV*a8BeJ=Y$s81Z~^W36i`|hgSW1*?8%5z8955D73|6I2y2BFLWOA#QqJmo za-z~#KgoO>HA@{Bmw%{W(VUvWU(TFOD8LsSKb>P - + + diff --git a/frontend/src/assets/svg/icon_member_outlined.svg b/frontend/src/assets/svg/icon_member_outlined.svg new file mode 100644 index 00000000..331f793e --- /dev/null +++ b/frontend/src/assets/svg/icon_member_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/svg/icon_setting_filled.svg b/frontend/src/assets/svg/icon_setting_filled.svg new file mode 100644 index 00000000..621a03c7 --- /dev/null +++ b/frontend/src/assets/svg/icon_setting_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/layout/Menu.vue b/frontend/src/components/layout/Menu.vue index d2b1032b..62be7dad 100644 --- a/frontend/src/components/layout/Menu.vue +++ b/frontend/src/components/layout/Menu.vue @@ -25,13 +25,16 @@ const routerList = computed(() => { if (showSysmenu.value) { return router.getRoutes().filter((route) => route.path.includes('/system') && !route.redirect) } + return router.getRoutes().filter((route) => { return ( !route.path.includes('canvas') && + !route.path.includes('member') && + !route.path.includes('permission') && !route.path.includes('preview') && route.path !== '/login' && !route.path.includes('/system') && - !route.redirect && + (route.path.includes('set') || !route.redirect) && route.path !== '/:pathMatch(.*)*' && !route.path.includes('dsTable') ) @@ -45,17 +48,28 @@ const routerList = computed(() => { - + diff --git a/frontend/src/views/system/permission/Card.vue b/frontend/src/views/system/permission/Card.vue new file mode 100644 index 00000000..7c32b983 --- /dev/null +++ b/frontend/src/views/system/permission/Card.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/frontend/src/views/system/permission/index.vue b/frontend/src/views/system/permission/index.vue new file mode 100644 index 00000000..224fdb02 --- /dev/null +++ b/frontend/src/views/system/permission/index.vue @@ -0,0 +1,579 @@ + + + + + + + diff --git a/frontend/src/views/system/user/User.vue b/frontend/src/views/system/user/User.vue index 6478cab2..e22d4769 100644 --- a/frontend/src/views/system/user/User.vue +++ b/frontend/src/views/system/user/User.vue @@ -165,8 +165,7 @@ :title="dialogTitle" destroy-on-close size="600px" - modle-class="sqlbot-drawer" - @close="onFormClose" + :before-close="onFormClose" > Date: Thu, 17 Jul 2025 18:18:49 +0800 Subject: [PATCH 06/14] feat: chat css --- backend/apps/chat/curd/chat.py | 7 +- frontend/src/views/chat/ChatToolBar.vue | 56 +++++++ frontend/src/views/chat/answer/BaseAnswer.vue | 13 +- .../src/views/chat/chat-block/ChartBlock.vue | 145 ++++++++++++++++++ .../chat/component/DisplayChartBlock.vue | 33 ++-- frontend/src/views/chat/index.vue | 95 ++++-------- 6 files changed, 262 insertions(+), 87 deletions(-) create mode 100644 frontend/src/views/chat/ChatToolBar.vue create mode 100644 frontend/src/views/chat/chat-block/ChartBlock.vue diff --git a/backend/apps/chat/curd/chat.py b/backend/apps/chat/curd/chat.py index 7425863a..1310b669 100644 --- a/backend/apps/chat/curd/chat.py +++ b/backend/apps/chat/curd/chat.py @@ -4,7 +4,6 @@ from typing import List import orjson from sqlalchemy import and_ from sqlalchemy.orm import load_only -from sqlmodel import select from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo, RenameChat, ChatQuestion from apps.datasource.models.datasource import CoreDatasource @@ -13,8 +12,9 @@ from common.utils.utils import extract_nested_json def list_chats(session: SessionDep, current_user: CurrentUser) -> List[Chat]: - chart_list = session.exec( - select(Chat).where(Chat.create_by == current_user.id).order_by(Chat.create_time.desc())).all() + oid = current_user.oid if current_user.oid is not None else 1 + chart_list = session.query(Chat).filter(and_(Chat.create_by == current_user.id, Chat.oid == oid)).order_by( + Chat.create_time.desc()).all() return chart_list @@ -149,6 +149,7 @@ def create_chat(session: SessionDep, current_user: CurrentUser, create_chat_obj: chat = Chat(create_time=datetime.datetime.now(), create_by=current_user.id, + oid=current_user.oid if current_user.oid is not None else 1, brief=create_chat_obj.question.strip()[:20]) ds: CoreDatasource | None = None if create_chat_obj.datasource: diff --git a/frontend/src/views/chat/ChatToolBar.vue b/frontend/src/views/chat/ChatToolBar.vue new file mode 100644 index 00000000..e065d10a --- /dev/null +++ b/frontend/src/views/chat/ChatToolBar.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/frontend/src/views/chat/answer/BaseAnswer.vue b/frontend/src/views/chat/answer/BaseAnswer.vue index efb30e67..e4b2df6b 100644 --- a/frontend/src/views/chat/answer/BaseAnswer.vue +++ b/frontend/src/views/chat/answer/BaseAnswer.vue @@ -70,9 +70,9 @@ onMounted(() => {
-
- {{ datetimeFormat(message.record?.create_time) }} -
-
+ @@ -296,9 +284,11 @@ import ChartAnswer from './answer/ChartAnswer.vue' import AnalysisAnswer from './answer/AnalysisAnswer.vue' import PredictAnswer from './answer/PredictAnswer.vue' import UserChat from './chat-block/UserChat.vue' +import ChartBlock from './chat-block/ChartBlock.vue' import RecommendQuestion from './RecommendQuestion.vue' import ChatListContainer from './ChatListContainer.vue' import ChatCreator from '@/views/chat/ChatCreator.vue' +import ChatToolBar from './ChatToolBar.vue' import { useI18n } from 'vue-i18n' import { endsWith, find, startsWith } from 'lodash-es' import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg' @@ -308,7 +298,6 @@ import icon_screen_outlined from '@/assets/svg/icon_screen_outlined.svg' 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 { datetimeFormat } from '@/utils/utils.ts' import { useAssistantStore } from '@/stores/assistant' const assistantStore = useAssistantStore() @@ -839,63 +828,43 @@ defineExpose({ color: rgba(31, 35, 41, 1); } -.tool-container { +.tool-btns { display: flex; flex-direction: row; align-items: center; - justify-content: space-between; flex-wrap: wrap; - min-height: 22px; + column-gap: 16px; + row-gap: 8px; - margin-top: 12px; - margin-bottom: 12px; - - .tool-times { + .tool-btn { font-size: 14px; font-weight: 400; line-height: 22px; color: rgba(100, 106, 115, 1); + + .tool-btn-inner { + display: flex; + flex-direction: row; + align-items: center; + } + + &:hover { + background: rgba(31, 35, 41, 0.1); + } + &:active { + background: rgba(31, 35, 41, 0.1); + } } - .tool-btns { - display: flex; - flex-direction: row; - align-items: center; - flex-wrap: wrap; + .btn-text { + margin-left: 4px; + } - column-gap: 16px; - row-gap: 8px; - - .tool-btn { - font-size: 14px; - font-weight: 400; - line-height: 22px; - color: rgba(100, 106, 115, 1); - - .tool-btn-inner { - display: flex; - flex-direction: row; - align-items: center; - } - - &:hover { - background: rgba(31, 35, 41, 0.1); - } - &:active { - background: rgba(31, 35, 41, 0.1); - } - } - - .btn-text { - margin-left: 4px; - } - - .divider { - width: 1px; - height: 16px; - border-left: 1px solid rgba(31, 35, 41, 0.15); - } + .divider { + width: 1px; + height: 16px; + border-left: 1px solid rgba(31, 35, 41, 0.15); } } From 6cd2d99e3d73cbee4e156efe6a7cdfdad725cb8e Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Thu, 17 Jul 2025 18:32:34 +0800 Subject: [PATCH 07/14] feat(datasource): datasource manage --- .../src/assets/svg/icon_member_filled.svg | 6 +-- frontend/src/i18n/en.json | 3 +- frontend/src/i18n/zh-CN.json | 3 +- frontend/src/views/ds/DataTable.vue | 3 +- frontend/src/views/ds/Datasource.vue | 13 +++++- frontend/src/views/ds/DatasourceForm.vue | 20 ++------ .../src/views/system/model/ModelListSide.vue | 2 +- frontend/src/views/system/user/User.vue | 46 +++++++++++-------- frontend/src/views/system/workspace/index.vue | 14 ++++-- 9 files changed, 63 insertions(+), 47 deletions(-) diff --git a/frontend/src/assets/svg/icon_member_filled.svg b/frontend/src/assets/svg/icon_member_filled.svg index fded3173..12a0f984 100644 --- a/frontend/src/assets/svg/icon_member_filled.svg +++ b/frontend/src/assets/svg/icon_member_filled.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 762623a8..5c3af9df 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -306,7 +306,8 @@ "add_successfully": "Add successfully", "member_management": "Member management", "permission_configuration": "Permission configuration", - "set": "Set" + "set": "Set", + "operate_with_caution": "After deletion, the users under the workspace will be removed and all resources will be deleted. Please operate with caution." }, "permission": { "search_rule_group": "Search rule group", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 10bcf900..f75ef684 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -327,7 +327,8 @@ "add_successfully": "添加成功", "member_management": "成员管理", "permission_configuration": "权限配置", - "set": "设置" + "set": "设置", + "operate_with_caution": "删除后,该工作空间下的用户将被移除,所有资源也将被删除,请谨慎操作。" }, "permission": { "search_rule_group": "搜索规则组", diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index 61f71902..f37cfa92 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -140,6 +140,7 @@ const refresh = () => { if (!nameArr.includes(currentTable.value.table_name)) { currentTable.value = {} } + emits('refresh') }) } @@ -173,7 +174,7 @@ const changeStatus = (row: any) => { }) } -const emits = defineEmits(['back']) +const emits = defineEmits(['back', 'refresh']) const back = () => { emits('back') } diff --git a/frontend/src/views/ds/Datasource.vue b/frontend/src/views/ds/Datasource.vue index 82ae6a61..a3aa27bb 100644 --- a/frontend/src/views/ds/Datasource.vue +++ b/frontend/src/views/ds/Datasource.vue @@ -115,6 +115,11 @@ const refresh = () => { search() } +const refreshData = () => { + console.log(123) + search() +} + const panelClick = () => { console.log('panelClick') } @@ -329,11 +334,17 @@ const back = () => { :active-name="activeName" :active-type="activeType" @refresh="refresh" + @close="beforeClose" @change-active-step="(val: number) => (activeStep = val)" >
- + diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index d66fdd94..548d0d88 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -116,7 +116,7 @@ @finish="onChartAnswerFinish" @error="onChartAnswerError" > - +
- +
Date: Thu, 17 Jul 2025 22:21:02 +0800 Subject: [PATCH 10/14] perf: User cache --- backend/apps/system/api/user.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py index cb4e9fdb..1c6ef120 100644 --- a/backend/apps/system/api/user.py +++ b/backend/apps/system/api/user.py @@ -1,7 +1,7 @@ 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.crud.user import clean_user_cache, get_db_user, single_delete, user_ws_options from apps.system.models.system_model import UserWsModel from apps.system.models.user import UserModel from apps.system.schemas.auth import CacheName, CacheNamespace @@ -95,13 +95,14 @@ async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Tran return await user_ws_options(session, current_user.id, trans) @router.put("/ws/{oid}") -@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") +# @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): 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!") user_model: UserModel = get_db_user(session = session, user_id = current_user.id) user_model.oid = oid + await clean_user_cache(user_model.id) session.add(user_model) session.commit() @@ -173,13 +174,14 @@ async def batch_del(session: SessionDep, id_list: list[int]): await single_delete(session, id) @router.put("/language") -@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") +#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") async def langChange(session: SessionDep, current_user: CurrentUser, language: UserLanguage): lang = language.language if lang not in ["zh-CN", "en"]: return {"message": "Language not supported"} db_user: UserModel = get_db_user(session=session, user_id=current_user.id) db_user.language = lang + await clean_user_cache(db_user.id) session.add(db_user) session.commit() @@ -194,11 +196,12 @@ async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int): session.commit() @router.put("/pwd") -@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") +#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") async def pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdEditor): 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) + await clean_user_cache(db_user.id) session.add(db_user) session.commit() \ No newline at end of file From ea6e654be5d4222da63436300589a023f7f9d2be Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Fri, 18 Jul 2025 09:16:00 +0800 Subject: [PATCH 11/14] perf: Fastapi cache lock --- backend/common/core/sqlbot_cache.py | 90 +++++++++++++---------------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/backend/common/core/sqlbot_cache.py b/backend/common/core/sqlbot_cache.py index 82c8812a..0d32917b 100644 --- a/backend/common/core/sqlbot_cache.py +++ b/backend/common/core/sqlbot_cache.py @@ -1,12 +1,27 @@ 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 +from typing import Optional, Set, Any, Dict, Tuple, Callable from inspect import Parameter, signature import logging +from contextlib import asynccontextmanager +import asyncio logger = logging.getLogger(__name__) +# 锁管理 +_cache_locks = {} + +@asynccontextmanager +async def _get_cache_lock(key: str): + lock = _cache_locks.setdefault(key, asyncio.Lock()) + async with lock: + try: + yield + finally: + if key in _cache_locks and not lock.locked(): + del _cache_locks[key] + def should_skip_param(param: Parameter) -> bool: """判断参数是否应该被忽略(依赖注入参数)""" return ( @@ -26,9 +41,7 @@ def custom_key_builder( cacheName: Optional[str] = None, keyExpression: Optional[str] = None, ) -> str: - """ - 完全兼容FastAPICache的键生成器 - """ + """完全兼容FastAPICache的键生成器""" if cacheName: base_key = f"{namespace}:{cacheName}:" @@ -38,7 +51,6 @@ def custom_key_builder( bound_args = sig.bind_partial(*args, **kwargs) bound_args.apply_defaults() - if keyExpression.startswith("args["): import re match = re.match(r"args\[(\d+)\]", keyExpression) @@ -47,7 +59,6 @@ def custom_key_builder( value = bound_args.args[index] base_key += f"{value}:" else: - parts = keyExpression.split('.') value = bound_args.arguments[parts[0]] for part in parts[1:]: @@ -58,37 +69,27 @@ def custom_key_builder( logger.warning(f"Failed to evaluate keyExpression '{keyExpression}': {str(e)}") return base_key - # 获取函数签名 - sig = signature(func) - # 自动识别要跳过的参数 + sig = signature(func) auto_skip_args = { name for name, param in sig.parameters.items() if should_skip_param(param) } - - # 合并用户指定的额外跳过参数 skip_args = auto_skip_args.union(additional_skip_args or set()) - - # 过滤kwargs filtered_kwargs = { k: v for k, v in kwargs.items() if k not in skip_args } - # 过滤args - 将位置参数映射到它们的参数名 bound_args = sig.bind_partial(*args, **kwargs) bound_args.apply_defaults() filtered_args = [] for i, (name, value) in enumerate(bound_args.arguments.items()): - # 只处理位置参数 (在args中的参数) if i < len(args) and name not in skip_args: filtered_args.append(value) filtered_args = tuple(filtered_args) - # 获取默认键生成器 default_key_builder = FastAPICache.get_key_builder() - # 调用默认键生成器(严格按照其要求的参数格式) return default_key_builder( func=func, namespace=namespace, @@ -105,9 +106,7 @@ def cache( cacheName: Optional[str] = None, keyExpression: Optional[str] = None, ): - """ - 完全兼容的缓存装饰器 - """ + """完全兼容的缓存装饰器""" def decorator(func): if key_builder is None: used_key_builder = partial( @@ -121,24 +120,23 @@ def cache( @wraps(func) async def wrapper(*args, **kwargs): - # 准备键生成器参数 - key_builder_args = { - "func": func, - "namespace": namespace, - "args": args, - "kwargs": kwargs - } + cache_key = used_key_builder( + func=func, + namespace=namespace or "", + args=args, + kwargs=kwargs + ) - # 生成缓存键 - cache_key = used_key_builder(**key_builder_args) - logger.debug(f"Generated cache key: {cache_key}") - - # 使用原始缓存装饰器 - return await original_cache( - expire=expire, - namespace=namespace, - key_builder=lambda *_, **__: cache_key # 直接使用预生成的key - )(func)(*args, **kwargs) + async with _get_cache_lock(cache_key): + backend = FastAPICache.get_backend() + cached_value = await backend.get(cache_key) + if cached_value is not None: + return cached_value + + result = await func(*args, **kwargs) + await backend.set(cache_key, result, expire) + return result + return wrapper return decorator @@ -147,17 +145,10 @@ def clear_cache( cacheName: Optional[str] = None, keyExpression: Optional[str] = None, ): - """ - 清除缓存的装饰器,参数与 @cache 保持一致 - 使用方式: - @clear_cache(namespace="user", cacheName="info", keyExpression="user_id") - async def update_user(user_id: int): - ... - """ + """清除缓存的装饰器""" def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): - # 1. 生成缓存键(复用 custom_key_builder 逻辑) cache_key = custom_key_builder( func=func, namespace=namespace or "", @@ -167,13 +158,10 @@ def clear_cache( keyExpression=keyExpression, ) - logger.debug(f"Clearing cache for key: {cache_key}") - # 2. 清除缓存 - if await FastAPICache.get_backend().get(cache_key): + async with _get_cache_lock(cache_key): await FastAPICache.clear(key=cache_key) - - # 3. 执行原函数 - return await func(*args, **kwargs) + result = await func(*args, **kwargs) + return result return wrapper return decorator \ No newline at end of file From 34bb7fba69a1a9612c7f0c280f403a5cd68c8a55 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Fri, 18 Jul 2025 09:27:19 +0800 Subject: [PATCH 12/14] feat(datasource): datasource manage --- frontend/src/views/ds/DataTable.vue | 2 +- frontend/src/views/ds/Datasource.vue | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/views/ds/DataTable.vue b/frontend/src/views/ds/DataTable.vue index f37cfa92..7a804af5 100644 --- a/frontend/src/views/ds/DataTable.vue +++ b/frontend/src/views/ds/DataTable.vue @@ -133,6 +133,7 @@ const closeField = () => { } const refresh = () => { + emits('refresh') datasourceApi.tableList(props.info.id).then((res) => { tableList.value = res if (!currentTable.value.table_name) return @@ -140,7 +141,6 @@ const refresh = () => { if (!nameArr.includes(currentTable.value.table_name)) { currentTable.value = {} } - emits('refresh') }) } diff --git a/frontend/src/views/ds/Datasource.vue b/frontend/src/views/ds/Datasource.vue index a3aa27bb..5be580dd 100644 --- a/frontend/src/views/ds/Datasource.vue +++ b/frontend/src/views/ds/Datasource.vue @@ -116,7 +116,6 @@ const refresh = () => { } const refreshData = () => { - console.log(123) search() } From a49330e91efd1d1ed8019298d17cd6e66d68ac11 Mon Sep 17 00:00:00 2001 From: dataeaseShu Date: Fri, 18 Jul 2025 09:36:37 +0800 Subject: [PATCH 13/14] feat(datasource): datasource manage --- frontend/src/views/system/workspace/index.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/system/workspace/index.vue b/frontend/src/views/system/workspace/index.vue index 73d891d2..f79bc0d1 100644 --- a/frontend/src/views/system/workspace/index.vue +++ b/frontend/src/views/system/workspace/index.vue @@ -421,7 +421,7 @@ const handleCurrentChange = (val: number) => { ? t('workspace.administrator') : t('workspace.ordinary_member') }} - +
@@ -715,6 +715,7 @@ const handleCurrentChange = (val: number) => { cursor: pointer; &:hover { .ed-icon { + display: block !important; transform: rotate(180deg); } } From 93798b7c58e15ef8f903042d841297b5000c45b8 Mon Sep 17 00:00:00 2001 From: junjun Date: Fri, 18 Jul 2025 10:14:41 +0800 Subject: [PATCH 14/14] feat: mcp tools --- backend/apps/mcp/mcp.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/apps/mcp/mcp.py b/backend/apps/mcp/mcp.py index 42f945e5..d5b2dd94 100644 --- a/backend/apps/mcp/mcp.py +++ b/backend/apps/mcp/mcp.py @@ -9,11 +9,15 @@ from fastapi.responses import StreamingResponse # from fastapi.security import OAuth2PasswordBearer from jwt.exceptions import InvalidTokenError from pydantic import ValidationError +from sqlmodel import select from apps.chat.api.chat import create_chat from apps.chat.models.chat_model import ChatMcp, CreateChat, ChatStart from apps.chat.task.llm import LLMService, run_task -from apps.system.crud.user import authenticate, get_user_info +from apps.system.crud.user import authenticate +from apps.system.crud.user import get_db_user +from apps.system.models.system_model import UserWsModel +from apps.system.models.user import UserModel from apps.system.schemas.system_schema import BaseUserDTO from apps.system.schemas.system_schema import UserInfoDTO from common.core import security @@ -83,7 +87,17 @@ async def mcp_question(session: SessionDep, chat: ChatMcp): status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials", ) - session_user = await get_user_info(session=session, user_id=token_data.id) + # session_user = await get_user_info(session=session, user_id=token_data.id) + + db_user: UserModel = get_db_user(session=session, user_id=token_data.id) + session_user = UserInfoDTO.model_validate(db_user.model_dump()) + session_user.isAdmin = session_user.id == 1 and session_user.account == 'admin' + if session_user.isAdmin: + session_user = session_user + ws_model: UserWsModel = session.exec( + select(UserWsModel).where(UserWsModel.uid == session_user.id, UserWsModel.oid == session_user.oid)).first() + session_user.weight = ws_model.weight if ws_model else -1 + session_user = UserInfoDTO.model_validate(session_user) if not session_user: raise HTTPException(status_code=404, detail="User not found")