Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions apps/chat/api/portal_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# coding=utf-8
"""
@project: MaxKB
@Author:MaxKB
@file: portal.py
@date:2026/8/14
@desc: 门户API文档
"""

from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter

from common.mixins.api_mixin import APIMixin
from common.result import DefaultResultSerializer
from users.serializers.login import LoginRequest


class PortalAPI(APIMixin):
class Get(APIMixin):
@staticmethod
def get_response():
return DefaultResultSerializer

class Save(APIMixin):
@staticmethod
def get_request():
return {
"multipart/form-data": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "门户名称"},
"description": {"type": "string", "description": "门户描述"},
"logo": {"type": "string", "format": "binary", "description": "门户Logo"},
"tab_logo": {"type": "string", "format": "binary", "description": "浏览器Tab Logo"},
"enable_public_access": {"type": "boolean", "description": "是否开启公开访问"},
"enable_api": {"type": "boolean", "description": "是否开启API服务"},
"enable_auth": {"type": "boolean", "description": "是否开启身份认证"},
"auth_config": {"type": "object", "description": "身份认证配置"},
"enable_cors": {"type": "boolean", "description": "是否开启跨域设置"},
"cors_config": {"type": "object", "description": "跨域配置"},
},
}
}

@staticmethod
def get_response():
return DefaultResultSerializer

class Application(APIMixin):
@staticmethod
def get_parameters():
return [
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="name",
description="应用名称搜索",
type=OpenApiTypes.STR,
location="query",
required=False,
),
]

@staticmethod
def get_response():
return DefaultResultSerializer

class Login(APIMixin):
@staticmethod
def get_request():
return LoginRequest

@staticmethod
def get_response():
return DefaultResultSerializer

class Info(APIMixin):
@staticmethod
def get_response():
return DefaultResultSerializer

class Logout(APIMixin):
@staticmethod
def get_response():
return DefaultResultSerializer

class Conversation(APIMixin):
@staticmethod
def get_parameters():
return [
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="name",
description="应用名称搜索",
type=OpenApiTypes.STR,
location="query",
required=False,
),
]

@staticmethod
def get_response():
return DefaultResultSerializer
127 changes: 127 additions & 0 deletions apps/chat/serializers/portal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# coding=utf-8
"""
@project: MaxKB
@Author:MaxKB
@file: portal.py
@date:2026/8/14
@desc: 门户配置序列化器
"""

from django.db.models import Exists, OuterRef
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from application.models import Application, Chat
from application.models.application_access_token import ApplicationAccessToken
from common.db.search import page_search
from system_manage.models.chat_user import (
ChatUser,
ResourceChatUserAuthorize,
ResourceChatUserGroupAuthorize,
ResourceType,
UserGroupRelation,
)


class PortalApplicationAuthMixin:
"""门户应用授权过滤公共逻辑"""

@staticmethod
def get_authorized_application_queryset(user_id):
public_apps = ApplicationAccessToken.objects.filter(application_id=OuterRef("id"), authentication=False)
if not ChatUser.objects.filter(id=user_id).exists():
return Application.objects.filter(Exists(public_apps))
authed_token_exists = ApplicationAccessToken.objects.filter(application_id=OuterRef("id"), authentication=True)
direct_auth = ResourceChatUserAuthorize.objects.filter(
resource_id=OuterRef("id"), resource_type=ResourceType.APPLICATION.value, is_auth=True, user_id=user_id
)
user_groups = UserGroupRelation.objects.filter(user_id=user_id).values_list("group_id", flat=True)
group_auth = ResourceChatUserGroupAuthorize.objects.filter(
resource_id=OuterRef("id"),
resource_type=ResourceType.APPLICATION.value,
is_auth=True,
user_group_id__in=user_groups,
)
return Application.objects.filter(
Exists(public_apps) | (Exists(authed_token_exists) & (Exists(direct_auth) | Exists(group_auth)))
)


class ApplicationResponseSerializer(serializers.Serializer):
id = serializers.CharField(required=True)
name = serializers.CharField(required=True)
desc = serializers.CharField(required=True)
icon = serializers.CharField(required=True)
type = serializers.CharField(required=True)
dialogue_number = serializers.IntegerField(required=True)
prologue = serializers.CharField(required=True)
is_publish = serializers.BooleanField(required=True)


class PortalApplicationSerializer(serializers.Serializer):
class Query(PortalApplicationAuthMixin, serializers.Serializer):
name = serializers.CharField(
required=False, allow_blank=True, label=_("Application Name"), help_text=_("Application name")
)

def get_query_set(self):
queryset = Application.objects.filter(is_publish=True)
name = self.data.get("name")
if name:
queryset = queryset.filter(name__icontains=name)
return queryset.order_by("-create_time")

