Skip to content

Commit cd53fa4

Browse files
committed
feat: chat portal
1 parent c0d7a84 commit cd53fa4

6 files changed

Lines changed: 395 additions & 44 deletions

File tree

apps/chat/api/portal_api.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:MaxKB
5+
@file: portal.py
6+
@date:2026/8/14
7+
@desc: 门户API文档
8+
"""
9+
10+
from drf_spectacular.types import OpenApiTypes
11+
from drf_spectacular.utils import OpenApiParameter
12+
13+
from common.mixins.api_mixin import APIMixin
14+
from common.result import DefaultResultSerializer
15+
from users.serializers.login import LoginRequest
16+
17+
18+
class PortalAPI(APIMixin):
19+
class Get(APIMixin):
20+
@staticmethod
21+
def get_response():
22+
return DefaultResultSerializer
23+
24+
class Save(APIMixin):
25+
@staticmethod
26+
def get_request():
27+
return {
28+
"multipart/form-data": {
29+
"type": "object",
30+
"properties": {
31+
"name": {"type": "string", "description": "门户名称"},
32+
"description": {"type": "string", "description": "门户描述"},
33+
"logo": {"type": "string", "format": "binary", "description": "门户Logo"},
34+
"tab_logo": {"type": "string", "format": "binary", "description": "浏览器Tab Logo"},
35+
"enable_public_access": {"type": "boolean", "description": "是否开启公开访问"},
36+
"enable_api": {"type": "boolean", "description": "是否开启API服务"},
37+
"enable_auth": {"type": "boolean", "description": "是否开启身份认证"},
38+
"auth_config": {"type": "object", "description": "身份认证配置"},
39+
"enable_cors": {"type": "boolean", "description": "是否开启跨域设置"},
40+
"cors_config": {"type": "object", "description": "跨域配置"},
41+
},
42+
}
43+
}
44+
45+
@staticmethod
46+
def get_response():
47+
return DefaultResultSerializer
48+
49+
class Application(APIMixin):
50+
@staticmethod
51+
def get_parameters():
52+
return [
53+
OpenApiParameter(
54+
name="current_page",
55+
description="当前页码",
56+
type=OpenApiTypes.INT,
57+
location="path",
58+
required=True,
59+
),
60+
OpenApiParameter(
61+
name="page_size",
62+
description="每页数量",
63+
type=OpenApiTypes.INT,
64+
location="path",
65+
required=True,
66+
),
67+
OpenApiParameter(
68+
name="name",
69+
description="应用名称搜索",
70+
type=OpenApiTypes.STR,
71+
location="query",
72+
required=False,
73+
),
74+
]
75+
76+
@staticmethod
77+
def get_response():
78+
return DefaultResultSerializer
79+
80+
class Login(APIMixin):
81+
@staticmethod
82+
def get_request():
83+
return LoginRequest
84+
85+
@staticmethod
86+
def get_response():
87+
return DefaultResultSerializer
88+
89+
class Info(APIMixin):
90+
@staticmethod
91+
def get_response():
92+
return DefaultResultSerializer
93+
94+
class Logout(APIMixin):
95+
@staticmethod
96+
def get_response():
97+
return DefaultResultSerializer
98+
99+
class Conversation(APIMixin):
100+
@staticmethod
101+
def get_parameters():
102+
return [
103+
OpenApiParameter(
104+
name="current_page",
105+
description="当前页码",
106+
type=OpenApiTypes.INT,
107+
location="path",
108+
required=True,
109+
),
110+
OpenApiParameter(
111+
name="page_size",
112+
description="每页数量",
113+
type=OpenApiTypes.INT,
114+
location="path",
115+
required=True,
116+
),
117+
OpenApiParameter(
118+
name="name",
119+
description="应用名称搜索",
120+
type=OpenApiTypes.STR,
121+
location="query",
122+
required=False,
123+
),
124+
]
125+
126+
@staticmethod
127+
def get_response():
128+
return DefaultResultSerializer

apps/chat/serializers/portal.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:MaxKB
5+
@file: portal.py
6+
@date:2026/8/14
7+
@desc: 门户配置序列化器
8+
"""
9+
10+
from django.db.models import Exists, OuterRef
11+
from django.utils.translation import gettext_lazy as _
12+
from rest_framework import serializers
13+
14+
from application.models import Application, Chat
15+
from application.models.application_access_token import ApplicationAccessToken
16+
from common.db.search import page_search
17+
from system_manage.models.chat_user import (
18+
ChatUser,
19+
ResourceChatUserAuthorize,
20+
ResourceChatUserGroupAuthorize,
21+
ResourceType,
22+
UserGroupRelation,
23+
)
24+
25+
26+
class PortalApplicationAuthMixin:
27+
"""门户应用授权过滤公共逻辑"""
28+
29+
@staticmethod
30+
def get_authorized_application_queryset(user_id):
31+
public_apps = ApplicationAccessToken.objects.filter(application_id=OuterRef("id"), authentication=False)
32+
if not ChatUser.objects.filter(id=user_id).exists():
33+
return Application.objects.filter(Exists(public_apps))
34+
authed_token_exists = ApplicationAccessToken.objects.filter(application_id=OuterRef("id"), authentication=True)
35+
direct_auth = ResourceChatUserAuthorize.objects.filter(
36+
resource_id=OuterRef("id"), resource_type=ResourceType.APPLICATION.value, is_auth=True, user_id=user_id
37+
)
38+
user_groups = UserGroupRelation.objects.filter(user_id=user_id).values_list("group_id", flat=True)
39+
group_auth = ResourceChatUserGroupAuthorize.objects.filter(
40+
resource_id=OuterRef("id"),
41+
resource_type=ResourceType.APPLICATION.value,
42+
is_auth=True,
43+
user_group_id__in=user_groups,
44+
)
45+
return Application.objects.filter(
46+
Exists(public_apps) | (Exists(authed_token_exists) & (Exists(direct_auth) | Exists(group_auth)))
47+
)
48+
49+
50+
class ApplicationResponseSerializer(serializers.Serializer):
51+
id = serializers.CharField(required=True)
52+
name = serializers.CharField(required=True)
53+
desc = serializers.CharField(required=True)
54+
icon = serializers.CharField(required=True)
55+
type = serializers.CharField(required=True)
56+
dialogue_number = serializers.IntegerField(required=True)
57+
prologue = serializers.CharField(required=True)
58+
is_publish = serializers.BooleanField(required=True)
59+
60+
61+
class PortalApplicationSerializer(serializers.Serializer):
62+
class Query(PortalApplicationAuthMixin, serializers.Serializer):
63+
name = serializers.CharField(
64+
required=False, allow_blank=True, label=_("Application Name"), help_text=_("Application name")
65+
)
66+
67+
def get_query_set(self):
68+
queryset = Application.objects.filter(is_publish=True)
69+
name = self.data.get("name")
70+
if name:
71+
queryset = queryset.filter(name__icontains=name)
72+
return queryset.order_by("-create_time")
73+
74+
def page(self, current_page, page_size, user_id, with_valid=True):
75+
if with_valid:
76+
self.is_valid(raise_exception=True)
77+
queryset = self.get_query_set()
78+
queryset = queryset.filter(id__in=self.get_authorized_application_queryset(user_id).values("id"))
79+
return page_search(
80+
current_page,
81+
page_size,
82+
queryset,
83+
post_records_handler=lambda app: ApplicationResponseSerializer(app).data,
84+
)
85+
86+
87+
class PortalHistoricalConversationResponseSerializer(serializers.Serializer):
88+
id = serializers.CharField(required=True)
89+
abstract = serializers.CharField(required=True)
90+
create_time = serializers.CharField(required=True)
91+
update_time = serializers.CharField(required=True)
92+
application = serializers.SerializerMethodField()
93+
94+
def get_application(self, chat):
95+
return {
96+
"id": str(chat.application_id),
97+
"name": chat.application.name,
98+
"icon": chat.application.icon,
99+
}
100+
101+
102+
class PortalHistoricalConversationSerializer(serializers.Serializer):
103+
class Query(PortalApplicationAuthMixin, serializers.Serializer):
104+
name = serializers.CharField(
105+
required=False, allow_blank=True, label=_("Application Name"), help_text=_("Application name")
106+
)
107+
108+
def get_query_set(self, user_id):
109+
queryset = Chat.objects.filter(
110+
chat_user_id=user_id,
111+
is_deleted=False,
112+
application_id__in=self.get_authorized_application_queryset(user_id).values("id"),
113+
)
114+
name = self.data.get("name")
115+
if name:
116+
queryset = queryset.filter(application__name__icontains=name)
117+
return queryset.select_related("application").order_by("-update_time", "id")
118+
119+
def page(self, current_page, page_size, user_id, with_valid=True):
120+
if with_valid:
121+
self.is_valid(raise_exception=True)
122+
return page_search(
123+
current_page,
124+
page_size,
125+
self.get_query_set(user_id),
126+
post_records_handler=lambda chat: PortalHistoricalConversationResponseSerializer(chat).data,
127+
)

