diff --git a/apps/chat/api/portal_api.py b/apps/chat/api/portal_api.py index f77137227cb..3e5ef12212a 100644 --- a/apps/chat/api/portal_api.py +++ b/apps/chat/api/portal_api.py @@ -130,3 +130,41 @@ def get_parameters(): @staticmethod def get_response(): return DefaultResultSerializer + + class ConversationQuery(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="application_id", + description="会话所属应用ID", + type=OpenApiTypes.STR, + location="path", + required=True, + ), + OpenApiParameter( + name="current_page", + description="当前页码", + type=OpenApiTypes.INT, + location="path", + required=True, + ), + OpenApiParameter( + name="page_size", + description="每页数量", + type=OpenApiTypes.INT, + location="path", + required=True, + ), + OpenApiParameter( + name="abstract", + description="会话摘要模糊搜索", + type=OpenApiTypes.STR, + location="query", + required=False, + ), + ] + + @staticmethod + def get_response(): + return DefaultResultSerializer diff --git a/apps/chat/serializers/portal.py b/apps/chat/serializers/portal.py index c5a2cd7648b..c3663af5d51 100644 --- a/apps/chat/serializers/portal.py +++ b/apps/chat/serializers/portal.py @@ -138,24 +138,28 @@ def page(self, current_page, page_size, user_id, with_valid=True): def get_recent_chats_map(user_id, application_ids, limit=5): - """批量返回 application_id -> 该应用最近的 limit 条历史会话;show_history=false 的应用不在此表里""" - chats = Chat.objects.filter(chat_user_id=user_id, is_deleted=False, application_id__in=application_ids).order_by( - "application_id", "-update_time", "id" + """批量返回 application_id -> 该应用最近的 limit 条历史会话及其总数;show_history=false 的应用不在此表里""" + chats = ( + Chat.objects.filter(chat_user_id=user_id, is_deleted=False, application_id__in=application_ids) + .order_by("application_id", "-update_time", "id") + .values("application_id", "id", "abstract", "create_time", "update_time") ) - result = {} + chat_map = {} + count_map = {} for chat in chats: - key = str(chat.application_id) - if len(result.get(key, [])) >= limit: - continue - result.setdefault(key, []).append( - { - "id": str(chat.id), - "abstract": chat.abstract, - "create_time": str(chat.create_time), - "update_time": str(chat.update_time), - } - ) - return result + key = str(chat["application_id"]) + count_map[key] = count_map.get(key, 0) + 1 + recent = chat_map.setdefault(key, []) + if len(recent) < limit: + recent.append( + { + "id": str(chat["id"]), + "abstract": chat["abstract"], + "create_time": str(chat["create_time"]), + "update_time": str(chat["update_time"]), + } + ) + return chat_map, count_map class PortalHistoricalConversationSerializer(serializers.Serializer): @@ -192,8 +196,49 @@ def page(self, current_page, page_size, user_id, with_valid=True): app_ids = [record["id"] for record in result["records"]] settings_map = get_application_settings_map(app_ids) show_history_ids = [aid for aid in app_ids if settings_map.get(aid, {}).get("show_history")] - chat_map = get_recent_chats_map(user_id, show_history_ids) if show_history_ids else {} + chat_map, count_map = get_recent_chats_map(user_id, show_history_ids) if show_history_ids else ({}, {}) for record in result["records"]: record.update(settings_map.get(record["id"], {})) record["conversations"] = chat_map.get(record["id"], []) + record["count"] = count_map.get(record["id"], 0) return result + + +class PortalConversationQuerySerializer(PortalApplicationAuthMixin, serializers.Serializer): + """门户单个智能体的历史会话分页查询(懒加载/滚动加载),仅返回摘要,支持摘要模糊查询。""" + + application_id = serializers.UUIDField(required=True, label=_("Application ID"), help_text=_("Application ID")) + abstract = serializers.CharField( + required=False, allow_blank=True, label=_("Abstract"), help_text=_("Conversation abstract keyword") + ) + + def _authorize(self, user_id): + application_id = str(self.data.get("application_id")) + authorized_ids = {str(item) for item in self.get_authorized_application_ids(user_id)} + if application_id not in authorized_ids: + raise serializers.ValidationError(_("Application is not authorized")) + if not get_application_settings_map([application_id]).get(application_id, {}).get("show_history"): + raise serializers.ValidationError(_("Application historical conversation is not enabled")) + + def page(self, current_page, page_size, user_id, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + self._authorize(user_id) + application_id = self.data.get("application_id") + queryset = Chat.objects.filter(application_id=application_id, chat_user_id=user_id, is_deleted=False).order_by( + "-update_time", "id" + ) + abstract = self.data.get("abstract") + if abstract: + queryset = queryset.filter(abstract__icontains=abstract) + return page_search( + current_page, + page_size, + queryset, + post_records_handler=lambda chat: { + "id": str(chat.id), + "abstract": chat.abstract, + "create_time": str(chat.create_time), + "update_time": str(chat.update_time), + }, + ) diff --git a/apps/chat/urls.py b/apps/chat/urls.py index da14872bd94..7c0b81d5738 100644 --- a/apps/chat/urls.py +++ b/apps/chat/urls.py @@ -46,6 +46,7 @@ path('api_key/', v3_views.ChatUserApiKeyView.Operate.as_view()), path('portal/application//', v3_views.PortalApplicationView.as_view(), name='v3_portal_application'), path('portal/chat//', v3_views.PortalHistoricalConversationView.as_view(), name='v3_portal_historical_conversation'), + path('portal/chat///', v3_views.PortalConversationQueryView.as_view(), name='v3_portal_conversation_query'), path('knowledge//retrieve', retrieve_view), path('knowledge//mcp', knowledge_mcp_view), ] diff --git a/apps/chat/views/v3/portal.py b/apps/chat/views/v3/portal.py index 1f5ec5614dc..1b5e7a1928e 100644 --- a/apps/chat/views/v3/portal.py +++ b/apps/chat/views/v3/portal.py @@ -20,6 +20,7 @@ from chat.api.portal_api import PortalAPI from chat.serializers.portal import ( PortalApplicationSerializer, + PortalConversationQuerySerializer, PortalHistoricalConversationSerializer, ) @@ -76,3 +77,26 @@ def get(self, request: Request, current_page: int, page_size: int): data={**query_params_to_single_dict(request.query_params)} ).page(current_page, page_size, str(request.user.id)) ) + + +class PortalConversationQueryView(APIView): + authentication_classes = [ChatTokenAuth] + + @extend_schema( + methods=["GET"], + description=_("Get portal application historical conversation by page"), + summary=_("Get portal application historical conversation by page"), + operation_id=_("Get portal application historical conversation by page"), # type: ignore + parameters=PortalAPI.ConversationQuery.get_parameters(), + responses=PortalAPI.ConversationQuery.get_response(), + tags=[_("V3 Chat")], # type: ignore + ) + def get(self, request: Request, application_id: str, current_page: int, page_size: int): + return result.success( + PortalConversationQuerySerializer( + data={ + **query_params_to_single_dict(request.query_params), + "application_id": application_id, + } + ).page(current_page, page_size, str(request.user.id)) + ) diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 29f48236f04..199222acdc0 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -9747,3 +9747,12 @@ msgstr "Width and height must be in [512, 2048] and the area must not exceed 102 msgid "api_key is required" msgstr "api_key is required" + + +#: apps/chat/serializers/portal.py:219 +msgid "Application is not authorized" +msgstr "Application is not authorized" + +#: apps/chat/serializers/portal.py:221 +msgid "Application historical conversation is not enabled" +msgstr "Application historical conversation is not enabled" diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index e84d07e06b6..e5298f4d677 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -9919,3 +9919,12 @@ msgstr "宽和高必须在 [512, 2048] 之间,且面积不得超过 1024x1024 #: apps/models_provider/impl/tencent_model_provider/credential/embedding.py msgid "api_key is required" msgstr "api_key 必填" + + +#: apps/chat/serializers/portal.py:219 +msgid "Application is not authorized" +msgstr "应用未授权" + +#: apps/chat/serializers/portal.py:221 +msgid "Application historical conversation is not enabled" +msgstr "应用未开启历史记录" diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index 0bb0ccaff54..dd735a6bbde 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -9917,3 +9917,12 @@ msgstr "寬和高必須在 [512, 2048] 之間,且面積不得超過 1024x1024 #: apps/models_provider/impl/tencent_model_provider/credential/embedding.py msgid "api_key is required" msgstr "api_key 必填" + + +#: apps/chat/serializers/portal.py:219 +msgid "Application is not authorized" +msgstr "應用未授權" + +#: apps/chat/serializers/portal.py:221 +msgid "Application historical conversation is not enabled" +msgstr "應用未開啟歷史記錄" diff --git a/ui/src/api/chat/conversation.ts b/ui/src/api/chat/conversation.ts index 362dc617729..10f34b2be25 100644 --- a/ui/src/api/chat/conversation.ts +++ b/ui/src/api/chat/conversation.ts @@ -24,6 +24,13 @@ const postResumeConversationMessage = (applicationId: string, chatId: string, ch const getConversationPage = (applicationId: string, page: number, size: number) => get>(`${getApplicationPath(applicationId)}/chat/${page}/${size}`) +/** 获取门户单个智能体的历史会话分页(懒加载),仅返回摘要,支持摘要模糊搜索。 */ +const getPortalConversationPage = (applicationId: string, page: number, size: number, abstract?: string) => + get>( + `/v3/portal/chat/${applicationId}/${page}/${size}`, + abstract ? { abstract } : undefined, + ) + /** 获取单条会话记录详情(含执行详情 execution_details、知识来源、tokens、耗时)。 */ const getConversationRecordDetail = (applicationId: string, chatId: string, chatRecordId: string) => get(`${getApplicationPath(applicationId)}/chat/${chatId}/chat_record/${chatRecordId}`) @@ -48,6 +55,7 @@ export default { postCancelConversationMessage, postResumeConversationMessage, getConversationPage, + getPortalConversationPage, getConversationRecordDetail, getConversationRecordPage, deleteConversation, diff --git a/ui/src/api/types/chat-application.ts b/ui/src/api/types/chat-application.ts index 52ef6c8dac9..25c7f3cd875 100644 --- a/ui/src/api/types/chat-application.ts +++ b/ui/src/api/types/chat-application.ts @@ -23,4 +23,5 @@ export interface PortalApplicationConversations { name: string icon: string conversations: PortalConversationSummary[] + count: number } diff --git a/ui/src/conversation-panel/view/portal/index.ts b/ui/src/conversation-panel/view/portal/index.ts index 6efb1d19445..b9fa1875c3e 100644 --- a/ui/src/conversation-panel/view/portal/index.ts +++ b/ui/src/conversation-panel/view/portal/index.ts @@ -55,7 +55,7 @@ export function createPortalConversation(options: PortalConversationOptions): Po const portalList = createPortalConversationListStore({ pageApplicationConversations: (page, size) => PortalApi.getPortalConversationPage(page, size), pageApplications: (page, size, name) => PortalApi.getPortalApplicationPage(page, size, name), - pageGroupConversations: (applicationId, page, size) => ConversationApi.getConversationPage(applicationId, page, size), + pageGroupConversations: (applicationId, page, size) => ConversationApi.getPortalConversationPage(applicationId, page, size), removeConversation: (applicationId, chatId) => ConversationApi.deleteConversation(applicationId, chatId), currentApplicationId, isApplicationListActive,