def page(self, current_page, page_size, user_id, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
queryset = self.get_query_set()
queryset = queryset.filter(id__in=self.get_authorized_application_queryset(user_id).values("id"))
return page_search(
current_page,
page_size,
queryset,
post_records_handler=lambda app: ApplicationResponseSerializer(app).data,
)


class PortalHistoricalConversationResponseSerializer(serializers.Serializer):
id = serializers.CharField(required=True)
abstract = serializers.CharField(required=True)
create_time = serializers.CharField(required=True)
update_time = serializers.CharField(required=True)
application = serializers.SerializerMethodField()

def get_application(self, chat):
return {
"id": str(chat.application_id),
"name": chat.application.name,
"icon": chat.application.icon,
}


class PortalHistoricalConversationSerializer(serializers.Serializer):
class Query(PortalApplicationAuthMixin, serializers.Serializer):
name = serializers.CharField(
required=False, allow_blank=True, label=_("Application Name"), help_text=_("Application name")
)

def get_query_set(self, user_id):
queryset = Chat.objects.filter(
chat_user_id=user_id,
is_deleted=False,
application_id__in=self.get_authorized_application_queryset(user_id).values("id"),
)
name = self.data.get("name")
if name:
queryset = queryset.filter(application__name__icontains=name)
return queryset.select_related("application").order_by("-update_time", "id")

def page(self, current_page, page_size, user_id, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
return page_search(
current_page,
page_size,
self.get_query_set(user_id),
post_records_handler=lambda chat: PortalHistoricalConversationResponseSerializer(chat).data,
)
4 changes: 3 additions & 1 deletion apps/chat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from application.views import ChatRecordDetailView, ChatRecordLinkView
from chat.views import v2 as v2_views, v3 as v3_views

app_name = 'chat'
app_name = "chat"
# @formatter:off
# fmt: off

Expand Down Expand Up @@ -41,6 +41,8 @@
path('api_key', v3_views.ChatUserApiKeyView.as_view()),
path('api_key/<int:current_page>/<int:page_size>', v3_views.ChatUserApiKeyView.Page.as_view()),
path('api_key/<str:api_key_id>', v3_views.ChatUserApiKeyView.Operate.as_view()),
path('portal/application/<int:current_page>/<int:page_size>', v3_views.PortalApplicationView.as_view(), name='v3_portal_application'),
path('portal/chat/<int:current_page>/<int:page_size>', v3_views.PortalHistoricalConversationView.as_view(), name='v3_portal_historical_conversation'),
]
v2=[
path('embed', v2_views.ChatEmbedView.as_view()),
Expand Down
12 changes: 7 additions & 5 deletions apps/chat/views/v3/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# coding=utf-8
"""
@project: MaxKB
@Author:虎虎
@file: __init__.py
@date:2025/6/6 11:18
@desc:
@project: MaxKB
@Author:虎虎
@file: __init__.py
@date:2025/6/6 11:18
@desc:
"""

from .chat_embed import *
from .chat import *
from .chat_record import *
from .chat_user_api_key import *
from .portal import *
from .mcp import mcp_view
63 changes: 63 additions & 0 deletions apps/chat/views/v3/portal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# coding=utf-8
"""
@project: MaxKB
@Author:MaxKB
@file: portal.py
@date:2026/8/14
@desc: 门户视图
"""

from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.views import APIView

from common import result
from common.auth import ChatTokenAuth
from common.utils.common import query_params_to_single_dict

from chat.api.portal_api import PortalAPI
from chat.serializers.portal import (
PortalApplicationSerializer,
PortalHistoricalConversationSerializer,
)


class PortalApplicationView(APIView):
authentication_classes = [ChatTokenAuth]

@extend_schema(
methods=["GET"],
description=_("Get published application list by page"),
summary=_("Get published application list by page"),
operation_id=_("Get published application list by page"),
parameters=PortalAPI.Application.get_parameters(),
responses=PortalAPI.Application.get_response(),
tags=[_("V3 Chat")],
)
def get(self, request: Request, current_page: int, page_size: int):
return result.success(
PortalApplicationSerializer.Query(data={**query_params_to_single_dict(request.query_params)}).page(
current_page, page_size, str(request.user.id)
)
)


class PortalHistoricalConversationView(APIView):
authentication_classes = [ChatTokenAuth]

@extend_schema(
methods=["GET"],
description=_("Get portal historical conversation by page"),
summary=_("Get portal historical conversation by page"),
operation_id=_("Get portal historical conversation by page"),
parameters=PortalAPI.Conversation.get_parameters(),
responses=PortalAPI.Conversation.get_response(),
tags=[_("V3 Chat")],
)
def get(self, request: Request, current_page: int, page_size: int):
return result.success(
PortalHistoricalConversationSerializer.Query(
data={**query_params_to_single_dict(request.query_params)}
).page(current_page, page_size, str(request.user.id))
)
Loading
Loading