This commit is contained in:
wanjia 2025-05-07 22:24:02 +08:00
parent f89e32b2bc
commit 0b067f5d87
14 changed files with 2255 additions and 194 deletions

View File

@ -1,142 +0,0 @@
# apps/common/services/chat_service.py
import logging
import json
from uuid import uuid4
from django.db import transaction
from apps.accounts.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.chat.models import ChatHistory
from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
logger = logging.getLogger(__name__)
class ChatService(KnowledgeBasePermissionMixin):
@transaction.atomic
def create_chat_record(self, user, data, conversation_id=None):
"""创建聊天记录供chat、gmail、feishu模块复用"""
try:
# 验证必填字段
if 'question' not in data:
raise ValueError("缺少必填字段: question")
# 如果未提供conversation_id生成新的
if not conversation_id:
conversation_id = str(uuid4())
logger.info(f"生成新的会话ID: {conversation_id}")
# 处理知识库ID
dataset_ids = []
if 'dataset_id' in data:
dataset_ids.append(str(data['dataset_id']))
elif 'dataset_id_list' in data:
if isinstance(data['dataset_id_list'], str):
try:
dataset_list = json.loads(data['dataset_id_list'])
dataset_ids = [str(id) for id in dataset_list if isinstance(dataset_list, list)]
except json.JSONDecodeError:
dataset_ids = [str(data['dataset_id_list'])]
else:
dataset_ids = [str(id) for id in data['dataset_id_list']]
if not dataset_ids:
raise ValueError("缺少必填字段: dataset_id 或 dataset_id_list")
# 验证知识库权限
knowledge_bases = []
external_id_list = []
for kb_id in dataset_ids:
knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first()
if not knowledge_base:
raise ValueError(f"知识库不存在: {kb_id}")
if not self.check_knowledge_base_permission(knowledge_base, user, 'read'):
raise ValueError(f"无权访问知识库: {knowledge_base.name}")
knowledge_bases.append(knowledge_base)
if knowledge_base.external_id:
external_id_list.append(str(knowledge_base.external_id))
# 创建metadata
metadata = {
'model_id': data.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'),
'dataset_id_list': dataset_ids,
'dataset_external_id_list': external_id_list,
'dataset_names': [kb.name for kb in knowledge_bases]
}
# 设置标题
title = data.get('title', 'New chat')
# 创建用户问题记录
question_record = ChatHistory.objects.create(
user=user,
knowledge_base=knowledge_bases[0], # 使用第一个知识库
conversation_id=conversation_id,
title=title,
role='user',
content=data['question'],
metadata=metadata
)
return question_record, conversation_id, metadata, knowledge_bases, external_id_list
except Exception as e:
logger.error(f"创建聊天记录失败: {str(e)}")
raise
def get_conversation_detail(self, user, conversation_id):
"""获取会话详情供chat、gmail、feishu模块复用"""
try:
# 获取用户有权限的知识库ID
accessible_kb_ids = [
kb.id for kb in KnowledgeBase.objects.all()
if self.check_knowledge_base_permission(kb, user, 'read')
]
# 查询会话记录
messages = ChatHistory.objects.filter(
conversation_id=conversation_id,
is_deleted=False
).filter(
Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids)
).order_by('created_at')
if not messages.exists():
raise ValueError("对话不存在或无权限")
# 获取知识库信息
first_message = messages.first()
dataset_info = []
if first_message and first_message.metadata and 'dataset_id_list' in first_message.metadata:
datasets = KnowledgeBase.objects.filter(id__in=first_message.metadata['dataset_id_list'])
accessible_datasets = [
ds for ds in datasets if self.check_knowledge_base_permission(ds, user, 'read')
]
dataset_info = [
{'id': str(ds.id), 'name': ds.name, 'type': ds.type}
for ds in accessible_datasets
]
# 构建消息列表
message_list = [
{
'id': str(msg.id),
'parent_id': msg.parent_id,
'role': msg.role,
'content': msg.content,
'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'metadata': msg.metadata
}
for msg in messages
]
return {
'conversation_id': conversation_id,
'datasets': dataset_info,
'messages': message_list
}
except Exception as e:
logger.error(f"获取会话详情失败: {str(e)}")
raise

View File

@ -0,0 +1,19 @@
# apps/chat/serializers.py
from rest_framework import serializers
from apps.chat.models import ChatHistory
from apps.knowledge_base.models import KnowledgeBase
class ChatHistorySerializer(serializers.ModelSerializer):
knowledge_base_id = serializers.UUIDField(source='knowledge_base.id', read_only=True)
dataset_name = serializers.CharField(source='knowledge_base.name', read_only=True)
user_id = serializers.UUIDField(source='user.id', read_only=True)
class Meta:
model = ChatHistory
fields = [
'id', 'user_id', 'knowledge_base_id', 'dataset_name', 'conversation_id',
'title', 'role', 'content', 'parent_id', 'metadata', 'created_at', 'is_deleted'
]
read_only_fields = ['id', 'user_id', 'knowledge_base_id', 'dataset_name', 'created_at', 'is_deleted']

View File

