feat: chat authentication (#3206)

This commit is contained in:
shaohuzhang1 2025-06-06 22:28:21 +08:00 committed by GitHub
parent 7abd9dfb71
commit 165de1ad73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 440 additions and 48 deletions

View File

@ -19,7 +19,7 @@ from rest_framework.exceptions import ValidationError, ErrorDetail
from application.flow.common import Answer, NodeChunk from application.flow.common import Answer, NodeChunk
from application.models import ChatRecord from application.models import ChatRecord
from application.models import ApplicationPublicAccessClient from application.models import ApplicationChatClientStats
from common.constants.authentication_type import AuthenticationType from common.constants.authentication_type import AuthenticationType
from common.field.common import InstanceField from common.field.common import InstanceField
@ -89,7 +89,7 @@ class WorkFlowPostHandler:
chat_cache.set(chat_id, chat_cache.set(chat_id,
self.chat_info, timeout=60 * 30) self.chat_info, timeout=60 * 30)
if self.client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value: if self.client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value:
application_public_access_client = (QuerySet(ApplicationPublicAccessClient) application_public_access_client = (QuerySet(ApplicationChatClientStats)
.filter(client_id=self.client_id, .filter(client_id=self.client_id,
application_id=self.chat_info.application.id).first()) application_id=self.chat_info.application.id).first())
if application_public_access_client is not None: if application_public_access_client is not None:

View File

@ -9,19 +9,17 @@
import os import os
from typing import List, Dict from typing import List, Dict
from django.db.models import QuerySet
from django.db import connection from django.db import connection
from django.db.models import QuerySet
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.search_dataset_node.i_search_dataset_node import ISearchDatasetStepNode from application.flow.step_node.search_dataset_node.i_search_dataset_node import ISearchDatasetStepNode
from common.config.embedding_config import VectorStore from common.config.embedding_config import VectorStore
from common.db.search import native_search from common.db.search import native_search
from common.utils.common import get_file_content from common.utils.common import get_file_content
from knowledge.models import Document, Paragraph, Knowledge from knowledge.models import Document, Paragraph, Knowledge, SearchMode
from models_provider.tools import get_model_instance_by_model_user_id
from maxkb.conf import PROJECT_DIR from maxkb.conf import PROJECT_DIR
from models_provider.tools import get_model_instance_by_model_user_id
SearchMode = None
def get_embedding_id(dataset_id_list): def get_embedding_id(dataset_id_list):

View File

@ -1,16 +1,16 @@
# Generated by Django 5.2 on 2025-06-04 11:57 # Generated by Django 5.2 on 2025-06-04 11:57
import application.models.application_chat import uuid
import common.encoder.encoder
import django.contrib.postgres.fields import django.contrib.postgres.fields
import django.db.models.deletion import django.db.models.deletion
import uuid
import uuid_utils.compat import uuid_utils.compat
from django.db import migrations, models from django.db import migrations, models
import common.encoder.encoder
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('application', '0001_initial'), ('application', '0001_initial'),
] ]
@ -21,12 +21,13 @@ class Migration(migrations.Migration):
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid.UUID('01973acd-fe4c-7fd1-94a8-f7cd668de562'), editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id', models.UUIDField(default=uuid.UUID('01973acd-fe4c-7fd1-94a8-f7cd668de562'), editable=False,
primary_key=True, serialize=False, verbose_name='主键id')),
('abstract', models.CharField(max_length=1024, verbose_name='摘要')), ('abstract', models.CharField(max_length=1024, verbose_name='摘要')),
('asker', models.JSONField(default=application.models.application_chat.default_asker, encoder=common.encoder.encoder.SystemEncoder, verbose_name='访问者')),
('client_id', models.UUIDField(default=None, null=True, verbose_name='客户端id')), ('client_id', models.UUIDField(default=None, null=True, verbose_name='客户端id')),
('is_deleted', models.BooleanField(default=False, verbose_name='')), ('is_deleted', models.BooleanField(default=False, verbose_name='')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')), ('application',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')),
], ],
options={ options={
'db_table': 'application_chat', 'db_table': 'application_chat',
@ -37,16 +38,25 @@ class Migration(migrations.Migration):
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id',
('vote_status', models.CharField(choices=[('-1', '未投票'), ('0', '赞同'), ('1', '反对')], default='-1', max_length=10, verbose_name='投票')), models.UUIDField(default=uuid_utils.compat.uuid1, editable=False, primary_key=True, serialize=False,
verbose_name='主键id')),
('vote_status',
models.CharField(choices=[('-1', '未投票'), ('0', '赞同'), ('1', '反对')], default='-1', max_length=10,
verbose_name='投票')),
('problem_text', models.CharField(max_length=10240, verbose_name='问题')), ('problem_text', models.CharField(max_length=10240, verbose_name='问题')),
('answer_text', models.CharField(max_length=40960, verbose_name='答案')), ('answer_text', models.CharField(max_length=40960, verbose_name='答案')),
('answer_text_list', django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(), default=list, size=None, verbose_name='改进标注列表')), ('answer_text_list',
django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(), default=list, size=None,
verbose_name='改进标注列表')),
('message_tokens', models.IntegerField(default=0, verbose_name='请求token数量')), ('message_tokens', models.IntegerField(default=0, verbose_name='请求token数量')),
('answer_tokens', models.IntegerField(default=0, verbose_name='响应token数量')), ('answer_tokens', models.IntegerField(default=0, verbose_name='响应token数量')),
('const', models.IntegerField(default=0, verbose_name='总费用')), ('const', models.IntegerField(default=0, verbose_name='总费用')),
('details', models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='对话详情')), ('details',
('improve_paragraph_id_list', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True), default=list, size=None, verbose_name='改进标注列表')), models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='对话详情')),
('improve_paragraph_id_list',
django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True), default=list,
size=None, verbose_name='改进标注列表')),
('run_time', models.FloatField(default=0, verbose_name='运行时长')), ('run_time', models.FloatField(default=0, verbose_name='运行时长')),
('index', models.IntegerField(verbose_name='对话下标')), ('index', models.IntegerField(verbose_name='对话下标')),
('chat', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.chat')), ('chat', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.chat')),
@ -60,13 +70,17 @@ class Migration(migrations.Migration):
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id',
('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')), models.UUIDField(default=uuid_utils.compat.uuid1, editable=False, primary_key=True, serialize=False,
verbose_name='主键id')),
('workspace_id',
models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('name', models.CharField(default='', max_length=128, verbose_name='版本名称')), ('name', models.CharField(default='', max_length=128, verbose_name='版本名称')),
('publish_user_id', models.UUIDField(default=None, null=True, verbose_name='发布者id')), ('publish_user_id', models.UUIDField(default=None, null=True, verbose_name='发布者id')),
('publish_user_name', models.CharField(default='', max_length=128, verbose_name='发布者名称')), ('publish_user_name', models.CharField(default='', max_length=128, verbose_name='发布者名称')),
('work_flow', models.JSONField(default=dict, verbose_name='工作流数据')), ('work_flow', models.JSONField(default=dict, verbose_name='工作流数据')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')), ('application',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')),
], ],
options={ options={
'db_table': 'application_work_flow_version', 'db_table': 'application_work_flow_version',
@ -77,16 +91,20 @@ class Migration(migrations.Migration):
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False,
verbose_name='主键id')),
('client_id', models.UUIDField(default=uuid.uuid1, verbose_name='公共访问链接客户端id')), ('client_id', models.UUIDField(default=uuid.uuid1, verbose_name='公共访问链接客户端id')),
('client_type', models.CharField(max_length=64, verbose_name='客户端类型')), ('client_type', models.CharField(max_length=64, verbose_name='客户端类型')),
('access_num', models.IntegerField(default=0, verbose_name='访问总次数次数')), ('access_num', models.IntegerField(default=0, verbose_name='访问总次数次数')),
('intraday_access_num', models.IntegerField(default=0, verbose_name='当日访问次数')), ('intraday_access_num', models.IntegerField(default=0, verbose_name='当日访问次数')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application', verbose_name='应用id')), ('application',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application',
verbose_name='应用id')),
], ],
options={ options={
'db_table': 'application_public_access_client', 'db_table': 'application_public_access_client',
'indexes': [models.Index(fields=['application_id', 'client_id'], name='application_applica_8aaf45_idx')], 'indexes': [
models.Index(fields=['application_id', 'client_id'], name='application_applica_8aaf45_idx')],
}, },
), ),
] ]

