diff --git a/backend/alembic/versions/030_permission_oid.py b/backend/alembic/versions/030_permission_oid.py new file mode 100644 index 00000000..f1610b7f --- /dev/null +++ b/backend/alembic/versions/030_permission_oid.py @@ -0,0 +1,43 @@ +"""030_permission_oid + +Revision ID: c1d7ac00b3a8 +Revises: 77d4c39ec22f +Create Date: 2025-07-21 11:49:43.115524 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'c1d7ac00b3a8' +down_revision = '77d4c39ec22f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('ds_rules', sa.Column('oid', sa.BigInteger(), nullable=True)) + op.alter_column('ds_permission', 'id', + existing_type=sa.INTEGER(), + type_=sa.BigInteger(), + existing_nullable=False, + autoincrement=True, + existing_server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, + maxvalue=2147483647, cycle=False, cache=1)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('ds_permission', 'id', + existing_type=sa.BigInteger(), + type_=sa.INTEGER(), + existing_nullable=False, + autoincrement=True, + existing_server_default=sa.Identity(always=True, start=1, increment=1, minvalue=1, + maxvalue=2147483647, cycle=False, cache=1)) + op.drop_column('ds_rules', 'oid') + # ### end Alembic commands ### diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index 56250aa0..f17f601a 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -11,7 +11,7 @@ import requests from langchain.chat_models.base import BaseChatModel from langchain_community.utilities import SQLDatabase from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage, BaseMessageChunk -from sqlalchemy import and_, cast +from sqlalchemy import and_, cast, or_ from sqlalchemy import select from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import load_only @@ -489,7 +489,8 @@ class LLMService: # check permission and user in same rules obj = self.session.query(DsRules).filter( and_(DsRules.permission_list.op('@>')(cast([permission.id], JSONB)), - DsRules.user_list.op('@>')(cast([self.current_user.id], JSONB))) + or_(DsRules.user_list.op('@>')(cast([f'{self.current_user.id}'], JSONB)), + DsRules.user_list.op('@>')(cast([self.current_user.id], JSONB)))) ).first() if obj is not None: res.append(transRecord2DTO(self.session, permission)) diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index da6d2918..d60a173e 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -3,7 +3,7 @@ import json from typing import List from fastapi import HTTPException -from sqlalchemy import and_, text, cast +from sqlalchemy import and_, text, cast, or_ from sqlalchemy.dialects.postgresql import JSONB from sqlbot_xpack.permissions.models.ds_permission import DsPermission from sqlbot_xpack.permissions.models.ds_rules import DsRules @@ -295,7 +295,8 @@ def get_table_obj_by_ds(session: SessionDep, current_user: CurrentUser, ds: Core # check permission and user in same rules obj = session.query(DsRules).filter( and_(DsRules.permission_list.op('@>')(cast([permission.id], JSONB)), - DsRules.user_list.op('@>')(cast([current_user.id], JSONB))) + or_(DsRules.user_list.op('@>')(cast([f'{current_user.id}'], JSONB)), + DsRules.user_list.op('@>')(cast([current_user.id], JSONB)))) ).first() if obj is not None: permission_list = json.loads(permission.permissions) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1c69d48e..ef279bc8 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "pyyaml (>=6.0.2,<7.0.0)", "fastapi-mcp (>=0.3.4,<0.4.0)", "tabulate>=0.9.0", - "sqlbot-xpack==0.0.3.5", + "sqlbot-xpack==0.0.3.6", "fastapi-cache2>=0.2.2", "sqlparse>=0.5.3", ] diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index eb4100f7..0dafe579 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -83,7 +83,7 @@ "error": "An error occurred, please try again later", "question_placeholder": "Press Enter to submit, or use Ctrl + Enter for a new line", "greeting": "Hello, I'm SQLBot. How can I assist you today?", - "hint_description": "I can query data, generate charts, detect anomalies, predict trends, and more. Please select a data source to begin!", + "hint_description": "You can query data, generate charts, analyze data, predict trends, and more. Please select a data source to start your smart data exploration~", "select_datasource": "Select Data Source", "view_more": "View More", "selected_datasource": "Selected Data Source", @@ -96,7 +96,11 @@ "chat_search": "Search", "thinking": "Thinking", "thinking_step": "Thought Process", - "ask_again": "Regenerate" + "ask_again": "Regenerate", + "today": "Today", + "week": "This Week", + "earlier": "Earlier", + "no_time": "No Time" }, "ds": { "title": "Data Source", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index ca4c7d84..1e9fe9f5 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -90,7 +90,7 @@ "error": "发生错误,请稍后再试", "question_placeholder": "按下 Enter 提交问题, 或使用 Ctrl + Enter 换行", "greeting": "你好,我是SQLBot,很高兴为你服务", - "hint_description": "我可以查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~", + "hint_description": "我可以查询数据、生成图表、分析数据、预测数据等,请选择一个数据源,开启智能问数吧~", "select_datasource": "选择数据源", "view_more": "查看更多", "selected_datasource": "已选择数据源", @@ -103,7 +103,11 @@ "chat_search": "搜索", "thinking": "思考中", "thinking_step": "思考过程", - "ask_again": "重新生成" + "ask_again": "重新生成", + "today": "今天", + "week": "7天内", + "earlier": "更早以前", + "no_time": "没有时间" }, "ds": { "title": "数据源", diff --git a/frontend/src/views/chat/ChatList.vue b/frontend/src/views/chat/ChatList.vue index 3a9ee8aa..d6d1f714 100644 --- a/frontend/src/views/chat/ChatList.vue +++ b/frontend/src/views/chat/ChatList.vue @@ -2,6 +2,10 @@ import { Delete, EditPen, MoreFilled } from '@element-plus/icons-vue' import { type Chat, chatApi } from '@/api/chat.ts' import { computed } from 'vue' +import dayjs from 'dayjs' +import { getDate } from '@/utils/utils.ts' +import { groupBy } from 'lodash-es' +import { useI18n } from 'vue-i18n' const props = withDefaults( defineProps<{ @@ -16,6 +20,64 @@ const props = withDefaults( } ) +const { t } = useI18n() + +function groupByDate(chat: Chat) { + const todayStart = dayjs(dayjs().format('YYYY-MM-DD') + ' 00:00:00').toDate() + const todayEnd = dayjs(dayjs().format('YYYY-MM-DD') + ' 23:59:59').toDate() + const weekStart = dayjs(dayjs().subtract(7, 'day').format('YYYY-MM-DD') + ' 00:00:00').toDate() + + const time = getDate(chat.create_time) + + if (time) { + if (time >= todayStart && time <= todayEnd) { + return t('qa.today') + } + if (time < todayStart && time >= weekStart) { + return t('qa.week') + } + if (time < weekStart) { + return t('qa.earlier') + } + } + + return t('qa.no_time') +} + +const computedChatGroup = computed(() => { + return groupBy(props.chatList, groupByDate) +}) + +const computedChatList = computed(() => { + const _list = [] + if (computedChatGroup.value[t('qa.today')]) { + _list.push({ + key: t('qa.today'), + list: computedChatGroup.value[t('qa.today')], + }) + } + if (computedChatGroup.value[t('qa.week')]) { + _list.push({ + key: t('qa.week'), + list: computedChatGroup.value[t('qa.week')], + }) + } + if (computedChatGroup.value[t('qa.earlier')]) { + _list.push({ + key: t('qa.earlier'), + list: computedChatGroup.value[t('qa.earlier')], + }) + } + if (computedChatGroup.value[t('qa.no_time')]) { + _list.push({ + key: t('qa.no_time'), + list: computedChatGroup.value[t('qa.no_time')], + }) + } + + return _list +}) + const emits = defineEmits(['chatSelected', 'chatRenamed', 'chatDeleted', 'update:loading']) const _loading = computed({ @@ -106,28 +168,31 @@ function handleCommand(command: string | number | object, chat: Chat) { @@ -144,15 +209,31 @@ function handleCommand(command: string | number | object, chat: Chat) { display: flex; flex-direction: column; - gap: 8px; + gap: 16px; + + .group { + display: flex; + flex-direction: column; + + .group-title { + padding: 0 8px; + color: rgba(100, 106, 115, 1); + line-height: 20px; + font-weight: 500; + font-size: 12px; + + margin-bottom: 4px; + } + } .chat-list-item { width: 100%; - height: 42px; + height: 40px; cursor: pointer; border-radius: 6px; - line-height: 1em; + line-height: 22px; font-size: 14px; + font-weight: 400; display: flex; align-items: center; diff --git a/frontend/src/views/chat/ChatListContainer.vue b/frontend/src/views/chat/ChatListContainer.vue index af3aca6f..edda76cf 100644 --- a/frontend/src/views/chat/ChatListContainer.vue +++ b/frontend/src/views/chat/ChatListContainer.vue @@ -194,6 +194,7 @@ function onChatRenamed(chat: Chat) { name="quick-search" autocomplete="off" :placeholder="t('qa.chat_search')" + clearable />