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
69 changes: 69 additions & 0 deletions apps/application/api/application_chat_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
@project: MaxKB
@Author: niu
@file: application_chat_link.py
@date: 2026/2/9 16:59
@desc:
"""
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from django.utils.translation import gettext_lazy as _

from application.serializers.application_chat_link import ChatRecordShareLinkRequestSerializer
from common.mixins.api_mixin import APIMixin
from common.result import DefaultResultSerializer


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

@staticmethod
def get_request():
return ChatRecordShareLinkRequestSerializer

@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="Application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="chat_id",
description=_("Chat ID"),
type=OpenApiTypes.STR,
location='path',
required=True,
),
]

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



@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="link",
description="链接",
type=OpenApiTypes.STR,
location='path',
required=True,
)
]
33 changes: 33 additions & 0 deletions apps/application/migrations/0010_chatsharelink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.2.9 on 2026-02-09 02:39

import django.contrib.postgres.fields
import django.db.models.deletion
import uuid_utils.compat
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('application', '0009_clean_application_knowledge_mapping'),
('users', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='ChatShareLink',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('share_type', models.CharField(choices=[('PUBLIC', 'public'), ('PRIVATE', 'private')], default='PUBLIC', max_length=20)),
('chat_record_ids', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), size=None)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')),
('chat', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.chat')),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.user')),
],
options={
'db_table': 'application_chat_share_link',
},
),
]
15 changes: 15 additions & 0 deletions apps/application/models/application_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from application.models import Application
from common.encoder.encoder import SystemEncoder
from common.mixins.app_model_mixin import AppModelMixin
from users.models import User


class ChatUserType(models.TextChoices):
Expand Down Expand Up @@ -64,6 +65,9 @@ class VoteReasonChoices(models.TextChoices):
INCOMPLETE = 'incomplete', '内容不完善'
OTHER = 'other', '其他'

class ShareLinkType(models.TextChoices):
PUBLIC = "PUBLIC", 'public'
PRIVATE = "PRIVATE", 'private'

class ChatSourceChoices(models.TextChoices):
ONLINE = "ONLINE", "线上使用"
Expand Down Expand Up @@ -138,3 +142,14 @@ class Meta:
indexes = [
models.Index(fields=['application_id', 'chat_user_id']),
]

class ChatShareLink(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
chat = models.ForeignKey(Chat, on_delete=models.CASCADE)
application = models.ForeignKey(Application,on_delete=models.CASCADE)
share_type = models.CharField(max_length=20, choices=ShareLinkType.choices, default=ShareLinkType.PUBLIC)
user = models.ForeignKey(User, on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True)
chat_record_ids = ArrayField(base_field=models.UUIDField(max_length=128))

class Meta:
db_table = "application_chat_share_link"
131 changes: 131 additions & 0 deletions apps/application/serializers/application_chat_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
@project: MaxKB
@Author: niu
@file: application_chat_link.py
@date: 2026/2/9 10:50
@desc:
"""
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from application.models import Chat, ChatShareLink, ShareLinkType, ChatRecord
from common.exception.app_exception import AppApiException
from common.utils.chat_link_code import UUIDEncoder
import uuid_utils.compat as uuid


class ShareChatRecordModelSerializer(serializers.ModelSerializer):
class Meta:
model = ChatRecord
fields = ['id', 'problem_text', 'answer_text', 'answer_text_list', 'create_time']

class ChatRecordShareLinkRequestSerializer(serializers.Serializer):
chat_record_ids = serializers.ListSerializer(
child=serializers.UUIDField(),
required=False,
allow_empty=False,
label=_("Chat record IDs")
)
is_current_all = serializers.BooleanField(required=False, default=False)

def validate(self, attrs):
if not attrs.get('is_current_all') and not attrs.get('chat_record_ids'):
raise serializers.ValidationError(_('Chat record ids can not be empty'))
return attrs

class ChatRecordShareLinkSerializer(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
application_id = serializers.UUIDField(required=True, label=_("Application ID"))
user_id = serializers.UUIDField(required=False, label=_("User ID"))

def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
chat_id = self.data.get('chat_id')
application_id = self.data.get('application_id')

chat_query_set = Chat.objects.filter(id=chat_id, application_id=application_id, is_deleted=False)
if not chat_query_set.exists():
raise AppApiException(500, _('Chat id does not exist'))

def generate_link(self, instance, with_valid=True):
if with_valid:
request_serializer = ChatRecordShareLinkRequestSerializer(data=instance)
request_serializer.is_valid(raise_exception=True)
self.is_valid(raise_exception=True)
if not instance.get('is_current_all', False):
chat_record_ids: list[str] = instance.get('chat_record_ids')

record_count = ChatRecord.objects.filter(id__in=chat_record_ids, chat_id=self.data.get('chat_id')).count()
if record_count != len(chat_record_ids):
raise AppApiException(500, _('Invalid chat record ids'))
chat_id = self.data.get('chat_id')
application_id = self.data.get('application_id')
user_id = self.data.get('user_id')

is_current_all = instance.get('is_current_all', False)
if is_current_all:
sorted_ids = list(
ChatRecord.objects.filter(chat_id=chat_id).order_by('create_time').values_list('id',flat=True)
)
else:
chat_record_ids: list[str] = instance.get('chat_record_ids')
sorted_ids = list(ChatRecord.objects.filter(id__in=chat_record_ids).order_by('create_time').values_list('id',flat=True))

existing = ChatShareLink.objects.filter(
chat_id=chat_id, application_id=application_id,
share_type=ShareLinkType.PUBLIC,
user_id=user_id,
chat_record_ids=sorted_ids
).first()

if existing:
return {'link': UUIDEncoder.encode(existing.id)}

chat_share_link_model = ChatShareLink(
id=uuid.uuid7(),
chat_id=chat_id,
application_id=application_id,
share_type=ShareLinkType.PUBLIC,
user_id=user_id,
chat_record_ids=sorted_ids
)
chat_share_link_model.save()

link = UUIDEncoder.encode(chat_share_link_model.id)

return {'link': link}


class ChatShareLinkDetailSerializer(serializers.Serializer):
link = serializers.CharField(required=True, label=_("Link"))

def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)