apps/chat/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from application.views import ChatRecordDetailView, ChatRecordLinkView
44
from chat.views import v2 as v2_views, v3 as v3_views
55

6-
app_name = 'chat'
6+
app_name = "chat"
77
# @formatter:off
88
# fmt: off
99

@@ -41,6 +41,8 @@
4141
path('api_key', v3_views.ChatUserApiKeyView.as_view()),
4242
path('api_key/<int:current_page>/<int:page_size>', v3_views.ChatUserApiKeyView.Page.as_view()),
4343
path('api_key/<str:api_key_id>', v3_views.ChatUserApiKeyView.Operate.as_view()),
44+
path('portal/application/<int:current_page>/<int:page_size>', v3_views.PortalApplicationView.as_view(), name='v3_portal_application'),
45+
path('portal/chat/<int:current_page>/<int:page_size>', v3_views.PortalHistoricalConversationView.as_view(), name='v3_portal_historical_conversation'),
4446
]
4547
v2=[
4648
path('embed', v2_views.ChatEmbedView.as_view()),

apps/chat/views/v3/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# coding=utf-8
22
"""
3-
@project: MaxKB
4-
@Author:虎虎
5-
@file: __init__.py
6-
@date:2025/6/6 11:18
7-
@desc:
3+
@project: MaxKB
4+
@Author:虎虎
5+
@file: __init__.py
6+
@date:2025/6/6 11:18
7+
@desc:
88
"""
9+
910
from .chat_embed import *
1011
from .chat import *
1112
from .chat_record import *
1213
from .chat_user_api_key import *
14+
from .portal import *
1315
from .mcp import mcp_view

apps/chat/views/v3/portal.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:MaxKB
5+
@file: portal.py
6+
@date:2026/8/14
7+
@desc: 门户视图
8+
"""
9+
10+
from django.utils.translation import gettext_lazy as _
11+
from drf_spectacular.utils import extend_schema
12+
from rest_framework.request import Request
13+
from rest_framework.views import APIView
14+
15+
from common import result
16+
from common.auth import ChatTokenAuth
17+
from common.utils.common import query_params_to_single_dict
18+
19+
from chat.api.portal_api import PortalAPI
20+
from chat.serializers.portal import (
21+
PortalApplicationSerializer,
22+
PortalHistoricalConversationSerializer,
23+
)
24+
25+
26+
class PortalApplicationView(APIView):
27+
authentication_classes = [ChatTokenAuth]
28+
29+
@extend_schema(
30+
methods=["GET"],
31+
description=_("Get published application list by page"),
32+
summary=_("Get published application list by page"),
33+
operation_id=_("Get published application list by page"),
34+
parameters=PortalAPI.Application.get_parameters(),
35+
responses=PortalAPI.Application.get_response(),
36+
tags=[_("V3 Chat")],
37+
)
38+
def get(self, request: Request, current_page: int, page_size: int):
39+
return result.success(
40+
PortalApplicationSerializer.Query(data={**query_params_to_single_dict(request.query_params)}).page(
41+
current_page, page_size, str(request.user.id)
42+
)
43+
)
44+
45+
46+
class PortalHistoricalConversationView(APIView):
47+
authentication_classes = [ChatTokenAuth]
48+
49+
@extend_schema(
50+
methods=["GET"],
51+
description=_("Get portal historical conversation by page"),
52+
summary=_("Get portal historical conversation by page"),
53+
operation_id=_("Get portal historical conversation by page"),
54+
parameters=PortalAPI.Conversation.get_parameters(),
55+
responses=PortalAPI.Conversation.get_response(),
56+
tags=[_("V3 Chat")],
57+
)
58+
def get(self, request: Request, current_page: int, page_size: int):
59+
return result.success(
60+
PortalHistoricalConversationSerializer.Query(
61+
data={**query_params_to_single_dict(request.query_params)}
62+
).page(current_page, page_size, str(request.user.id))
63+
)

0 commit comments

Comments
 (0)