diff --git a/apps/brands/models.py b/apps/brands/models.py
index 6b23166..97c4460 100644
--- a/apps/brands/models.py
+++ b/apps/brands/models.py
@@ -27,7 +27,6 @@ class Brand(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
- db_table = 'brands'
verbose_name = '品牌'
verbose_name_plural = '品牌'
@@ -66,7 +65,6 @@ class Product(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
- db_table = 'products'
verbose_name = '产品'
verbose_name_plural = '产品'
unique_together = ['brand', 'name']
@@ -144,7 +142,6 @@ class Campaign(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
- db_table = 'campaigns'
verbose_name = '活动'
verbose_name_plural = '活动'
unique_together = ['brand', 'name']
@@ -186,7 +183,6 @@ class BrandChatSession(models.Model):
is_active = models.BooleanField(default=True, verbose_name='是否激活')
class Meta:
- db_table = 'brand_chat_sessions'
verbose_name = '品牌聊天会话'
verbose_name_plural = '品牌聊天会话'
indexes = [
diff --git a/apps/chat/migrations/0001_initial.py b/apps/chat/migrations/0001_initial.py
index a6424b3..1364d2f 100644
--- a/apps/chat/migrations/0001_initial.py
+++ b/apps/chat/migrations/0001_initial.py
@@ -1,6 +1,7 @@
-# Generated by Django 5.2.1 on 2025-05-28 09:51
+# Generated by Django 5.2 on 2025-05-07 03:40
import django.db.models.deletion
+import uuid
from django.conf import settings
from django.db import migrations, models
@@ -15,6 +16,24 @@ class Migration(migrations.Migration):
]
operations = [
+ migrations.CreateModel(
+ name='ConversationSummary',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('talent_email', models.EmailField(max_length=254, verbose_name='达人邮箱')),
+ ('conversation_id', models.CharField(max_length=100, verbose_name='对话ID')),
+ ('summary', models.TextField(verbose_name='对话总结')),
+ ('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_summaries', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': '对话总结',
+ 'verbose_name_plural': '对话总结',
+ 'db_table': 'conversation_summaries',
+ },
+ ),
migrations.CreateModel(
name='ChatHistory',
fields=[
@@ -32,8 +51,9 @@ class Migration(migrations.Migration):
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
+ 'db_table': 'chat_history',
'ordering': ['created_at'],
- 'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_chathi_convers_90ca0c_idx'), models.Index(fields=['user', 'created_at'], name='chat_chathi_user_id_326d36_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_chathi_convers_bcd094_idx')],
+ 'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'), models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_histor_convers_89bc43_idx')],
},
),
]
diff --git a/apps/chat/migrations/0002_alter_chathistory_content.py b/apps/chat/migrations/0002_alter_chathistory_content.py
new file mode 100644
index 0000000..2087581
--- /dev/null
+++ b/apps/chat/migrations/0002_alter_chathistory_content.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2 on 2025-05-15 10:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('chat', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='chathistory',
+ name='content',
+ field=models.TextField(help_text='消息内容,支持存储长文本', max_length=65535),
+ ),
+ ]
diff --git a/apps/chat/migrations/0003_alter_chathistory_content.py b/apps/chat/migrations/0003_alter_chathistory_content.py
new file mode 100644
index 0000000..74d8014
--- /dev/null
+++ b/apps/chat/migrations/0003_alter_chathistory_content.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2 on 2025-05-20 09:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('chat', '0002_alter_chathistory_content'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='chathistory',
+ name='content',
+ field=models.TextField(),
+ ),
+ ]
diff --git a/apps/chat/migrations/0004_remove_conversationsummary_user_and_more.py b/apps/chat/migrations/0004_remove_conversationsummary_user_and_more.py
new file mode 100644
index 0000000..070fc4c
--- /dev/null
+++ b/apps/chat/migrations/0004_remove_conversationsummary_user_and_more.py
@@ -0,0 +1,39 @@
+# Generated by Django 5.2 on 2025-05-21 04:30
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('chat', '0003_alter_chathistory_content'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='conversationsummary',
+ name='user',
+ ),
+ migrations.RenameIndex(
+ model_name='chathistory',
+ new_name='chat_chathi_convers_90ca0c_idx',
+ old_name='chat_histor_convers_33721a_idx',
+ ),
+ migrations.RenameIndex(
+ model_name='chathistory',
+ new_name='chat_chathi_user_id_326d36_idx',
+ old_name='chat_histor_user_id_aa050a_idx',
+ ),
+ migrations.RenameIndex(
+ model_name='chathistory',
+ new_name='chat_chathi_convers_bcd094_idx',
+ old_name='chat_histor_convers_89bc43_idx',
+ ),
+ migrations.AlterModelTable(
+ name='chathistory',
+ table=None,
+ ),
+ migrations.DeleteModel(
+ name='ConversationSummary',
+ ),
+ ]
diff --git a/apps/chat/views.py b/apps/chat/views.py
index b764e8f..382bfda 100644
--- a/apps/chat/views.py
+++ b/apps/chat/views.py
@@ -14,15 +14,16 @@ 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.user.authentication import CustomTokenAuthentication
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.user.authentication import CustomTokenAuthentication
+# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
logger = logging.getLogger(__name__)
+# class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet):
class ChatHistoryViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
authentication_classes = [CustomTokenAuthentication]
@@ -463,83 +464,83 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
}, 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)
+ # @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]
+ # 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))
+ # 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)
+ # 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 = []
+ # 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)
- }
- })
+ # 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)
+ # 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):
"""高亮关键词"""
diff --git a/apps/common/services/chat_service.py b/apps/common/services/chat_service.py
index 68b88cd..24078cb 100644
--- a/apps/common/services/chat_service.py
+++ b/apps/common/services/chat_service.py
@@ -6,11 +6,12 @@ from django.db import transaction
from apps.user.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.chat.models import ChatHistory
-
+# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
from django.db.models import Q
logger = logging.getLogger(__name__)
+# class ChatService(KnowledgeBasePermissionMixin):
class ChatService():
@transaction.atomic
def create_chat_record(self, user, data, conversation_id=None):
diff --git a/apps/common/services/notification_service.py b/apps/common/services/notification_service.py
index e69de29..5bfbf73 100644
--- a/apps/common/services/notification_service.py
+++ b/apps/common/services/notification_service.py
@@ -0,0 +1,51 @@
+# # apps/common/services/notification_service.py
+# import logging
+# from asgiref.sync import async_to_sync
+# from channels.layers import get_channel_layer
+# from apps.notification.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,
+# )
+
+# # 准备发送到WebSocket的数据
+# notification_data = {
+# "id": str(notification.id),
+# "title": notification.title,
+# "content": notification.content,
+# "type": notification.type,
+# "created_at": notification.created_at.isoformat(),
+# }
+
+# # 只有当sender不为None时才添加sender信息
+# if notification.sender:
+# notification_data["sender"] = {
+# "id": str(notification.sender.id),
+# "name": notification.sender.name
+# }
+
+# channel_layer = get_channel_layer()
+# async_to_sync(channel_layer.group_send)(
+# f"notification_user_{user.id}",
+# {
+# "type": "notification",
+# "data": notification_data
+# }
+# )
+# return notification
+# except Exception as e:
+# logger.error(f"发送通知失败: {str(e)}")
+# return None
+
+
\ No newline at end of file
diff --git a/apps/common/services/permission_service.py b/apps/common/services/permission_service.py
index c9f427f..1cf9866 100644
--- a/apps/common/services/permission_service.py
+++ b/apps/common/services/permission_service.py
@@ -1,338 +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.user.models import User
-from apps.knowledge_base.models import KnowledgeBase
+# # 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.user.models import User
+# from apps.knowledge_base.models import KnowledgeBase
+# from apps.permissions.models import Permission, KnowledgeBasePermission as KBPermissionModel
+# logger = logging.getLogger(__name__)
-logger = logging.getLogger(__name__)
+# class PermissionService:
+# def can_manage_knowledge_base(self, user, knowledge_base):
+# """检查用户是否是知识库的创建者"""
+# return str(knowledge_base.user_id) == str(user.id)
-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 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
+# })
- 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')
- 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 any([requested_permissions.get('can_read'),
- requested_permissions.get('can_edit'),
- requested_permissions.get('can_delete')]):
- raise ValidationError("至少需要申请一种权限(读/改/删)")
+# if not expires_at:
+# 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_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("您已有此知识库的访问权限")
- 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'
+# )
- 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)
- 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)
+# )
- 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
- 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
+# })
- 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()
- 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()
- 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}")
- 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)
+# )
- 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
- 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
+# })
- 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 permission.status != 'pending':
- raise ValidationError({
- 'code': 400,
- 'message': '该申请已被处理',
- 'data': None
- })
+# if not response_message:
+# 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()
- 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)
+# )
- 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
- 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
+# })
- 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
+# })
- 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
+# })
- 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()
- 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()
- 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)
+# )
- 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
- 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
+# })
- 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
+# })
- 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
+# })
- 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
+# })
- 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 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
+# })
- 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
+# })
- 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
+# }
+# )
- 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)
+# )
- 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
+# return permission, created
\ No newline at end of file
diff --git a/apps/feishu/services/bitable_service.py b/apps/feishu/services/bitable_service.py
index e91e026..e08b8b7 100644
--- a/apps/feishu/services/bitable_service.py
+++ b/apps/feishu/services/bitable_service.py
@@ -1,15 +1,11 @@
import json
import re
-import logging
import requests
-from urllib.parse import urljoin, urlparse, parse_qs
+from urllib.parse import urljoin
# 基础URL地址
BASE_API_URL = "https://open.feishu.cn/open-apis/"
-# 获取日志记录器
-logger = logging.getLogger(__name__)
-
class BitableService:
"""
@@ -36,11 +32,6 @@ class BitableService:
if headers is None:
headers = {}
- # 记录请求信息,避免记录敏感信息
- logger.info(f"请求飞书API: {method} {full_url}")
- if params:
- logger.info(f"请求参数: {params}")
-
response = requests.request(
method=method,
url=full_url,
@@ -52,29 +43,7 @@ class BitableService:
# 检查响应
if not response.ok:
error_msg = f"API 请求失败: {response.status_code}, 响应: {response.text}"
- logger.error(error_msg)
-
- # 解析错误响应
- try:
- error_json = response.json()
- error_code = error_json.get("code")
- error_msg = error_json.get("msg", "")
-
- # 根据错误代码提供更具体的错误信息
- if error_code == 91402: # NOTEXIST
- error_detail = "请求的资源不存在,请检查app_token和table_id是否正确,以及应用是否有权限访问该资源"
- logger.error(f"资源不存在错误: {error_detail}")
- raise Exception(f"资源不存在: {error_detail}")
- elif error_code == 99991663: # TOKEN_INVALID
- error_detail = "访问令牌无效或已过期"
- logger.error(f"令牌错误: {error_detail}")
- raise Exception(f"访问令牌错误: {error_detail}")
- else:
- logger.error(f"飞书API错误: 代码={error_code}, 消息={error_msg}")
- except ValueError:
- # 响应不是有效的JSON
- pass
-
+ print(error_msg)
raise Exception(error_msg)
return response.json()
@@ -83,11 +52,6 @@ class BitableService:
def extract_params_from_url(table_url):
"""
从URL中提取app_token和table_id
- 支持多种飞书多维表格URL格式:
- 1. https://xxx.feishu.cn/base/{app_token}?table={table_id}
- 2. https://xxx.feishu.cn/wiki/{app_token}?sheet={table_id}
- 3. https://xxx.feishu.cn/wiki/wikcnXXX?sheet=XXX
- 4. https://xxx.feishu.cn/base/bascnXXX?table=tblXXX
Args:
table_url: 飞书多维表格URL
@@ -98,76 +62,13 @@ class BitableService:
Raises:
ValueError: 如果无法从URL中提取必要参数
"""
- # 记录原始URL
- logger.info(f"解析多维表格URL: {table_url}")
+ app_token_match = re.search(r'base/([^?]+)', table_url)
+ table_id_match = re.search(r'table=([^&]+)', table_url)
- # 处理URL中的多余空格
- table_url = table_url.strip()
+ if not app_token_match or not table_id_match:
+ raise ValueError("无法从URL中提取必要参数,请确认URL格式正确")
- # 解析URL
- parsed_url = urlparse(table_url)
- query_params = parse_qs(parsed_url.query)
- path = parsed_url.path
-
- logger.debug(f"URL路径: {path}, 查询参数: {query_params}")
-
- # 尝试从查询参数中获取table_id
- table_id = None
- if 'table' in query_params:
- table_id = query_params['table'][0].strip()
- elif 'sheet' in query_params:
- table_id = query_params['sheet'][0].strip()
-
- # 尝试从路径中获取app_token
- app_token = None
- # 处理标准格式 /base/{app_token} 或 /wiki/{app_token}
- standard_match = re.search(r'/(base|wiki)/([^/?]+)', path)
- if standard_match:
- app_token = standard_match.group(2).strip()
- else:
- # 处理简短ID格式,如 /wiki/wikcnXXX
- short_id_match = re.search(r'/(base|wiki)/([a-zA-Z0-9]+)', path)
- if short_id_match:
- app_token = short_id_match.group(2).strip()
-
- # 检查是否成功提取
- if not app_token or not table_id:
- error_msg = "无法从URL中提取必要参数,请确认URL格式正确"
- logger.error(f"{error_msg}. URL: {table_url}")
- raise ValueError(error_msg)
-
- logger.info(f"成功从URL提取参数: app_token={app_token}, table_id={table_id}")
- return app_token, table_id
-
- @staticmethod
- def validate_access(app_token, access_token):
- """
- 验证应用是否有权限访问多维表格
-
- Args:
- app_token: 应用令牌
- access_token: 访问令牌
-
- Returns:
- bool: 是否有权限
- """
- try:
- # 构造请求
- url = f"bitable/v1/apps/{app_token}"
- headers = {
- "Authorization": f"Bearer {access_token}",
- "Content-Type": "application/json"
- }
-
- # 发送请求
- response = BitableService.make_request("GET", url, headers=headers)
-
- # 检查响应
- return response and "code" in response and response["code"] == 0
-
- except Exception as e:
- logger.error(f"验证访问权限失败: {str(e)}")
- return False
+ return app_token_match.group(1), table_id_match.group(1)
@staticmethod
def get_metadata(app_token, table_id, access_token):
@@ -183,10 +84,6 @@ class BitableService:
dict: 表格元数据
"""
try:
- # 先验证应用是否有权限访问
- if not BitableService.validate_access(app_token, access_token):
- logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
-
# 构造请求
url = f"bitable/v1/apps/{app_token}/tables/{table_id}"
headers = {
@@ -199,18 +96,16 @@ class BitableService:
# 检查响应
if response and "code" in response and response["code"] == 0:
- metadata = response.get("data", {}).get("table", {})
- logger.info(f"成功获取多维表格元数据: {metadata.get('name', table_id)}")
- return metadata
+ return response.get("data", {}).get("table", {})
# 发生错误
error_msg = f"获取多维表格元数据失败: {json.dumps(response)}"
- logger.error(error_msg)
+ print(error_msg)
raise Exception(error_msg)
except Exception as e:
# 如果正常API调用失败,使用替代方法
- logger.error(f"获取多维表格元数据失败: {str(e)}")
+ print(f"获取多维表格元数据失败: {str(e)}")
# 简单返回一个基本名称
return {
"name": f"table_{table_id}",
@@ -238,8 +133,6 @@ class BitableService:
Exception: 查询失败时抛出异常
"""
try:
- logger.info(f"查询多维表格记录: app_token={app_token}, table_id={table_id}")
-
# 构造请求URL
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/records/search"
@@ -266,18 +159,16 @@ class BitableService:
# 检查响应
if response and "code" in response and response["code"] == 0:
- result = response.get("data", {})
- logger.info(f"查询成功,获取到 {len(result.get('items', []))} 条记录")
- return result
+ return response.get("data", {})
# 发生错误
error_msg = f"查询飞书多维表格失败: {json.dumps(response)}"
- logger.error(error_msg)
+ print(error_msg)
raise Exception(error_msg)
except Exception as e:
# 记录详细错误
- logger.error(f"查询飞书多维表格发生错误: {str(e)}")
+ print(f"查询飞书多维表格发生错误: {str(e)}")
raise
@staticmethod
@@ -294,8 +185,6 @@ class BitableService:
list: 字段信息列表
"""
try:
- logger.info(f"获取多维表格字段: app_token={app_token}, table_id={table_id}")
-
# 构造请求
url = f"bitable/v1/apps/{app_token}/tables/{table_id}/fields"
headers = {
@@ -309,20 +198,17 @@ class BitableService:
# 检查响应
if response and "code" in response and response["code"] == 0:
- fields = response.get("data", {}).get("items", [])
- logger.info(f"成功获取到 {len(fields)} 个字段")
- return fields
+ return response.get("data", {}).get("items", [])
# 发生错误
error_msg = f"获取多维表格字段失败: {json.dumps(response)}"
- logger.error(error_msg)
+ print(error_msg)
raise Exception(error_msg)
except Exception as e:
# 记录详细错误
- logger.error(f"获取字段信息失败: {str(e)}")
+ print(f"获取字段信息失败: {str(e)}")
# 如果获取失败,返回一个基本字段集
- logger.warning("返回默认字段集")
return [
{"field_name": "title", "type": "text", "property": {}},
{"field_name": "description", "type": "text", "property": {}},
diff --git a/apps/feishu/services/data_sync_service.py b/apps/feishu/services/data_sync_service.py
index 6746598..fa9f5f8 100644
--- a/apps/feishu/services/data_sync_service.py
+++ b/apps/feishu/services/data_sync_service.py
@@ -201,42 +201,12 @@ class DataSyncService:
dict: 同步结果
"""
try:
- # 记录原始URL,便于排查问题
- logger.info(f"开始数据同步,URL: {table_url}")
-
# 提取参数
- try:
- app_token, table_id = BitableService.extract_params_from_url(table_url)
- logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
- except ValueError as ve:
- logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
- return {
- 'success': False,
- 'error': f"URL解析失败: {str(ve)}",
- 'details': "URL格式可能不正确,请参考飞书多维表格文档"
- }
-
- # 验证应用是否有权限访问该多维表格
- if not BitableService.validate_access(app_token, access_token):
- error_msg = f"应用无权限访问多维表格: app_token={app_token}"
- logger.warning(error_msg)
- return {
- 'success': False,
- 'error': error_msg,
- 'details': "请确认access_token是否有效,以及应用是否已被添加为表格协作者"
- }
+ app_token, table_id = BitableService.extract_params_from_url(table_url)
# 1. 获取表格元数据
- try:
- metadata = BitableService.get_metadata(app_token, table_id, access_token)
- feishu_table_name = metadata.get('name', f'table_{table_id}')
- logger.info(f"获取到表格元数据: 名称={feishu_table_name}")
- except Exception as e:
- logger.error(f"获取表格元数据失败: {str(e)}")
- return {
- 'success': False,
- 'error': f"获取表格元数据失败: {str(e)}"
- }
+ metadata = BitableService.get_metadata(app_token, table_id, access_token)
+ feishu_table_name = metadata.get('name', f'table_{table_id}')
# 2. 获取或创建表格映射
if auto_sync:
@@ -244,7 +214,6 @@ class DataSyncService:
try:
mapping = FeishuTableMapping.objects.get(app_token=app_token, table_id=table_id)
final_table_name = mapping.table_name
- logger.info(f"使用已有映射: {final_table_name}")
except FeishuTableMapping.DoesNotExist:
# 如果找不到映射,自动创建一个
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
@@ -256,7 +225,6 @@ class DataSyncService:
table_name or default_table_name
)
final_table_name = mapping.table_name
- logger.info(f"创建新映射: {final_table_name}")
else:
# 非自动同步模式,优先使用用户提供的表名
default_table_name = f"feishu_{feishu_table_name.lower().replace(' ', '_').replace('-', '_')}"
@@ -270,22 +238,12 @@ class DataSyncService:
feishu_table_name,
final_table_name
)
- logger.info(f"使用表名: {final_table_name}")
# 3. 获取字段信息
- try:
- fields = BitableService.get_table_fields(app_token, table_id, access_token)
- logger.info(f"获取到 {len(fields)} 个字段")
- except Exception as e:
- logger.error(f"获取字段信息失败: {str(e)}")
- return {
- 'success': False,
- 'error': f"获取字段信息失败: {str(e)}"
- }
+ fields = BitableService.get_table_fields(app_token, table_id, access_token)
# 4. 创建模型
model = DataSyncService.create_model_from_fields(final_table_name, fields)
- logger.info(f"创建模型: {model.__name__}")
# 5. 检查表是否存在,不存在则创建
table_exists = DataSyncService.check_table_exists(final_table_name)
@@ -300,92 +258,74 @@ class DataSyncService:
page_token = None
page_size = 100
- try:
- while True:
- # 查询记录
- result = BitableService.search_records(
- app_token=app_token,
- table_id=table_id,
- access_token=access_token,
- page_size=page_size,
- page_token=page_token
- )
-
- records = result.get('items', [])
- all_records.extend(records)
-
- logger.info(f"获取第 {len(all_records) // page_size + 1} 页数据, 共 {len(records)} 条记录")
-
- # 检查是否有更多数据
- page_token = result.get('page_token')
- if not page_token or not records:
- break
- except Exception as e:
- logger.error(f"获取记录失败: {str(e)}")
- return {
- 'success': False,
- 'error': f"获取记录失败: {str(e)}"
- }
+ while True:
+ # 查询记录
+ result = BitableService.search_records(
+ app_token=app_token,
+ table_id=table_id,
+ access_token=access_token,
+ page_size=page_size,
+ page_token=page_token
+ )
+
+ records = result.get('items', [])
+ all_records.extend(records)
+
+ # 检查是否有更多数据
+ page_token = result.get('page_token')
+ if not page_token or not records:
+ break
# 7. 同步数据到数据库
- try:
- with transaction.atomic():
- # 统计数据
- created_count = 0
- updated_count = 0
+ with transaction.atomic():
+ # 统计数据
+ created_count = 0
+ updated_count = 0
+
+ for record in all_records:
+ record_id = record.get('record_id')
+ fields_data = record.get('fields', {})
- for record in all_records:
- record_id = record.get('record_id')
- fields_data = record.get('fields', {})
+ # 准备数据
+ data = {'feishu_record_id': record_id}
+
+ # 处理每个字段的数据
+ for field_name, field_value in fields_data.items():
+ # 将字段名转换为Python合法标识符
+ db_field_name = field_name.lower().replace(' ', '_').replace('-', '_')
- # 准备数据
- data = {'feishu_record_id': record_id}
-
- # 处理每个字段的数据
- for field_name, field_value in fields_data.items():
- # 将字段名转换为Python合法标识符
- db_field_name = field_name.lower().replace(' ', '_').replace('-', '_')
-
- # 跳过已保留的字段名
- if db_field_name in ['id', 'created_at', 'updated_at']:
- continue
-
- # 确保字段存在于模型中
- if hasattr(model, db_field_name):
- # 处理不同类型的字段值
- if isinstance(field_value, (list, dict)):
- data[db_field_name] = json.dumps(field_value)
- else:
- data[db_field_name] = field_value
-
- # 尝试更新或创建记录
- try:
- # 总是使用 feishu_record_id 作为唯一标识符进行更新或创建
- obj, created = model.objects.update_or_create(
- feishu_record_id=record_id,
- defaults=data
- )
-
- if created:
- created_count += 1
- else:
- updated_count += 1
-
- except Exception as e:
- logger.error(f"更新或创建记录失败: {str(e)}, 记录ID: {record_id}")
+ # 跳过已保留的字段名
+ if db_field_name in ['id', 'created_at', 'updated_at']:
continue
+
+ # 确保字段存在于模型中
+ if hasattr(model, db_field_name):
+ # 处理不同类型的字段值
+ if isinstance(field_value, (list, dict)):
+ data[db_field_name] = json.dumps(field_value)
+ else:
+ data[db_field_name] = field_value
- # 更新映射表中的记录数
- mapping.total_records = len(all_records)
- mapping.save(update_fields=['total_records', 'last_sync_time'])
-
- logger.info(f"数据同步完成: 总记录数={len(all_records)}, 新增={created_count}, 更新={updated_count}")
- except Exception as e:
- logger.error(f"数据同步到数据库失败: {str(e)}")
- return {
- 'success': False,
- 'error': f"数据同步到数据库失败: {str(e)}"
- }
+ # 尝试更新或创建记录
+ try:
+ # 总是使用 feishu_record_id 作为唯一标识符进行更新或创建
+ obj, created = model.objects.update_or_create(
+ feishu_record_id=record_id,
+ defaults=data
+ )
+
+ if created:
+ created_count += 1
+ else:
+ updated_count += 1
+
+ except Exception as e:
+ logger.error(f"更新或创建记录失败: {str(e)}, 记录ID: {record_id}")
+ continue
+
+ # 更新映射表中的记录数
+ mapping.total_records = len(all_records)
+ mapping.save(update_fields=['total_records', 'last_sync_time'])
return {
'success': True,
@@ -399,8 +339,8 @@ class DataSyncService:
}
except Exception as e:
- logger.error(f"数据同步失败: {str(e)}", exc_info=True)
+ logger.error(f"数据同步失败: {str(e)}")
return {
'success': False,
- 'error': f"数据同步失败: {str(e)}"
+ 'error': str(e)
}
\ No newline at end of file
diff --git a/apps/feishu/services/feishu_service.py b/apps/feishu/services/feishu_service.py
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/apps/feishu/services/feishu_service.py
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/feishu/views.py b/apps/feishu/views.py
index 4355396..b0e262f 100644
--- a/apps/feishu/views.py
+++ b/apps/feishu/views.py
@@ -14,7 +14,7 @@ from .services.data_sync_service import DataSyncService
from .services.gmail_extraction_service import GmailExtractionService
from .services.auto_gmail_conversation_service import AutoGmailConversationService
from rest_framework.permissions import IsAuthenticated
-from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig
+from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig
from apps.gmail.services.gmail_service import GmailService
from apps.gmail.serializers import AutoReplyConfigSerializer
from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary
@@ -22,6 +22,7 @@ from apps.chat.models import ChatHistory
from apps.knowledge_base.models import KnowledgeBase
from apps.user.authentication import CustomTokenAuthentication
+
logger = logging.getLogger(__name__)
@@ -47,64 +48,23 @@ class FeishuTableRecordsView(APIView):
)
try:
- # 记录原始URL,以便于排查问题
- logger.info(f"处理查询请求,URL: {table_url}")
-
# 从URL中提取app_token和table_id
- try:
- app_token, table_id = BitableService.extract_params_from_url(table_url)
- logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
- except ValueError as ve:
- logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
- return Response(
- {"error": str(ve), "details": "URL格式可能不正确,请参考飞书多维表格文档"},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证应用是否有权限访问该多维表格
- if not BitableService.validate_access(app_token, access_token):
- logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
- return Response(
- {"error": "无权限访问该多维表格,请确认access_token是否有效,以及应用是否已被添加为表格协作者"},
- status=status.HTTP_403_FORBIDDEN
- )
+ app_token, table_id = BitableService.extract_params_from_url(table_url)
# 先获取一些样本数据,检查我们能否访问多维表格
- try:
- sample_data = BitableService.search_records(
- app_token=app_token,
- table_id=table_id,
- access_token=access_token,
- filter_exp=filter_exp,
- sort=sort,
- page_size=page_size,
- page_token=page_token
- )
-
- logger.info(f"成功获取多维表格数据,记录数: {len(sample_data.get('items', []))}")
- return Response(sample_data, status=status.HTTP_200_OK)
- except Exception as e:
- logger.error(f"查询多维表格数据失败: {str(e)}")
- # 提供更友好的错误信息
- error_message = str(e)
- if "资源不存在" in error_message:
- return Response(
- {"error": "表格资源不存在,请检查URL是否正确,以及应用是否有权限"},
- status=status.HTTP_404_NOT_FOUND
- )
- elif "令牌错误" in error_message:
- return Response(
- {"error": "访问令牌无效或已过期,请重新获取"},
- status=status.HTTP_401_UNAUTHORIZED
- )
- else:
- return Response(
- {"error": f"查询多维表格失败: {error_message}"},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
+ sample_data = BitableService.search_records(
+ app_token=app_token,
+ table_id=table_id,
+ access_token=access_token,
+ filter_exp=filter_exp,
+ sort=sort,
+ page_size=page_size,
+ page_token=page_token
+ )
+
+ return Response(sample_data, status=status.HTTP_200_OK)
except ValueError as ve:
- logger.error(f"URL解析或参数验证失败: {str(ve)}")
return Response(
{"error": str(ve), "details": "URL格式可能不正确"},
status=status.HTTP_400_BAD_REQUEST
@@ -112,7 +72,6 @@ class FeishuTableRecordsView(APIView):
except Exception as e:
error_details = traceback.format_exc()
- logger.error(f"查询飞书多维表格失败: {str(e)}\n{error_details}")
return Response(
{
"error": f"查询飞书多维表格失败: {str(e)}",
@@ -143,77 +102,32 @@ class FeishuDataSyncView(APIView):
# 提取参数
try:
- # 记录原始URL,以便于排查问题
- logger.info(f"处理数据同步请求,URL: {table_url}")
-
- # 从URL中提取app_token和table_id
- try:
- app_token, table_id = BitableService.extract_params_from_url(table_url)
- logger.info(f"成功解析URL参数: app_token={app_token}, table_id={table_id}")
- except ValueError as ve:
- logger.error(f"URL解析失败: {str(ve)}, URL: {table_url}")
- return Response(
- {"error": str(ve), "details": "URL格式可能不正确,请参考飞书多维表格文档"},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证应用是否有权限访问该多维表格
- if not BitableService.validate_access(app_token, access_token):
- logger.warning(f"应用无权限访问多维表格: app_token={app_token}")
- return Response(
- {"error": "无权限访问该多维表格,请确认access_token是否有效,以及应用是否已被添加为表格协作者"},
- status=status.HTTP_403_FORBIDDEN
- )
+ app_token, table_id = BitableService.extract_params_from_url(table_url)
# 先获取一些样本数据,检查我们能否访问多维表格
- try:
- sample_data = BitableService.search_records(
- app_token=app_token,
- table_id=table_id,
- access_token=access_token,
- page_size=5
- )
-
- logger.info(f"成功获取多维表格样本数据,记录数: {len(sample_data.get('items', []))}")
-
- # 执行数据同步
- result = DataSyncService.sync_data_to_db(
- table_url=table_url,
- access_token=access_token,
- table_name=table_name,
- primary_key=primary_key
- )
-
- # 添加样本数据到结果中
- if result.get('success'):
- result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据
- logger.info(f"数据同步成功: {result}")
- return Response(result, status=status.HTTP_200_OK)
- else:
- logger.error(f"数据同步失败: {result.get('error')}")
- return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- except Exception as e:
- logger.error(f"数据同步过程失败: {str(e)}")
- # 提供更友好的错误信息
- error_message = str(e)
- if "资源不存在" in error_message:
- return Response(
- {"error": "表格资源不存在,请检查URL是否正确,以及应用是否有权限"},
- status=status.HTTP_404_NOT_FOUND
- )
- elif "令牌错误" in error_message:
- return Response(
- {"error": "访问令牌无效或已过期,请重新获取"},
- status=status.HTTP_401_UNAUTHORIZED
- )
- else:
- return Response(
- {"error": f"数据同步失败: {error_message}"},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
+ sample_data = BitableService.search_records(
+ app_token=app_token,
+ table_id=table_id,
+ access_token=access_token,
+ page_size=5
+ )
+
+ # 执行数据同步
+ result = DataSyncService.sync_data_to_db(
+ table_url=table_url,
+ access_token=access_token,
+ table_name=table_name,
+ primary_key=primary_key
+ )
+
+ # 添加样本数据到结果中
+ if result.get('success'):
+ result['sample_data'] = sample_data.get('items', [])[:3] # 只返回最多3条样本数据
+ return Response(result, status=status.HTTP_200_OK)
+ else:
+ return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except ValueError as ve:
- logger.error(f"URL解析或参数验证失败: {str(ve)}")
return Response(
{"error": str(ve), "details": "URL格式可能不正确"},
status=status.HTTP_400_BAD_REQUEST
@@ -222,7 +136,6 @@ class FeishuDataSyncView(APIView):
except Exception as e:
import traceback
error_details = traceback.format_exc()
- logger.error(f"数据同步失败: {str(e)}\n{error_details}")
return Response(
{
"error": f"数据同步失败: {str(e)}",
diff --git a/apps/gmail/migrations/0001_initial.py b/apps/gmail/migrations/0001_initial.py
index 892b643..d241c1f 100644
--- a/apps/gmail/migrations/0001_initial.py
+++ b/apps/gmail/migrations/0001_initial.py
@@ -1,7 +1,7 @@
-# Generated by Django 5.2.1 on 2025-05-28 08:45
+# Generated by Django 5.2 on 2025-05-13 06:43
import django.db.models.deletion
-import uuid
+import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
@@ -19,16 +19,14 @@ class Migration(migrations.Migration):
name='GmailConversation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('conversation_id', models.CharField(help_text='Unique conversation identifier', max_length=100, unique=True)),
- ('user_email', models.EmailField(help_text="User's Gmail address", max_length=254)),
- ('influencer_email', models.EmailField(help_text="Influencer's email address", max_length=254)),
- ('title', models.CharField(help_text='Conversation title', max_length=255)),
+ ('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
+ ('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
+ ('conversation_id', models.CharField(help_text='关联到chat_history的会话ID', max_length=100, unique=True)),
+ ('title', models.CharField(default='Gmail对话', help_text='对话标题', max_length=100)),
+ ('last_sync_time', models.DateTimeField(default=django.utils.timezone.now, help_text='最后同步时间')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
- ('last_sync_time', models.DateTimeField(blank=True, help_text='Last time conversation was synced with Gmail', null=True)),
- ('is_active', models.BooleanField(default=True, help_text='Whether this conversation is active')),
- ('has_sent_greeting', models.BooleanField(default=False, help_text='Whether a greeting message has been sent to this conversation')),
- ('metadata', models.JSONField(blank=True, help_text='Additional metadata for the conversation', null=True)),
+ ('is_active', models.BooleanField(default=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
],
options={
@@ -41,7 +39,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)),
- ('attachment_id', models.TextField(help_text='Gmail附件的唯一标识符,可能很长')),
+ ('attachment_id', models.CharField(help_text='Gmail附件ID', max_length=100)),
('filename', models.CharField(help_text='原始文件名', max_length=255)),
('file_path', models.CharField(help_text='保存在服务器上的路径', max_length=255)),
('content_type', models.CharField(help_text='MIME类型', max_length=100)),
@@ -55,107 +53,6 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'],
},
),
- migrations.CreateModel(
- name='ConversationSummary',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('content', models.TextField(verbose_name='摘要内容')),
- ('last_message_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='最后处理的消息ID或ChatHistory的ID')),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('conversation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='summary', to='gmail.gmailconversation')),
- ],
- options={
- 'verbose_name': 'Gmail对话摘要',
- 'verbose_name_plural': 'Gmail对话摘要',
- 'db_table': 'gmail_conversation_summaries',
- },
- ),
- migrations.CreateModel(
- name='ProcessedPushNotification',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('message_id', models.CharField(help_text='Pub/Sub消息ID', max_length=255, unique=True)),
- ('email_address', models.EmailField(help_text='通知关联的Gmail邮箱', max_length=254)),
- ('history_id', models.CharField(help_text='Gmail历史ID', max_length=100)),
- ('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
- ('is_successful', models.BooleanField(default=True, help_text='处理是否成功')),
- ('metadata', models.JSONField(blank=True, default=dict, help_text='额外信息')),
- ],
- options={
- 'verbose_name': '已处理推送通知',
- 'verbose_name_plural': '已处理推送通知',
- 'db_table': 'gmail_processed_push_notifications',
- 'ordering': ['-processed_at'],
- 'indexes': [models.Index(fields=['message_id'], name='gmail_proce_message_912a0c_idx'), models.Index(fields=['email_address', 'history_id'], name='gmail_proce_email_a_2e3770_idx')],
- },
- ),
- migrations.CreateModel(
- name='UnmatchedEmail',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('message_id', models.CharField(help_text='Gmail邮件ID', max_length=255, unique=True)),
- ('user_id', models.CharField(help_text='用户ID (UUID字符串形式)', max_length=36)),
- ('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
- ('from_email', models.EmailField(help_text='发件人邮箱', max_length=254)),
- ('to_email', models.EmailField(help_text='收件人邮箱', max_length=254)),
- ('subject', models.CharField(blank=True, help_text='邮件主题', max_length=500)),
- ('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
- ],
- options={
- 'verbose_name': '未匹配邮件',
- 'verbose_name_plural': '未匹配邮件',
- 'db_table': 'gmail_unmatched_emails',
- 'ordering': ['-processed_at'],
- 'indexes': [models.Index(fields=['message_id'], name='gmail_unmat_message_c1924d_idx'), models.Index(fields=['user_id'], name='gmail_unmat_user_id_ee06fe_idx'), models.Index(fields=['user_email', 'from_email'], name='gmail_unmat_user_em_a096af_idx')],
- },
- ),
- migrations.CreateModel(
- name='UserGoal',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('description', models.TextField(verbose_name='目标描述')),
- ('status', models.CharField(choices=[('pending', '待处理'), ('in_progress', '进行中'), ('completed', '已完成'), ('failed', '失败')], default='pending', max_length=20, verbose_name='目标状态')),
- ('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('completion_time', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')),
- ('last_activity_time', models.DateTimeField(blank=True, null=True, verbose_name='最后活动时间')),
- ('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息', null=True)),
- ('conversation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='goals', to='gmail.gmailconversation')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'verbose_name': '用户目标',
- 'verbose_name_plural': '用户目标',
- 'ordering': ['-updated_at'],
- },
- ),
- migrations.CreateModel(
- name='AutoReplyConfig',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
- ('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
- ('is_enabled', models.BooleanField(default=True, help_text='是否启用自动回复')),
- ('goal_description', models.TextField(help_text='AI回复时参考的目标', verbose_name='自动回复的目标描述')),
- ('reply_template', models.TextField(blank=True, help_text='回复模板(可选,为空则由AI生成)', null=True)),
- ('max_replies', models.IntegerField(default=5, help_text='最大自动回复次数')),
- ('current_replies', models.IntegerField(default=0, help_text='当前已自动回复次数')),
- ('last_reply_time', models.DateTimeField(blank=True, null=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息,如已处理的消息ID等', null=True)),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auto_reply_configs', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'verbose_name': 'Gmail自动回复配置',
- 'verbose_name_plural': 'Gmail自动回复配置',
- 'db_table': 'gmail_auto_reply_configs',
- 'ordering': ['-updated_at'],
- 'unique_together': {('user', 'user_email', 'influencer_email')},
- },
- ),
migrations.CreateModel(
name='GmailCredential',
fields=[
@@ -166,7 +63,6 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_valid', models.BooleanField(default=True, help_text='Whether the credential is valid')),
- ('last_history_id', models.CharField(blank=True, help_text='Last processed Gmail history ID', max_length=50, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
],
options={
diff --git a/apps/gmail/migrations/0002_gmailcredential_last_history_id.py b/apps/gmail/migrations/0002_gmailcredential_last_history_id.py
new file mode 100644
index 0000000..79fe075
--- /dev/null
+++ b/apps/gmail/migrations/0002_gmailcredential_last_history_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2 on 2025-05-13 08:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='gmailcredential',
+ name='last_history_id',
+ field=models.CharField(blank=True, help_text='Last processed Gmail history ID', max_length=50, null=True),
+ ),
+ ]
diff --git a/apps/gmail/migrations/0003_gmailconversation_metadata.py b/apps/gmail/migrations/0003_gmailconversation_metadata.py
new file mode 100644
index 0000000..3c2ac0b
--- /dev/null
+++ b/apps/gmail/migrations/0003_gmailconversation_metadata.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2 on 2025-05-13 09:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0002_gmailcredential_last_history_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='gmailconversation',
+ name='metadata',
+ field=models.JSONField(blank=True, default=dict, help_text='存储额外信息,如已处理的消息ID等', null=True),
+ ),
+ ]
diff --git a/apps/gmail/migrations/0004_conversationsummary.py b/apps/gmail/migrations/0004_conversationsummary.py
new file mode 100644
index 0000000..6f05b3f
--- /dev/null
+++ b/apps/gmail/migrations/0004_conversationsummary.py
@@ -0,0 +1,31 @@
+# Generated by Django 5.2 on 2025-05-13 10:11
+
+import django.db.models.deletion
+import uuid
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0003_gmailconversation_metadata'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ConversationSummary',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('content', models.TextField(verbose_name='摘要内容')),
+ ('last_message_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='最后处理的消息ID或ChatHistory的ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('conversation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='summary', to='gmail.gmailconversation')),
+ ],
+ options={
+ 'verbose_name': 'Gmail对话摘要',
+ 'verbose_name_plural': 'Gmail对话摘要',
+ 'db_table': 'gmail_conversation_summaries',
+ },
+ ),
+ ]
diff --git a/apps/gmail/migrations/0005_usergoal.py b/apps/gmail/migrations/0005_usergoal.py
new file mode 100644
index 0000000..760772c
--- /dev/null
+++ b/apps/gmail/migrations/0005_usergoal.py
@@ -0,0 +1,34 @@
+# Generated by Django 5.2 on 2025-05-14 02:52
+
+import django.db.models.deletion
+import uuid
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0004_conversationsummary'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserGoal',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('description', models.TextField(verbose_name='目标描述')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': '用户目标',
+ 'verbose_name_plural': '用户目标',
+ 'db_table': 'user_goals',
+ 'ordering': ['-updated_at'],
+ },
+ ),
+ ]
diff --git a/apps/gmail/migrations/0006_usergoal_completion_time_usergoal_conversation_and_more.py b/apps/gmail/migrations/0006_usergoal_completion_time_usergoal_conversation_and_more.py
new file mode 100644
index 0000000..124a447
--- /dev/null
+++ b/apps/gmail/migrations/0006_usergoal_completion_time_usergoal_conversation_and_more.py
@@ -0,0 +1,39 @@
+# Generated by Django 5.2 on 2025-05-14 09:45
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0005_usergoal'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='usergoal',
+ name='completion_time',
+ field=models.DateTimeField(blank=True, null=True, verbose_name='完成时间'),
+ ),
+ migrations.AddField(
+ model_name='usergoal',
+ name='conversation',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='goals', to='gmail.gmailconversation'),
+ ),
+ migrations.AddField(
+ model_name='usergoal',
+ name='last_activity_time',
+ field=models.DateTimeField(blank=True, null=True, verbose_name='最后活动时间'),
+ ),
+ migrations.AddField(
+ model_name='usergoal',
+ name='metadata',
+ field=models.JSONField(blank=True, default=dict, help_text='存储额外信息', null=True),
+ ),
+ migrations.AddField(
+ model_name='usergoal',
+ name='status',
+ field=models.CharField(choices=[('pending', '待处理'), ('in_progress', '进行中'), ('completed', '已完成'), ('failed', '失败')], default='pending', max_length=20, verbose_name='目标状态'),
+ ),
+ ]
diff --git a/apps/gmail/migrations/0007_alter_gmailattachment_attachment_id.py b/apps/gmail/migrations/0007_alter_gmailattachment_attachment_id.py
new file mode 100644
index 0000000..9334986
--- /dev/null
+++ b/apps/gmail/migrations/0007_alter_gmailattachment_attachment_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2 on 2025-05-19 07:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0006_usergoal_completion_time_usergoal_conversation_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='gmailattachment',
+ name='attachment_id',
+ field=models.CharField(help_text='Gmail附件的唯一标识符,可能很长', max_length=255),
+ ),
+ ]
diff --git a/apps/gmail/migrations/0008_alter_gmailattachment_attachment_id.py b/apps/gmail/migrations/0008_alter_gmailattachment_attachment_id.py
new file mode 100644
index 0000000..8ae3d49
--- /dev/null
+++ b/apps/gmail/migrations/0008_alter_gmailattachment_attachment_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2 on 2025-05-19 07:12
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0007_alter_gmailattachment_attachment_id'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='gmailattachment',
+ name='attachment_id',
+ field=models.TextField(help_text='Gmail附件的唯一标识符,可能很长'),
+ ),
+ ]
diff --git a/apps/gmail/migrations/0009_gmailconversation_has_sent_greeting_and_more.py b/apps/gmail/migrations/0009_gmailconversation_has_sent_greeting_and_more.py
new file mode 100644
index 0000000..dd45b75
--- /dev/null
+++ b/apps/gmail/migrations/0009_gmailconversation_has_sent_greeting_and_more.py
@@ -0,0 +1,82 @@
+# Generated by Django 5.2 on 2025-05-20 06:52
+
+import django.db.models.deletion
+import uuid
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0008_alter_gmailattachment_attachment_id'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='gmailconversation',
+ name='has_sent_greeting',
+ field=models.BooleanField(default=False, help_text='Whether a greeting message has been sent to this conversation'),
+ ),
+ migrations.AlterField(
+ model_name='gmailconversation',
+ name='conversation_id',
+ field=models.CharField(help_text='Unique conversation identifier', max_length=100, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='gmailconversation',
+ name='influencer_email',
+ field=models.EmailField(help_text="Influencer's email address", max_length=254),
+ ),
+ migrations.AlterField(
+ model_name='gmailconversation',
+ name='is_active',
+ field=models.BooleanField(default=True, help_text='Whether this conversation is active'),
+ ),
+ migrations.AlterField(
+ model_name='gmailconversation',
+ name='last_sync_time',
+ field=models.DateTimeField(blank=True, help_text='Last time conversation was synced with Gmail', null=True),
+ ),
+ migrations.AlterField(
+ model_name='gmailconversation',
+ name='metadata',
+ field=models.JSONField(blank=True, help_text='Additional metadata for the conversation', null=True),
+ ),
+ migrations.AlterField(
+ model_name='gmailconversation',
+ name='title',
+ field=models.CharField(help_text='Conversation title', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='gmailconversation',
+ name='user_email',
+ field=models.EmailField(help_text="User's Gmail address", max_length=254),
+ ),
+ migrations.CreateModel(
+ name='AutoReplyConfig',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
+ ('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
+ ('is_enabled', models.BooleanField(default=True, help_text='是否启用自动回复')),
+ ('goal_description', models.TextField(help_text='AI回复时参考的目标', verbose_name='自动回复的目标描述')),
+ ('reply_template', models.TextField(blank=True, help_text='回复模板(可选,为空则由AI生成)', null=True)),
+ ('max_replies', models.IntegerField(default=5, help_text='最大自动回复次数')),
+ ('current_replies', models.IntegerField(default=0, help_text='当前已自动回复次数')),
+ ('last_reply_time', models.DateTimeField(blank=True, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息,如已处理的消息ID等', null=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auto_reply_configs', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Gmail自动回复配置',
+ 'verbose_name_plural': 'Gmail自动回复配置',
+ 'db_table': 'gmail_auto_reply_configs',
+ 'ordering': ['-updated_at'],
+ 'unique_together': {('user', 'user_email', 'influencer_email')},
+ },
+ ),
+ ]
diff --git a/apps/gmail/migrations/0010_processedpushnotification_unmatchedemail.py b/apps/gmail/migrations/0010_processedpushnotification_unmatchedemail.py
new file mode 100644
index 0000000..68d587b
--- /dev/null
+++ b/apps/gmail/migrations/0010_processedpushnotification_unmatchedemail.py
@@ -0,0 +1,53 @@
+# Generated by Django 5.2 on 2025-05-20 09:21
+
+import uuid
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0009_gmailconversation_has_sent_greeting_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ProcessedPushNotification',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('message_id', models.CharField(help_text='Pub/Sub消息ID', max_length=255, unique=True)),
+ ('email_address', models.EmailField(help_text='通知关联的Gmail邮箱', max_length=254)),
+ ('history_id', models.CharField(help_text='Gmail历史ID', max_length=100)),
+ ('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
+ ('is_successful', models.BooleanField(default=True, help_text='处理是否成功')),
+ ('metadata', models.JSONField(blank=True, default=dict, help_text='额外信息')),
+ ],
+ options={
+ 'verbose_name': '已处理推送通知',
+ 'verbose_name_plural': '已处理推送通知',
+ 'db_table': 'gmail_processed_push_notifications',
+ 'ordering': ['-processed_at'],
+ 'indexes': [models.Index(fields=['message_id'], name='gmail_proce_message_912a0c_idx'), models.Index(fields=['email_address', 'history_id'], name='gmail_proce_email_a_2e3770_idx')],
+ },
+ ),
+ migrations.CreateModel(
+ name='UnmatchedEmail',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('message_id', models.CharField(help_text='Gmail邮件ID', max_length=255, unique=True)),
+ ('user_id', models.IntegerField(help_text='用户ID')),
+ ('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
+ ('from_email', models.EmailField(help_text='发件人邮箱', max_length=254)),
+ ('to_email', models.EmailField(help_text='收件人邮箱', max_length=254)),
+ ('subject', models.CharField(blank=True, help_text='邮件主题', max_length=500)),
+ ('processed_at', models.DateTimeField(auto_now_add=True, help_text='处理时间')),
+ ],
+ options={
+ 'verbose_name': '未匹配邮件',
+ 'verbose_name_plural': '未匹配邮件',
+ 'db_table': 'gmail_unmatched_emails',
+ 'ordering': ['-processed_at'],
+ 'indexes': [models.Index(fields=['message_id'], name='gmail_unmat_message_c1924d_idx'), models.Index(fields=['user_id'], name='gmail_unmat_user_id_ee06fe_idx'), models.Index(fields=['user_email', 'from_email'], name='gmail_unmat_user_em_a096af_idx')],
+ },
+ ),
+ ]
diff --git a/apps/gmail/migrations/0011_alter_unmatchedemail_user_id.py b/apps/gmail/migrations/0011_alter_unmatchedemail_user_id.py
new file mode 100644
index 0000000..73e4ae2
--- /dev/null
+++ b/apps/gmail/migrations/0011_alter_unmatchedemail_user_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2 on 2025-05-20 09:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0010_processedpushnotification_unmatchedemail'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='unmatchedemail',
+ name='user_id',
+ field=models.CharField(help_text='用户ID (UUID字符串形式)', max_length=36),
+ ),
+ ]
diff --git a/apps/gmail/migrations/0012_alter_usergoal_table.py b/apps/gmail/migrations/0012_alter_usergoal_table.py
new file mode 100644
index 0000000..e51e1e1
--- /dev/null
+++ b/apps/gmail/migrations/0012_alter_usergoal_table.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.2 on 2025-05-21 04:22
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gmail', '0011_alter_unmatchedemail_user_id'),
+ ]
+
+ operations = [
+ migrations.AlterModelTable(
+ name='usergoal',
+ table=None,
+ ),
+ ]
diff --git a/apps/gmail/services/gmail_service.py b/apps/gmail/services/gmail_service.py
index ade1c6c..429d2b4 100644
--- a/apps/gmail/services/gmail_service.py
+++ b/apps/gmail/services/gmail_service.py
@@ -17,6 +17,7 @@ from ..models import GmailCredential, GmailConversation, GmailAttachment, Conver
from apps.chat.models import ChatHistory
from apps.knowledge_base.models import KnowledgeBase
import requests
+# from apps.common.services.notification_service import NotificationService
import threading
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
diff --git a/apps/gmail/urls.py b/apps/gmail/urls.py
index a8a4f20..30d52f7 100644
--- a/apps/gmail/urls.py
+++ b/apps/gmail/urls.py
@@ -1,6 +1,6 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
-from apps.gmail.views import (
+from .views import (
GmailAuthInitiateView,
GmailAuthCompleteView,
GmailConversationView,
diff --git a/apps/gmail/views.py b/apps/gmail/views.py
index d7a6033..0118b63 100644
--- a/apps/gmail/views.py
+++ b/apps/gmail/views.py
@@ -35,85 +35,34 @@ class GmailAuthInitiateView(APIView):
"""
API 视图,用于启动 Gmail OAuth2 认证流程。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
-
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
def post(self, request):
"""
处理 POST 请求,启动 Gmail OAuth2 认证并返回授权 URL。
- 支持两种方式提供客户端密钥:
- 1. 在请求体中提供client_secret_json字段
- 2. 上传名为client_secret_file的JSON文件
+ 直接使用系统中已配置的客户端密钥文件。
Args:
- request: Django REST Framework 请求对象,包含客户端密钥 JSON 数据或文件。
+ request: Django REST Framework 请求对象。
Returns:
Response: 包含授权 URL 的 JSON 响应(成功时),或错误信息(失败时)。
Status Codes:
200: 成功生成授权 URL。
- 400: 请求数据无效。
500: 服务器内部错误(如认证服务失败)。
"""
- logger.debug(f"Received auth initiate request: {request.data}")
+ logger.debug(f"Received auth initiate request from user {request.user.id}")
- # 检查是否是文件上传方式
- client_secret_json = None
- if 'client_secret_file' in request.FILES:
- try:
- # 读取上传的JSON文件内容
- client_secret_file = request.FILES['client_secret_file']
- client_secret_json = json.loads(client_secret_file.read().decode('utf-8'))
- logger.info(f"从上传文件读取到客户端密钥JSON")
- except json.JSONDecodeError as e:
- logger.error(f"解析客户端密钥JSON文件失败: {str(e)}")
- return Response({
- 'code': 400,
- 'message': f'无效的JSON文件格式: {str(e)}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- 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)
+ # 直接使用系统中已配置的客户端密钥文件
+ client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
- # 如果不是文件上传,则尝试从请求数据中提取JSON
- if not client_secret_json:
- serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
- if serializer.is_valid():
- try:
- # 从请求数据中提取客户端密钥 JSON
- client_secret_json = serializer.validated_data['client_secret_json']
- except Exception as e:
- logger.error(f"未提供客户端密钥JSON: {str(e)}")
- return Response({
- 'code': 400,
- 'message': '请提供client_secret_json字段或上传client_secret_file文件',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- else:
- # 记录无效请求数据并返回错误响应
- logger.warning(f"Invalid request data: {serializer.errors}")
- return Response({
- 'code': 400,
- 'message': '请求数据无效,请提供client_secret_json字段或上传client_secret_file文件',
- 'data': serializer.errors
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 如果此时仍然没有client_secret_json,返回错误
- if not client_secret_json:
- return Response({
- 'code': 400,
- 'message': '请提供client_secret_json字段或上传client_secret_file文件',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
try:
+ # 读取客户端密钥文件
+ with open(client_secret_path, 'r') as f:
+ client_secret_json = json.load(f)
+
# 调用 GmailService 生成授权 URL
auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
logger.info(f"Generated auth URL for user {request.user.id}")
@@ -136,19 +85,16 @@ class GmailAuthCompleteView(APIView):
"""
API 视图,用于完成 Gmail OAuth2 认证流程。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
-
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
def post(self, request):
"""
处理 POST 请求,使用授权代码完成 Gmail OAuth2 认证并保存凭证。
- 支持两种方式提供客户端密钥:
- 1. 在请求体中提供client_secret_json字段
- 2. 上传名为client_secret_file的JSON文件
+ 直接使用系统中已配置的客户端密钥文件。
Args:
- request: Django REST Framework 请求对象,包含授权代码和客户端密钥 JSON 或文件。
+ request: Django REST Framework 请求对象,包含授权代码。
Returns:
Response: 包含已保存凭证数据的 JSON 响应(成功时),或错误信息(失败时)。
@@ -158,72 +104,25 @@ class GmailAuthCompleteView(APIView):
400: 请求数据无效。
500: 服务器内部错误(如认证失败)。
"""
- logger.debug(f"Received auth complete request: {request.data}")
+ logger.debug(f"Received auth complete request from user {request.user.id}")
- # 检查是否是文件上传方式
- client_secret_json = None
- if 'client_secret_file' in request.FILES:
- try:
- # 读取上传的JSON文件内容
- client_secret_file = request.FILES['client_secret_file']
- client_secret_json = json.loads(client_secret_file.read().decode('utf-8'))
- logger.info(f"从上传文件读取到客户端密钥JSON")
- except json.JSONDecodeError as e:
- logger.error(f"解析客户端密钥JSON文件失败: {str(e)}")
- return Response({
- 'code': 400,
- 'message': f'无效的JSON文件格式: {str(e)}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- 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)
+ # 直接使用系统中已配置的客户端密钥文件
+ client_secret_path = 'media/secret/client_secret_266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com.json'
- # 获取授权码,无论是哪种方式都需要
+ # 获取授权代码
auth_code = request.data.get('auth_code')
if not auth_code:
return Response({
'code': 400,
- 'message': '必须提供授权码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 如果不是文件上传,则尝试从请求数据中提取JSON
- if not client_secret_json:
- serializer = GmailCredentialSerializer(data=request.data, context={'request': request})
- if serializer.is_valid():
- try:
- # 从请求数据中提取客户端密钥 JSON
- client_secret_json = serializer.validated_data['client_secret_json']
- except Exception as e:
- logger.error(f"未提供客户端密钥JSON: {str(e)}")
- return Response({
- 'code': 400,
- 'message': '请提供client_secret_json字段或上传client_secret_file文件',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- else:
- # 记录无效请求数据并返回错误响应
- logger.warning(f"Invalid request data: {serializer.errors}")
- return Response({
- 'code': 400,
- 'message': '请求数据无效,请提供client_secret_json字段或上传client_secret_file文件',
- 'data': serializer.errors
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 如果此时仍然没有client_secret_json,返回错误
- if not client_secret_json:
- return Response({
- 'code': 400,
- 'message': '请提供client_secret_json字段或上传client_secret_file文件',
+ 'message': '缺少必要的授权代码',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
+ # 读取客户端密钥文件
+ with open(client_secret_path, 'r') as f:
+ client_secret_json = json.load(f)
+
# 完成认证并保存凭证
credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json)
# 序列化凭证数据以返回
@@ -525,9 +424,9 @@ class GmailConversationView(APIView):
"""
API视图,用于获取和保存Gmail对话。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
-
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
+
def post(self, request):
"""
处理POST请求,获取Gmail对话并保存到聊天历史。
@@ -630,9 +529,9 @@ class GmailAttachmentListView(APIView):
"""
API视图,用于获取Gmail附件列表。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
-
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
+
def get(self, request, conversation_id=None):
"""
处理GET请求,获取指定对话的附件列表。
@@ -679,9 +578,9 @@ class GmailPubSubView(APIView):
"""
API视图,用于设置Gmail的Pub/Sub实时通知。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
-
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
+
def post(self, request):
"""
处理POST请求,为用户的Gmail账户设置Pub/Sub推送通知。
@@ -761,9 +660,9 @@ class GmailPubSubView(APIView):
credentials = request.user.gmail_credentials.filter(is_valid=True)
# 构建响应数据
- user = []
+ accounts = []
for cred in credentials:
- user.append({
+ accounts.append({
'id': cred.id,
'email': cred.email,
'is_default': cred.is_default
@@ -772,7 +671,7 @@ class GmailPubSubView(APIView):
return Response({
'code': 200,
'message': '获取账户列表成功',
- 'data': {'user': user}
+ 'data': {'accounts': accounts}
}, status=status.HTTP_200_OK)
@@ -780,9 +679,9 @@ class GmailSendEmailView(APIView):
"""
API视图,用于发送Gmail邮件(支持附件)。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
-
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
+
def post(self, request):
"""
处理POST请求,发送Gmail邮件。
@@ -915,9 +814,9 @@ class GmailSendEmailView(APIView):
credentials = request.user.gmail_credentials.filter(is_valid=True)
# 构建响应数据
- user = []
+ accounts = []
for cred in credentials:
- user.append({
+ accounts.append({
'id': cred.id,
'email': cred.email,
'is_default': cred.is_default
@@ -926,7 +825,7 @@ class GmailSendEmailView(APIView):
return Response({
'code': 200,
'message': '获取账户列表成功',
- 'data': {'user': user}
+ 'data': {'accounts': accounts}
}, status=status.HTTP_200_OK)
@@ -1012,9 +911,9 @@ class GmailConversationSummaryView(APIView):
"""
API视图,用于获取Gmail对话的总结。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
-
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
+
def get(self, request, conversation_id=None):
"""
处理GET请求,获取指定Gmail对话的总结。
@@ -1094,7 +993,8 @@ class GmailGoalView(APIView):
用户目标API - 支持用户为每个对话设置不同的目标
"""
permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
+
def get(self, request, conversation_id=None):
"""
获取用户的目标
@@ -1172,6 +1072,13 @@ class GmailGoalView(APIView):
'data': None
}, status=status.HTTP_404_NOT_FOUND)
+ # 检查权限
+ # if conversation.user.id != request.user.id:
+ # return Response({
+ # 'code': 403,
+ # 'message': '无权限访问此对话',
+ # 'data': None
+ # }, status=status.HTTP_403_FORBIDDEN)
# 查找现有目标
existing_goal = UserGoal.objects.filter(
@@ -1274,7 +1181,8 @@ class SimpleRecommendedReplyView(APIView):
通过conversation_id一键获取目标、对话摘要和推荐回复
"""
permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
+
def post(self, request):
"""
直接通过conversation_id获取推荐回复
@@ -1312,12 +1220,12 @@ class SimpleRecommendedReplyView(APIView):
'data': None
}, status=status.HTTP_404_NOT_FOUND)
- if conversation.user != request.user:
- return Response({
- 'code': 403,
- 'message': '无权限访问该对话',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
+ # if conversation.user != request.user:
+ # return Response({
+ # 'code': 403,
+ # 'message': '无权限访问该对话',
+ # 'data': None
+ # }, status=status.HTTP_403_FORBIDDEN)
except Exception as e:
logger.error(f"查找对话失败: {str(e)}")
return Response({
@@ -1399,8 +1307,8 @@ class GmailExportView(APIView):
"""
API视图,用于导出已回复的达人Gmail列表为Excel文件。
"""
- permission_classes = [IsAuthenticated]
- authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
+ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户
+ authentication_classes = [CustomTokenAuthentication]
def get(self, request, format=None):
"""
diff --git a/apps/knowledge_base/migrations/0001_initial.py b/apps/knowledge_base/migrations/0001_initial.py
index ab64961..1670805 100644
--- a/apps/knowledge_base/migrations/0001_initial.py
+++ b/apps/knowledge_base/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.2.1 on 2025-05-28 08:33
+# Generated by Django 5.2 on 2025-05-07 03:40
import django.db.models.deletion
import uuid
@@ -31,7 +31,8 @@ class Migration(migrations.Migration):
('update_time', models.DateTimeField(auto_now=True)),
],
options={
- 'indexes': [models.Index(fields=['type'], name='knowledge_b_type_7a8dcc_idx'), models.Index(fields=['department'], name='knowledge_b_departm_8f1cf3_idx'), models.Index(fields=['group'], name='knowledge_b_group_ae45d4_idx')],
+ 'db_table': 'knowledge_bases',
+ 'indexes': [models.Index(fields=['type'], name='knowledge_b_type_0439e7_idx'), models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'), models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx')],
},
),
migrations.CreateModel(
@@ -50,7 +51,8 @@ class Migration(migrations.Migration):
options={
'verbose_name': '知识库文档',
'verbose_name_plural': '知识库文档',
- 'indexes': [models.Index(fields=['knowledge_base', 'status'], name='knowledge_b_knowled_6653e6_idx'), models.Index(fields=['document_id'], name='knowledge_b_documen_1fd896_idx'), models.Index(fields=['external_id'], name='knowledge_b_externa_ae1a97_idx')],
+ 'db_table': 'knowledge_base_documents',
+ 'indexes': [models.Index(fields=['knowledge_base', 'status'], name='knowledge_b_knowled_a4db1b_idx'), models.Index(fields=['document_id'], name='knowledge_b_documen_dab90f_idx'), models.Index(fields=['external_id'], name='knowledge_b_externa_b0060c_idx')],
'unique_together': {('knowledge_base', 'document_id')},
},
),
diff --git a/apps/knowledge_base/migrations/0002_rename_knowledge_b_type_0439e7_idx_knowledge_b_type_7a8dcc_idx_and_more.py b/apps/knowledge_base/migrations/0002_rename_knowledge_b_type_0439e7_idx_knowledge_b_type_7a8dcc_idx_and_more.py
new file mode 100644
index 0000000..58c2424
--- /dev/null
+++ b/apps/knowledge_base/migrations/0002_rename_knowledge_b_type_0439e7_idx_knowledge_b_type_7a8dcc_idx_and_more.py
@@ -0,0 +1,51 @@
+# Generated by Django 5.2 on 2025-05-21 04:30
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('knowledge_base', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameIndex(
+ model_name='knowledgebase',
+ new_name='knowledge_b_type_7a8dcc_idx',
+ old_name='knowledge_b_type_0439e7_idx',
+ ),
+ migrations.RenameIndex(
+ model_name='knowledgebase',
+ new_name='knowledge_b_departm_8f1cf3_idx',
+ old_name='knowledge_b_departm_e739fd_idx',
+ ),
+ migrations.RenameIndex(
+ model_name='knowledgebase',
+ new_name='knowledge_b_group_ae45d4_idx',
+ old_name='knowledge_b_group_3dcf34_idx',
+ ),
+ migrations.RenameIndex(
+ model_name='knowledgebasedocument',
+ new_name='knowledge_b_knowled_6653e6_idx',
+ old_name='knowledge_b_knowled_a4db1b_idx',
+ ),
+ migrations.RenameIndex(
+ model_name='knowledgebasedocument',
+ new_name='knowledge_b_documen_1fd896_idx',
+ old_name='knowledge_b_documen_dab90f_idx',
+ ),
+ migrations.RenameIndex(
+ model_name='knowledgebasedocument',
+ new_name='knowledge_b_externa_ae1a97_idx',
+ old_name='knowledge_b_externa_b0060c_idx',
+ ),
+ migrations.AlterModelTable(
+ name='knowledgebase',
+ table=None,
+ ),
+ migrations.AlterModelTable(
+ name='knowledgebasedocument',
+ table=None,
+ ),
+ ]
diff --git a/apps/knowledge_base/models.py b/apps/knowledge_base/models.py
index e9d6e63..79525f6 100644
--- a/apps/knowledge_base/models.py
+++ b/apps/knowledge_base/models.py
@@ -34,6 +34,34 @@ class KnowledgeBase(models.Model):
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now=True)
+ # def is_owner(self, user):
+ # """检查用户是否是所有者(通过权限表检查)"""
+ # from apps.permissions.models import KnowledgeBasePermission
+ # return str(user.id) == str(self.user_id) or KnowledgeBasePermission.objects.filter(
+ # knowledge_base=self,
+ # user=user,
+ # can_read=True,
+ # can_edit=True,
+ # can_delete=True,
+ # status='active'
+ # ).exists()
+
+ # def get_owners(self):
+ # """获取所有所有者(包括创建者和具有完整权限的用户)"""
+ # from apps.user.models import User
+ # from apps.permissions.models import KnowledgeBasePermission
+ # # 获取创建者
+ # owners = [self.user_id]
+ # # 获取具有完整权限的用户
+ # permission_owners = KnowledgeBasePermission.objects.filter(
+ # knowledge_base=self,
+ # can_read=True,
+ # can_edit=True,
+ # can_delete=True,
+ # status='active'
+ # ).values_list('user_id', flat=True)
+ # owners.extend(permission_owners)
+ # return User.objects.filter(id__in=set(owners))
def calculate_stats(self):
"""计算文档统计信息"""
diff --git a/apps/knowledge_base/urls.py b/apps/knowledge_base/urls.py
index e69de29..6d0a584 100644
--- a/apps/knowledge_base/urls.py
+++ b/apps/knowledge_base/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path, include
+from rest_framework.routers import DefaultRouter
+from apps.knowledge_base.views import KnowledgeBaseViewSet
+
+router = DefaultRouter()
+router.register(r'', KnowledgeBaseViewSet, basename='knowledge-base') # 移除 'knowledge-bases'
+
+urlpatterns = [
+ path('', include(router.urls)),
+]
\ No newline at end of file
diff --git a/apps/knowledge_base/views.py b/apps/knowledge_base/views.py
index e69de29..605b195 100644
--- a/apps/knowledge_base/views.py
+++ b/apps/knowledge_base/views.py
@@ -0,0 +1,1270 @@
+# # apps/knowledge_base/views.py
+# import logging
+# import json
+# import traceback
+# from django.db.models import Q
+# from django.db import transaction
+# from django.utils import timezone
+# from django.http import Http404
+# from rest_framework import viewsets, status
+# from rest_framework.response import Response
+# from rest_framework.permissions import IsAuthenticated
+# from rest_framework.decorators import action
+# import requests
+# from apps.user.models import User
+# from apps.knowledge_base.models import KnowledgeBase, KnowledgeBaseDocument
+# from apps.permissions.models import KnowledgeBasePermission as KBPermissionModel
+# from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin
+# from apps.knowledge_base.serializers import KnowledgeBaseSerializer, KnowledgeBaseDocumentSerializer
+# from apps.common.services.external_api_service import (
+# create_external_dataset, delete_external_dataset, call_split_api_multiple,
+# call_upload_api, call_delete_document_api, ExternalAPIError,
+# get_external_document_list, get_external_document_paragraphs,
+# call_delete_document_api
+# )
+# from daren import settings
+
+# logger = logging.getLogger(__name__)
+
+# class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet):
+# serializer_class = KnowledgeBaseSerializer
+# permission_classes = [IsAuthenticated]
+
+# def list(self, request, *args, **kwargs):
+# try:
+# queryset = self.get_queryset()
+# keyword = request.query_params.get('keyword', '')
+
+# if keyword:
+# query = Q(name__icontains=keyword) | Q(desc__icontains=keyword) | \
+# Q(department__icontains=keyword) | Q(group__icontains=keyword)
+# queryset = queryset.filter(query)
+
+# try:
+# page = int(request.query_params.get('page', 1))
+# page_size = int(request.query_params.get('page_size', 10))
+# except ValueError:
+# page = 1
+# page_size = 10
+
+# total = queryset.count()
+# start = (page - 1) * page_size
+# end = start + page_size
+# paginated_queryset = queryset[start:end]
+
+# serializer = self.get_serializer(paginated_queryset, many=True)
+# data = serializer.data
+# user = request.user
+
+# for item in data:
+# kb_type = item['type']
+# department = item.get('department')
+# group = item.get('group')
+# creator_id = item.get('user_id')
+# kb_id = item['id']
+
+# explicit_permission = KBPermissionModel.objects.filter(
+# knowledge_base_id=kb_id,
+# user=user,
+# status='active'
+# ).first()
+
+# if explicit_permission:
+# item['permissions'] = {
+# 'can_read': explicit_permission.can_read,
+# 'can_edit': explicit_permission.can_edit,
+# 'can_delete': explicit_permission.can_delete
+# }
+# item['expires_at'] = explicit_permission.expires_at.strftime("%Y-%m-%d %H:%M:%S") if explicit_permission.expires_at else None
+# else:
+# item['permissions'] = {
+# 'can_read': self._can_read(kb_type, user, department, group, creator_id, kb_id),
+# 'can_edit': self._can_edit(kb_type, user, department, group, creator_id, kb_id),
+# 'can_delete': self._can_delete(kb_type, user, department, group, creator_id, kb_id)
+# }
+# item['expires_at'] = None if kb_type == 'admin' else None
+
+# if keyword:
+# if 'name' in item and keyword.lower() in item['name'].lower():
+# item['highlighted_name'] = item['name'].replace(
+# keyword, f'{keyword}'
+# )
+# if 'desc' in item and item.get('desc') is not None:
+# desc_text = str(item['desc'])
+# if keyword.lower() in desc_text.lower():
+# item['highlighted_desc'] = desc_text.replace(
+# keyword, f'{keyword}'
+# )
+
+# return Response({
+# "code": 200,
+# "message": "获取知识库列表成功",
+# "data": {
+# "total": total,
+# "page": page,
+# "page_size": page_size,
+# "keyword": keyword if keyword else None,
+# "items": data
+# }
+# })
+# 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 get_queryset(self):
+# user = self.request.user
+# queryset = KnowledgeBase.objects.all()
+# permission_conditions = Q()
+
+# permission_conditions |= Q(type='admin')
+# permission_conditions |= Q(user_id=user.id)
+
+# active_permissions = KBPermissionModel.objects.filter(
+# user=user,
+# can_read=True,
+# status='active',
+# expires_at__gt=timezone.now()
+# ).values_list('knowledge_base_id', flat=True)
+
+# if active_permissions:
+# permission_conditions |= Q(id__in=active_permissions)
+
+# if user.role == 'admin':
+# permission_conditions |= ~Q(type='private') | Q(user_id=user.id)
+# elif user.role == 'leader':
+# permission_conditions |= Q(type__in=['leader', 'member'], department=user.department)
+# elif user.role in ['member', 'user']:
+# permission_conditions |= Q(type='leader', department=user.department)
+# permission_conditions |= Q(type='member', department=user.department, group=user.group)
+
+# return queryset.filter(permission_conditions).distinct()
+
+# def create(self, request, *args, **kwargs):
+# try:
+# name = request.data.get('name')
+# if not name:
+# return Response({
+# 'code': 400,
+# 'message': '知识库名称不能为空',
+# 'data': None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# if KnowledgeBase.objects.filter(name=name).exists():
+# return Response({
+# 'code': 400,
+# 'message': f'知识库名称 "{name}" 已存在',
+# 'data': None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# user = request.user
+# type = request.data.get('type', 'private')
+# department = request.data.get('department')
+# group = request.data.get('group')
+
+# if type == 'admin':
+# department = None
+# group = None
+# elif type == 'secret':
+# if user.role != 'admin':
+# return Response({
+# 'code': 403,
+# 'message': '只有管理员可以创建保密级知识库',
+# 'data': None
+# }, status=status.HTTP_403_FORBIDDEN)
+# department = None
+# group = None
+# elif type == 'leader':
+# if user.role != 'admin':
+# return Response({
+# 'code': 403,
+# 'message': '只有管理员可以创建组长级知识库',
+# 'data': None
+# }, status=status.HTTP_403_FORBIDDEN)
+# if not department:
+# return Response({
+# 'code': 400,
+# 'message': '创建组长级知识库时必须指定部门',
+# 'data': None
+# }, status=status.HTTP_400_BAD_REQUEST)
+# elif type == 'member':
+# if user.role not in ['admin', 'leader']:
+# return Response({
+# 'code': 403,
+# 'message': '只有管理员和组长可以创建成员级知识库',
+# 'data': None
+# }, status=status.HTTP_403_FORBIDDEN)
+# if user.role == 'admin' and not department:
+# return Response({
+# 'code': 400,
+# 'message': '管理员创建成员知识库时必须指定部门',
+# 'data': None
+# }, status=status.HTTP_400_BAD_REQUEST)
+# elif user.role == 'leader':
+# department = user.department
+# if not group:
+# return Response({
+# 'code': 400,
+# 'message': '创建成员知识库时必须指定组',
+# 'data': None
+# }, status=status.HTTP_400_BAD_REQUEST)
+# elif type == 'private':
+# department = None
+# group = None
+
+# data = request.data.copy()
+# data['department'] = department
+# data['group'] = group
+
+# serializer = self.get_serializer(data=data, context={'request': request})
+# if not serializer.is_valid():
+# logger.error(f"数据验证失败: {serializer.errors}")
+# return Response({
+# 'code': 400,
+# 'message': '数据验证失败',
+# 'data': serializer.errors
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# with transaction.atomic():
+# knowledge_base = serializer.save()
+# logger.info(f"知识库创建成功: id={knowledge_base.id}, name={knowledge_base.name}, user_id={knowledge_base.user_id}")
+
+# external_id = create_external_dataset(knowledge_base)
+# logger.info(f"外部知识库创建成功,获取ID: {external_id}")
+
+# knowledge_base.external_id = external_id
+# knowledge_base.save()
+# logger.info(f"更新knowledge_base的external_id为: {external_id}")
+
+# KBPermissionModel.objects.create(
+# knowledge_base=knowledge_base,
+# user=request.user,
+# can_read=True,
+# can_edit=True,
+# can_delete=True,
+# granted_by=request.user,
+# status='active'
+# )
+# logger.info(f"创建者权限创建成功")
+
+# permissions = []
+# if type == 'admin':
+# users_query = User.objects.exclude(id=request.user.id)
+# permissions = [
+# KBPermissionModel(
+# knowledge_base=knowledge_base,
+# user=user,
+# can_read=True,
+# can_edit=True,
+# can_delete=True,
+# granted_by=request.user,
+# status='active'
+# ) for user in users_query
+# ]
+# elif type == 'secret':
+# users_query = User.objects.filter(role='admin').exclude(id=request.user.id)
+# permissions = [
+# KBPermissionModel(
+# knowledge_base=knowledge_base,
+# user=user,
+# can_read=True,
+# can_edit=self._can_edit(type, user),
+# can_delete=self._can_delete(type, user),
+# granted_by=request.user,
+# status='active'
+# ) for user in users_query
+# ]
+# elif type == 'leader':
+# users_query = User.objects.filter(
+# Q(role='admin') | Q(role='leader', department=department)
+# ).exclude(id=request.user.id)
+# permissions = [
+# KBPermissionModel(
+# knowledge_base=knowledge_base,
+# user=user,
+# can_read=True,
+# can_edit=self._can_edit(type, user),
+# can_delete=self._can_delete(type, user),
+# granted_by=request.user,
+# status='active'
+# ) for user in users_query
+# ]
+# elif type == 'member':
+# users_query = User.objects.filter(
+# Q(role='admin') | Q(department=department, role='leader') |
+# Q(department=department, group=group, role='member')
+# ).exclude(id=request.user.id)
+# permissions = [
+# KBPermissionModel(
+# knowledge_base=knowledge_base,
+# user=user,
+# can_read=True,
+# can_edit=self._can_edit(type, user),
+# can_delete=self._can_delete(type, user),
+# granted_by=request.user,
+# status='active'
+# ) for user in users_query
+# ]
+
+# if permissions:
+# KBPermissionModel.objects.bulk_create(permissions)
+# logger.info(f"{type}类型权限创建完成: {len(permissions)}条记录")
+
+# return Response({
+# 'code': 200,
+# 'message': '知识库创建成功',
+# 'data': {
+# 'knowledge_base': serializer.data,
+# 'external_id': knowledge_base.external_id
+# }
+# })
+
+# 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)
+# 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 update(self, request, *args, **kwargs):
+# try:
+# instance = self.get_object()
+# user = request.user
+
+# if not self.check_knowledge_base_permission(instance, user, 'edit'):
+# return Response({
+# "code": 403,
+# "message": "没有编辑权限",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# with transaction.atomic():
+# serializer = self.get_serializer(instance, data=request.data, partial=True)
+# serializer.is_valid(raise_exception=True)
+# self.perform_update(serializer)
+
+# if instance.external_id:
+# try:
+# api_data = {
+# "name": serializer.validated_data.get('name', instance.name),
+# "desc": serializer.validated_data.get('desc', instance.desc),
+# "type": "0",
+# "meta": {},
+# "documents": []
+# }
+
+# response = requests.put(
+# f'{settings.API_BASE_URL}/api/dataset/{instance.external_id}',
+# json=api_data,
+# headers={'Content-Type': 'application/json'},
+# )
+
+# if response.status_code != 200:
+# raise ExternalAPIError(f"更新外部知识库失败,状态码: {response.status_code}, 响应: {response.text}")
+
+# api_response = response.json()
+# if not api_response.get('code') == 200:
+# raise ExternalAPIError(f"更新外部知识库失败: {api_response.get('message', '未知错误')}")
+
+# logger.info(f"外部知识库更新成功: {instance.external_id}")
+
+# except requests.exceptions.Timeout:
+# raise ExternalAPIError("请求超时,请稍后重试")
+# except requests.exceptions.RequestException as e:
+# raise ExternalAPIError(f"API请求失败: {str(e)}")
+# except Exception as e:
+# raise ExternalAPIError(f"更新外部知识库失败: {str(e)}")
+
+# return Response({
+# "code": 200,
+# "message": "知识库更新成功",
+# "data": serializer.data
+# })
+
+# except Http404:
+# return Response({
+# "code": 404,
+# "message": "知识库不存在",
+# "data": None
+# }, status=status.HTTP_404_NOT_FOUND)
+# except ExternalAPIError as e:
+# logger.error(f"更新外部知识库失败: {str(e)}")
+# logger.error(traceback.format_exc())
+# return Response({
+# "code": 500,
+# "message": str(e),
+# "data": None
+# }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+# 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 destroy(self, request, *args, **kwargs):
+# try:
+# instance = self.get_object()
+# user = request.user
+
+# if not self.check_knowledge_base_permission(instance, user, 'delete'):
+# return Response({
+# "code": 403,
+# "message": "没有删除权限",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# external_delete_success = True
+# external_error_message = None
+# if instance.external_id:
+# external_delete_success = delete_external_dataset(instance.external_id)
+# if not external_delete_success:
+# external_error_message = "外部知识库删除失败"
+# logger.warning(f"外部知识库删除失败,将继续删除本地知识库: {external_error_message}")
+
+# self.perform_destroy(instance)
+# logger.info(f"本地知识库删除成功: id={instance.id}, name={instance.name}")
+
+# if not external_delete_success:
+# return Response({
+# "code": 200,
+# "message": f"知识库已删除,但外部知识库删除失败: {external_error_message}",
+# "data": None
+# })
+
+# return Response({
+# "code": 200,
+# "message": "知识库删除成功",
+# "data": None
+# })
+
+# except Http404:
+# return Response({
+# "code": 404,
+# "message": "知识库不存在",
+# "data": None
+# }, status=status.HTTP_404_NOT_FOUND)
+# 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)
+
+# @action(detail=True, methods=['get'])
+# def permissions(self, request, pk=None):
+# try:
+# instance = self.get_object()
+# user = request.user
+
+# permissions_data = {
+# "can_read": self.check_knowledge_base_permission(instance, user, 'read'),
+# "can_edit": self.check_knowledge_base_permission(instance, user, 'edit'),
+# "can_delete": self.check_knowledge_base_permission(instance, user, 'delete')
+# }
+
+# return Response({
+# "code": 200,
+# "message": "获取权限信息成功",
+# "data": {
+# "knowledge_base_id": instance.id,
+# "knowledge_base_name": instance.name,
+# "permissions": permissions_data
+# }
+# })
+
+# except Http404:
+# return Response({
+# "code": 404,
+# "message": "知识库不存在",
+# "data": None
+# }, status=status.HTTP_404_NOT_FOUND)
+# 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)
+
+# @action(detail=False, methods=['get'])
+# def summary(self, request):
+# try:
+# user = request.user
+# queryset = KnowledgeBase.objects.exclude(type='secret')
+# summaries = []
+
+# for kb in queryset:
+# permissions = {
+# 'can_read': self.check_knowledge_base_permission(kb, user, 'read'),
+# 'can_edit': self.check_knowledge_base_permission(kb, user, 'edit'),
+# 'can_delete': self.check_knowledge_base_permission(kb, user, 'delete')
+# }
+
+# explicit_permission = KBPermissionModel.objects.filter(
+# knowledge_base_id=kb.id,
+# user=user,
+# status='active'
+# ).first()
+
+# expires_at = None
+# if explicit_permission:
+# expires_at = explicit_permission.expires_at.strftime("%Y-%m-%d %H:%M:%S") if explicit_permission.expires_at else None
+# elif kb.type == 'admin':
+# expires_at = None
+
+# summary = {
+# 'id': str(kb.id),
+# 'name': kb.name,
+# 'desc': kb.desc,
+# 'type': kb.type,
+# 'department': kb.department,
+# 'permissions': permissions,
+# 'expires_at': expires_at
+# }
+# summaries.append(summary)
+
+# return Response({
+# 'code': 200,
+# 'message': '获取知识库概要信息成功',
+# 'data': summaries
+# })
+
+# 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 retrieve(self, request, *args, **kwargs):
+# try:
+# instance = self.get_object()
+# serializer = self.get_serializer(instance)
+# data = serializer.data
+# user = request.user
+
+# data['permissions'] = {
+# 'can_read': self.check_knowledge_base_permission(instance, user, 'read'),
+# 'can_edit': self.check_knowledge_base_permission(instance, user, 'edit'),
+# 'can_delete': self.check_knowledge_base_permission(instance, user, 'delete')
+# }
+
+# explicit_permission = KBPermissionModel.objects.filter(
+# knowledge_base_id=instance.id,
+# user=user,
+# status='active'
+# ).first()
+
+# if explicit_permission:
+# data['expires_at'] = explicit_permission.expires_at.strftime("%Y-%m-%d %H:%M:%S") if explicit_permission.expires_at else None
+# else:
+# data['expires_at'] = None if instance.type == 'admin' else None
+
+# return Response({
+# 'code': 200,
+# 'message': '获取知识库详情成功',
+# 'data': data
+# })
+# 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)
+
+# @action(detail=False, methods=['get'])
+# def search(self, request):
+# try:
+# keyword = request.query_params.get('keyword', '')
+# if not keyword:
+# return Response({
+# "code": 400,
+# "message": "搜索关键字不能为空",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# try:
+# page = int(request.query_params.get('page', 1))
+# page_size = int(request.query_params.get('page_size', 10))
+# except ValueError:
+# page = 1
+# page_size = 10
+
+# query = Q(name__icontains=keyword) | Q(desc__icontains=keyword) | \
+# Q(department__icontains=keyword) | Q(group__icontains=keyword)
+
+# queryset = KnowledgeBase.objects.filter(query).exclude(type='secret')
+# user = request.user
+
+# active_permissions = KBPermissionModel.objects.filter(
+# user=user,
+# status='active',
+# expires_at__gt=timezone.now()
+# ).select_related('knowledge_base')
+
+# permission_map = {
+# str(perm.knowledge_base.id): {
+# 'can_read': perm.can_read,
+# 'can_edit': perm.can_edit,
+# 'can_delete': perm.can_delete
+# }
+# for perm in active_permissions
+# }
+
+# total = queryset.count()
+# start = (page - 1) * page_size
+# end = start + page_size
+# paginated_queryset = queryset[start:end]
+
+# serializer = self.get_serializer(paginated_queryset, many=True)
+# data = serializer.data
+# result_items = []
+
+# for item in data:
+# temp_kb = KnowledgeBase(
+# id=item['id'],
+# type=item['type'],
+# department=item.get('department'),
+# group=item.get('group'),
+# user_id=item.get('user_id')
+# )
+
+# explicit_permission = KBPermissionModel.objects.filter(
+# knowledge_base_id=item['id'],
+# user=user,
+# status='active'
+# ).first()
+
+# if explicit_permission:
+# kb_permissions = {
+# 'can_read': explicit_permission.can_read,
+# 'can_edit': explicit_permission.can_edit,
+# 'can_delete': explicit_permission.can_delete
+# }
+# item['expires_at'] = explicit_permission.expires_at.strftime("%Y-%m-%d %H:%M:%S") if explicit_permission.expires_at else None
+# else:
+# kb_permissions = {
+# 'can_read': self.check_knowledge_base_permission(temp_kb, user, 'read'),
+# 'can_edit': self.check_knowledge_base_permission(temp_kb, user, 'edit'),
+# 'can_delete': self.check_knowledge_base_permission(temp_kb, user, 'delete')
+# }
+# item['expires_at'] = None if item['type'] == 'admin' else None
+
+# item['permissions'] = kb_permissions
+
+# if kb_permissions['can_read']:
+# result_items.append(item)
+# else:
+# summary_info = {
+# 'id': item['id'],
+# 'name': item['name'],
+# 'type': item['type'],
+# 'department': item.get('department'),
+# 'permissions': kb_permissions
+# }
+# result_items.append(summary_info)
+
+# if 'name' in item and keyword.lower() in item['name'].lower():
+# item['highlighted_name'] = item['name'].replace(
+# keyword, f'{keyword}'
+# )
+# if 'desc' in item and item.get('desc') is not None:
+# desc_text = str(item['desc'])
+# if keyword.lower() in desc_text.lower():
+# item['highlighted_desc'] = desc_text.replace(
+# keyword, f'{keyword}'
+# )
+
+# return Response({
+# "code": 200,
+# "message": "搜索知识库成功",
+# "data": {
+# "total": total,
+# "page": page,
+# "page_size": page_size,
+# "keyword": keyword,
+# "items": result_items
+# }
+# })
+# 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)
+
+# @action(detail=True, methods=['post'])
+# def change_type(self, request, pk=None):
+# try:
+# instance = self.get_object()
+# user = request.user
+
+# if not self.check_knowledge_base_permission(instance, user, 'edit'):
+# return Response({
+# "code": 403,
+# "message": "没有修改权限",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# new_type = request.data.get('type')
+# if not new_type:
+# return Response({
+# "code": 400,
+# "message": "新类型不能为空",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# valid_types = ['private', 'admin', 'secret', 'leader', 'member']
+# if new_type not in valid_types:
+# return Response({
+# "code": 400,
+# "message": f"无效的知识库类型,可选值: {', '.join(valid_types)}",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# if new_type == 'leader' and not user.role == 'admin':
+# if new_type not in ['private', 'member']:
+# return Response({
+# "code": 403,
+# "message": "组长只能将知识库设置为private或member类型",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# department = request.data.get('department')
+# group = request.data.get('group')
+
+# if new_type == 'leader' and not user.role == 'admin':
+# if department and department != user.department:
+# return Response({
+# "code": 403,
+# "message": "组长只能为本部门设置知识库",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+# department = user.department
+
+# if new_type == 'leader':
+# if not department:
+# return Response({
+# "code": 400,
+# "message": "组长级知识库必须指定部门",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# if new_type == 'member':
+# if not department:
+# return Response({
+# "code": 400,
+# "message": "成员级知识库必须指定部门",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+# if not group:
+# return Response({
+# "code": 400,
+# "message": "成员级知识库必须指定组",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# if new_type in ['admin', 'secret']:
+# department = None
+# group = None
+
+# if new_type == 'private':
+# if department is None:
+# department = instance.department
+# if group is None:
+# group = instance.group
+
+# instance.type = new_type
+# instance.department = department
+# instance.group = group
+# instance.save()
+
+# return Response({
+# "code": 200,
+# "message": f"知识库类型已更新为{new_type}",
+# "data": {
+# "id": instance.id,
+# "name": instance.name,
+# "type": instance.type,
+# "department": instance.department,
+# "group": instance.group
+# }
+# })
+
+# except Http404:
+# return Response({
+# "code": 404,
+# "message": "知识库不存在",
+# "data": None
+# }, status=status.HTTP_404_NOT_FOUND)
+# 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)
+
+# @action(detail=True, methods=['post'])
+# def upload_document(self, request, pk=None):
+# try:
+# instance = self.get_object()
+# user = request.user
+
+# if not self.check_knowledge_base_permission(instance, user, 'edit'):
+# return Response({
+# "code": 403,
+# "message": "没有编辑权限",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# logger.info(f"请求内容: {request.data}")
+# logger.info(f"请求FILES: {request.FILES}")
+
+# files = []
+# if 'files' in request.FILES:
+# files = request.FILES.getlist('files')
+# elif 'file' in request.FILES:
+# files = request.FILES.getlist('file')
+# elif any(key.startswith('files[') for key in request.FILES):
+# files = [file for key, file in request.FILES.items() if key.startswith('files[')]
+# elif any(key.startswith('file[') for key in request.FILES):
+# files = [file for key, file in request.FILES.items() if key.startswith('file[')]
+# elif len(request.FILES) > 0:
+# files = list(request.FILES.values())
+
+# if not files:
+# return Response({
+# "code": 400,
+# "message": "未找到上传文件,请确保表单字段名为'files'或'file'",
+# "data": {
+# "available_fields": list(request.FILES.keys())
+# }
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# logger.info(f"接收到 {len(files)} 个文件上传请求")
+
+# saved_documents = []
+# failed_documents = []
+
+# if not instance.external_id:
+# return Response({
+# "code": 400,
+# "message": "知识库没有有效的external_id,请先创建知识库",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# try:
+# verify_url = f'{settings.API_BASE_URL}/api/dataset/{instance.external_id}'
+# verify_response = requests.get(verify_url)
+# if verify_response.status_code != 200:
+# logger.error(f"外部知识库不存在或无法访问: {instance.external_id}, 状态码: {verify_response.status_code}")
+# return Response({
+# "code": 404,
+# "message": f"外部知识库不存在或无法访问: {instance.external_id}",
+# "data": None
+# }, status=status.HTTP_404_NOT_FOUND)
+
+# verify_data = verify_response.json()
+# if verify_data.get('code') != 200:
+# logger.error(f"验证外部知识库失败: {verify_data.get('message')}")
+# return Response({
+# "code": verify_data.get('code', 500),
+# "message": f"验证外部知识库失败: {verify_data.get('message', '未知错误')}",
+# "data": None
+# }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+# logger.info(f"外部知识库验证成功: {instance.external_id}")
+# 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)
+
+# # 批量处理所有文件
+# split_response = call_split_api_multiple(files)
+# if not split_response or split_response.get('code') != 200:
+# error_msg = f"文件分割失败: {split_response.get('message', '未知错误') if split_response else '请求失败'}"
+# logger.error(error_msg)
+# return Response({
+# "code": 400,
+# "message": error_msg,
+# "data": {
+# "uploaded_count": 0,
+# "failed_count": len(files),
+# "total_files": len(files),
+# "documents": [],
+# "failed_documents": [{"name": file.name, "error": error_msg} for file in files]
+# }
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# # 处理分割结果
+# documents_data = split_response.get('data', [])
+# if not documents_data:
+# logger.warning(f"批量分割API未返回文档数据")
+# return Response({
+# "code": 400,
+# "message": "文件分割未返回有效数据",
+# "data": {
+# "uploaded_count": 0,
+# "failed_count": len(files),
+# "total_files": len(files),
+# "documents": [],
+# "failed_documents": [{"name": file.name, "error": "分割未返回有效数据"} for file in files]
+# }
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# logger.info(f"成功分割出 {len(documents_data)} 个文档,准备上传")
+
+# # 处理每个文档
+# for doc in documents_data:
+# doc_name = doc.get('name', '未命名文档')
+# doc_content = doc.get('content', [])
+
+# logger.info(f"处理文档: {doc_name}, 包含 {len(doc_content)} 个段落")
+
+# if not doc_content:
+# doc_content = [{
+# 'title': '文档内容',
+# 'content': '文件内容无法自动分割,请检查文件格式。'
+# }]
+
+# doc_data = {
+# "name": doc_name,
+# "paragraphs": []
+# }
+
+# for paragraph in doc_content:
+# doc_data["paragraphs"].append({
+# "content": paragraph.get('content', ''),
+# "title": paragraph.get('title', ''),
+# "is_active": True,
+# "problem_list": []
+# })
+
+# upload_response = call_upload_api(instance.external_id, doc_data)
+
+# if upload_response and upload_response.get('code') == 200 and upload_response.get('data'):
+# document_id = upload_response['data']['id']
+# doc_record = KnowledgeBaseDocument.objects.create(
+# knowledge_base=instance,
+# document_id=document_id,
+# document_name=doc_name,
+# external_id=document_id,
+# uploader_name=user.name
+# )
+
+# saved_documents.append({
+# "id": str(doc_record.id),
+# "name": doc_record.document_name,
+# "external_id": doc_record.external_id
+# })
+
+# logger.info(f"文档 '{doc_name}' 上传成功,ID: {document_id}")
+# else:
+# error_msg = upload_response.get('message', '未知错误') if upload_response else '上传API调用失败'
+# logger.error(f"文档 '{doc_name}' 上传失败: {error_msg}")
+# failed_documents.append({
+# "name": doc_name,
+# "error": error_msg
+# })
+
+# if saved_documents:
+# return Response({
+# "code": 200,
+# "message": f"文档上传完成,成功: {len(saved_documents)},失败: {len(failed_documents)}",
+# "data": {
+# "uploaded_count": len(saved_documents),
+# "failed_count": len(failed_documents),
+# "total_files": len(files),
+# "documents": saved_documents,
+# "failed_documents": failed_documents
+# }
+# })
+# else:
+# return Response({
+# "code": 400,
+# "message": f"所有文档上传失败",
+# "data": {
+# "uploaded_count": 0,
+# "failed_count": len(failed_documents),
+# "total_files": len(files),
+# "documents": [],
+# "failed_documents": failed_documents
+# }
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# 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)
+
+# @action(detail=True, methods=['get'])
+# def documents(self, request, pk=None):
+# """获取知识库的文档列表"""
+# try:
+# instance = self.get_object()
+# user = request.user
+
+# # 权限检查
+# if not self.check_knowledge_base_permission(instance, user, 'read'):
+# return Response({
+# "code": 403,
+# "message": "没有查看权限",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# # 检查external_id是否存在
+# if not instance.external_id:
+# return Response({
+# "code": 400,
+# "message": "知识库没有有效的external_id",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# # 调用外部API获取文档列表
+# try:
+# external_documents = get_external_document_list(instance.external_id)
+
+# # 同步外部文档到本地数据库
+# for doc in external_documents:
+# external_id = doc.get('id')
+# doc_name = doc.get('name')
+
+# if external_id and doc_name:
+# kb_doc, created = KnowledgeBaseDocument.objects.update_or_create(
+# knowledge_base=instance,
+# external_id=external_id,
+# defaults={
+# 'document_id': external_id,
+# 'document_name': doc_name,
+# 'status': 'active' if doc.get('is_active', True) else 'deleted'
+# }
+# )
+
+# if created:
+# logger.info(f"同步创建文档: {doc_name}, ID: {external_id}")
+# else:
+# logger.info(f"同步更新文档: {doc_name}, ID: {external_id}")
+
+# # 获取最新的本地文档数据
+# documents = KnowledgeBaseDocument.objects.filter(
+# knowledge_base=instance,
+# status='active'
+# ).order_by('-create_time')
+
+# # 构建响应数据
+# documents_data = [{
+# "id": str(doc.id),
+# "document_id": doc.document_id,
+# "name": doc.document_name,
+# "external_id": doc.external_id,
+# "created_at": doc.create_time.strftime('%Y-%m-%d %H:%M:%S'),
+# "char_length": next((d.get('char_length', 0) for d in external_documents if d.get('id') == doc.external_id), 0),
+# "paragraph_count": next((d.get('paragraph_count', 0) for d in external_documents if d.get('id') == doc.external_id), 0),
+# "is_active": next((d.get('is_active', True) for d in external_documents if d.get('id') == doc.external_id), True),
+# "uploader_name": doc.uploader_name
+# } for doc in documents]
+
+# return Response({
+# "code": 200,
+# "message": "获取文档列表成功",
+# "data": documents_data
+# })
+
+# except ExternalAPIError as e:
+# logger.error(f"获取文档列表失败: {str(e)}")
+# return Response({
+# "code": 500,
+# "message": str(e),
+# "data": None
+# }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+# 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)
+
+# @action(detail=True, methods=['get'])
+# def document_content(self, request, pk=None):
+# """获取文档内容 - 段落列表"""
+# try:
+# knowledge_base = self.get_object()
+# user = request.user
+
+# # 权限检查
+# if not self.check_knowledge_base_permission(knowledge_base, user, 'read'):
+# return Response({
+# "code": 403,
+# "message": "没有查看权限",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# # 获取文档ID
+# document_id = request.query_params.get('document_id')
+# if not document_id:
+# return Response({
+# "code": 400,
+# "message": "缺少document_id参数",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# # 验证文档存在
+# document = KnowledgeBaseDocument.objects.filter(
+# knowledge_base=knowledge_base,
+# document_id=document_id,
+# status='active'
+# ).first()
+
+# if not document:
+# return Response({
+# "code": 404,
+# "message": "文档不存在或已删除",
+# "data": None
+# }, status=status.HTTP_404_NOT_FOUND)
+
+# # 调用外部API获取文档段落内容
+# try:
+# paragraphs = get_external_document_paragraphs(knowledge_base.external_id, document.external_id)
+
+# # 直接返回外部API的段落数据
+# return Response({
+# "code": 200,
+# "message": "获取文档内容成功",
+# "data": {
+# "document_id": document_id,
+# "name": document.document_name,
+# "paragraphs": paragraphs
+# }
+# })
+
+# except ExternalAPIError as e:
+# logger.error(f"获取文档段落内容失败: {str(e)}")
+# return Response({
+# "code": 500,
+# "message": str(e),
+# "data": None
+# }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+# 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)
+
+# @action(detail=True, methods=['delete'])
+# def delete_document(self, request, pk=None):
+# """删除知识库文档"""
+# try:
+# knowledge_base = self.get_object()
+# user = request.user
+
+# # 权限检查
+# if not self.check_knowledge_base_permission(knowledge_base, user, 'edit'):
+# return Response({
+# "code": 403,
+# "message": "没有编辑权限",
+# "data": None
+# }, status=status.HTTP_403_FORBIDDEN)
+
+# # 获取文档ID
+# document_id = request.query_params.get('document_id')
+# if not document_id:
+# return Response({
+# "code": 400,
+# "message": "缺少document_id参数",
+# "data": None
+# }, status=status.HTTP_400_BAD_REQUEST)
+
+# # 验证文档存在
+# document = KnowledgeBaseDocument.objects.filter(
+# knowledge_base=knowledge_base,
+# document_id=document_id,
+# status='active'
+# ).first()
+
+# if not document:
+# return Response({
+# "code": 404,
+# "message": "文档不存在或已删除",
+# "data": None
+# }, status=status.HTTP_404_NOT_FOUND)
+
+# # 调用外部API删除文档
+# external_id = document.external_id
+# delete_result = call_delete_document_api(knowledge_base.external_id, external_id)
+
+# # 无论外部API结果如何,都更新本地状态
+# document.status = 'deleted'
+# document.save()
+
+# # 检查外部API结果
+# if delete_result.get('code') != 200:
+# logger.warning(f"外部API删除文档失败,但本地标记已更新: {delete_result.get('message')}")
+# return Response({
+# "code": 200,
+# "message": "文档在系统中已标记为删除,但外部API调用失败",
+# "data": {
+# "document_id": document_id,
+# "name": document.document_name,
+# "error": delete_result.get('message')
+# }
+# })
+
+# return Response({
+# "code": 200,
+# "message": "文档删除成功",
+# "data": {
+# "document_id": document_id,
+# "name": document.document_name
+# }
+# })
+
+# 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)
+
\ No newline at end of file