View File

@ -24,16 +24,3 @@ class ApplicationApiKey(AppModelMixin):
db_table = "application_api_key" db_table = "application_api_key"
class ApplicationPublicAccessClient(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
client_id = models.UUIDField(max_length=128, default=uuid.uuid1, verbose_name="公共访问链接客户端id")
client_type = models.CharField(max_length=64, verbose_name="客户端类型")
application = models.ForeignKey(Application, on_delete=models.CASCADE, verbose_name="应用id")
access_num = models.IntegerField(default=0, verbose_name="访问总次数次数")
intraday_access_num = models.IntegerField(default=0, verbose_name="当日访问次数")
class Meta:
db_table = "application_public_access_client"
indexes = [
models.Index(fields=['application_id', 'client_id']),
]

View File

@ -17,17 +17,20 @@ from common.encoder.encoder import SystemEncoder
from common.mixins.app_model_mixin import AppModelMixin from common.mixins.app_model_mixin import AppModelMixin
def default_asker(): class ClientType(models.TextChoices):
return {'user_name': '游客'} ANONYMOUS_USER = "ANONYMOUS_USER", '匿名用户'
CHAT_USER = "CHAT_USER", "对话用户"
SYSTEM_API_KEY = "SYSTEM_API_KEY", "系统API_KEY"
APPLICATION_API_KEY = "APPLICATION_API_KEY", "应用API_KEY"
class Chat(AppModelMixin): class Chat(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7(), editable=False, verbose_name="主键id") id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7(), editable=False, verbose_name="主键id")
application = models.ForeignKey(Application, on_delete=models.CASCADE) application = models.ForeignKey(Application, on_delete=models.CASCADE)
abstract = models.CharField(max_length=1024, verbose_name="摘要") abstract = models.CharField(max_length=1024, verbose_name="摘要")
asker = models.JSONField(verbose_name="访问者", default=default_asker, encoder=SystemEncoder)
client_id = models.UUIDField(verbose_name="客户端id", default=None, null=True) client_id = models.UUIDField(verbose_name="客户端id", default=None, null=True)
is_deleted = models.BooleanField(verbose_name="", default=False) client_type = models.CharField(max_length=64, verbose_name="客户端类型", choices=ClientType.choices)
is_deleted = models.BooleanField(verbose_name="逻辑删除", default=False)
class Meta: class Meta:
db_table = "application_chat" db_table = "application_chat"
@ -80,3 +83,18 @@ class ChatRecord(AppModelMixin):
class Meta: class Meta:
db_table = "application_chat_record" db_table = "application_chat_record"
class ApplicationChatClientStats(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
client_id = models.UUIDField(max_length=128, default=uuid.uuid7, verbose_name="公共访问链接客户端id")
client_type = models.CharField(max_length=64, verbose_name="客户端类型", choices=ClientType.choices)
application = models.ForeignKey(Application, on_delete=models.CASCADE, verbose_name="应用id")
access_num = models.IntegerField(default=0, verbose_name="访问总次数次数")
intraday_access_num = models.IntegerField(default=0, verbose_name="当日访问次数")
class Meta:
db_table = "application_chat_client_stats"
indexes = [
models.Index(fields=['application_id', 'client_id']),
]

View File

@ -34,10 +34,11 @@ class ApplicationKey(APIView):
parameters=ApplicationKeyCreateAPI.get_parameters(), parameters=ApplicationKeyCreateAPI.get_parameters(),
tags=[_('Application Api Key')] # type: ignore tags=[_('Application Api Key')] # type: ignore
) )
@log(menu='Application', operate="Add ApiKey", @log(menu='Application', operate="Add ApiKey",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_api_key_id'))) get_operation_object=lambda r, k: get_application_operation_object(k.get('application_api_key_id')))
@has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission()) @has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission())
def post(self, request: Request, application_id: str, workspace_id: str): def post(self, request: Request, workspace_id: str, application_id: str):
return result.success(ApplicationKeySerializer( return result.success(ApplicationKeySerializer(
data={'application_id': application_id, 'user_id': request.user.id, data={'application_id': application_id, 'user_id': request.user.id,
'workspace_id': workspace_id}).generate()) 'workspace_id': workspace_id}).generate())
@ -51,7 +52,7 @@ class ApplicationKey(APIView):
tags=[_('Application Api Key')] # type: ignore tags=[_('Application Api Key')] # type: ignore
) )
@has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission()) @has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission())
def get(self, request: Request, application_id: str, workspace_id: str): def get(self, request: Request, workspace_id: str, application_id: str ):
return result, success(ApplicationKeySerializer( return result, success(ApplicationKeySerializer(
data={'application_id': application_id, 'user_id': request.user.id, data={'application_id': application_id, 'user_id': request.user.id,
'workspace_id': workspace_id}).list()) 'workspace_id': workspace_id}).list())

