diff --git a/apps/homepage/__init__.py b/apps/homepage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/homepage/admin.py b/apps/homepage/admin.py new file mode 100644 index 00000000000..8c38f3f3dad --- /dev/null +++ b/apps/homepage/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/homepage/api/home_page_api.py b/apps/homepage/api/home_page_api.py new file mode 100644 index 00000000000..1da38c7a96e --- /dev/null +++ b/apps/homepage/api/home_page_api.py @@ -0,0 +1,284 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: home_page_api.py + @date:2026/5/18 16:02 + @desc: +""" + +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import ( + OpenApiParameter, + OpenApiTypes, + inline_serializer, +) +from rest_framework import serializers + +from common.mixins.api_mixin import APIMixin + + +class RankingBaseAPI(APIMixin): + + @staticmethod + def get_request(): + return None + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + description=_("Workspace ID"), + ), + OpenApiParameter( + name="current_page", + type=OpenApiTypes.INT, + location=OpenApiParameter.PATH, + required=True, + description=_("Current page"), + ), + OpenApiParameter( + name="page_size", + type=OpenApiTypes.INT, + location=OpenApiParameter.PATH, + required=True, + description=_("Page size"), + ), + ] + + +class ApplicationTokensRankingAPI(RankingBaseAPI): + + @staticmethod + def get_response(): + return inline_serializer( + name="ApplicationTokensRankingResponse", + fields={ + "code": serializers.IntegerField(help_text=_("Response code")), + "message": serializers.CharField(help_text=_("Response message")), + "data": inline_serializer( + name="ApplicationTokensRankingPage", + fields={ + "total": serializers.IntegerField(help_text=_("Total count")), + "records": serializers.ListField( + help_text=_("Application tokens ranking list"), + child=inline_serializer( + name="ApplicationTokensRankingItem", + fields={ + "application_id": serializers.CharField(help_text=_("Application ID")), + "application_name": serializers.CharField(help_text=_("Application name")), + "total_tokens": serializers.IntegerField(help_text=_("Total consumed tokens")), + }, + ), + ), + }, + ), + }, + ) + + +class ApplicationQuestionRankingAPI(RankingBaseAPI): + + @staticmethod + def get_response(): + return inline_serializer( + name="ApplicationQuestionRankingResponse", + fields={ + "code": serializers.IntegerField(help_text=_("Response code")), + "message": serializers.CharField(help_text=_("Response message")), + "data": inline_serializer( + name="ApplicationQuestionRankingPage", + fields={ + "total": serializers.IntegerField(help_text=_("Total count")), + "records": serializers.ListField( + help_text=_("Application question ranking list"), + child=inline_serializer( + name="ApplicationQuestionRankingItem", + fields={ + "application_id": serializers.CharField(help_text=_("Application ID")), + "application_name": serializers.CharField(help_text=_("Application name")), + "chat_record_count": serializers.IntegerField(help_text=_("Question count")), + }, + ), + ), + }, + ), + }, + ) + + +class UserTokensRankingAPI(RankingBaseAPI): + + @staticmethod + def get_response(serializer=inline_serializer(name="UserTokensRankingResponse", fields={ + "code": serializers.IntegerField(help_text=_("Response code")), + "message": serializers.CharField(help_text=_("Response message")), + "data": inline_serializer(name="UserTokensRankingPage", + fields={"total": serializers.IntegerField(help_text=_("Total count")), + "records": serializers.ListField(help_text=_("User tokens ranking list"), + child=inline_serializer( + name="UserTokensRankingItem", fields={ + "chat_user_id": serializers.CharField( + help_text=_("Chat user ID")), + "chat_user_type": serializers.CharField( + help_text=_("Chat user type")), + "total_tokens": serializers.IntegerField( + help_text=_( + "Total consumed tokens")), + "chat_record_count": serializers.IntegerField( + help_text=_("Question count")), + "asker": serializers.JSONField( + help_text=_( + "Asker user information")), }, ), ), }, ), }, )): + return serializer + + +class ApplicationAggregationAPI(APIMixin): + + @staticmethod + def get_request(): + return None + + @staticmethod + def get_response(): + return inline_serializer( + name="ApplicationAggregationResponse", + fields={ + "code": serializers.IntegerField(help_text=_("Response code")), + "message": serializers.CharField(help_text=_("Response message")), + "data": inline_serializer( + name="ApplicationAggregationData", + fields={ + "total": serializers.IntegerField(help_text=_("Total application count")), + "published": serializers.IntegerField(help_text=_("Published application count")), + "unpublished": serializers.IntegerField(help_text=_("Unpublished application count")), + }, + ), + }, + ) + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + description=_("Workspace ID"), + ), + ] + + +class KnowledgeAggregationAPI(APIMixin): + + @staticmethod + def get_request(): + return None + + @staticmethod + def get_response(): + return inline_serializer( + name="KnowledgeAggregationResponse", + fields={ + "code": serializers.IntegerField(help_text=_("Response code")), + "message": serializers.CharField(help_text=_("Response message")), + "data": inline_serializer( + name="KnowledgeAggregationData", + fields={ + "total": serializers.IntegerField(help_text=_("Total knowledge count")), + "document_count": serializers.IntegerField(help_text=_("Total document count")), + "failed_document_count": serializers.IntegerField(help_text=_("Failed document count")), + }, + ), + }, + ) + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + description=_("Workspace ID"), + ), + ] + + +class ToolAggregationAPI(APIMixin): + + @staticmethod + def get_request(): + return None + + @staticmethod + def get_response(): + return inline_serializer( + name="ToolAggregationResponse", + fields={ + "code": serializers.IntegerField(help_text=_("Response code")), + "message": serializers.CharField(help_text=_("Response message")), + "data": inline_serializer( + name="ToolAggregationData", + fields={ + "total": serializers.IntegerField(help_text=_("Total tool count")), + "active": serializers.IntegerField(help_text=_("Active tool count")), + "inactive": serializers.IntegerField(help_text=_("Inactive tool count")), + }, + ), + }, + ) + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + description=_("Workspace ID"), + ), + ] + +class ModelAggregationAPI(APIMixin): + + @staticmethod + def get_request(): + return None + + @staticmethod + def get_response(): + return inline_serializer( + name="ModelAggregationResponse", + fields={ + "code": serializers.IntegerField(help_text=_("Response code")), + "message": serializers.CharField(help_text=_("Response message")), + "data": inline_serializer( + name="ModelAggregationData", + fields={ + "total": serializers.IntegerField(help_text=_("Total model count")), + "active": serializers.IntegerField(help_text=_("Active model count")), + "inactive": serializers.IntegerField(help_text=_("Inactive model count")), + }, + ), + }, + ) + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + description=_("Workspace ID"), + ), + ] diff --git a/apps/homepage/apps.py b/apps/homepage/apps.py new file mode 100644 index 00000000000..39b7fd98206 --- /dev/null +++ b/apps/homepage/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HomepageConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'homepage' diff --git a/apps/homepage/migrations/__init__.py b/apps/homepage/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/homepage/serializers/__init__.py b/apps/homepage/serializers/__init__.py new file mode 100644 index 00000000000..27428155739 --- /dev/null +++ b/apps/homepage/serializers/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2026/5/13 14:31 + @desc: +""" diff --git a/apps/homepage/serializers/homepage.py b/apps/homepage/serializers/homepage.py new file mode 100644 index 00000000000..e4ed19e71ae --- /dev/null +++ b/apps/homepage/serializers/homepage.py @@ -0,0 +1,527 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: homepage.py + @date:2026/5/13 14:34 + @desc: +""" +import datetime +import os +from typing import List, Dict + +from django.db import models +from django.db.models import QuerySet, Count, Q, UUIDField, Sum, F, BigIntegerField, Value, ExpressionWrapper, \ + IntegerField, OuterRef, Subquery, JSONField +from django.db.models.functions import Cast, Coalesce +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from application.models import Application, ApplicationChatUserStats, Chat +from common.constants.permission_constants import RoleConstants +from common.db.search import native_search, get_dynamics_model, page_search +from common.utils.common import get_file_content +from knowledge.models import Knowledge +from maxkb.conf import PROJECT_DIR +from models_provider.base_model_provider import ModelTypeConst +from models_provider.models import Model +from system_manage.models import WorkspaceUserResourcePermission +from tools.models import Tool, ToolType + + +def hasPermission(auth, permission): + if 'USER' in auth.role_list: + return True + if permission in auth.permission_list: + return True + return False + + +def is_workspace_manage(auth, workspace_id): + return RoleConstants.WORKSPACE_MANAGE.value.__str__() + ":/WORKSPACE/" + workspace_id in auth.role_list + + +class HomePageSerializer(serializers.Serializer): + class ApplicationUserTokenRanking(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_("Workspace ID")) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + + def ranking(self, auth, current_page, page_size, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + + workspace_id = self.validated_data.get("workspace_id") + user_id = self.validated_data.get("user_id") + + base_queryset = Chat.objects.filter( + is_deleted=False, + chat_user_id__isnull=False, + ).exclude( + chat_user_id="" + ) + + workspace_manage = is_workspace_manage(auth, workspace_id) + if workspace_manage: + base_queryset = base_queryset.filter( + application__workspace_id=workspace_id + ) + else: + permission_list = ( + ["VIEW", "MANAGE", "ROLE"] + if hasPermission(auth, "APPLICATION:READ") + else ["VIEW", "MANAGE"] + ) + + application_id_queryset = QuerySet(WorkspaceUserResourcePermission).filter( + workspace_id=workspace_id, + user_id=user_id, + auth_type="APPLICATION", + permission_list__overlap=permission_list, + ).annotate( + target_uuid=Cast("target", output_field=UUIDField()) + ).values_list( + "target_uuid", + flat=True + ) + + base_queryset = base_queryset.filter( + application_id__in=application_id_queryset + ) + + token_expr = ExpressionWrapper( + F("chatrecord__message_tokens") + F("chatrecord__answer_tokens"), + output_field=BigIntegerField() + ) + + latest_asker_queryset = base_queryset.filter( + chat_user_id=OuterRef("chat_user_id"), + chat_user_type=OuterRef("chat_user_type"), + ).order_by( + "-create_time" + ).values( + "asker" + )[:1] + + queryset = base_queryset.values( + "chat_user_id", + "chat_user_type", + ).annotate( + total_tokens=Coalesce( + Sum(token_expr), + Value(0), + output_field=BigIntegerField() + ), + chat_record_count=Count( + "chatrecord__id", + distinct=True + ), + asker=Subquery( + latest_asker_queryset, + output_field=JSONField() + ) + ).order_by( + "-total_tokens" + ) + + return page_search( + current_page, + page_size, + queryset, + lambda item: { + "chat_user_id": item["chat_user_id"], + "chat_user_type": item["chat_user_type"], + "asker": item["asker"], + "total_tokens": item["total_tokens"], + "chat_record_count": item["chat_record_count"], + } + ) + + class ApplicationQuestionRanking(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_('Workspace ID')) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + + def ranking(self, auth, current_page, page_size, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + + workspace_id = self.validated_data.get("workspace_id") + user_id = self.validated_data.get("user_id") + queryset = Application.objects.filter(workspace_id=workspace_id) + workspace_manage = is_workspace_manage(auth, workspace_id) + if not workspace_manage: + permission_list = ( + ["VIEW", "MANAGE", "ROLE"] + if hasPermission(auth, "APPLICATION:READ") + else ["VIEW", "MANAGE"] + ) + + queryset = queryset.filter( + id__in=QuerySet(WorkspaceUserResourcePermission) + .filter( + workspace_id=workspace_id, + user_id=user_id, + auth_type="APPLICATION", + permission_list__overlap=permission_list, + ) + .annotate( + target_uuid=Cast("target", output_field=UUIDField()) + ) + .values_list("target_uuid", flat=True) + ) + + queryset = queryset.annotate( + # 问题数 / 对话轮次数量 + chat_record_count_total=Coalesce( + Sum( + "chat__chat_record_count", + filter=Q(chat__is_deleted=False), + ), + Value(0), + output_field=BigIntegerField(), + ), + + # 对话用户数量,按 chat_user_id 去重 + chat_user_count=Count( + "chat__chat_user_id", + filter=( + Q(chat__is_deleted=False) + & Q(chat__chat_user_id__isnull=False) + & ~Q(chat__chat_user_id="") + ), + distinct=True, + ), + ).order_by( + "-chat_record_count_total" + ) + + return page_search( + current_page, + page_size, + queryset, + lambda a: { + "id": a.id, + "name": a.name, + "chat_record_count": a.chat_record_count_total, + "chat_user_count": a.chat_user_count, + }, + ) + + class ApplicationTokensRanking(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_('Workspace ID')) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + + def ranking(self, auth, current_page, page_size, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + + workspace_id = self.data.get("workspace_id") + user_id = self.data.get("user_id") + + token_expr = ExpressionWrapper( + F("chat__chatrecord__message_tokens") + F("chat__chatrecord__answer_tokens"), + output_field=BigIntegerField() + ) + + queryset = Application.objects.all() + + workspace_manage = is_workspace_manage(auth, workspace_id) + + if workspace_manage: + queryset = queryset.filter(workspace_id=workspace_id) + else: + permission_list = ["VIEW", "MANAGE", "ROLE"] if hasPermission( + auth, + "APPLICATION:READ" + ) else ["VIEW", "MANAGE"] + + queryset = queryset.filter( + id__in=QuerySet(WorkspaceUserResourcePermission) + .filter( + workspace_id=workspace_id, + user_id=user_id, + auth_type="APPLICATION", + permission_list__overlap=permission_list + ) + .annotate(target_uuid=Cast("target", output_field=UUIDField())) + .values_list("target_uuid", flat=True) + ) + + queryset = queryset.annotate( + total_tokens=Coalesce( + Sum( + token_expr, + filter=Q(chat__is_deleted=False) + ), + Value(0), + output_field=BigIntegerField() + ), + chat_record_count_total=Count( + "chat__chatrecord__id", + filter=Q(chat__is_deleted=False), + output_field=IntegerField() + ) + ).order_by("-total_tokens") + + return page_search( + current_page, + page_size, + queryset, + lambda a: { + "id": a.id, + "name": a.name, + "total_tokens": a.total_tokens, + "chat_record_count": a.chat_record_count_total, + } + ) + + class ApplicationMonitoring(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_('Workspace ID')) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + application_id = serializers.UUIDField(required=False, allow_null=True, label=_("Application ID")) + start_time = serializers.DateField(format='%Y-%m-%d', label=_("Start time")) + end_time = serializers.DateField(format='%Y-%m-%d', label=_("End time")) + + def get_end_time(self): + d = datetime.datetime.strptime(self.data.get('end_time'), '%Y-%m-%d').date() + naive = datetime.datetime.combine(d, datetime.time.max) + return timezone.make_aware(naive, timezone.get_default_timezone()) + + def get_start_time(self): + d = datetime.datetime.strptime(self.data.get('start_time'), '%Y-%m-%d').date() + naive = datetime.datetime.combine(d, datetime.time.min) + return timezone.make_aware(naive, timezone.get_default_timezone()) + + def get_customer_count_trend(self, application_queryset, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + start_time = self.get_start_time() + end_time = self.get_end_time() + query_set = QuerySet(ApplicationChatUserStats).filter( + create_time__gte=start_time, + create_time__lte=end_time) + application_id = self.data.get('application_id') + if application_id: + query_set.filter(application_id=application_id) + else: + query_set.filter(application_id__in=application_queryset) + return native_search( + {'default_sql': query_set}, + select_string=get_file_content( + os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'customer_count_trend.sql'))) + + def get_chat_record_aggregate_trend(self, auth, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + user_id = self.data.get("user_id") + workspace_id = self.data.get("workspace_id") + start_time = self.get_start_time() + end_time = self.get_end_time() + application_id = self.data.get('application_id') + applicationSerializer = HomePageSerializer.Application( + data={"user_id": user_id, 'workspace_id': workspace_id}) + applicationSerializer.is_valid(raise_exception=True) + application_query_set = applicationSerializer.get_aggregation_query_set( + auth) + chat_record_aggregate_trend = native_search( + {'default_sql': QuerySet(model=get_dynamics_model( + {'application_chat.application_id': models.UUIDField(), + 'application_chat_record.create_time': models.DateTimeField()})).filter( + **{**({'application_chat.application_id': application_id} if application_id else { + 'application_chat.application_id__in': application_query_set}), + 'application_chat_record.create_time__gte': start_time, + 'application_chat_record.create_time__lte': end_time} + )}, + select_string=get_file_content( + os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'chat_record_count_trend.sql'))) + customer_count_trend = self.get_customer_count_trend(application_query_set, with_valid=False) + return self.merge_customer_chat_record(chat_record_aggregate_trend, customer_count_trend) + + def merge_customer_chat_record(self, chat_record_aggregate_trend: List[Dict], customer_count_trend: List[Dict]): + + return [{**self.find(chat_record_aggregate_trend, lambda c: c.get('day').strftime('%Y-%m-%d') == day, + {'star_num': 0, 'trample_num': 0, 'tokens_num': 0, 'chat_record_count': 0, + 'customer_num': 0, + 'day': day}), + **self.find(customer_count_trend, lambda c: c.get('day').strftime('%Y-%m-%d') == day, + {'customer_added_count': 0})} + for + day in + self.get_days_between_dates(self.data.get('start_time'), self.data.get('end_time'))] + + @staticmethod + def find(source_list, condition, default): + value_list = [row for row in source_list if condition(row)] + if len(value_list) > 0: + return value_list[0] + return default + + @staticmethod + def get_days_between_dates(start_date, end_date): + start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d') + end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') + days = [] + current_date = start_date + while current_date <= end_date: + days.append(current_date.strftime('%Y-%m-%d')) + current_date += datetime.timedelta(days=1) + return days + + class Application(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_('Workspace ID')) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + + def get_aggregation_query_set(self, auth): + workspace_id = self.data.get("workspace_id") + user_id = self.data.get("user_id") + workspace_manage = is_workspace_manage(auth, workspace_id) + if workspace_manage: + return QuerySet(Application).filter(workspace_id=workspace_id) + permission_list = ["VIEW", "MANAGE", "ROLE"] if hasPermission(auth, "APPLICATION:READ") else ['VIEW', + 'MANAGE'] + return QuerySet(Application).filter( + id__in=QuerySet(WorkspaceUserResourcePermission) + .filter(workspace_id=workspace_id, + user_id=user_id, + auth_type="APPLICATION", + permission_list__overlap=permission_list + ).annotate(target_uuid=Cast("target", output_field=UUIDField())) + .values_list("target_uuid", flat=True)) + + def aggregation(self, auth, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + query_set = self.get_aggregation_query_set(auth) + result = query_set.aggregate( + total=Count("id"), + publish_count=Count("id", filter=Q(is_publish=True)), + un_publish_count=Count("id", filter=Q(is_publish=False)), + ) + return { + "total": result["total"], + "publish_count": result["publish_count"], + "un_publish_count": result["un_publish_count"], + } + + class Knowledge(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_('Workspace ID')) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + + def get_aggregation_query_set(self, auth): + workspace_id = self.data.get("workspace_id") + user_id = self.data.get("user_id") + if is_workspace_manage(auth, workspace_id): + return QuerySet(Knowledge).filter(workspace_id=workspace_id) + permission_list = ["VIEW", "MANAGE", "ROLE"] if hasPermission(auth, "APPLICATION:READ") else ['VIEW', + 'MANAGE'] + return QuerySet(Knowledge).filter( + id__in=QuerySet(WorkspaceUserResourcePermission).filter(workspace_id=workspace_id, + user_id=user_id, + auth_type="KNOWLEDGE", + permission_list__overlap=permission_list + ).annotate( + target_uuid=Cast("target", output_field=UUIDField())) + .values_list("target_uuid", flat=True)) + + def aggregation(self, auth, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + query_set = self.get_aggregation_query_set(auth) + result = query_set.aggregate( + total=Count("id", distinct=True), + document_count=Count( + "document", + distinct=True, + ), + failure_count=Count( + "document", + filter=Q( + document__status__contains="3", + ), + distinct=True, + ), + ) + return { + "total": result["total"] or 0, + "document_count": result["document_count"] or 0, + "failure_count": result["failure_count"] or 0, + } + + class Tool(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_('Workspace ID')) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + + def get_aggregation_query_set(self, auth): + workspace_id = self.data.get("workspace_id") + user_id = self.data.get("user_id") + if is_workspace_manage(auth, workspace_id): + return QuerySet(Tool).filter(workspace_id=workspace_id) + permission_list = ["VIEW", "MANAGE", "ROLE"] if hasPermission(auth, "APPLICATION:READ") else ['VIEW', + 'MANAGE'] + return QuerySet(Tool).filter( + id__in=QuerySet(WorkspaceUserResourcePermission).filter(workspace_id=workspace_id, + user_id=user_id, + auth_type="TOOL", + permission_list__overlap=permission_list + ).annotate( + target_uuid=Cast("target", output_field=UUIDField())) + .values_list("target_uuid", flat=True)) + + def aggregation(self, auth, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + query_set = self.get_aggregation_query_set(auth) + result = query_set.aggregate( + total=Count("id"), + custom_count=Count("id", filter=Q(tool_type=ToolType.CUSTOM)), + skill_count=Count("id", filter=Q(tool_type=ToolType.SKILL)), + mcp_count=Count("id", filter=Q(tool_type=ToolType.MCP)), + workflow_count=Count("id", filter=Q(tool_type=ToolType.WORKFLOW)), + data_source_count=Count("id", filter=Q(tool_type=ToolType.DATA_SOURCE)), + ) + return { + "total": result["total"] or 0, + "custom_count": result["custom_count"] or 0, + "skill_count": result["skill_count"] or 0, + "mcp_count": result["mcp_count"] or 0, + "workflow_count": result["workflow_count"] or 0, + "data_source_count": result["data_source_count"] or 0, + } + + class Model(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_('Workspace ID')) + user_id = serializers.UUIDField(required=True, label=_("User ID")) + + def get_aggregation_query_set(self, auth): + workspace_id = self.data.get("workspace_id") + user_id = self.data.get("user_id") + if is_workspace_manage(auth, workspace_id): + return QuerySet(Model).filter(workspace_id=workspace_id) + permission_list = ["VIEW", "MANAGE", "ROLE"] if hasPermission(auth, "APPLICATION:READ") else ['VIEW', + 'MANAGE'] + return QuerySet(Model).filter( + id__in=QuerySet(WorkspaceUserResourcePermission).filter(workspace_id=workspace_id, + user_id=user_id, + auth_type="MODEL", + permission_list__overlap=permission_list + ).annotate( + target_uuid=Cast("target", output_field=UUIDField())) + .values_list("target_uuid", flat=True)) + + def aggregation(self, auth, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + query_set = self.get_aggregation_query_set(auth) + result = query_set.aggregate( + total=Count("id"), + embedding_count=Count("id", filter=Q(model_type=ModelTypeConst.EMBEDDING.name)), + llm_count=Count("id", filter=Q(model_type=ModelTypeConst.LLM.name)), + ) + total = result["total"] or 0 + embedding_count = result["embedding_count"] or 0 + llm_count = result["llm_count"] or 0 + return { + "total": total, + "embedding_count": embedding_count, + "llm_count": llm_count + } diff --git a/apps/homepage/sql/has_permission.sql b/apps/homepage/sql/has_permission.sql new file mode 100644 index 00000000000..1d723665ec3 --- /dev/null +++ b/apps/homepage/sql/has_permission.sql @@ -0,0 +1,12 @@ +select exists ( + select 1 + from user_role_relation urr + left join role_permission rp + on rp.role_id = urr.role_id + where urr.user_id = %s + and urr.workspace_id = %s + and ( + urr.role_id = any (array['USER']) + or rp.permission_id = %s + ) +) as has_permission; \ No newline at end of file diff --git a/apps/homepage/tests.py b/apps/homepage/tests.py new file mode 100644 index 00000000000..7ce503c2dd9 --- /dev/null +++ b/apps/homepage/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/homepage/urls.py b/apps/homepage/urls.py new file mode 100644 index 00000000000..d574c7db471 --- /dev/null +++ b/apps/homepage/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from . import views + +app_name = "homepage" +# @formatter:off + +urlpatterns = [ + path("workspace//homepage/application/aggregation",views.HomePageAPI.ApplicationAggregation.as_view()), + path("workspace//homepage/application/tokens_ranking//",views.HomePageAPI.ApplicationTokensRanking.as_view()), + path("workspace//homepage/application/question_ranking//",views.HomePageAPI.ApplicationQuestionRanking.as_view()), + path("workspace//homepage/application/user_tokens_ranking//",views.HomePageAPI.UserTokensRanking.as_view()), + path("workspace//homepage/monitoring/aggregation",views.HomePageAPI.ApplicationMonitoring.as_view()), + path("workspace//homepage/knowledge/aggregation",views.HomePageAPI.KnowledgeAggregation.as_view()), + path("workspace//homepage/tool/aggregation",views.HomePageAPI.ToolAggregation.as_view()), + path("workspace//homepage/model/aggregation",views.HomePageAPI.ModelAggregation.as_view()) +] diff --git a/apps/homepage/views/__init__.py b/apps/homepage/views/__init__.py new file mode 100644 index 00000000000..2c39e42d9bb --- /dev/null +++ b/apps/homepage/views/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2026/5/13 14:31 + @desc: +""" +from .homepage import * diff --git a/apps/homepage/views/homepage.py b/apps/homepage/views/homepage.py new file mode 100644 index 00000000000..05e0d9ff8f3 --- /dev/null +++ b/apps/homepage/views/homepage.py @@ -0,0 +1,170 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: homepage.py + @date:2026/5/13 16:40 + @desc: +""" +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from application.api.application_stats import ApplicationStatsAPI +from common import result +from common.auth import TokenAuth +from homepage.api.home_page_api import ApplicationTokensRankingAPI, ApplicationQuestionRankingAPI, UserTokensRankingAPI, \ + ApplicationAggregationAPI, KnowledgeAggregationAPI, ToolAggregationAPI, ModelAggregationAPI +from homepage.serializers.homepage import HomePageSerializer +from django.utils.translation import gettext_lazy as _ + + +class HomePageAPI(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + description=_("Top applications by token consumption"), + summary=_("Top applications by token consumption"), + operation_id="homepage_application_tokens_ranking", + parameters=ApplicationTokensRankingAPI.get_parameters(), + responses=ApplicationTokensRankingAPI.get_response(), + tags=[_("Home page")], + ) + class ApplicationTokensRanking(APIView): + authentication_classes = [TokenAuth] + + def get(self, request: Request, workspace_id: str, current_page: int, page_size: int): + return result.success(HomePageSerializer.ApplicationTokensRanking( + data={'user_id': request.user.id, 'workspace_id': workspace_id}) + .ranking(request.auth, current_page, page_size)) + + @extend_schema( + methods=["GET"], + description=_("Top applications by question count"), + summary=_("Top applications by question count"), + operation_id="homepage_application_question_ranking", + parameters=ApplicationQuestionRankingAPI.get_parameters(), + responses=ApplicationQuestionRankingAPI.get_response(), + tags=[_("Home page")], + ) + class ApplicationQuestionRanking(APIView): + authentication_classes = [TokenAuth] + + def get(self, request: Request, workspace_id: str, current_page: int, page_size: int): + return result.success(HomePageSerializer.ApplicationQuestionRanking( + data={'user_id': request.user.id, 'workspace_id': workspace_id}) + .ranking(request.auth, current_page, page_size)) + + class UserTokensRanking(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + description=_("Top users by token consumption"), + summary=_("Top users by token consumption"), + operation_id="homepage_user_tokens_ranking", + parameters=UserTokensRankingAPI.get_parameters(), + responses=UserTokensRankingAPI.get_response(), + tags=[_("Home page")], + ) + def get(self, request: Request, workspace_id: str, current_page: int, page_size: int): + return result.success(HomePageSerializer.ApplicationUserTokenRanking( + data={'user_id': request.user.id, 'workspace_id': workspace_id}) + .ranking(request.auth, current_page, page_size)) + + class ApplicationMonitoring(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_('Dialogue-related statistical trends'), + summary=_('Dialogue-related statistical trends'), + operation_id='Dialogue-related statistical trends', # type: ignore + parameters=ApplicationStatsAPI.get_parameters(), + responses=ApplicationStatsAPI.get_response(), + tags=[_('Home page')] # type: ignore + ) + def get(self, request: Request, workspace_id: str): + return result.success( + HomePageSerializer.ApplicationMonitoring( + data={'application_id': request.query_params.get("application_id"), + "user_id": request.user.id, + 'workspace_id': workspace_id, + 'start_time': request.query_params.get( + 'start_time'), + 'end_time': request.query_params.get( + 'end_time') + }).get_chat_record_aggregate_trend(request.auth)) + + class ApplicationAggregation(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + description=_("Application data aggregation"), + summary=_("Application data aggregation"), + operation_id="homepage_application_aggregation", + parameters=ApplicationAggregationAPI.get_parameters(), + responses=ApplicationAggregationAPI.get_response(), + tags=[_("Home page")], + ) + def get(self, request: Request, workspace_id: str): + return result.success( + HomePageSerializer.Application( + data={'workspace_id': workspace_id, 'user_id': request.user.id}).aggregation( + request.auth)) + + class KnowledgeAggregation(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + description=_("Knowledge data aggregation"), + summary=_("Knowledge data aggregation"), + operation_id="homepage_knowledge_aggregation", + parameters=KnowledgeAggregationAPI.get_parameters(), + responses=KnowledgeAggregationAPI.get_response(), + tags=[_("Home page")], + ) + def get(self, request: Request, workspace_id: str): + return result.success( + HomePageSerializer.Knowledge( + data={'workspace_id': workspace_id, 'user_id': request.user.id}).aggregation( + request.auth)) + + class ToolAggregation(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + description=_("Tool data aggregation"), + summary=_("Tool data aggregation"), + operation_id="homepage_tool_aggregation", + parameters=ToolAggregationAPI.get_parameters(), + responses=ToolAggregationAPI.get_response(), + tags=[_("Home page")], + ) + def get(self, request: Request, workspace_id: str): + return result.success( + HomePageSerializer.Tool( + data={'workspace_id': workspace_id, 'user_id': request.user.id}).aggregation( + request.auth)) + + class ModelAggregation(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["GET"], + description=_("Model data aggregation"), + summary=_("Model data aggregation"), + operation_id="homepage_model_aggregation", + parameters=ModelAggregationAPI.get_parameters(), + responses=ModelAggregationAPI.get_response(), + tags=[_("Home page")], + ) + def get(self, request: Request, workspace_id: str): + return result.success( + HomePageSerializer.Model( + data={'workspace_id': workspace_id, 'user_id': request.user.id}).aggregation( + request.auth)) diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 1f2e7721527..8b814374cbd 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -9252,4 +9252,100 @@ msgid "Publish" msgstr "" msgid "token is required for EVENT triggers" +msgstr "" + +msgid "Home page" +msgstr "" + +msgid "Response code" +msgstr "" + +msgid "Response message" +msgstr "" + +msgid "Total count" +msgstr "" + +msgid "Application data aggregation" +msgstr "" + +msgid "Knowledge data aggregation" +msgstr "" + +msgid "Tool data aggregation" +msgstr "" + +msgid "Model data aggregation" +msgstr "" + +msgid "Total application count" +msgstr "" + +msgid "Published application count" +msgstr "" + +msgid "Unpublished application count" +msgstr "" + +msgid "Total knowledge count" +msgstr "" + +msgid "Total document count" +msgstr "" + +msgid "Failed document count" +msgstr "" + +msgid "Total tool count" +msgstr "" + +msgid "Active tool count" +msgstr "" + +msgid "Inactive tool count" +msgstr "" + +msgid "Total model count" +msgstr "" + +msgid "Active model count" +msgstr "" + +msgid "Inactive model count" +msgstr "" + +msgid "Top applications by token consumption" +msgstr "" + +msgid "Top applications by question count" +msgstr "" + +msgid "Top users by token consumption" +msgstr "" + +msgid "Application tokens ranking list" +msgstr "" + +msgid "Application question ranking list" +msgstr "" + +msgid "User tokens ranking list" +msgstr "" + +msgid "Application name" +msgstr "" + +msgid "Total consumed tokens" +msgstr "" + +msgid "Question count" +msgstr "" + +msgid "Chat user ID" +msgstr "" + +msgid "Chat user type" +msgstr "" + +msgid "Asker user information" msgstr "" \ No newline at end of file diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index 9bafa2a542d..1cc0c4976e4 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -9375,4 +9375,100 @@ msgid "Publish" msgstr "发布" msgid "token is required for EVENT triggers" -msgstr "事件触发器必须设置 token" \ No newline at end of file +msgstr "事件触发器必须设置 token" + +msgid "Home page" +msgstr "首页" + +msgid "Response code" +msgstr "响应码" + +msgid "Response message" +msgstr "响应消息" + +msgid "Total count" +msgstr "总数量" + +msgid "Application data aggregation" +msgstr "应用数据聚合" + +msgid "Knowledge data aggregation" +msgstr "知识库数据聚合" + +msgid "Tool data aggregation" +msgstr "工具数据聚合" + +msgid "Model data aggregation" +msgstr "模型数据聚合" + +msgid "Total application count" +msgstr "应用总数" + +msgid "Published application count" +msgstr "已发布应用数量" + +msgid "Unpublished application count" +msgstr "未发布应用数量" + +msgid "Total knowledge count" +msgstr "知识库总数" + +msgid "Total document count" +msgstr "文档总数" + +msgid "Failed document count" +msgstr "失败文档数量" + +msgid "Total tool count" +msgstr "工具总数" + +msgid "Active tool count" +msgstr "启用工具数量" + +msgid "Inactive tool count" +msgstr "停用工具数量" + +msgid "Total model count" +msgstr "模型总数" + +msgid "Active model count" +msgstr "启用模型数量" + +msgid "Inactive model count" +msgstr "停用模型数量" + +msgid "Top applications by token consumption" +msgstr "tokens 消耗 TOP 智能体" + +msgid "Top applications by question count" +msgstr "提问次数 TOP 智能体" + +msgid "Top users by token consumption" +msgstr "tokens 消耗 TOP 用户" + +msgid "Application tokens ranking list" +msgstr "应用 tokens 消耗排行榜列表" + +msgid "Application question ranking list" +msgstr "应用提问次数排行榜列表" + +msgid "User tokens ranking list" +msgstr "用户 tokens 消耗排行榜列表" + +msgid "Application name" +msgstr "应用名称" + +msgid "Total consumed tokens" +msgstr "消耗 tokens 总数" + +msgid "Question count" +msgstr "提问次数" + +msgid "Chat user ID" +msgstr "对话用户 ID" + +msgid "Chat user type" +msgstr "对话用户类型" + +msgid "Asker user information" +msgstr "提问用户信息" \ No newline at end of file diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index eb2cb07d85d..7d5ea0d8a7c 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -9372,4 +9372,100 @@ msgid "Publish" msgstr "發布" msgid "token is required for EVENT triggers" -msgstr "事件觸發器必須設定 token" \ No newline at end of file +msgstr "事件觸發器必須設定 token" + +msgid "Home page" +msgstr "首頁" + +msgid "Response code" +msgstr "回應碼" + +msgid "Response message" +msgstr "回應訊息" + +msgid "Total count" +msgstr "總數量" + +msgid "Application data aggregation" +msgstr "應用資料彙總" + +msgid "Knowledge data aggregation" +msgstr "知識庫資料彙總" + +msgid "Tool data aggregation" +msgstr "工具資料彙總" + +msgid "Model data aggregation" +msgstr "模型資料彙總" + +msgid "Total application count" +msgstr "應用總數" + +msgid "Published application count" +msgstr "已發布應用數量" + +msgid "Unpublished application count" +msgstr "未發布應用數量" + +msgid "Total knowledge count" +msgstr "知識庫總數" + +msgid "Total document count" +msgstr "文件總數" + +msgid "Failed document count" +msgstr "失敗文件數量" + +msgid "Total tool count" +msgstr "工具總數" + +msgid "Active tool count" +msgstr "啟用工具數量" + +msgid "Inactive tool count" +msgstr "停用工具數量" + +msgid "Total model count" +msgstr "模型總數" + +msgid "Active model count" +msgstr "啟用模型數量" + +msgid "Inactive model count" +msgstr "停用模型數量" + +msgid "Top applications by token consumption" +msgstr "tokens 消耗 TOP 智慧體" + +msgid "Top applications by question count" +msgstr "提問次數 TOP 智慧體" + +msgid "Top users by token consumption" +msgstr "tokens 消耗 TOP 使用者" + +msgid "Application tokens ranking list" +msgstr "應用 tokens 消耗排行榜列表" + +msgid "Application question ranking list" +msgstr "應用提問次數排行榜列表" + +msgid "User tokens ranking list" +msgstr "使用者 tokens 消耗排行榜列表" + +msgid "Application name" +msgstr "應用名稱" + +msgid "Total consumed tokens" +msgstr "消耗 tokens 總數" + +msgid "Question count" +msgstr "提問次數" + +msgid "Chat user ID" +msgstr "對話使用者 ID" + +msgid "Chat user type" +msgstr "對話使用者類型" + +msgid "Asker user information" +msgstr "提問使用者資訊" \ No newline at end of file diff --git a/apps/maxkb/urls/web.py b/apps/maxkb/urls/web.py index fb043d56a64..f43983256fb 100644 --- a/apps/maxkb/urls/web.py +++ b/apps/maxkb/urls/web.py @@ -43,6 +43,7 @@ path(admin_api_prefix, include("application.urls")), path(admin_api_prefix, include("trigger.urls")), path(admin_api_prefix, include("oss.urls")), + path(admin_api_prefix, include("homepage.urls")), path(chat_api_prefix, include("oss.urls")), path(chat_api_prefix, include("chat.urls")), path(f'{admin_ui_prefix[1:]}/', include('oss.retrieval_urls')),