diff --git a/backend/django_mindmap/settings.py b/backend/django_mindmap/settings.py index b8dce01..5541cf7 100644 --- a/backend/django_mindmap/settings.py +++ b/backend/django_mindmap/settings.py @@ -103,6 +103,44 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # CORS settings CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] +CORS_ALLOW_METHODS = [ + 'DELETE', + 'GET', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', +] + +# REST Framework settings +REST_FRAMEWORK = { + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + ], + 'DEFAULT_PARSER_CLASSES': [ + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.MultiPartParser', + 'rest_framework.parsers.FormParser', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100, +} + +# File upload settings +MAX_UPLOAD_SIZE = 16 * 1024 * 1024 # 16MB +ALLOWED_FILE_TYPES = ['.docx', '.pdf', '.txt', '.md'] + # REST Framework settings REST_FRAMEWORK = { diff --git a/backend/mindmap.db b/backend/mindmap.db index 7f95f7a..fc6b5fb 100644 Binary files a/backend/mindmap.db and b/backend/mindmap.db differ diff --git a/backend/mindmap/ai_service.py b/backend/mindmap/ai_service.py index 8cc26de..84b2c80 100644 --- a/backend/mindmap/ai_service.py +++ b/backend/mindmap/ai_service.py @@ -2,7 +2,7 @@ import re import traceback import json -def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://open.bigmodel.cn/api/paas/v4/", api_key="ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn"): +def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://open.bigmodel.cn/api/paas/v4/", api_key="ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn", stream=False): """ 调用AI API生成Markdown """ @@ -12,7 +12,33 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o from openai import OpenAI except ImportError: print("OpenAI库未安装,返回模拟数据") - return f"""# {user_prompt} + if stream: + # 返回模拟流式数据 + def mock_stream(): + mock_content = f"""# {user_prompt} + +## 概述 +{user_prompt}是一个重要的概念和领域。 + +## 核心要素 +- 要素1 +- 要素2 +- 要素3 + +## 应用场景 +- 场景1 +- 场景2 +- 场景3 + +## 发展趋势 +- 趋势1 +- 趋势2 +- 趋势3""" + for char in mock_content: + yield char + return mock_stream() + else: + return f"""# {user_prompt} ## 概述 {user_prompt}是一个重要的概念和领域。 @@ -40,57 +66,76 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o print(f"发送AI API请求到: {base_url}") print(f"模型: {model}") + print(f"流式模式: {stream}") # 创建OpenAI客户端 client = OpenAI(api_key=api_key, base_url=base_url) - # 使用非流式调用,更简单可靠 + # 根据stream参数决定是否使用流式调用 try: response = client.chat.completions.create( model=model, messages=messages, temperature=0.7, max_tokens=8000, # 增加token限制,避免内容截断 - stream=False + stream=stream ) except Exception as e: print(f"API调用失败: {e}") # 如果API调用失败,抛出异常而不是返回模拟数据 raise Exception(f"AI API调用失败: {e}") - # 获取响应内容 - content = response.choices[0].message.content - print(f"AI原始响应: {content}") - - # 处理可能的JSON格式响应 - try: - # 尝试解析为JSON - json_data = json.loads(content) - if 'answer' in json_data: - content = json_data['answer'] - print(f"从JSON中提取answer: {content[:100]}...") - elif 'content' in json_data: - content = json_data['content'] - print(f"从JSON中提取content: {content[:100]}...") - elif 'markdown' in json_data: - content = json_data['markdown'] - print(f"从JSON中提取markdown: {content[:100]}...") - except json.JSONDecodeError: - # 不是JSON格式,直接使用内容 - print("响应不是JSON格式,直接使用内容") - - # 清理内容 - content = content.strip() - - # 如果返回的内容包含代码块标记,提取其中的内容 - markdown_match = re.search(r"```(?:markdown)?\n(.*?)```", content, re.DOTALL) - if markdown_match: - content = markdown_match.group(1).strip() - - # 如果内容为空,返回模拟数据 - if not content: - print("AI返回内容为空,使用模拟数据") - return f"""# {user_prompt} + if stream: + # 流式响应处理 + def generate_stream(): + full_content = "" + try: + for chunk in response: + if chunk.choices[0].delta.content is not None: + content_chunk = chunk.choices[0].delta.content + full_content += content_chunk + yield content_chunk + print(f"流式响应完成,总长度: {len(full_content)}") + except Exception as e: + print(f"流式响应处理失败: {e}") + raise e + return full_content + + return generate_stream() + else: + # 非流式响应处理 + content = response.choices[0].message.content + print(f"AI原始响应: {content}") + + # 处理可能的JSON格式响应 + try: + # 尝试解析为JSON + json_data = json.loads(content) + if 'answer' in json_data: + content = json_data['answer'] + print(f"从JSON中提取answer: {content[:100]}...") + elif 'content' in json_data: + content = json_data['content'] + print(f"从JSON中提取content: {content[:100]}...") + elif 'markdown' in json_data: + content = json_data['markdown'] + print(f"从JSON中提取markdown: {content[:100]}...") + except json.JSONDecodeError: + # 不是JSON格式,直接使用内容 + print("响应不是JSON格式,直接使用内容") + + # 清理内容 + content = content.strip() + + # 如果返回的内容包含代码块标记,提取其中的内容 + markdown_match = re.search(r"```(?:markdown)?\n(.*?)```", content, re.DOTALL) + if markdown_match: + content = markdown_match.group(1).strip() + + # 如果内容为空,返回模拟数据 + if not content: + print("AI返回内容为空,使用模拟数据") + return f"""# {user_prompt} ## 概述 {user_prompt}是一个重要的概念和领域。 @@ -109,8 +154,8 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o - 趋势1 - 趋势2 - 趋势3""" - - return content + + return content except Exception as e: print(f"AI API调用异常: {e}") diff --git a/backend/mindmap/urls.py b/backend/mindmap/urls.py index e2baa83..0a91384 100644 --- a/backend/mindmap/urls.py +++ b/backend/mindmap/urls.py @@ -11,4 +11,6 @@ urlpatterns = [ # AI接口 path('ai/generate-markdown', views_doc.generate_markdown), -] + path('ai/generate-stream', views_doc.generate_ai_content_stream), + path('ai/test-stream', views_doc.test_stream), # 添加测试端点 +] \ No newline at end of file diff --git a/backend/mindmap/views_doc.py b/backend/mindmap/views_doc.py index 7f93aaf..942112b 100644 --- a/backend/mindmap/views_doc.py +++ b/backend/mindmap/views_doc.py @@ -3,6 +3,8 @@ from rest_framework.response import Response from rest_framework import status from django.db import transaction from django.utils import timezone +from django.http import StreamingHttpResponse +import json from .models import mindMap, Node from .serializers import map_mindmap_to_doc, map_node_to_doc @@ -468,3 +470,141 @@ def generate_markdown(request): }, status=500) +@api_view(['POST', 'OPTIONS']) +def generate_ai_content_stream(request): + """ + 流式生成AI内容 + """ + # 处理OPTIONS请求(CORS预检请求) + if request.method == 'OPTIONS': + response = Response() + response['Access-Control-Allow-Origin'] = '*' + response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' + response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With' + response['Access-Control-Allow-Credentials'] = 'true' + response['Access-Control-Max-Age'] = '86400' + return response + + try: + data = request.data + user_prompt = data.get('user_prompt', '') + system_prompt = data.get('system_prompt', '你是一个专业的思维导图内容生成助手,请根据用户的需求生成结构化的Markdown内容。') + model = data.get('model', 'glm-4.5') + base_url = data.get('base_url', 'https://open.bigmodel.cn/api/paas/v4/') + api_key = data.get('api_key', '') + + if not user_prompt: + return Response({'error': '用户提示词不能为空'}, status=400) + + # 导入AI服务 + from .ai_service import call_ai_api + + def generate_stream(): + try: + print(f"开始调用流式AI API...") + # 调用流式AI API + stream = call_ai_api(system_prompt, user_prompt, model, base_url, api_key, stream=True) + + if stream is None: + print("AI API返回None,发送错误信号") + yield f"data: {json.dumps({'type': 'error', 'content': 'AI API调用失败'})}\n\n" + return + + print("开始发送流式数据...") + # 发送开始信号 + yield f"data: {json.dumps({'type': 'start', 'content': ''})}\n\n" + + # 发送流式内容 + chunk_count = 0 + for chunk in stream: + if chunk: + chunk_count += 1 + print(f"发送第{chunk_count}个数据块: {chunk[:50]}...") + yield f"data: {json.dumps({'type': 'chunk', 'content': chunk})}\n\n" + + print(f"流式数据发送完成,总共{chunk_count}个数据块") + # 发送结束信号 + yield f"data: {json.dumps({'type': 'end', 'content': ''})}\n\n" + + except Exception as e: + print(f"流式生成过程中发生错误: {e}") + import traceback + traceback.print_exc() + # 发送错误信号 + yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n" + + response = StreamingHttpResponse( + generate_stream(), + content_type='text/event-stream' + ) + + # 修复CORS配置,移除不允许的头部 + response['Cache-Control'] = 'no-cache' + response['Access-Control-Allow-Origin'] = '*' + response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' + response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With' + response['Access-Control-Allow-Credentials'] = 'true' + response['Access-Control-Max-Age'] = '86400' + + return response + + except Exception as e: + print(f"流式API处理过程中发生错误: {e}") + import traceback + traceback.print_exc() + return Response({ + 'error': str(e), + 'success': False + }, status=500) + + +@api_view(['POST', 'OPTIONS']) +def test_stream(request): + """ + 测试流式响应 + """ + # 处理OPTIONS请求(CORS预检请求) + if request.method == 'OPTIONS': + response = Response() + response['Access-Control-Allow-Origin'] = '*' + response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' + response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With' + response['Access-Control-Allow-Credentials'] = 'true' + response['Access-Control-Max-Age'] = '86400' + return response + + def generate_test_stream(): + try: + # 发送开始信号 + yield f"data: {json.dumps({'type': 'start', 'content': ''})}\n\n" + + # 发送测试数据 + test_content = "# 测试思维导图\n\n## 主要主题\n- 主题1\n- 主题2\n\n## 详细内容\n- 内容1\n- 内容2" + for i, char in enumerate(test_content): + yield f"data: {json.dumps({'type': 'chunk', 'content': char})}\n\n" + # 添加小延迟模拟流式效果 + import time + time.sleep(0.01) + + # 发送结束信号 + yield f"data: {json.dumps({'type': 'end', 'content': ''})}\n\n" + + except Exception as e: + yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n" + + response = StreamingHttpResponse( + generate_test_stream(), + content_type='text/event-stream' + ) + + # 修复CORS配置,移除不允许的头部 + response['Cache-Control'] = 'no-cache' + response['Access-Control-Allow-Origin'] = '*' + response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' + response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Cache-Control, X-Requested-With' + response['Access-Control-Allow-Credentials'] = 'true' + response['Access-Control-Max-Age'] = '86400' + + return response + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e5ee3da..4557a84 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,7 +1,7 @@