From 7898219d2cca2b941943d80e2dd4046dd3057201 Mon Sep 17 00:00:00 2001 From: wanjia Date: Thu, 29 May 2025 17:21:16 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BE=A4=E5=8F=91demo1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/brands/models.py | 4 - apps/chat/migrations/0001_initial.py | 24 +- .../0002_alter_chathistory_content.py | 18 + .../0003_alter_chathistory_content.py | 18 + ...emove_conversationsummary_user_and_more.py | 39 + apps/chat/views.py | 147 +- apps/common/services/chat_service.py | 3 +- apps/common/services/notification_service.py | 51 + apps/common/services/permission_service.py | 590 ++++---- apps/feishu/services/bitable_service.py | 146 +- apps/feishu/services/data_sync_service.py | 196 +-- apps/feishu/services/feishu_service.py | 1 + apps/feishu/views.py | 159 +-- apps/gmail/migrations/0001_initial.py | 122 +- .../0002_gmailcredential_last_history_id.py | 18 + .../0003_gmailconversation_metadata.py | 18 + .../migrations/0004_conversationsummary.py | 31 + apps/gmail/migrations/0005_usergoal.py | 34 + ...ion_time_usergoal_conversation_and_more.py | 39 + ...007_alter_gmailattachment_attachment_id.py | 18 + ...008_alter_gmailattachment_attachment_id.py | 18 + ...conversation_has_sent_greeting_and_more.py | 82 ++ ...rocessedpushnotification_unmatchedemail.py | 53 + .../0011_alter_unmatchedemail_user_id.py | 18 + .../migrations/0012_alter_usergoal_table.py | 17 + apps/gmail/services/gmail_service.py | 1 + apps/gmail/urls.py | 2 +- apps/gmail/views.py | 220 +-- .../knowledge_base/migrations/0001_initial.py | 8 +- ...dx_knowledge_b_type_7a8dcc_idx_and_more.py | 51 + apps/knowledge_base/models.py | 28 + apps/knowledge_base/urls.py | 10 + apps/knowledge_base/views.py | 1270 +++++++++++++++++ 33 files changed, 2425 insertions(+), 1029 deletions(-) create mode 100644 apps/chat/migrations/0002_alter_chathistory_content.py create mode 100644 apps/chat/migrations/0003_alter_chathistory_content.py create mode 100644 apps/chat/migrations/0004_remove_conversationsummary_user_and_more.py create mode 100644 apps/feishu/services/feishu_service.py create mode 100644 apps/gmail/migrations/0002_gmailcredential_last_history_id.py create mode 100644 apps/gmail/migrations/0003_gmailconversation_metadata.py create mode 100644 apps/gmail/migrations/0004_conversationsummary.py create mode 100644 apps/gmail/migrations/0005_usergoal.py create mode 100644 apps/gmail/migrations/0006_usergoal_completion_time_usergoal_conversation_and_more.py create mode 100644 apps/gmail/migrations/0007_alter_gmailattachment_attachment_id.py create mode 100644 apps/gmail/migrations/0008_alter_gmailattachment_attachment_id.py create mode 100644 apps/gmail/migrations/0009_gmailconversation_has_sent_greeting_and_more.py create mode 100644 apps/gmail/migrations/0010_processedpushnotification_unmatchedemail.py create mode 100644 apps/gmail/migrations/0011_alter_unmatchedemail_user_id.py create mode 100644 apps/gmail/migrations/0012_alter_usergoal_table.py create mode 100644 apps/knowledge_base/migrations/0002_rename_knowledge_b_type_0439e7_idx_knowledge_b_type_7a8dcc_idx_and_more.py 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