@ -0,0 +1,346 @@
import requests
import json
import logging
from django.conf import settings
from rest_framework.exceptions import APIException
from daren_project import settings
logger = logging.getLogger(__name__)
class ExternalAPIError(APIException):
status_code = 500
default_detail = '外部API调用失败'
default_code = 'external_api_error'
def stream_chat_answer(conversation_id, question, dataset_external_ids, metadata):
"""流式调用外部聊天API返回生成器以实时传输回答"""
try:
# 构造聊天请求数据
chat_request_data = {
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
"model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006",
"dataset_id_list": [str(id) for id in dataset_external_ids],
"multiple_rounds_dialogue": False,
"dataset_setting": {
"top_n": 10, "similarity": "0.3",
"max_paragraph_char_number": 10000,
"search_mode": "blend",
"no_references_setting": {
"value": "{question}",
"status": "ai_questioning"
}
},
"model_setting": {
"prompt": "**相关文档内容**{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**{question}"
},
"problem_optimization": False
}
# 发起聊天请求
logger.info(f"调用流式聊天API: {settings.API_BASE_URL}/api/application/chat/open")
chat_response = requests.post(
url=f"{settings.API_BASE_URL}/api/application/chat/open",
json=chat_request_data,
headers={"Content-Type": "application/json"}
)
logger.info(f"聊天API响应状态码: {chat_response.status_code}")
if chat_response.status_code != 200:
error_msg = f"聊天API调用失败: {chat_response.text}"
logger.error(error_msg)
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
return
chat_data = chat_response.json()
if chat_data.get('code') != 200 or not chat_data.get('data'):
error_msg = f"聊天API返回错误: {chat_data}"
logger.error(error_msg)
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
return
chat_id = chat_data['data']
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
logger.info(f"调用消息API: {message_url}")
# 发起消息请求(流式)
message_request = requests.post(
url=message_url,
json={"message": question, "re_chat": False, "stream": True},
headers={"Content-Type": "application/json"},
stream=True
)
if message_request.status_code != 200:
error_msg = f"消息API调用失败: {message_request.status_code}, {message_request.text}"
logger.error(error_msg)
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
return
buffer = ""
for chunk in message_request.iter_content(chunk_size=1):
if not chunk:
continue
chunk_str = chunk.decode('utf-8')
buffer += chunk_str
if '\n\n' in buffer:
lines = buffer.split('\n\n')
for line in lines[:-1]:
if line.startswith('data: '):
try:
json_str = line[6:]
data = json.loads(json_str)
if 'content' in data:
content_part = data['content']
response_data = {
'code': 200,
'message': 'partial',
'data': {
'content': content_part,
'is_end': data.get('is_end', False)
}
}
yield f"data: {json.dumps(response_data)}\n\n"
if data.get('is_end', False):
return
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}, 数据: {line}")
buffer = lines[-1]
if buffer and buffer.startswith('data: '):
try:
json_str = buffer[6:]
data = json.loads(json_str)
if 'content' in data:
content_part = data['content']
response_data = {
'code': 200,
'message': 'partial',
'data': {
'content': content_part,
'is_end': data.get('is_end', False)
}
}
yield f"data: {json.dumps(response_data)}\n\n"
except json.JSONDecodeError:
logger.error(f"处理剩余数据时JSON解析错误: {buffer}")
except Exception as e:
logger.error(f"流式聊天API处理出错: {str(e)}")
yield f"data: {json.dumps({'code': 500, 'message': f'流式处理出错: {str(e)}', 'data': {'is_end': True}})}\n\n"
def get_chat_answer(dataset_external_ids, question):
"""非流式调用外部聊天API获取回答"""
try:
chat_request_data = {
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
"model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006",
"dataset_id_list": [str(id) for id in dataset_external_ids],
"multiple_rounds_dialogue": False,
"dataset_setting": {
"top_n": 10,
"similarity": "0.3",
"max_paragraph_char_number": 10000,
"search_mode": "blend",
"no_references_setting": {
"value": "{question}",
"status": "ai_questioning"
}
},
"model_setting": {
"prompt": "**相关文档内容**{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**{question}"
},
"problem_optimization": False
}
logger.info(f"调用非流式聊天API: {settings.API_BASE_URL}/api/application/chat/open")
chat_response = requests.post(
url=f"{settings.API_BASE_URL}/api/application/chat/open",
json=chat_request_data,
headers={"Content-Type": "application/json"}
)
logger.info(f"聊天API响应状态码: {chat_response.status_code}")
if chat_response.status_code != 200:
logger.error(f"聊天API调用失败: {chat_response.text}")
raise ExternalAPIError(f"聊天API调用失败: {chat_response.text}")
chat_data = chat_response.json()
if chat_data.get('code') != 200 or not chat_data.get('data'):
logger.error(f"聊天API返回错误: {chat_data}")
raise ExternalAPIError(f"聊天API返回错误: {chat_data}")
chat_id = chat_data['data']
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
logger.info(f"调用消息API: {message_url}")
message_response = requests.post(
url=message_url,
json={"message": question, "re_chat": False, "stream": False},
headers={"Content-Type": "application/json"}
)
if message_response.status_code != 200:
logger.error(f"消息API调用失败: {message_response.status_code}, {message_response.text}")
raise ExternalAPIError(f"消息API调用失败: {message_response.status_code}, {message_response.text}")
response_data = message_response.json()
if response_data.get('code') != 200 or 'data' not in response_data:
logger.error(f"消息API返回错误: {response_data}")
raise ExternalAPIError(f"消息API返回错误: {response_data}")
answer_content = response_data.get('data', {}).get('content', '')
return answer_content if answer_content else "无法获取回答内容"
except requests.exceptions.RequestException as e:
logger.error(f"聊天API网络错误: {str(e)}")
raise ExternalAPIError(f"聊天API网络错误: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"解析聊天API响应JSON失败: {str(e)}")
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
except Exception as e:
logger.error(f"调用聊天API失败: {str(e)}")
raise ExternalAPIError(f"调用聊天API失败: {str(e)}")
def generate_conversation_title(question, answer):
"""调用DeepSeek API生成会话标题"""
try:
prompt = f"请根据以下用户问题和AI回答为本次对话生成一个简洁的标题不超过20个字:\n\n**用户问题**:\n{question}\n\n**AI回答**:\n{answer}\n\n生成的标题应概括对话主题,语言简洁明了,使用中文。"
deepseek_url = "https://api.deepseek.com/v1/chat/completions"
deepseek_data = {
"model": "deepseek-pro",
"messages": [
{"role": "system", "content": "你是一个擅长总结的助手,生成简洁的对话标题。"},
{"role": "user", "content": prompt}
],
"max_tokens": 50,
"temperature": 0.7
}
logger.info(f"调用DeepSeek API生成标题: {deepseek_url}")
response = requests.post(
deepseek_url,
json=deepseek_data,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {settings.DEEPSEEK_API_KEY}"
}
)
logger.info(f"DeepSeek API响应状态码: {response.status_code}")
if response.status_code != 200:
logger.error(f"DeepSeek API调用失败: {response.text}")
raise ExternalAPIError(f"DeepSeek API调用失败: {response.text}")
result = response.json()
title = result.get('choices', [{}])[0].get('message', {}).get('content', '').strip()
return title if title else None
except requests.exceptions.RequestException as e:
logger.error(f"DeepSeek API网络错误: {str(e)}")
raise ExternalAPIError(f"DeepSeek API网络错误: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"解析DeepSeek API响应JSON失败: {str(e)}")
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
except Exception as e:
logger.error(f"调用DeepSeek API失败: {str(e)}")
raise ExternalAPIError(f"调用DeepSeek API失败: {str(e)}")
def get_hit_test_documents(dataset_id, query_text):
"""调用知识库hit_test接口获取相关文档信息"""
try:
url = f"{settings.API_BASE_URL}/api/dataset/{dataset_id}/hit_test"
params = {
"query_text": query_text,
"top_number": 10,
"similarity": 0.3,
"search_mode": "blend"
}
logger.info(f"调用hit_test API: {url}")
response = requests.get(url=url, params=params)
logger.info(f"hit_test API响应状态码: {response.status_code}")
if response.status_code != 200:
logger.error(f"hit_test API调用失败: {response.status_code}, {response.text}")
raise ExternalAPIError(f"hit_test API调用失败: {response.status_code}, {response.text}")
result = response.json()
if result.get('code') != 200:
logger.error(f"hit_test API业务错误: {result}")
raise ExternalAPIError(f"hit_test API业务错误: {result}")
documents = result.get('data', [])
return [
{
"document_name": doc.get("document_name", ""),
"dataset_name": doc.get("dataset_name", ""),
"similarity": doc.get("similarity", 0),
"comprehensive_score": doc.get("comprehensive_score", 0)
}
for doc in documents
]
except requests.exceptions.RequestException as e:
logger.error(f"hit_test API网络错误: {str(e)}")
raise ExternalAPIError(f"hit_test API网络错误: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"解析hit_test API响应JSON失败: {str(e)}")
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
except Exception as e:
logger.error(f"调用hit_test API失败: {str(e)}")
raise ExternalAPIError(f"调用hit_test API失败: {str(e)}")
def generate_conversation_title_from_deepseek(user_question, assistant_answer):
"""调用SiliconCloud API生成会话标题直接基于当前问题和回答内容"""
try:
# 从Django设置中获取API密钥
api_key = settings.SILICON_CLOUD_API_KEY
if not api_key:
return "新对话"
# 构建提示信息
prompt = f"请根据用户的问题和助手的回答生成一个简短的对话标题不超过20个字\n\n用户问题: {user_question}\n\n助手回答: {assistant_answer}"
import requests
url = "https://api.siliconflow.cn/v1/chat/completions"
payload = {
"model": "deepseek-ai/DeepSeek-V3",
"stream": False,
"max_tokens": 512,
"temperature": 0.7,
"top_p": 0.7,
"top_k": 50,
"frequency_penalty": 0.5,
"n": 1,
"stop": [],
"messages": [
{
"role": "user",
"content": prompt
}
]
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
response_data = response.json()
if response.status_code == 200 and 'choices' in response_data and response_data['choices']:
title = response_data['choices'][0]['message']['content'].strip()
return title[:50] # 截断过长的标题
else:
logger.error(f"生成标题时出错: {response.text}")
return "新对话"
except Exception as e:
logger.exception(f"生成对话标题时发生错误: {str(e)}")
return "新对话"

View File

@ -0,0 +1,11 @@
# apps/chat/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.chat.views import ChatHistoryViewSet
router = DefaultRouter()
router.register(r'', ChatHistoryViewSet, basename='chat-history')
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -1,3 +1,792 @@
from django.shortcuts import render import logging
import json
import traceback
import uuid
from datetime import datetime
from django.db.models import Q, Max, Count
from django.http import HttpResponse, StreamingHttpResponse
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.decorators import action
from apps.accounts.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.chat.models import ChatHistory
from apps.chat.serializers import ChatHistorySerializer
from apps.common.services.chat_service import ChatService
from apps.chat.services.chat_api import (
ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title,
get_hit_test_documents, generate_conversation_title_from_deepseek
)
from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
# Create your views here. logger = logging.getLogger(__name__)
class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = ChatHistorySerializer
queryset = ChatHistory.objects.all()
def get_queryset(self):
"""确保用户只能看到自己的未删除的聊天记录以及有权限的知识库关联的聊天记录"""
user = self.request.user
accessible_kb_ids = [
kb.id for kb in KnowledgeBase.objects.all()
if self.check_knowledge_base_permission(kb, user, 'read')
]
return ChatHistory.objects.filter(
Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids),
is_deleted=False
)
def list(self, request):
"""获取对话列表概览"""
try:
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
latest_chats = self.get_queryset().values(
'conversation_id'
).annotate(
latest_id=Max('id'),
message_count=Count('id'),
last_message=Max('created_at')
).order_by('-last_message')
total = latest_chats.count()
start = (page - 1) * page_size
end = start + page_size
chats = latest_chats[start:end]
results = []
for chat in chats:
latest_record = ChatHistory.objects.get(id=chat['latest_id'])
dataset_info = []
if latest_record.metadata:
dataset_id_list = latest_record.metadata.get('dataset_id_list', [])
dataset_names = latest_record.metadata.get('dataset_names', [])
if dataset_id_list:
if dataset_names and len(dataset_names) == len(dataset_id_list):
dataset_info = [
{'id': str(id), 'name': name}
for id, name in zip(dataset_id_list, dataset_names)
]
else:
datasets = KnowledgeBase.objects.filter(id__in=dataset_id_list)
dataset_info = [
{'id': str(ds.id), 'name': ds.name}
for ds in datasets
]
results.append({
'conversation_id': chat['conversation_id'],
'message_count': chat['message_count'],
'last_message': latest_record.content,
'last_time': chat['last_message'].strftime('%Y-%m-%d %H:%M:%S'),
'dataset_id_list': [ds['id'] for ds in dataset_info],
'datasets': dataset_info
})
return Response({
'code': 200,
'message': '获取成功',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'results': results
}
})
except Exception as e:
logger.error(f"获取聊天记录失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'获取聊天记录失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['get'])
def conversation_detail(self, request):
"""获取特定对话的详细信息"""
try:
conversation_id = request.query_params.get('conversation_id')
if not conversation_id:
return Response({
'code': 400,
'message': '缺少conversation_id参数',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
chat_service = ChatService()
result = chat_service.get_conversation_detail(request.user, conversation_id)
return Response({
'code': 200,
'message': '获取成功',
'data': result
})
except ValueError as e:
return Response({
'code': 404,
'message': str(e),
'data': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"获取对话详情失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'获取对话详情失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['get'])
def available_datasets(self, request):
"""获取用户可访问的知识库列表"""
try:
user = request.user
accessible_datasets = [
dataset for dataset in KnowledgeBase.objects.all()
if self.check_knowledge_base_permission(dataset, user, 'read')
]
return Response({
'code': 200,
'message': '获取成功',
'data': [
{
'id': str(ds.id),
'name': ds.name,
'type': ds.type,
'department': ds.department,
'description': ds.desc
}
for ds in accessible_datasets
]
})
except Exception as e:
logger.error(f"获取可用知识库列表失败: {str(e)}")
return Response({
'code': 500,
'message': f'获取可用知识库列表失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post'])
def create_conversation(self, request):
"""创建会话 - 先选择知识库创建会话ID不发送问题"""
try:
data = request.data
# 检查知识库ID支持dataset_id或dataset_id_list格式
dataset_ids = []
if 'dataset_id' in data:
dataset_id = data['dataset_id']
# 直接使用标准UUID格式
dataset_ids.append(str(dataset_id))
elif 'dataset_id_list' in data and isinstance(data['dataset_id_list'], (list, str)):
# 处理可能的字符串格式
if isinstance(data['dataset_id_list'], str):
try:
# 尝试解析JSON字符串
dataset_list = json.loads(data['dataset_id_list'])
if isinstance(dataset_list, list):
dataset_ids = [str(id) for id in dataset_list]
except json.JSONDecodeError:
# 如果解析失败可能是单个ID
dataset_ids = [str(data['dataset_id_list'])]
else:
# 如果已经是列表直接使用标准UUID格式
dataset_ids = [str(id) for id in data['dataset_id_list']]
else:
return Response({
'code': 400,
'message': '缺少必填字段: dataset_id 或 dataset_id_list',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not dataset_ids:
return Response({
'code': 400,
'message': '至少需要提供一个知识库ID',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证所有知识库
user = request.user
knowledge_bases = [] # 存储所有知识库对象
for kb_id in dataset_ids:
try:
knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first()
if not knowledge_base:
return Response({
'code': 404,
'message': f'知识库不存在: {kb_id}',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
knowledge_bases.append(knowledge_base)
# 使用统一的权限检查方法
if not self.check_knowledge_base_permission(knowledge_base, user, 'read'):
return Response({
'code': 403,
'message': f'无权访问知识库: {knowledge_base.name}',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except Exception as e:
return Response({
'code': 400,
'message': f'处理知识库ID出错: {str(e)}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 创建一个新的会话ID
conversation_id = str(uuid.uuid4())
logger.info(f"创建新的会话ID: {conversation_id}")
# 准备metadata (仍然保存知识库名称用于内部处理)
metadata = {
'dataset_id_list': [str(id) for id in dataset_ids],
'dataset_names': [kb.name for kb in knowledge_bases]
}
return Response({
'code': 200,
'message': '会话创建成功',
'data': {
'conversation_id': conversation_id,
'dataset_id_list': metadata['dataset_id_list']
}
})
except Exception as e:
logger.error(f"创建会话失败: {str(e)}")
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'创建会话失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def create(self, request):
"""创建聊天记录"""
try:
chat_service = ChatService()
question_record, conversation_id, metadata, knowledge_bases, external_id_list = chat_service.create_chat_record(
request.user, request.data, request.data.get('conversation_id')
)
use_stream = request.data.get('stream', True)
title = request.data.get('title', 'New chat')
if use_stream:
def stream_response():
answer_record = ChatHistory.objects.create(
user=question_record.user,
knowledge_base=knowledge_bases[0],
conversation_id=conversation_id,
title=title,
parent_id=str(question_record.id),
role='assistant',
content="",
metadata=metadata
)
yield f"data: {json.dumps({'code': 200, 'message': '开始流式传输', 'data': {'id': str(answer_record.id), 'conversation_id': conversation_id, 'content': '', 'is_end': False}})}\n\n"
full_content = ""
for data in stream_chat_answer(conversation_id, request.data['question'], external_id_list, metadata):
parsed_data = json.loads(data[5:-2]) # 移除"data: "和"\n\n"
if parsed_data['code'] == 200 and 'content' in parsed_data['data']:
content_part = parsed_data['data']['content']
full_content += content_part
response_data = {
'code': 200,
'message': 'partial',
'data': {
'id': str(answer_record.id),
'conversation_id': conversation_id,
'title': title,
'content': content_part,
'is_end': parsed_data['data']['is_end']
}
}
yield f"data: {json.dumps(response_data)}\n\n"
if parsed_data['data']['is_end']:
answer_record.content = full_content.strip()
answer_record.save()
current_title = ChatHistory.objects.filter(
conversation_id=conversation_id
).exclude(
title__in=["New chat", "新对话", ""]
).values_list('title', flat=True).first()
if current_title:
title_updated = current_title
else:
try:
generated_title = generate_conversation_title(
request.data['question'], full_content.strip()
)
if generated_title:
ChatHistory.objects.filter(
conversation_id=conversation_id
).update(title=generated_title)
title_updated = generated_title
else:
title_updated = "新对话"
except ExternalAPIError as e:
logger.error(f"自动生成标题失败: {str(e)}")
title_updated = "新对话"
final_response = {
'code': 200,
'message': '完成',
'data': {
'id': str(answer_record.id),
'conversation_id': conversation_id,
'title': title_updated,
'dataset_id_list': metadata.get('dataset_id_list', []),
'dataset_names': metadata.get('dataset_names', []),
'role': 'assistant',
'content': full_content.strip(),
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'is_end': True
}
}
yield f"data: {json.dumps(final_response)}\n\n"
break
elif parsed_data['code'] != 200:
yield data
break
if full_content:
try:
answer_record.content = full_content.strip()
answer_record.save()
except Exception as save_error:
logger.error(f"保存部分内容失败: {str(save_error)}")
response = StreamingHttpResponse(
stream_response(),
content_type='text/event-stream',
status=status.HTTP_201_CREATED
)
response['Cache-Control'] = 'no-cache, no-store'
response['Connection'] = 'keep-alive'
return response
else:
logger.info("使用非流式输出模式")
try:
answer = get_chat_answer(external_id_list, request.data['question'])
except ExternalAPIError as e:
logger.error(f"获取回答失败: {str(e)}")
return Response({
'code': 500,
'message': f'获取回答失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if answer is None:
return Response({
'code': 500,
'message': '获取回答失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
answer_record = ChatHistory.objects.create(
user=request.user,
knowledge_base=knowledge_bases[0],
conversation_id=conversation_id,
title=title,
parent_id=str(question_record.id),
role='assistant',
content=answer,
metadata=metadata
)
existing_records = ChatHistory.objects.filter(conversation_id=conversation_id)
should_generate_title = not existing_records.exclude(id=question_record.id).exists() and (not title or title == 'New chat')
if should_generate_title:
try:
generated_title = generate_conversation_title(
request.data['question'], answer
)
if generated_title:
ChatHistory.objects.filter(conversation_id=conversation_id).update(title=generated_title)
title = generated_title
except ExternalAPIError as e:
logger.error(f"自动生成标题失败: {str(e)}")
return Response({
'code': 200,
'message': '成功',
'data': {
'id': str(answer_record.id),
'conversation_id': conversation_id,
'title': title,
'dataset_id_list': metadata.get('dataset_id_list', []),
'dataset_names': metadata.get('dataset_names', []),
'role': 'assistant',
'content': answer,
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S')
}
}, status=status.HTTP_201_CREATED)
except ValueError as e:
return Response({
'code': 400,
'message': str(e),
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"创建聊天记录失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'创建聊天记录失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post'])
def hit_test(self, request):
"""获取问题与知识库文档的匹配度"""
try:
data = request.data
if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']:
return Response({
'code': 400,
'message': '缺少必填字段: question 或 dataset_id_list',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
question = data['question']
dataset_ids = data['dataset_id_list']
if not isinstance(dataset_ids, list):
try:
dataset_ids = json.loads(dataset_ids)
if not isinstance(dataset_ids, list):
dataset_ids = [dataset_ids]
except (json.JSONDecodeError, TypeError):
dataset_ids = [dataset_ids]
external_id_list = []
for kb_id in dataset_ids:
kb = KnowledgeBase.objects.filter(id=kb_id).first()
if not kb:
return Response({
'code': 404,
'message': f'知识库不存在: {kb_id}',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
if not self.check_knowledge_base_permission(kb, request.user, 'read'):
return Response({
'code': 403,
'message': f'无权访问知识库: {kb.name}',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
if kb.external_id:
external_id_list.append(str(kb.external_id))
if not external_id_list:
return Response({
'code': 400,
'message': '没有有效的知识库external_id',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
all_documents = []
for dataset_id in external_id_list:
try:
doc_info = get_hit_test_documents(dataset_id, question)
if doc_info:
all_documents.extend(doc_info)
except ExternalAPIError as e:
logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}")
continue # 宽松处理,跳过失败的知识库
all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True)
return Response({
'code': 200,
'message': '成功',
'data': {
'question': question,
'matched_documents': all_documents,
'total_count': len(all_documents)
}
})
except Exception as e:
logger.error(f"hit_test接口调用失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'hit_test接口调用失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def _highlight_keyword(self, text, keyword):
"""高亮关键词"""
if not keyword or not text:
return text
return text.replace(keyword, f'<em class="highlight">{keyword}</em>')
def update(self, request, pk=None):
"""更新聊天记录"""
try:
record = self.get_queryset().filter(id=pk).first()
if not record:
return Response({
'code': 404,
'message': '记录不存在或无权限',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
data = request.data
updateable_fields = ['content', 'metadata']
if 'content' in data:
record.content = data['content']
if 'metadata' in data:
current_metadata = record.metadata or {}
current_metadata.update(data['metadata'])
record.metadata = current_metadata
record.save()
return Response({
'code': 200,
'message': '更新成功',
'data': {
'id': str(record.id),
'conversation_id': record.conversation_id,
'role': record.role,
'content': record.content,
'metadata': record.metadata,
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
})
except Exception as e:
logger.error(f"更新聊天记录失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'更新聊天记录失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def destroy(self, request, pk=None):
"""删除聊天记录(软删除)"""
try:
record = self.get_queryset().filter(id=pk).first()
if not record:
return Response({
'code': 404,
'message': '记录不存在或无权限',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
record.soft_delete()
return Response({
'code': 200,
'message': '删除成功',
'data': None
})
except Exception as e:
logger.error(f"删除聊天记录失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'删除聊天记录失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['get'])
def search(self, request):
"""搜索聊天记录"""
try:
keyword = request.query_params.get('keyword', '').strip()
dataset_id = request.query_params.get('dataset_id')
start_date = request.query_params.get('start_date')
end_date = request.query_params.get('end_date')
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
query = self.get_queryset()
if keyword:
query = query.filter(
Q(content__icontains=keyword) |
Q(knowledge_base__name__icontains=keyword)
)
if dataset_id:
knowledge_base = KnowledgeBase.objects.filter(id=dataset_id).first()
if knowledge_base and not self.check_knowledge_base_permission(knowledge_base, request.user, 'read'):
return Response({
'code': 403,
'message': '无权访问该知识库',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
query = query.filter(knowledge_base__id=dataset_id)
if start_date:
query = query.filter(created_at__gte=start_date)
if end_date:
query = query.filter(created_at__lte=end_date)
total = query.count()
start = (page - 1) * page_size
end = start + page_size
records = query.order_by('-created_at')[start:end]
results = [
{
'id': str(record.id),
'conversation_id': record.conversation_id,
'dataset_id': str(record.knowledge_base.id),
'dataset_name': record.knowledge_base.name,
'role': record.role,
'content': record.content,
'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'metadata': record.metadata,
'highlights': {'content': self._highlight_keyword(record.content, keyword)} if keyword else {}
}
for record in records
]
return Response({
'code': 200,
'message': '搜索成功',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'results': results
}
})
except Exception as e:
logger.error(f"搜索聊天记录失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'搜索失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['delete'])
def delete_conversation(self, request):
"""通过conversation_id删除一组会话"""
try:
conversation_id = request.query_params.get('conversation_id')
if not conversation_id:
return Response({
'code': 400,
'message': '缺少必要参数: conversation_id',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
records = self.get_queryset().filter(conversation_id=conversation_id)
if not records.exists():
return Response({
'code': 404,
'message': '未找到该会话或无权限访问',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
records_count = records.count()
for record in records:
record.soft_delete()
return Response({
'code': 200,
'message': '删除成功',
'data': {
'conversation_id': conversation_id,
'deleted_count': records_count
}
})
except Exception as e:
logger.error(f"删除会话失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'删除会话失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['get'], url_path='generate-conversation-title')
def generate_conversation_title(self, request):
"""更新会话标题"""
try:
conversation_id = request.query_params.get('conversation_id')
if not conversation_id:
return Response({
'code': 400,
'message': '缺少conversation_id参数',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 检查对话是否存在
messages = self.get_queryset().filter(
conversation_id=conversation_id,
is_deleted=False,
user=request.user
).order_by('created_at')
if not messages.exists():
return Response({
'code': 404,
'message': '对话不存在或无权访问',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 检查是否有自定义标题参数
custom_title = request.query_params.get('title')
if not custom_title:
return Response({
'code': 400,
'message': '缺少title参数',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 更新所有相关记录的标题
ChatHistory.objects.filter(
conversation_id=conversation_id,
user=request.user
).update(title=custom_title)
return Response({
'code': 200,
'message': '更新会话标题成功',
'data': {
'conversation_id': conversation_id,
'title': custom_title
}
})
except Exception as e:
logger.error(f"更新会话标题失败: {str(e)}")
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f"更新会话标题失败: {str(e)}",
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -0,0 +1,143 @@
# apps/common/services/chat_service.py
import logging
import json
from uuid import uuid4
from django.db import transaction
from apps.accounts.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.chat.models import ChatHistory
from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
logger = logging.getLogger(__name__)
class ChatService(KnowledgeBasePermissionMixin):
@transaction.atomic
def create_chat_record(self, user, data, conversation_id=None):
"""创建聊天记录供chat、gmail、feishu模块复用"""
try:
# 验证必填字段
if 'question' not in data:
raise ValueError("缺少必填字段: question")
# 如果未提供conversation_id生成新的
if not conversation_id:
conversation_id = str(uuid4())
logger.info(f"生成新的会话ID: {conversation_id}")
# 处理知识库ID
dataset_ids = []
if 'dataset_id' in data:
dataset_ids.append(str(data['dataset_id']))
elif 'dataset_id_list' in data:
if isinstance(data['dataset_id_list'], str):
try:
dataset_list = json.loads(data['dataset_id_list'])
dataset_ids = [str(id) for id in dataset_list if isinstance(dataset_list, list)]
except json.JSONDecodeError:
dataset_ids = [str(data['dataset_id_list'])]
else:
dataset_ids = [str(id) for id in data['dataset_id_list']]
if not dataset_ids:
raise ValueError("缺少必填字段: dataset_id 或 dataset_id_list")
# 验证知识库权限
knowledge_bases = []
external_id_list = []
for kb_id in dataset_ids:
knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first()
if not knowledge_base:
raise ValueError(f"知识库不存在: {kb_id}")
if not self.check_knowledge_base_permission(knowledge_base, user, 'read'):
raise ValueError(f"无权访问知识库: {knowledge_base.name}")
knowledge_bases.append(knowledge_base)
if knowledge_base.external_id:
external_id_list.append(str(knowledge_base.external_id))
# 创建metadata
metadata = {
'model_id': data.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'),
'dataset_id_list': dataset_ids,
'dataset_external_id_list': external_id_list,
'dataset_names': [kb.name for kb in knowledge_bases]
}
# 设置标题
title = data.get('title', 'New chat')
# 创建用户问题记录
question_record = ChatHistory.objects.create(
user=user,
knowledge_base=knowledge_bases[0], # 使用第一个知识库
conversation_id=conversation_id,
title=title,
role='user',
content=data['question'],
metadata=metadata
)
return question_record, conversation_id, metadata, knowledge_bases, external_id_list
except Exception as e:
logger.error(f"创建聊天记录失败: {str(e)}")
raise
def get_conversation_detail(self, user, conversation_id):
"""获取会话详情供chat、gmail、feishu模块复用"""
try:
# 获取用户有权限的知识库ID
accessible_kb_ids = [
kb.id for kb in KnowledgeBase.objects.all()
if self.check_knowledge_base_permission(kb, user, 'read')
]
# 查询会话记录
messages = ChatHistory.objects.filter(
conversation_id=conversation_id,
is_deleted=False
).filter(
Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids)
).order_by('created_at')
if not messages.exists():
raise ValueError("对话不存在或无权限")
# 获取知识库信息
first_message = messages.first()
dataset_info = []
if first_message and first_message.metadata and 'dataset_id_list' in first_message.metadata:
datasets = KnowledgeBase.objects.filter(id__in=first_message.metadata['dataset_id_list'])
accessible_datasets = [
ds for ds in datasets if self.check_knowledge_base_permission(ds, user, 'read')
]
dataset_info = [
{'id': str(ds.id), 'name': ds.name, 'type': ds.type}
for ds in accessible_datasets
]
# 构建消息列表
message_list = [
{
'id': str(msg.id),
'parent_id': msg.parent_id,
'role': msg.role,
'content': msg.content,
'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'metadata': msg.metadata
}
for msg in messages
]
return {
'conversation_id': conversation_id,
'datasets': dataset_info,
'messages': message_list
}
except Exception as e:
logger.error(f"获取会话详情失败: {str(e)}")
raise

View File

@ -103,29 +103,29 @@ def call_split_api_multiple(files):
try: try:
url = f'{settings.API_BASE_URL}/api/dataset/document/split' url = f'{settings.API_BASE_URL}/api/dataset/document/split'
# 准备多文件上传数据 # 准备请求数据 - 将所有文件作为 'file' 字段
files_data = {} files_data = [('file', (file.name, file, file.content_type)) for file in files]
for i, file_obj in enumerate(files):
if hasattr(file_obj, 'seek'):
file_obj.seek(0)
logger.info(f"准备上传文件 {i+1}/{len(files)}: {file_obj.name}, 大小: {file_obj.size}字节, 类型: {file_obj.content_type}") # 记录上传的文件信息
for file in files:
logger.info(f"准备上传文件: {file.name}, 大小: {file.size}字节, 类型: {file.content_type}")
# 读取文件内容前100个字符进行记录
if hasattr(file, 'read') and hasattr(file, 'seek'):
file.seek(0)
content_preview = file.read(100).decode('utf-8', errors='ignore')
logger.info(f"文件内容预览: {content_preview}")
file.seek(0) # 重置文件指针
# 添加文件预览日志 logger.info(f"调用分割API URL: {url}")
if hasattr(file_obj, 'read') and hasattr(file_obj, 'seek'): logger.info(f"上传文件数量: {len(files_data)}")
content_preview = file_obj.read(100).decode('utf-8', errors='ignore')
logger.info(f"文件 {i+1} 内容预览: {content_preview}")
file_obj.seek(0)
# 使用唯一的键名添加到files_data # 发送请求
files_data[f'file{i}'] = file_obj response = requests.post(
url,
logger.info(f"调用分割API URL: {url}, 批量处理 {len(files_data)} 个文件") files=files_data
logger.info(f"请求字段: {list(files_data.keys())}") )
# 发送批量请求
response = requests.post(url, files=files_data)
# 记录请求头和响应信息
logger.info(f"请求头: {response.request.headers}") logger.info(f"请求头: {response.request.headers}")
logger.info(f"响应状态码: {response.status_code}") logger.info(f"响应状态码: {response.status_code}")
@ -133,25 +133,29 @@ def call_split_api_multiple(files):
logger.error(f"分割API返回错误状态码: {response.status_code}, 响应: {response.text}") logger.error(f"分割API返回错误状态码: {response.status_code}, 响应: {response.text}")
return None return None
# 解析响应
result = response.json() result = response.json()
logger.info(f"分割API响应详情: {result}") logger.info(f"分割API响应详情: {result}")
logger.info(f"成功获取 {len(result.get('data', []))} 个文档结果")
# 如果数据为空可能是API处理失败尝试后备方案
if len(result.get('data', [])) == 0: if len(result.get('data', [])) == 0:
logger.warning("分割API返回的数据为空使用后备方案") logger.warning("分割API返回的数据为空尝试使用后备方案")
# 为所有文件创建后备数据
fallback_data = { fallback_data = {
'code': 200, 'code': 200,
'message': '成功(后备)', 'message': '成功',
'data': [{ 'data': [
{
'name': file.name, 'name': file.name,
'content': [{ 'content': [
{
'title': '文档内容', 'title': '文档内容',
'content': '文件内容无法自动分割请检查外部API。这是一个后备内容。' 'content': '文件内容无法自动分割请检查外部API。这是一个后备内容。'
}]
} for file in files]
} }
logger.info(f"使用后备数据结构,为 {len(files)} 个文件生成数据") ]
} for file in files
]
}
logger.info("使用后备数据结构")
return fallback_data return fallback_data
return result return result
@ -160,19 +164,23 @@ def call_split_api_multiple(files):
logger.error(f"调用分割API失败: {str(e)}") logger.error(f"调用分割API失败: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
# 为所有文件创建后备响应 # 创建后备响应
fallback_response = { fallback_response = {
'code': 200, 'code': 200,
'message': '成功(后备)', 'message': '成功',
'data': [{ 'data': [
'name': file.name if hasattr(file, 'name') else f'文件_{i}', {
'content': [{ 'name': file.name,
'content': [
{
'title': '文档内容', 'title': '文档内容',
'content': '文件内容无法自动分割请检查API连接。' 'content': '文件内容无法自动分割请检查API连接。'
}]
} for i, file in enumerate(files)]
} }
logger.info(f"由于异常,返回后备响应,包含 {len(fallback_response['data'])} 个条目") ]
} for file in files
]
}
logger.info("由于异常,返回后备响应")
return fallback_response return fallback_response
def call_upload_api(external_id, doc_data): def call_upload_api(external_id, doc_data):

View File

@ -0,0 +1,43 @@
# apps/common/services/notification_service.py
import logging
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from apps.notifications.models import Notification
logger = logging.getLogger(__name__)
class NotificationService:
def send_notification(self, user, title, content, notification_type, related_object_id, sender=None):
"""发送通知并通过WebSocket推送"""
try:
notification = Notification.objects.create(
sender=sender,
receiver=user,
title=title,
content=content,
type=notification_type,
related_resource=related_object_id,
)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
f"notification_user_{user.id}",
{
"type": "notification",
"data": {
"id": str(notification.id),
"title": notification.title,
"content": notification.content,
"type": notification.type,
"created_at": notification.created_at.isoformat(),
"sender": {
"id": str(notification.sender.id),
"name": notification.sender.name
} if notification.sender else None
}
}
)
except Exception as e:
logger.error(f"发送通知失败: {str(e)}")

View File

@ -0,0 +1,338 @@
# apps/common/services/permission_service.py
import logging
from django.db import transaction
from django.db.models import Q
from django.utils import timezone
from rest_framework.exceptions import ValidationError
from apps.accounts.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.permissions.models import Permission, KnowledgeBasePermission as KBPermissionModel
logger = logging.getLogger(__name__)
class PermissionService:
def can_manage_knowledge_base(self, user, knowledge_base):
"""检查用户是否是知识库的创建者"""
return str(knowledge_base.user_id) == str(user.id)
def check_extend_permission(self, permission, user):
"""检查是否有权限延长权限有效期"""
knowledge_base = permission.knowledge_base
if knowledge_base.type == 'private':
return knowledge_base.user_id == user.id
if knowledge_base.type == 'leader':
return user.role == 'admin'
if knowledge_base.type == 'member':
return user.role == 'admin' or (
user.role == 'leader' and user.department == knowledge_base.department
)
return False
def create_permission_request(self, user, validated_data, notification_service):
"""创建权限申请并发送通知"""
knowledge_base = validated_data['knowledge_base']
if str(knowledge_base.user_id) == str(user.id):
raise ValidationError({
"code": 400,
"message": "您是此知识库的创建者,无需申请权限",
"data": None
})
approver = User.objects.get(id=knowledge_base.user_id)
requested_permissions = validated_data.get('permissions', {})
expires_at = validated_data.get('expires_at')
if not any([requested_permissions.get('can_read'),
requested_permissions.get('can_edit'),
requested_permissions.get('can_delete')]):
raise ValidationError("至少需要申请一种权限(读/改/删)")
if not expires_at:
raise ValidationError("请指定权限到期时间")
existing_request = Permission.objects.filter(
knowledge_base=knowledge_base,
applicant=user,
status='pending'
).first()
if existing_request:
raise ValidationError("您已有一个待处理的权限申请")
existing_permission = Permission.objects.filter(
knowledge_base=knowledge_base,
applicant=user,
status='approved',
expires_at__gt=timezone.now()
).first()
if existing_permission:
raise ValidationError("您已有此知识库的访问权限")
with transaction.atomic():
permission = Permission.objects.create(
knowledge_base=knowledge_base,
applicant=user,
approver=approver,
permissions=requested_permissions,
expires_at=expires_at,
status='pending'
)
permission_types = []
if requested_permissions.get('can_read'):
permission_types.append('读取')
if requested_permissions.get('can_edit'):
permission_types.append('编辑')
if requested_permissions.get('can_delete'):
permission_types.append('删除')
permission_str = ''.join(permission_types)
notification_service.send_notification(
user=approver,
title="新的权限申请",
content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}'{permission_str}权限",
notification_type="permission_request",
related_object_id=str(permission.id)
)
return permission
def approve_permission(self, user, permission, response_message, notification_service):
"""审批权限申请"""
if not self.can_manage_knowledge_base(user, permission.knowledge_base):
raise ValidationError({
'code': 403,
'message': '只有知识库创建者可以审批此申请',
'data': None
})
with transaction.atomic():
permission.status = 'approved'
permission.approver = user
permission.response_message = response_message
permission.save()
kb_permission = KBPermissionModel.objects.filter(
knowledge_base=permission.knowledge_base,
user=permission.applicant
).first()
if kb_permission:
kb_permission.can_read = permission.permissions.get('can_read', False)
kb_permission.can_edit = permission.permissions.get('can_edit', False)
kb_permission.can_delete = permission.permissions.get('can_delete', False)
kb_permission.granted_by = user
kb_permission.status = 'active'
kb_permission.expires_at = permission.expires_at
kb_permission.save()
logger.info(f"更新知识库权限记录: {kb_permission.id}")
else:
kb_permission = KBPermissionModel.objects.create(
knowledge_base=permission.knowledge_base,
user=permission.applicant,
can_read=permission.permissions.get('can_read', False),
can_edit=permission.permissions.get('can_edit', False),
can_delete=permission.permissions.get('can_delete', False),
granted_by=user,
status='active',
expires_at=permission.expires_at
)
logger.info(f"创建新的知识库权限记录: {kb_permission.id}")
notification_service.send_notification(
user=permission.applicant,
title="权限申请已通过",
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过",
notification_type="permission_approved",
related_object_id=str(permission.id)
)
return permission
def reject_permission(self, user, permission, response_message, notification_service):
"""拒绝权限申请"""
if not self.can_manage_knowledge_base(user, permission.knowledge_base):
raise ValidationError({
'code': 403,
'message': '只有知识库创建者可以审批此申请',
'data': None
})
if permission.status != 'pending':
raise ValidationError({
'code': 400,
'message': '该申请已被处理',
'data': None
})
if not response_message:
raise ValidationError({
'code': 400,
'message': '请填写拒绝原因',
'data': None
})
with transaction.atomic():
permission.status = 'rejected'
permission.approver = user
permission.response_message = response_message
permission.save()
notification_service.send_notification(
user=permission.applicant,
title="权限申请已拒绝",
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已被拒绝\n拒绝原因:{response_message}",
notification_type="permission_rejected",
related_object_id=str(permission.id)
)
return permission
def extend_permission(self, user, permission, new_expires_at, notification_service):
"""延长权限有效期"""
if not self.check_extend_permission(permission, user):
raise ValidationError({
"code": 403,
"message": "您没有权限延长此权限",
"data": None
})
if not new_expires_at:
raise ValidationError({
"code": 400,
"message": "请设置新的过期时间",
"data": None
})
try:
new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ')
new_expires_at = timezone.make_aware(new_expires_at)
if new_expires_at <= timezone.now():
raise ValidationError({
"code": 400,
"message": "过期时间不能早于或等于当前时间",
"data": None
})
except ValueError:
raise ValidationError({
"code": 400,
"message": "过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)",
"data": None
})
with transaction.atomic():
permission.expires_at = new_expires_at
permission.save()
kb_permission = KBPermissionModel.objects.get(
knowledge_base=permission.knowledge_base,
user=permission.applicant
)
kb_permission.expires_at = new_expires_at
kb_permission.save()
notification_service.send_notification(
user=permission.applicant,
title="权限有效期延长",
content=f"您对知识库 '{permission.knowledge_base.name}' 的权限有效期已延长至 {new_expires_at.strftime('%Y-%m-%d %H:%M:%S')}",
notification_type="permission_extended",
related_object_id=str(permission.id)
)
return permission
def update_user_permission(self, admin_user, user_id, knowledge_base_id, permissions, expires_at_str, notification_service):
"""管理员更新用户权限"""
if admin_user.role != 'admin':
raise ValidationError({
'code': 403,
'message': '只有管理员可以直接修改权限',
'data': None
})
if not all([user_id, knowledge_base_id, permissions]):
raise ValidationError({
'code': 400,
'message': '缺少必要参数',
'data': None
})
required_permission_fields = ['can_read', 'can_edit', 'can_delete']
if not all(field in permissions for field in required_permission_fields):
raise ValidationError({
'code': 400,
'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete',
'data': None
})
try:
user = User.objects.get(id=user_id)
knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id)
except User.DoesNotExist:
raise ValidationError({
'code': 404,
'message': f'用户ID {user_id} 不存在',
'data': None
})
except KnowledgeBase.DoesNotExist:
raise ValidationError({
'code': 404,
'message': f'知识库ID {knowledge_base_id} 不存在',
'data': None
})
if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id):
raise ValidationError({
'code': 403,
'message': '不能修改其他用户的私有知识库权限',
'data': None
})
if user.role == 'member' and permissions.get('can_delete'):
raise ValidationError({
'code': 400,
'message': '普通成员不能获得删除权限',
'data': None
})
expires_at = None
if expires_at_str:
try:
expires_at = timezone.datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M:%SZ')
expires_at = timezone.make_aware(expires_at)
if expires_at <= timezone.now():
raise ValidationError({
'code': 400,
'message': '过期时间不能早于或等于当前时间',
'data': None
})
except ValueError:
raise ValidationError({
'code': 400,
'message': '过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)',
'data': None
})
with transaction.atomic():
permission, created = KBPermissionModel.objects.update_or_create(
user=user,
knowledge_base=knowledge_base,
defaults={
'can_read': permissions.get('can_read', False),
'can_edit': permissions.get('can_edit', False),
'can_delete': permissions.get('can_delete', False),
'granted_by': admin_user,
'status': 'active',
'expires_at': expires_at
}
)
notification_service.send_notification(
user=user,
title="知识库权限更新",
content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限",
notification_type="permission_updated",
related_object_id=str(permission.id)
)
return permission, created

View File

@ -0,0 +1,18 @@
# apps/permissions/serializers.py
from rest_framework import serializers
from apps.permissions.models import Permission
from apps.knowledge_base.models import KnowledgeBase
from apps.accounts.models import User
class PermissionSerializer(serializers.ModelSerializer):
knowledge_base = serializers.PrimaryKeyRelatedField(queryset=KnowledgeBase.objects.all())
applicant = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)
approver = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), allow_null=True, required=False)
class Meta:
model = Permission
fields = [
'id', 'knowledge_base', 'applicant', 'approver', 'permissions',
'status', 'created_at', 'expires_at', 'response_message'
]
read_only_fields = ['id', 'created_at', 'applicant', 'approver', 'status', 'response_message']

View File

@ -0,0 +1,11 @@
# apps/permissions/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.permissions.views import PermissionViewSet
router = DefaultRouter()
router.register(r'', PermissionViewSet, basename='permission')
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -1,3 +1,480 @@
from django.shortcuts import render # apps/permissions/views.py
import logging
from django.db.models import Q
from django.utils import timezone
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.decorators import action
from apps.accounts.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.permissions.models import Permission, KnowledgeBasePermission as KBPermissionModel
from apps.permissions.serializers import PermissionSerializer
from apps.common.services.permission_service import PermissionService
from apps.common.services.notification_service import NotificationService
logger = logging.getLogger(__name__)
class PermissionViewSet(viewsets.ModelViewSet):
serializer_class = PermissionSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""获取权限申请列表:申请人或审批人是当前用户"""
user_id = str(self.request.user.id)
query = Q(applicant_id=user_id) | Q(approver_id=user_id)
return Permission.objects.filter(query).select_related(
'knowledge_base', 'applicant', 'approver'
)
def list(self, request, *args, **kwargs):
"""获取权限申请列表,包含详细信息"""
try:
queryset = self.get_queryset()
user_id = str(request.user.id)
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
total = queryset.count()
start = (page - 1) * page_size
end = start + page_size
permissions = queryset[start:end]
data = []
for permission in permissions:
if user_id not in [str(permission.applicant_id), str(permission.approver_id)]:
continue
permission_data = {
'id': str(permission.id),
'knowledge_base': {
'id': str(permission.knowledge_base.id),
'name': permission.knowledge_base.name,
'type': permission.knowledge_base.type,
},
'applicant': {
'id': str(permission.applicant.id),
'username': permission.applicant.username,
'name': permission.applicant.name,
'department': permission.applicant.department,
},
'approver': {
'id': str(permission.approver.id) if permission.approver else '',
'username': permission.approver.username if permission.approver else '',
'name': permission.applicant.name if permission.approver else '',
'department': permission.approver.department if permission.approver else '',
},
'permissions': permission.permissions,
'status': permission.status,
'created_at': permission.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'expires_at': permission.expires_at.strftime('%Y-%m-%d %H:%M:%S') if permission.expires_at else None,
'response_message': permission.response_message or '',
'role': 'applicant' if str(permission.applicant_id) == user_id else 'approver'
}
data.append(permission_data)
return Response({
'code': 200,
'message': '获取权限申请列表成功',
'data': {
'total': len(data),
'page': page,
'page_size': page_size,
'results': data
}
})
except Exception as e:
logger.error(f"获取权限申请列表失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'获取权限申请列表失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def perform_create(self, serializer):
"""创建权限申请"""
try:
permission_service = PermissionService()
notification_service = NotificationService()
permission = permission_service.create_permission_request(
self.request.user, serializer.validated_data, notification_service
)
serializer.instance = permission
except Exception as e:
logger.error(f"创建权限申请失败: {str(e)}")
raise
@action(detail=True, methods=['post'])
def approve(self, request, pk=None):
"""批准权限申请"""
try:
permission = self.get_object()
permission_service = PermissionService()
notification_service = NotificationService()
response_message = request.data.get('response_message', '')
permission = permission_service.approve_permission(
request.user, permission, response_message, notification_service
)
return Response({
'code': 200,
'message': '权限申请已批准',
'data': None
})
except Permission.DoesNotExist:
return Response({
'code': 404,
'message': '权限申请不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"处理权限申请失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'处理权限申请失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=True, methods=['post'])
def reject(self, request, pk=None):
"""拒绝权限申请"""
try:
permission = self.get_object()
permission_service = PermissionService()
notification_service = NotificationService()
response_message = request.data.get('response_message')
permission = permission_service.reject_permission(
request.user, permission, response_message, notification_service
)
return Response({
'code': 200,
'message': '权限申请已拒绝',
'data': PermissionSerializer(permission).data
})
except Permission.DoesNotExist:
return Response({
'code': 404,
'message': '权限申请不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"拒绝权限申请失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'拒绝权限申请失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=True, methods=['post'])
def extend(self, request, pk=None):
"""延长权限有效期"""
try:
permission = self.get_object()
permission_service = PermissionService()
notification_service = NotificationService()
new_expires_at = request.data.get('expires_at')
permission = permission_service.extend_permission(
request.user, permission, new_expires_at, notification_service
)
return Response({
'code': 200,
'message': '权限有效期延长成功',
'data': PermissionSerializer(permission).data
})
except Permission.DoesNotExist:
return Response({
'code': 404,
'message': '权限申请不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"延长权限有效期失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'延长权限有效期失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['get'])
def user_permissions(self, request):
"""获取指定用户的所有知识库权限"""
try:
username = request.query_params.get('username')
if not username:
return Response({
'code': 400,
'message': '请提供用户名',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
target_user = User.objects.get(username=username)
except User.DoesNotExist:
return Response({
'code': 404,
'message': f'用户 {username} 不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
permissions = KBPermissionModel.objects.filter(
user=target_user,
status='active'
).select_related('knowledge_base', 'granted_by')
permissions_data = [
{
'id': str(perm.id),
'knowledge_base': {
'id': str(perm.knowledge_base.id),
'name': perm.knowledge_base.name,
'type': perm.knowledge_base.type,
'department': perm.knowledge_base.department,
'group': perm.knowledge_base.group
},
'permissions': {
'can_read': perm.can_read,
'can_edit': perm.can_edit,
'can_delete': perm.can_delete
},
'granted_by': {
'id': str(perm.granted_by.id) if perm.granted_by else None,
'username': perm.granted_by.username if perm.granted_by else None,
'name': perm.granted_by.name if perm.granted_by else None
},
'created_at': perm.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'expires_at': perm.expires_at.strftime('%Y-%m-%d %H:%M:%S') if perm.expires_at else None,
'status': perm.status
}
for perm in permissions
]
return Response({
'code': 200,
'message': '获取用户权限成功',
'data': {
'user': {
'id': str(target_user.id),
'username': target_user.username,
'name': target_user.name,
'department': target_user.department,
'role': target_user.role
},
'permissions': permissions_data
}
})
except Exception as e:
logger.error(f"获取用户权限失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'获取用户权限失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['get'])
def all_permissions(self, request):
"""管理员获取所有用户的知识库权限(不包括私有知识库)"""
try:
if request.user.role != 'admin':
return Response({
'code': 403,
'message': '只有管理员可以查看所有权限',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
status_filter = request.query_params.get('status')
department = request.query_params.get('department')
kb_type = request.query_params.get('kb_type')
queryset = KBPermissionModel.objects.filter(
~Q(knowledge_base__type='private')
).select_related('user', 'knowledge_base', 'granted_by')
if status_filter == 'active':
queryset = queryset.filter(
Q(expires_at__gt=timezone.now()) | Q(expires_at__isnull=True),
status='active'
)
elif status_filter == 'expired':
queryset = queryset.filter(
Q(expires_at__lte=timezone.now()) | Q(status='inactive')
)
if department:
queryset = queryset.filter(user__department=department)
if kb_type:
queryset = queryset.filter(knowledge_base__type=kb_type)
user_permissions = {}
for perm in queryset:
user_id = str(perm.user.id)
if user_id not in user_permissions:
user_permissions[user_id] = {
'user_info': {
'id': user_id,
'username': perm.user.username,
'name': getattr(perm.user, 'name', perm.user.username),
'department': getattr(perm.user, 'department', None),
'role': getattr(perm.user, 'role', None)
},
'permissions': [],
'stats': {
'total': 0,
'by_type': {
'admin': 0,
'secret': 0,
'leader': 0,
'member': 0
},
'by_permission': {
'read_only': 0,
'read_write': 0,
'full_access': 0
}
}
}
perm_data = {
'id': str(perm.id),
'knowledge_base': {
'id': str(perm.knowledge_base.id),
'name': perm.knowledge_base.name,
'type': perm.knowledge_base.type,
'department': perm.knowledge_base.department,
'group': perm.knowledge_base.group,
'creator': {
'id': str(perm.knowledge_base.user_id),
'name': getattr(User.objects.filter(id=perm.knowledge_base.user_id).first(), 'name', None),
'username': getattr(User.objects.filter(id=perm.knowledge_base.user_id).first(), 'username', None)
}
},
'permissions': {
'can_read': perm.can_read,
'can_edit': perm.can_edit,
'can_delete': perm.can_delete
},
'granted_by': {
'id': str(perm.granted_by.id) if perm.granted_by else None,
'username': perm.granted_by.username if perm.granted_by else None,
'name': getattr(perm.granted_by, 'name', None) if perm.granted_by else None
},
'granted_at': perm.granted_at.strftime('%Y-%m-%d %H:%M:%S'),
'expires_at': perm.expires_at.strftime('%Y-%m-%d %H:%M:%S') if perm.expires_at else None,
'status': perm.status
}
user_permissions[user_id]['permissions'].append(perm_data)
stats = user_permissions[user_id]['stats']
stats['total'] += 1
stats['by_type'][perm.knowledge_base.type] += 1
if perm.can_delete:
stats['by_permission']['full_access'] += 1
elif perm.can_edit:
stats['by_permission']['read_write'] += 1
elif perm.can_read:
stats['by_permission']['read_only'] += 1
users_list = list(user_permissions.values())
total = len(users_list)
start = (page - 1) * page_size
end = start + page_size
paginated_users = users_list[start:end]
return Response({
'code': 200,
'message': '获取权限列表成功',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'results': paginated_users
}
})
except Exception as e:
logger.error(f"获取所有权限失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'获取所有权限失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post'])
def update_permission(self, request):
"""管理员更新用户的知识库权限"""
try:
permission_service = PermissionService()
notification_service = NotificationService()
permission, created = permission_service.update_user_permission(
request.user,
request.data.get('user_id'),
request.data.get('knowledge_base_id'),
request.data.get('permissions'),
request.data.get('expires_at'),
notification_service
)
return Response({
'code': 200,
'message': f"{'创建' if created else '更新'}权限成功",
'data': {
'id': str(permission.id),
'user': {
'id': str(permission.user.id),
'username': permission.user.username,
'name': permission.user.name,
'department': permission.user.department,
'role': permission.user.role
},
'knowledge_base': {
'id': str(permission.knowledge_base.id),
'name': permission.knowledge_base.name,
'type': permission.knowledge_base.type,
'department': permission.knowledge_base.department,
'group': permission.knowledge_base.group
},
'permissions': {
'can_read': permission.can_read,
'can_edit': permission.can_edit,
'can_delete': permission.can_delete
},
'granted_by': {
'id': str(request.user.id),
'username': request.user.username,
'name': request.user.name
},
'expires_at': permission.expires_at.strftime('%Y-%m-%d %H:%M:%S') if permission.expires_at else None,
'created': created
}
})
except Exception as e:
logger.error(f"更新权限失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'更新权限失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# Create your views here.

View File

@ -171,4 +171,4 @@ AUTH_USER_MODEL = 'accounts.User'
API_BASE_URL = 'http://81.69.223.133:48329' API_BASE_URL = 'http://81.69.223.133:48329'
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf' SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'
GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/' GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/'
APPLICATION_ID = 'd5d11efa-ea9a-11ef-9933-0242ac120006'

View File

@ -21,8 +21,8 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/auth/', include('apps.accounts.urls')), path('api/auth/', include('apps.accounts.urls')),
path('api/knowledge-bases/', include('apps.knowledge_base.urls')), path('api/knowledge-bases/', include('apps.knowledge_base.urls')),
# path('api/chat/', include('apps.chat.urls')), path('api/chat-history/', include('apps.chat.urls')),
# path('api/permissions/', include('apps.permissions.urls')), path('api/permissions/', include('apps.permissions.urls')),
# path('api/message/', include('apps.message.urls')), # path('api/message/', include('apps.message.urls')),
# path('api/gmail/', include('apps.gmail.urls')), # path('api/gmail/', include('apps.gmail.urls')),
# path('api/feishu/', include('apps.feishu.urls')), # path('api/feishu/', include('apps.feishu.urls')),