View File

@ -12,7 +12,7 @@ from rest_framework.request import Request
from rest_framework.views import APIView from rest_framework.views import APIView
from application.api.application_version import ApplicationVersionListAPI, ApplicationVersionPageAPI, \ from application.api.application_version import ApplicationVersionListAPI, ApplicationVersionPageAPI, \
ApplicationVersionAPI, ApplicationVersionOperateAPI ApplicationVersionOperateAPI
from application.serializers.application_version import ApplicationVersionSerializer from application.serializers.application_version import ApplicationVersionSerializer
from application.views import get_application_operation_object from application.views import get_application_operation_object
from common import result from common import result
@ -90,6 +90,7 @@ class ApplicationVersionView(APIView):
responses=ApplicationVersionOperateAPI.get_response(), responses=ApplicationVersionOperateAPI.get_response(),
tags=[_('Application/Version')] # type: ignore tags=[_('Application/Version')] # type: ignore
) )
@has_permissions(PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission())
@log(menu='Application', operate="Modify application version information", @log(menu='Application', operate="Modify application version information",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id'))) get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, workspace_id: str, application_id: str, work_flow_version_id: str): def put(self, request: Request, workspace_id: str, application_id: str, work_flow_version_id: str):

View File

@ -0,0 +1,24 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file chat_authentication_api.py
@date2025/6/6 19:59
@desc:
"""
from chat.serializers.chat_authentication import AuthenticationSerializer
from common.mixins.api_mixin import APIMixin
class ChatAuthenticationAPI(APIMixin):
@staticmethod
def get_request():
return AuthenticationSerializer()
@staticmethod
def get_parameters():
pass
@staticmethod
def get_response():
pass

View File

@ -0,0 +1,147 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file ChatAuthentication.py
@date2025/6/6 13:48
@desc:
"""
import uuid
from django.core import signing
from django.core.cache import cache
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from application.models import ApplicationAccessToken, ClientType, Application, ApplicationTypeChoices, WorkFlowVersion
from application.serializers.application import ApplicationSerializerModel
from common.auth.common import ChatUserToken, ChatAuthentication
from common.constants.authentication_type import AuthenticationType
from common.constants.cache_version import Cache_Version
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.exception.app_exception import NotFound404, AppApiException, AppUnauthorizedFailed
def auth(application_id, access_token, authentication_value, token_details):
client_id = token_details.get('client_id')
if client_id is None:
client_id = str(uuid.uuid1())
_type = AuthenticationType.CHAT_ANONYMOUS_USER
if authentication_value is not None:
application_setting_model = DatabaseModelManage.get_model('application_setting')
if application_setting_model is not None:
application_setting = QuerySet(application_setting_model).filter(application_id=application_id).first()
if application_setting.authentication:
auth_type = application_setting.authentication_value.get('type')
auth_value = authentication_value.get(auth_type + '_value')
if auth_type == 'password':
if authentication_value.get('type') == 'password':
if auth_value == authentication_value.get(auth_type + '_value'):
return ChatUserToken(application_id, None, access_token, _type, ClientType.ANONYMOUS_USER,
client_id, ChatAuthentication(auth_type, True, True))
else:
raise AppApiException(500, '认证方式不匹配')
return ChatUserToken(application_id, None, access_token, _type, ClientType.ANONYMOUS_USER,
client_id, ChatAuthentication(None, False, False))
class AuthenticationSerializer(serializers.Serializer):
access_token = serializers.CharField(required=True, label=_("access_token"))
authentication_value = serializers.JSONField(required=False, allow_null=True,
label=_("Certification Information"))
def auth(self, request, with_valid=True):
token = request.META.get('HTTP_AUTHORIZATION')
token_details = {}
try:
# 校验token
if token is not None:
token_details = signing.loads(token)
except Exception as e:
pass
if with_valid:
self.is_valid(raise_exception=True)
access_token = self.data.get("access_token")
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
authentication_value = self.data.get('authentication_value', None)
if application_access_token is not None and application_access_token.is_active:
chat_user_token = auth(application_access_token.application_id, access_token, authentication_value,
token_details)
return chat_user_token.to_token()
else:
raise NotFound404(404, _("Invalid access_token"))
class ApplicationProfileSerializer(serializers.Serializer):
application_id = serializers.UUIDField(required=True, label=_("Application ID"))
def profile(self, with_valid=True):
if with_valid:
self.is_valid()
application_id = self.data.get("application_id")
application = QuerySet(Application).get(id=application_id)
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application.id).first()
if application_access_token is None:
raise AppUnauthorizedFailed(500, _("Illegal User"))
application_setting_model = DatabaseModelManage.get_model('application_setting')
if application.type == ApplicationTypeChoices.WORK_FLOW:
work_flow_version = QuerySet(WorkFlowVersion).filter(application_id=application.id).order_by(
'-create_time')[0:1].first()
if work_flow_version is not None:
application.work_flow = work_flow_version.work_flow
license_is_valid = cache.get(Cache_Version.SYSTEM.get_key(key='license_is_valid'),
version=Cache_Version.SYSTEM.get_version())
application_setting_dict = {}
if application_setting_model is not None and license_is_valid:
application_setting = QuerySet(application_setting_model).filter(
application_id=application_access_token.application_id).first()
if application_setting is not None:
custom_theme = getattr(application_setting, 'custom_theme', {})
float_location = getattr(application_setting, 'float_location', {})
if not custom_theme:
application_setting.custom_theme = {
'theme_color': '',
'header_font_color': ''
}
if not float_location:
application_setting.float_location = {
'x': {'type': '', 'value': ''},
'y': {'type': '', 'value': ''}
}
application_setting_dict = {'show_source': application_access_token.show_source,
'show_history': application_setting.show_history,
'draggable': application_setting.draggable,
'show_guide': application_setting.show_guide,
'avatar': application_setting.avatar,
'show_avatar': application_setting.show_avatar,
'float_icon': application_setting.float_icon,
'authentication': application_setting.authentication,
'authentication_type': application_setting.authentication_value.get(
'type', 'password'),
'login_value': application_setting.authentication_value.get(
'login_value', []),
'disclaimer': application_setting.disclaimer,
'disclaimer_value': application_setting.disclaimer_value,
'custom_theme': application_setting.custom_theme,
'user_avatar': application_setting.user_avatar,
'show_user_avatar': application_setting.show_user_avatar,
'float_location': application_setting.float_location}
return {**ApplicationSerializerModel(application).data,
'stt_model_id': application.stt_model_id,
'tts_model_id': application.tts_model_id,
'stt_model_enable': application.stt_model_enable,
'tts_model_enable': application.tts_model_enable,
'tts_type': application.tts_type,
'tts_autoplay': application.tts_autoplay,
'stt_autosend': application.stt_autosend,
'file_upload_enable': application.file_upload_enable,
'file_upload_setting': application.file_upload_setting,
'work_flow': {'nodes': [node for node in ((application.work_flow or {}).get('nodes', []) or []) if
node.get('id') == 'base-node']},
'show_source': application_access_token.show_source,
'language': application_access_token.language,
**application_setting_dict}

View File

@ -6,4 +6,6 @@ app_name = 'chat'
urlpatterns = [ urlpatterns = [
path('chat/embed', views.ChatEmbedView.as_view()), path('chat/embed', views.ChatEmbedView.as_view()),
path('application/authentication', views.Authentication.as_view()),
path('profile', views.ApplicationProfile.as_view())
] ]

View File

@ -7,3 +7,4 @@
@desc: @desc:
""" """
from .chat_embed import * from .chat_embed import *
from .chat import *

70
apps/chat/views/chat.py Normal file
View File

@ -0,0 +1,70 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file chat.py
@date2025/6/6 11:18
@desc:
"""
from django.http import HttpResponse
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 chat.api.chat_authentication_api import ChatAuthenticationAPI
from chat.serializers.chat_authentication import AuthenticationSerializer, ApplicationProfileSerializer
from common.auth import TokenAuth
from common.exception.app_exception import AppAuthenticationFailed
from common.result import result
class Authentication(APIView):
def options(self, request, *args, **kwargs):
return HttpResponse(
headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}, )
@extend_schema(
methods=['POST'],
description=_('Application Certification'),
summary=_('Application Certification'),
operation_id=_('Application Certification'), # type: ignore
request=ChatAuthenticationAPI.get_request(),
responses=None,
tags=[_('Chat')] # type: ignore
)
def post(self, request: Request):
return result.success(
AuthenticationSerializer(data={'access_token': request.data.get("access_token"),
'authentication_value': request.data.get(
'authentication_value')}).auth(
request),
headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}
)
class ApplicationProfile(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
methods=['GET'],
description=_("Get application related information"),
summary=_("Get application related information"),
operation_id=_("Get application related information"), # type: ignore
request=None,
responses=None,
tags=[_('Chat')] # type: ignore
)
def get(self, request: Request):
if 'application_id' in request.auth.keywords:
return result.success(ApplicationProfileSerializer(
data={'application_id': request.auth.keywords.get('application_id')}).profile())
raise AppAuthenticationFailed(401, "身份异常")
class ChatView(APIView):
pass

View File

@ -0,0 +1,64 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file common.py
@date2025/6/6 19:55
@desc:
"""
import json
from django.core import signing
from common.utils.rsa_util import encrypt, decrypt
class ChatAuthentication:
def __init__(self, auth_type: str | None, is_auth: bool, auth_passed: bool):
self.is_auth = is_auth
self.auth_passed = auth_passed
self.auth_type = auth_type
def to_dict(self):
return {'is_auth': self.is_auth, 'auth_passed': self.auth_passed, 'auth_type': self.auth_type}
def to_string(self):
return encrypt(json.dumps(self.to_dict()))
@staticmethod
def new_instance(authentication: str):
auth = json.loads(decrypt(authentication))
return ChatAuthentication(auth.get('auth_type'), auth.get('is_auth'), auth.get('auth_passed'))
class ChatUserToken:
def __init__(self, application_id, user_id, access_token, _type, client_type, client_id,
authentication: ChatAuthentication):
self.application_id = application_id
self.user_id = user_id,
self.access_token = access_token
self.type = _type
self.client_type = client_type
self.client_id = client_id
self.authentication = authentication
def to_dict(self):
return {
'application_id': str(self.application_id),
'user_id': str(self.user_id),
'access_token': self.access_token,
'type': str(self.type.value),
'client_type': str(self.client_type),
'client_id': str(self.client_id),
'authentication': self.authentication.to_string()
}
def to_token(self):
return signing.dumps(self.to_dict())
@staticmethod
def new_instance(token_dict):
return ChatUserToken(token_dict.get('application_id'), token_dict.get('user_id'),
token_dict.get('access_token'), token_dict.get('type'), token_dict.get('client_type'),
token_dict.get('client_id'),
ChatAuthentication.new_instance(token_dict.get('authentication')))

View File

@ -0,0 +1,55 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file chat_anonymous_user_token.py
@date2025/6/6 15:08
@desc:
"""
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from application.models import ApplicationAccessToken, ClientType
from common.auth.common import ChatUserToken
from common.auth.handle.auth_base_handle import AuthBaseHandle
from common.constants.authentication_type import AuthenticationType
from common.constants.permission_constants import RoleConstants, Permission, Group, Operate, Auth
from common.exception.app_exception import AppAuthenticationFailed, ChatException
class ChatAnonymousUserToken(AuthBaseHandle):
def support(self, request, token: str, get_token_details):
token_details = get_token_details()
if token_details is None:
return False
return (
'application_id' in token_details and
'access_token' in token_details and
token_details.get('type') == AuthenticationType.CHAT_ANONYMOUS_USER.value)
def handle(self, request, token: str, get_token_details):
auth_details = get_token_details()
chat_user_token = ChatUserToken.new_instance(auth_details)
application_id = chat_user_token.application_id
access_token = chat_user_token.access_token
application_access_token = QuerySet(ApplicationAccessToken).filter(
application_id=application_id).first()
if application_access_token is None:
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
if not application_access_token.is_active:
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
if not application_access_token.access_token == access_token:
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
# 匿名用户 除了/api/application/profile 都需要校验是否开启了密码认证
if request.path != '/api/application/profile':
if chat_user_token.authentication.is_auth and not chat_user_token.authentication.auth_passed:
raise ChatException(1002, _('Authentication information is incorrect'))
return None, Auth(
current_role_list=[RoleConstants.CHAT_ANONYMOUS_USER],
permission_list=[
Permission(group=Group.APPLICATION,
operate=Operate.USE)],
application_id=application_access_token.application_id,
client_id=auth_details.get('client_id'),
client_type=ClientType.ANONYMOUS_USER)

View File

@ -15,6 +15,7 @@ from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.auth.handle.auth_base_handle import AuthBaseHandle from common.auth.handle.auth_base_handle import AuthBaseHandle
from common.constants.authentication_type import AuthenticationType
from common.constants.cache_version import Cache_Version from common.constants.cache_version import Cache_Version
from common.constants.permission_constants import Auth, PermissionConstants, ResourcePermissionGroup, \ from common.constants.permission_constants import Auth, PermissionConstants, ResourcePermissionGroup, \
get_permission_list_by_resource_group, ResourceAuthType, \ get_permission_list_by_resource_group, ResourceAuthType, \
@ -233,7 +234,7 @@ class UserToken(AuthBaseHandle):
auth_details = get_token_details() auth_details = get_token_details()
if auth_details is None: if auth_details is None:
return False return False
return True return 'id' in auth_details and auth_details.get('type') == AuthenticationType.SYSTEM_USER.value
def handle(self, request, token: str, get_token_details): def handle(self, request, token: str, get_token_details):
version, get_key = Cache_Version.TOKEN.value version, get_key = Cache_Version.TOKEN.value

View File

@ -170,6 +170,8 @@ class RoleConstants(Enum):
ADMIN = Role("ADMIN", '超级管理员', RoleGroup.SYSTEM_USER) ADMIN = Role("ADMIN", '超级管理员', RoleGroup.SYSTEM_USER)
WORKSPACE_MANAGE = Role("WORKSPACE_MANAGE", '工作空间管理员', RoleGroup.SYSTEM_USER) WORKSPACE_MANAGE = Role("WORKSPACE_MANAGE", '工作空间管理员', RoleGroup.SYSTEM_USER)
USER = Role("USER", '普通用户', RoleGroup.SYSTEM_USER) USER = Role("USER", '普通用户', RoleGroup.SYSTEM_USER)
CHAT_ANONYMOUS_USER = Role("CHAT_ANONYMOUS_USER", "对话匿名用户", RoleGroup.CHAT_USER)
CHAT_USER = Role("CHAT_USER", "对话用户", RoleGroup.CHAT_USER)
def get_workspace_role(self): def get_workspace_role(self):
return lambda r, kwargs: Role(name=self.value.name, return lambda r, kwargs: Role(name=self.value.name,
@ -901,7 +903,7 @@ class Auth:
""" """
def __init__(self, def __init__(self,
current_role_list: List[Role], current_role_list: List[RoleConstants | Role],
permission_list: List[PermissionConstants | Permission], permission_list: List[PermissionConstants | Permission],
**keywords): **keywords):
# 权限列表 # 权限列表

View File

@ -7,7 +7,9 @@
@desc: @desc:
""" """
USER_TOKEN_AUTH = 'common.auth.handle.impl.user_token.UserToken' USER_TOKEN_AUTH = 'common.auth.handle.impl.user_token.UserToken'
CHAT_ANONYMOUS_USER_AURH = 'common.auth.handle.impl.chat_anonymous_user_token.ChatAnonymousUserToken'
AUTH_HANDLES = [ AUTH_HANDLES = [
USER_TOKEN_AUTH, USER_TOKEN_AUTH,
CHAT_ANONYMOUS_USER_AURH
] ]

View File

@ -46,6 +46,7 @@ INSTALLED_APPS = [
'models_provider', 'models_provider',
'django_celery_beat', 'django_celery_beat',
'application', 'application',
'chat'
'oss' 'oss'
] ]