diff --git a/apps/application/serializers/application_serializers.py b/apps/application/serializers/application_serializers.py
index 171c2a59..55114a3f 100644
--- a/apps/application/serializers/application_serializers.py
+++ b/apps/application/serializers/application_serializers.py
@@ -17,6 +17,8 @@ from django.core import cache
from django.core import signing
from django.db import transaction, models
from django.db.models import QuerySet
+from django.http import HttpResponse
+from django.template import Template, Context
from rest_framework import serializers
from application.models import Application, ApplicationDatasetMapping
@@ -26,8 +28,10 @@ from common.constants.authentication_type import AuthenticationType
from common.db.search import get_dynamics_model, native_search, native_page_search
from common.db.sql_execute import select_list
from common.exception.app_exception import AppApiException, NotFound404
+from common.util.common import getRestSeconds, set_embed_identity_cookie
from common.util.field_message import ErrMessage
from common.util.file_util import get_file_content
+from common.util.rsa_util import encrypt
from dataset.models import DataSet, Document
from dataset.serializers.common_serializers import list_paragraph
from setting.models import AuthOperate
@@ -38,6 +42,7 @@ from smartdoc.conf import PROJECT_DIR
from smartdoc.settings import JWT_AUTH
token_cache = cache.caches['token_cache']
+chat_cache = cache.caches['chat_cache']
class ModelDatasetAssociation(serializers.Serializer):
@@ -104,6 +109,31 @@ class ApplicationSerializer(serializers.Serializer):
ModelDatasetAssociation(data={'user_id': user_id, 'model_id': self.data.get('model_id'),
'dataset_id_list': self.data.get('dataset_id_list')}).is_valid()
+ class Embed(serializers.Serializer):
+ host = serializers.CharField(required=True, error_messages=ErrMessage.char("主机"))
+ protocol = serializers.CharField(required=True, error_messages=ErrMessage.char("协议"))
+ token = serializers.CharField(required=True, error_messages=ErrMessage.char("token"))
+
+ def get_embed(self, request, with_valid=True):
+ if with_valid:
+ self.is_valid(raise_exception=True)
+ index_path = os.path.join(PROJECT_DIR, 'apps', "application", 'template', 'embed.js')
+ file = open(index_path, "r", encoding='utf-8')
+ content = file.read()
+ file.close()
+ is_auth = 'true'
+ try:
+ ApplicationSerializer.Authentication(data={'access_token': self.data.get('token')}).auth()
+ except Exception as e:
+ is_auth = 'false'
+ t = Template(content)
+ s = t.render(
+ Context(
+ {'is_auth': is_auth, 'protocol': 'http', 'host': 'localhost:8000', 'token': '0a8d892c755f1a75'}))
+ response = HttpResponse(s, status=200, headers={'Content-Type': 'text/javascript'})
+ set_embed_identity_cookie(request, response)
+ return response
+
class AccessTokenSerializer(serializers.Serializer):
application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.boolean("应用id"))
diff --git a/apps/application/serializers/chat_message_serializers.py b/apps/application/serializers/chat_message_serializers.py
index d8fc7300..0b042ab4 100644
--- a/apps/application/serializers/chat_message_serializers.py
+++ b/apps/application/serializers/chat_message_serializers.py
@@ -24,6 +24,7 @@ from application.chat_pipeline.step.reset_problem_step.impl.base_reset_problem_s
from application.chat_pipeline.step.search_dataset_step.impl.base_search_dataset_step import BaseSearchDatasetStep
from application.models import ChatRecord, Chat, Application, ApplicationDatasetMapping
from common.exception.app_exception import AppApiException
+from common.util.field_message import ErrMessage
from common.util.rsa_util import decrypt
from common.util.split_model import flat_map
from dataset.models import Paragraph, Document
@@ -31,6 +32,7 @@ from setting.models import Model
from setting.models_provider.constants.model_provider_constants import ModelProvideConstants
chat_cache = caches['model_cache']
+chat_embed_identity_cache = caches['chat_cache']
class ChatInfo:
diff --git a/apps/application/template/embed.js b/apps/application/template/embed.js
new file mode 100644
index 00000000..69f1ff4d
--- /dev/null
+++ b/apps/application/template/embed.js
@@ -0,0 +1,294 @@
+const guideHtml=`
+
+
+
+
+
🌟 遇见问题,不再是障碍!
+
你好,我是你的智能小助手。
+ 点我,开启高效解答模式,让问题变成过去式。
+
+
+
+
+
+`
+const chatButtonHtml=
+``
+
+
+
+const getChatContainerHtml=(protocol,host,token)=>{
+ return `
+
+
+
+
`
+}
+/**
+ * 初始化引导
+ * @param {*} root
+ */
+const initGuide=(root)=>{
+ root.insertAdjacentHTML("beforeend",guideHtml)
+ const button=root.querySelector(".button")
+ const close_icon=root.querySelector('.close')
+ const close_func=()=>{
+ root.removeChild(root.querySelector('.tips'))
+ root.removeChild(root.querySelector('.mask'))
+ localStorage.setItem('maxkbMaskTip',true)
+ }
+ button.onclick=close_func
+ close_icon.onclick=close_func
+}
+const initChat=(root)=>{
+ // 添加对话icon
+ root.insertAdjacentHTML("beforeend",chatButtonHtml)
+ // 添加对话框
+ root.insertAdjacentHTML('beforeend',getChatContainerHtml('{{protocol}}','{{host}}','{{token}}'))
+ // 按钮元素
+ const chat_button=root.querySelector('.chat_button')
+ // 对话框元素
+ const chat_container=root.querySelector('#chat_container')
+
+ const viewport=root.querySelector('.openviewport')
+ const closeviewport=root.querySelector('.closeviewport')
+ const close_func=()=>{
+ chat_container.style['display']=chat_container.style['display']=='block'?'none':'block'
+ }
+ close_icon=chat_container.querySelector('.close')
+ chat_button.onclick = close_func
+ close_icon.onclick=close_func
+ const viewport_func=()=>{
+ if(chat_container.classList.contains('enlarge')){
+ chat_container.classList.remove("enlarge");
+ viewport.classList.remove('viewportnone')
+ closeviewport.classList.add('viewportnone')
+ }else{
+ chat_container.classList.add("enlarge");
+ viewport.classList.add('viewportnone')
+ closeviewport.classList.remove('viewportnone')
+ }
+ }
+ viewport.onclick=viewport_func
+ closeviewport.onclick=viewport_func
+}
+/**
+ * 第一次进来的引导提示
+ */
+function initMaxkb(){
+ initMaxkbStyle()
+ const root=document.createElement('div')
+ root.id="maxkb"
+ document.body.appendChild(root)
+ const maxkbMaskTip=localStorage.getItem('maxkbMaskTip')
+ if(maxkbMaskTip==null){
+ initGuide(root)
+ }
+ initChat(root)
+}
+
+
+// 初始化全局样式
+function initMaxkbStyle(){
+ style=document.createElement('style')
+ style.type='text/css'
+ style.innerText= `/* 放大 */
+ #maxkb .enlarge {
+ width: 50%!important;
+ height: 100%!important;
+ bottom: 0!important;
+ right: 0 !important;
+ }
+ @media only screen and (max-width: 768px){
+ #maxkb .enlarge {
+ width: 100%!important;
+ height: 100%!important;
+ right: 0 !important;
+ bottom: 0!important;
+ }
+ }
+
+ /* 引导 */
+
+ #maxkb .mask {
+ position: fixed;
+ z-index: 999;
+ background-color: transparent;
+ height: 100%;
+ width: 100%;
+ top: 0;
+ left: 0;
+ }
+ #maxkb .mask .content {
+ width: 45px;
+ height: 50px;
+ box-shadow: 1px 1px 1px 2000px rgba(0,0,0,.6);
+ border-radius: 50% 0 0 50%;
+ position: absolute;
+ right: 0;
+ bottom: 42px;
+ z-index: 1000;
+ }
+ #maxkb .tips {
+ position: absolute;
+ bottom: 30px;
+ right: 60px;
+ padding: 22px 24px 24px;
+ border-radius: 6px;
+ color: #ffffff;
+ font-size: 14px;
+ background: #3370FF;
+ z-index: 1000;
+ }
+ #maxkb .tips .arrow {
+ position: absolute;
+ background: #3370FF;
+ width: 10px;
+ height: 10px;
+ pointer-events: none;
+ transform: rotate(45deg);
+ box-sizing: border-box;
+ /* left */
+ right: -5px;
+ bottom: 33px;
+ border-left-color: transparent;
+ border-bottom-color: transparent
+ }
+ #maxkb .tips .title {
+ font-size: 20px;
+ font-weight: 500;
+ margin-bottom: 8px;
+ }
+ #maxkb .tips .button {
+ text-align: right;
+ margin-top: 24px;
+ }
+ #maxkb .tips .button button {
+ border-radius: 4px;
+ background: #FFF;
+ padding: 3px 12px;
+ color: #3370FF;
+ cursor: pointer;
+ outline: none;
+ border: none;
+ }
+ #maxkb .tips .button button::after{
+ border: none;
+ }
+ #maxkb .tips .close {
+ position: absolute;
+ right: 20px;
+ top: 20px;
+ cursor: pointer;
+
+ }
+ #chat_container {
+ width: 420px;
+ height: 600px;
+ display:none;
+ }
+ @media only screen and (max-width: 768px) {
+ #chat_container {
+ width: 100%;
+ height: 70%;
+ right: 0 !important;
+ }
+ }
+
+ #maxkb .chat_button{
+ position: fixed;
+ bottom: 30px;
+ right: 0;
+ cursor: pointer;
+ }
+ #maxkb #chat_container{
+ z-index:10000;position: relative;
+ border-radius: 8px;
+ border: 1px solid var(--N300, #DEE0E3);
+ background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1;
+ box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.10);
+ position: fixed;bottom: 20px;right: 45px;overflow: hidden;
+ }
+ #maxkb #chat_container .close{
+ position: absolute;
+ top: 15px;
+ right: 10px;
+ cursor: pointer;
+ }
+ #maxkb #chat_container .openviewport{
+ position: absolute;
+ top: 15px;
+ right: 50px;
+ cursor: pointer;
+ }
+ #maxkb #chat_container .closeviewport{
+ position: absolute;
+ top: 15px;
+ right: 50px;
+ cursor: pointer;
+ }
+ #maxkb #chat_container .viewportnone{
+ display:none;
+ }
+ #maxkb #chat_container #chat{
+ height:100%;
+ width:100%;
+ border: none;
+}
+ #maxkb #chat_container {
+ animation: appear .4s ease-in-out;
+ }
+ @keyframes appear {
+ from {
+ height: 0;;
+ }
+
+ to {
+ height: 600px;
+ }
+ }`
+ document.head.appendChild(style)
+}
+
+function embedChatbot() {
+ if ({{is_auth}}) {
+ // 初始化maxkb智能小助手
+ initMaxkb()
+ } else console.error('invalid parameter')
+}
+window.onload = embedChatbot
diff --git a/apps/application/urls.py b/apps/application/urls.py
index 7b530eef..9c70def5 100644
--- a/apps/application/urls.py
+++ b/apps/application/urls.py
@@ -6,6 +6,7 @@ app_name = "application"
urlpatterns = [
path('application', views.Application.as_view(), name="application"),
path('application/profile', views.Application.Profile.as_view()),
+ path('application/embed', views.Application.Embed.as_view()),
path('application/authentication', views.Application.Authentication.as_view()),
path('application/
/model', views.Application.Model.as_view()),
path('application//hit_test', views.Application.HitTest.as_view()),
diff --git a/apps/application/views/application_views.py b/apps/application/views/application_views.py
index f1d7595d..d894f61f 100644
--- a/apps/application/views/application_views.py
+++ b/apps/application/views/application_views.py
@@ -6,6 +6,7 @@
@date:2023/10/27 14:56
@desc:
"""
+
from django.http import HttpResponse
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
@@ -20,13 +21,24 @@ from common.constants.permission_constants import CompareConstants, PermissionCo
from common.exception.app_exception import AppAuthenticationFailed
from common.response import result
from common.swagger_api.common_api import CommonApi
-from common.util.common import query_params_to_single_dict
+from common.util.common import query_params_to_single_dict, set_embed_identity_cookie
from dataset.serializers.dataset_serializers import DataSetSerializers
class Application(APIView):
authentication_classes = [TokenAuth]
+ class Embed(APIView):
+ @action(methods=["GET"], detail=False)
+ @swagger_auto_schema(operation_summary="获取嵌入js",
+ operation_id="获取嵌入js",
+ tags=["应用"],
+ manual_parameters=ApplicationApi.ApiKey.get_request_params_api())
+ def get(self, request: Request):
+ return ApplicationSerializer.Embed(
+ data={'protocol': request.query_params.get('protocol'), 'token': request.query_params.get('token'),
+ 'host': request.query_params.get('host'), }).get_embed(request)
+
class Model(APIView):
authentication_classes = [TokenAuth]
@@ -185,7 +197,7 @@ class Application(APIView):
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}
)
-
+ set_embed_identity_cookie(request, response)
return response
@action(methods=['POST'], detail=False)
diff --git a/apps/application/views/chat_views.py b/apps/application/views/chat_views.py
index 6e1efc45..936ed4b8 100644
--- a/apps/application/views/chat_views.py
+++ b/apps/application/views/chat_views.py
@@ -18,7 +18,7 @@ from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import Permission, Group, Operate, \
RoleConstants, ViewPermission, CompareConstants
from common.response import result
-from common.util.common import query_params_to_single_dict
+from common.util.common import query_params_to_single_dict, set_embed_identity_cookie
class ChatView(APIView):
@@ -71,9 +71,13 @@ class ChatView(APIView):
dynamic_tag=keywords.get('application_id'))])
)
def post(self, request: Request, chat_id: str):
- return ChatMessageSerializer(data={'chat_id': chat_id}).chat(request.data.get('message'), request.data.get(
- 're_chat') if 're_chat' in request.data else False, request.data.get(
- 'stream') if 'stream' in request.data else True)
+ response = ChatMessageSerializer(data={'chat_id': chat_id}).chat(request.data.get('message'),
+ request.data.get(
+ 're_chat') if 're_chat' in request.data else False,
+ request.data.get(
+ 'stream') if 'stream' in request.data else True)
+ set_embed_identity_cookie(request, response)
+ return response
@action(methods=['GET'], detail=False)
@swagger_auto_schema(operation_summary="获取对话列表",
diff --git a/apps/common/auth/authenticate.py b/apps/common/auth/authenticate.py
index 4d009bdb..9cc6cac8 100644
--- a/apps/common/auth/authenticate.py
+++ b/apps/common/auth/authenticate.py
@@ -6,21 +6,28 @@
@date:2023/9/4 11:16
@desc: 认证类
"""
+import datetime
+import traceback
+from urllib.parse import urlparse
from django.core import cache
from django.core import signing
from django.db.models import QuerySet
+from ipware import get_client_ip
from rest_framework.authentication import TokenAuthentication
from application.models.api_key_model import ApplicationAccessToken, ApplicationApiKey
from common.constants.authentication_type import AuthenticationType
from common.constants.permission_constants import Auth, get_permission_list_by_role, RoleConstants, Permission, Group, \
Operate
-from common.exception.app_exception import AppAuthenticationFailed
+from common.exception.app_exception import AppAuthenticationFailed, AppEmbedIdentityFailed, AppChatNumOutOfBoundsFailed
+from common.util.common import getRestSeconds
+from common.util.rsa_util import decrypt
from smartdoc.settings import JWT_AUTH
from users.models.user import User, get_user_dynamics_permission
token_cache = cache.caches['token_cache']
+chat_cache = cache.caches['chat_cache']
class AnonymousAuthentication(TokenAuthentication):
@@ -80,6 +87,35 @@ class TokenAuth(TokenAuthentication):
raise AppAuthenticationFailed(1002, "身份验证信息不正确")
if not application_access_token.access_token == auth_details.get('access_token'):
raise AppAuthenticationFailed(1002, "身份验证信息不正确")
+ if application_access_token.white_active:
+ referer = request.META.get('HTTP_REFERER')
+ if referer is not None:
+ client_ip = urlparse(referer).hostname
+ else:
+ client_ip = get_client_ip(request)
+ if not application_access_token.white_list.__contains__(client_ip):
+ raise AppAuthenticationFailed(1002, "身份验证信息不正确")
+ if 'embed_identity' in request.COOKIES and request.path.__contains__('/api/application/chat_message/'):
+ embed_identity = request.COOKIES['embed_identity']
+ try:
+ # 如果无法解密 说明embed_identity并非系统颁发
+ value = decrypt(embed_identity)
+ except Exception as e:
+ raise AppEmbedIdentityFailed(1004, '嵌入cookie不正确')
+ embed_identity_number = chat_cache.get(value)
+ if embed_identity_number is not None:
+ if application_access_token.access_num <= embed_identity_number:
+ raise AppChatNumOutOfBoundsFailed(1003, '访问次数超过今日访问量')
+ # 对话次数+1
+ try:
+ if not chat_cache.incr(value):
+ # 如果修改失败则设置为1
+ chat_cache.set(value, 1,
+ timeout=getRestSeconds())
+ except Exception as e:
+ # 如果修改失败则设置为1 证明 key不存在
+ chat_cache.add(value, 1,
+ timeout=getRestSeconds())
return application_access_token.application.user, Auth(
role_list=[RoleConstants.APPLICATION_ACCESS_TOKEN],
permission_list=[
@@ -94,4 +130,7 @@ class TokenAuth(TokenAuthentication):
raise AppAuthenticationFailed(1002, "身份验证信息不正确!非法用户")
except Exception as e:
+ traceback.format_exc()
+ if isinstance(e, AppEmbedIdentityFailed) or isinstance(e, AppChatNumOutOfBoundsFailed):
+ raise e
raise AppAuthenticationFailed(1002, "身份验证信息不正确!非法用户")
diff --git a/apps/common/exception/app_exception.py b/apps/common/exception/app_exception.py
index ffa9e91e..3646efb0 100644
--- a/apps/common/exception/app_exception.py
+++ b/apps/common/exception/app_exception.py
@@ -51,3 +51,25 @@ class AppUnauthorizedFailed(AppApiException):
def __init__(self, code, message):
self.code = code
self.message = message
+
+
+class AppEmbedIdentityFailed(AppApiException):
+ """
+ 嵌入cookie异常
+ """
+ status_code = 460
+
+ def __init__(self, code, message):
+ self.code = code
+ self.message = message
+
+
+class AppChatNumOutOfBoundsFailed(AppApiException):
+ """
+ 访问次数超过今日访问量
+ """
+ status_code = 461
+
+ def __init__(self, code, message):
+ self.code = code
+ self.message = message
diff --git a/apps/common/util/common.py b/apps/common/util/common.py
index 52d90ec8..d6492e35 100644
--- a/apps/common/util/common.py
+++ b/apps/common/util/common.py
@@ -6,9 +6,36 @@
@date:2023/10/16 16:42
@desc:
"""
+import datetime
import importlib
+import uuid
from functools import reduce
from typing import Dict, List
+from django.core import cache
+
+from .rsa_util import encrypt
+
+chat_cache = cache.caches['chat_cache']
+
+
+def set_embed_identity_cookie(request, response):
+ if 'embed_identity' in request.COOKIES:
+ embed_identity = request.COOKIES['embed_identity']
+ else:
+ value = str(uuid.uuid1())
+ embed_identity = encrypt(value)
+ chat_cache.set(value, 0, timeout=getRestSeconds())
+ response.set_cookie("embed_identity", embed_identity, max_age=3600 * 24 * 100, samesite='None',
+ secure=True)
+ return response
+
+
+def getRestSeconds():
+ now = datetime.datetime.now()
+ today_begin = datetime.datetime(now.year, now.month, now.day, 0, 0, 0)
+ tomorrow_begin = today_begin + datetime.timedelta(days=1)
+ rest_seconds = (tomorrow_begin - now).seconds
+ return rest_seconds
def sub_array(array: List, item_num=10):
diff --git a/apps/smartdoc/settings/base.py b/apps/smartdoc/settings/base.py
index 38f626b6..53f723a5 100644
--- a/apps/smartdoc/settings/base.py
+++ b/apps/smartdoc/settings/base.py
@@ -103,8 +103,7 @@ CACHES = {
'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "token_cache") # 文件夹路径
},
"chat_cache": {
- 'BACKEND': 'common.cache.file_cache.FileCache',
- 'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "chat_cache") # 文件夹路径
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
diff --git a/pyproject.toml b/pyproject.toml
index df9fdd69..ca72630c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ pycryptodome = "^3.19.0"
beautifulsoup4 = "^4.12.2"
html2text = "^2024.2.26"
langchain-openai = "^0.0.8"
+django-ipware = "^6.0.4"
[build-system]
requires = ["poetry-core"]
diff --git a/ui/src/components/ai-chat/index.vue b/ui/src/components/ai-chat/index.vue
index 5ea1d778..e434a6b6 100644
--- a/ui/src/components/ai-chat/index.vue
+++ b/ui/src/components/ai-chat/index.vue
@@ -399,10 +399,10 @@ const getWrite = (chat: any, reader: any, stream: boolean) => {
}
return stream ? write_stream : write_json
}
-const errorWrite = (chat: any) => {
+const errorWrite = (chat: any, message?: string) => {
ChatManagement.addChatRecord(chat, 50, loading)
ChatManagement.write(chat.id)
- ChatManagement.append(chat.id, '抱歉,当前正在维护,无法提供服务,请稍后再试!')
+ ChatManagement.append(chat.id, message || '抱歉,当前正在维护,无法提供服务,请稍后再试!')
ChatManagement.close(chat.id)
}
function chatMessage(chat?: any, problem?: string) {
@@ -444,6 +444,10 @@ function chatMessage(chat?: any, problem?: string) {
.catch((err) => {
errorWrite(chat)
})
+ } else if (response.status === 460) {
+ return Promise.reject('无法识别用户身份')
+ } else if (response.status === 461) {
+ return Promise.reject('抱歉,您的提问已达到最大限制,请明天再来吧!')
} else {
nextTick(() => {
// 将滚动条滚动到最下面
@@ -468,7 +472,7 @@ function chatMessage(chat?: any, problem?: string) {
ChatManagement.close(chat.id)
})
.catch((e: any) => {
- MsgError(e)
+ errorWrite(chat, e + '')
})
}
}
diff --git a/ui/src/request/index.ts b/ui/src/request/index.ts
index 1f994ec7..d8918346 100644
--- a/ui/src/request/index.ts
+++ b/ui/src/request/index.ts
@@ -58,7 +58,10 @@ instance.interceptors.response.use(
}
}
if (err.response?.status === 401) {
- if (!err.response.config.url.includes('chat/open')) {
+ if (
+ !err.response.config.url.includes('chat/open') &&
+ !err.response.config.url.includes('application/profile')
+ ) {
router.push({ name: 'login' })
}
}
diff --git a/ui/src/views/applicaiton-overview/component/EmbedDialog.vue b/ui/src/views/applicaiton-overview/component/EmbedDialog.vue
index 54f4c861..942b5c62 100644
--- a/ui/src/views/applicaiton-overview/component/EmbedDialog.vue
+++ b/ui/src/views/applicaiton-overview/component/EmbedDialog.vue
@@ -29,7 +29,7 @@
-
@@ -68,13 +68,13 @@ frameborder="0"
allow="microphone">
`
- source2.value = `