link = self.data.get('link')
share_link_id = UUIDEncoder.decode_to_str(link)

share_link_query_set = ChatShareLink.objects.filter(id=share_link_id).first()
if not share_link_query_set:
raise AppApiException(500, _('Share link does not exist'))
if share_link_query_set.chat.is_deleted:
raise AppApiException(500, _('Chat has been deleted'))

return share_link_query_set

def get_record_list(self):
share_link_model = self.is_valid(raise_exception=True)

chat_record_model_list = ChatRecord.objects.filter(id__in=share_link_model.chat_record_ids,
chat_id=share_link_model.chat_id).order_by('create_time')

abstract = Chat.objects.filter(
id=share_link_model.chat_id
).values_list('abstract', flat=True).first()
chat_record_list = ShareChatRecordModelSerializer(chat_record_model_list, many=True).data

return {
'abstract': abstract,
'chat_record_list': chat_record_list
}
3 changes: 2 additions & 1 deletion apps/application/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
path('workspace/<str:workspace_id>/application/<str:application_id>/chat', views.ApplicationChat.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/export', views.ApplicationChat.Export.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<int:current_page>/<int:page_size>', views.ApplicationChat.Page.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/share_chat', views.ChatRecordLinkView.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record', views.ApplicationChatRecord.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record/<str:chat_record_id>', views.ApplicationChatRecordOperateAPI.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record/<int:current_page>/<int:page_size>', views.ApplicationChatRecord.Page.as_view()),
Expand All @@ -39,5 +40,5 @@
path('workspace/<str:workspace_id>/application/<str:application_id>/mcp_tools', views.McpServers.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/model/<str:model_id>/prompt_generate', views.PromptGenerateView.as_view()),
path('chat_message/<str:chat_id>', views.ChatView.as_view()),

path('chat/share/<str:link>', views.ChatRecordDetailView.as_view()),
]
1 change: 1 addition & 0 deletions apps/application/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
from .application_stats import *
from .application_chat import *
from .application_chat_record import *
from .application_chat_link import *
56 changes: 56 additions & 0 deletions apps/application/views/application_chat_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
@project: MaxKB
@Author: niu
@file: application_chat_link.py
@date: 2026/2/9 10:44
@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 application.api.application_chat_link import ChatRecordLinkAPI, ChatRecordDetailShareAPI
from application.serializers.application_chat_link import ChatRecordShareLinkSerializer, ChatShareLinkDetailSerializer
from common import result
from common.auth import ChatTokenAuth


class ChatRecordLinkView(APIView):
authentication_classes = [ChatTokenAuth]

@extend_schema(
methods=['POST'],
description=_("Generate share link"),
summary=_("Generate share link"),
operation_id=_("Generate share link"), # type: ignore
request=ChatRecordLinkAPI.get_request(),
parameters=ChatRecordLinkAPI.get_parameters(),
responses=ChatRecordLinkAPI.get_response(),
tags=[_("Chat record link")] # type: ignore
)

def post(self, request: Request, workspace_id: str, application_id: str, chat_id: str):
return result.success(ChatRecordShareLinkSerializer(data={
"workspace_id": workspace_id,
"application_id": application_id,
"chat_id": chat_id,
"user_id": request.auth.chat_user_id
}).generate_link(request.data))


class ChatRecordDetailView(APIView):

@extend_schema(
methods=['GET'],
description=_("Get chat record by share link"),
summary=_("Get chat record by share link"),
operation_id=_("Get chat record by share link"), # type: ignore
parameters=ChatRecordDetailShareAPI.get_parameters(),
responses=ChatRecordDetailShareAPI.get_response(),
tags=[_("Chat record link")] # type: ignore
)
def get(self, request, link: str):
return result.success(
ChatShareLinkDetailSerializer(data={'link':link}).get_record_list()
)
48 changes: 48 additions & 0 deletions apps/common/utils/chat_link_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
@project: MaxKB
@Author: niu
@file: chat_link_code.py
@date: 2026/2/9 11:31
@desc:
"""
from typing import Union

import uuid_utils.compat as uuid


class UUIDEncoder:

BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
BASE62_LEN = 62

@staticmethod
def encode(uuid_obj: Union[uuid.UUID, str] = None) -> str:

if uuid_obj is None:
uuid_obj = uuid.uuid7()
elif isinstance(uuid_obj, str):
uuid_obj = uuid.UUID(uuid_obj)

num = int(uuid_obj.hex, 16)

if num == 0:
return UUIDEncoder.BASE62_ALPHABET[0]

result = []
while num:
num, rem = divmod(num,62)
result.append(UUIDEncoder.BASE62_ALPHABET[rem])
return ''.join(reversed(result))

@staticmethod
def decode(encoded: str) -> uuid.UUID:

num = 0
for char in encoded:
num = num * UUIDEncoder.BASE62_LEN + UUIDEncoder.BASE62_ALPHABET.index(char)

return uuid.UUID(int=num)

@staticmethod
def decode_to_str(encoded: str) -> str:
return str(UUIDEncoder.decode(encoded))
Loading