处理流式输出
This commit is contained in:
parent
cc2c647044
commit
113f13d501
0
apps/chat/__init__.py
Normal file
0
apps/chat/__init__.py
Normal file
3
apps/chat/admin.py
Normal file
3
apps/chat/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
apps/chat/apps.py
Normal file
6
apps/chat/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ChatConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.chat'
|
419
apps/chat/chat.md
Normal file
419
apps/chat/chat.md
Normal file
@ -0,0 +1,419 @@
|
||||
# Chat 模块接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
Chat 模块提供了一组 API,用于创建和管理对话。这包括创建新对话、发送消息、接收回答、查询历史记录等功能。所有接口都基于 REST 风格设计。
|
||||
|
||||
## 认证
|
||||
|
||||
除特殊说明外,所有接口都需要认证。请在请求头中添加认证信息:
|
||||
|
||||
```
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```
|
||||
Authorization: Token <token>
|
||||
```
|
||||
|
||||
## REST API接口列表
|
||||
|
||||
### 1. 创建会话
|
||||
|
||||
创建一个新的对话会话。
|
||||
|
||||
- **URL**: `/api/chat-history/create_conversation/`
|
||||
- **方法**: `POST`
|
||||
- **认证**: 不需要认证
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "会话创建成功",
|
||||
"data": {
|
||||
"conversation_id": "uuid-string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 发送消息并接收回答
|
||||
|
||||
发送用户问题并获取 AI 回答。
|
||||
|
||||
- **URL**: `/api/chat-history/`
|
||||
- **方法**: `POST`
|
||||
- **参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|-------|------|------|------|
|
||||
| conversation_id | string | 是 | 会话 ID |
|
||||
| question | string | 是 | 用户问题 |
|
||||
| title | string | 否 | 会话标题,默认为 "New chat" |
|
||||
| stream | boolean | 否 | 是否使用流式响应,默认为 true |
|
||||
|
||||
- **响应** (非流式):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "记录ID",
|
||||
"conversation_id": "会话ID",
|
||||
"title": "会话标题",
|
||||
"role": "assistant",
|
||||
"content": "AI回答内容",
|
||||
"created_at": "创建时间"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **响应** (流式):
|
||||
|
||||
返回 `text/event-stream` 类型的响应,包含多个 data 事件:
|
||||
|
||||
```
|
||||
data: {"code": 200, "message": "开始流式传输", "data": {"id": "记录ID", "conversation_id": "会话ID", "content": "", "is_end": false}}
|
||||
|
||||
data: {"code": 200, "message": "partial", "data": {"id": "记录ID", "conversation_id": "会话ID", "title": "标题", "content": "部分内容", "is_end": false}}
|
||||
|
||||
...
|
||||
|
||||
data: {"code": 200, "message": "完成", "data": {"id": "记录ID", "conversation_id": "会话ID", "title": "标题", "role": "assistant", "content": "完整内容", "created_at": "时间", "is_end": true}}
|
||||
```
|
||||
|
||||
### 3. 获取对话列表
|
||||
|
||||
获取用户的所有对话列表。
|
||||
|
||||
- **URL**: `/api/chat-history/`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|-------|------|------|------|
|
||||
| page | integer | 否 | 页码,默认为 1 |
|
||||
| page_size | integer | 否 | 每页条数,默认为 10 |
|
||||
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功",
|
||||
"data": {
|
||||
"total": 总记录数,
|
||||
"page": 当前页码,
|
||||
"page_size": 每页条数,
|
||||
"results": [
|
||||
{
|
||||
"conversation_id": "会话ID",
|
||||
"message_count": 消息数量,
|
||||
"last_message": "最后一条消息内容",
|
||||
"last_time": "最后更新时间"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 获取对话详情
|
||||
|
||||
获取特定对话的所有消息。
|
||||
|
||||
- **URL**: `/api/chat-history/conversation_detail/`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|-------|------|------|------|
|
||||
| conversation_id | string | 是 | 会话 ID |
|
||||
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功",
|
||||
"data": {
|
||||
"conversation_id": "会话ID",
|
||||
"title": "会话标题",
|
||||
"created_at": "创建时间",
|
||||
"messages": [
|
||||
{
|
||||
"id": "消息ID",
|
||||
"role": "用户角色(user/assistant)",
|
||||
"content": "消息内容",
|
||||
"created_at": "创建时间"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 搜索聊天记录
|
||||
|
||||
根据关键词搜索聊天记录。
|
||||
|
||||
- **URL**: `/api/chat-history/search/`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|-------|------|------|------|
|
||||
| keyword | string | 否 | 搜索关键词 |
|
||||
| start_date | string | 否 | 开始日期,格式为 YYYY-MM-DD |
|
||||
| end_date | string | 否 | 结束日期,格式为 YYYY-MM-DD |
|
||||
| page | integer | 否 | 页码,默认为 1 |
|
||||
| page_size | integer | 否 | 每页条数,默认为 10 |
|
||||
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "搜索成功",
|
||||
"data": {
|
||||
"total": 总记录数,
|
||||
"page": 当前页码,
|
||||
"page_size": 每页条数,
|
||||
"results": [
|
||||
{
|
||||
"id": "记录ID",
|
||||
"conversation_id": "会话ID",
|
||||
"role": "角色",
|
||||
"content": "内容",
|
||||
"created_at": "创建时间",
|
||||
"metadata": {},
|
||||
"highlights": {
|
||||
"content": "高亮后的内容"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 删除会话
|
||||
|
||||
删除整个会话及其所有消息。
|
||||
|
||||
- **URL**: `/api/chat-history/delete_conversation/`
|
||||
- **方法**: `DELETE`
|
||||
- **参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|-------|------|------|------|
|
||||
| conversation_id | string | 是 | 要删除的会话 ID |
|
||||
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "删除成功",
|
||||
"data": {
|
||||
"conversation_id": "会话ID",
|
||||
"deleted_count": 删除的消息数量
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 更新消息内容
|
||||
|
||||
更新单条消息的内容。
|
||||
|
||||
- **URL**: `/api/chat-history/{id}/`
|
||||
- **方法**: `PUT` 或 `PATCH`
|
||||
- **参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|-------|------|------|------|
|
||||
| content | string | 否 | 更新的消息内容 |
|
||||
| metadata | object | 否 | 更新的元数据 |
|
||||
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "更新成功",
|
||||
"data": {
|
||||
"id": "记录ID",
|
||||
"conversation_id": "会话ID",
|
||||
"role": "角色",
|
||||
"content": "更新后的内容",
|
||||
"metadata": {},
|
||||
"updated_at": "更新时间"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. 删除单条消息
|
||||
|
||||
删除单条消息(软删除)。
|
||||
|
||||
- **URL**: `/api/chat-history/{id}/`
|
||||
- **方法**: `DELETE`
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "删除成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 9. 更新会话标题
|
||||
|
||||
设置或更新会话的标题。
|
||||
|
||||
- **URL**: `/api/chat-history/generate-conversation-title/`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|-------|------|------|------|
|
||||
| conversation_id | string | 是 | 会话 ID |
|
||||
| title | string | 是 | 新标题 |
|
||||
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "更新会话标题成功",
|
||||
"data": {
|
||||
"conversation_id": "会话ID",
|
||||
"title": "新标题"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10. 获取可用知识库
|
||||
|
||||
获取系统中可用的知识库列表。
|
||||
|
||||
- **URL**: `/api/chat-history/available_datasets/`
|
||||
- **方法**: `GET`
|
||||
- **响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket API
|
||||
|
||||
除了REST API之外,Chat模块还提供了WebSocket接口,用于实时流式获取对话回答。
|
||||
|
||||
### 1. 建立WebSocket连接
|
||||
|
||||
- **URL**: `ws://yourdomain:port/ws/chat/stream/?token=<your_token>`
|
||||
- **认证**: 需要在URL参数中提供token
|
||||
- **说明**: token是用户的认证token,即登录接口获取的token,通过UserToken模型验证
|
||||
- **获取token**: 通过API登录接口获取token:
|
||||
```
|
||||
POST /api/user/login/
|
||||
{
|
||||
"email": "用户邮箱",
|
||||
"password": "用户密码"
|
||||
}
|
||||
```
|
||||
成功登录后,响应将返回token字段值
|
||||
|
||||
### 2. 发送消息
|
||||
|
||||
连接建立后,可以向WebSocket发送JSON格式的消息:
|
||||
|
||||
```json
|
||||
{
|
||||
"conversation_id": "会话ID",
|
||||
"question": "用户问题"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 接收响应
|
||||
|
||||
WebSocket会返回以下JSON格式的消息:
|
||||
|
||||
1. 开始流式传输:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "开始流式传输",
|
||||
"data": {
|
||||
"id": "记录ID",
|
||||
"conversation_id": "会话ID",
|
||||
"content": "",
|
||||
"is_end": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 部分内容:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "partial",
|
||||
"data": {
|
||||
"id": "记录ID",
|
||||
"conversation_id": "会话ID",
|
||||
"content": "部分内容",
|
||||
"is_end": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 完成响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "完成",
|
||||
"data": {
|
||||
"id": "记录ID",
|
||||
"conversation_id": "会话ID",
|
||||
"title": "会话标题",
|
||||
"role": "assistant",
|
||||
"content": "完整内容",
|
||||
"created_at": "时间",
|
||||
"is_end": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. 错误响应:
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "错误信息",
|
||||
"data": {
|
||||
"is_end": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误响应
|
||||
|
||||
所有接口在出错时都会返回类似的错误响应结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 错误代码,
|
||||
"message": "错误描述",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
常见错误代码:
|
||||
- 400: 请求参数错误
|
||||
- 401: 未认证或认证失败
|
||||
- 404: 资源不存在
|
||||
- 500: 服务器内部错误
|
384
apps/chat/consumers.py
Normal file
384
apps/chat/consumers.py
Normal file
@ -0,0 +1,384 @@
|
||||
# apps/chat/consumers.py
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
import json
|
||||
from channels.db import database_sync_to_async
|
||||
from apps.chat.models import ChatHistory
|
||||
from apps.user.models import UserToken
|
||||
from django.conf import settings
|
||||
import logging
|
||||
import traceback
|
||||
import uuid
|
||||
import aiohttp
|
||||
from urllib.parse import parse_qs
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ChatStreamConsumer(AsyncWebsocketConsumer):
|
||||
# 固定知识库ID
|
||||
DEFAULT_KNOWLEDGE_BASE_ID = "b680a4fa-37be-11f0-a7cb-0242ac120002"
|
||||
|
||||
async def connect(self):
|
||||
"""建立WebSocket连接"""
|
||||
try:
|
||||
# 从URL参数中获取token
|
||||
query_string = self.scope.get('query_string', b'').decode()
|
||||
query_params = parse_qs(query_string)
|
||||
token_key = query_params.get('token', [''])[0]
|
||||
|
||||
if not token_key:
|
||||
logger.warning("WebSocket连接尝试,但没有提供token")
|
||||
await self.close()
|
||||
return
|
||||
|
||||
# 验证token
|
||||
self.user = await self.get_user_from_token(token_key)
|
||||
if not self.user:
|
||||
logger.warning(f"WebSocket连接尝试,但token无效: {token_key}")
|
||||
await self.close()
|
||||
return
|
||||
|
||||
# 将用户信息存储在scope中
|
||||
self.scope["user"] = self.user
|
||||
await self.accept()
|
||||
logger.info(f"用户 {self.user.email} 流式输出WebSocket连接成功")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket连接错误: {str(e)}")
|
||||
await self.close()
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user_from_token(self, token_key):
|
||||
try:
|
||||
# 使用项目的UserToken模型而不是rest_framework的Token
|
||||
token = UserToken.objects.select_related('user').get(
|
||||
token=token_key,
|
||||
expired_at__gt=timezone.now() # 确保token未过期
|
||||
)
|
||||
return token.user
|
||||
except UserToken.DoesNotExist:
|
||||
return None
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
"""关闭WebSocket连接"""
|
||||
logger.info(f"用户 {self.user.email if hasattr(self, 'user') else 'unknown'} WebSocket连接断开,代码: {close_code}")
|
||||
|
||||
async def receive(self, text_data):
|
||||
"""接收消息并处理"""
|
||||
try:
|
||||
data = json.loads(text_data)
|
||||
|
||||
# 检查必填字段
|
||||
if 'question' not in data:
|
||||
await self.send_error("缺少必填字段: question")
|
||||
return
|
||||
|
||||
if 'conversation_id' not in data:
|
||||
await self.send_error("缺少必填字段: conversation_id")
|
||||
return
|
||||
|
||||
# 处理新会话或现有会话
|
||||
await self.process_chat_request(data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理消息时出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
await self.send_error(f"处理消息时出错: {str(e)}")
|
||||
|
||||
async def process_chat_request(self, data):
|
||||
"""处理聊天请求"""
|
||||
try:
|
||||
conversation_id = data['conversation_id']
|
||||
question = data['question']
|
||||
|
||||
# 准备metadata
|
||||
metadata = {}
|
||||
|
||||
# 创建问题记录
|
||||
question_record = await self.create_question_record(
|
||||
conversation_id,
|
||||
question,
|
||||
metadata
|
||||
)
|
||||
|
||||
if not question_record:
|
||||
return
|
||||
|
||||
# 创建AI回答记录
|
||||
answer_record = await self.create_answer_record(
|
||||
conversation_id,
|
||||
question_record,
|
||||
metadata
|
||||
)
|
||||
|
||||
# 发送初始响应
|
||||
await self.send_json({
|
||||
'code': 200,
|
||||
'message': '开始流式传输',
|
||||
'data': {
|
||||
'id': str(answer_record.id),
|
||||
'conversation_id': str(conversation_id),
|
||||
'content': '',
|
||||
'is_end': False
|
||||
}
|
||||
})
|
||||
|
||||
# 设置外部API需要的ID列表 - 简化为空列表
|
||||
dataset_external_id_list = []
|
||||
|
||||
# 调用外部API获取流式响应
|
||||
await self.stream_from_external_api(
|
||||
conversation_id,
|
||||
question,
|
||||
dataset_external_id_list,
|
||||
answer_record,
|
||||
metadata
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理聊天请求时出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
await self.send_error(f"处理聊天请求时出错: {str(e)}")
|
||||
|
||||
@database_sync_to_async
|
||||
def create_question_record(self, conversation_id, question, metadata):
|
||||
"""创建问题记录"""
|
||||
try:
|
||||
title = "New chat"
|
||||
|
||||
# 创建用户问题记录
|
||||
return ChatHistory.objects.create(
|
||||
user=self.scope["user"],
|
||||
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
|
||||
conversation_id=str(conversation_id),
|
||||
title=title,
|
||||
role='user',
|
||||
content=question,
|
||||
metadata=metadata
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"创建问题记录时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def create_answer_record(self, conversation_id, question_record, metadata):
|
||||
"""创建AI回答记录"""
|
||||
try:
|
||||
return ChatHistory.objects.create(
|
||||
user=self.scope["user"],
|
||||
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
|
||||
conversation_id=str(conversation_id),
|
||||
title=question_record.title,
|
||||
parent_id=str(question_record.id),
|
||||
role='assistant',
|
||||
content="", # 初始内容为空
|
||||
metadata=metadata
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"创建回答记录时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
async def stream_from_external_api(self, conversation_id, question, dataset_external_id_list, answer_record, metadata):
|
||||
"""从外部API获取流式响应"""
|
||||
try:
|
||||
# 获取标题
|
||||
title = answer_record.title or 'New chat'
|
||||
|
||||
# 异步收集完整内容,用于最后保存
|
||||
full_content = ""
|
||||
|
||||
# 使用aiohttp进行异步HTTP请求
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 第一步: 创建聊天会话
|
||||
async with session.post(
|
||||
f"{settings.API_BASE_URL}/api/application/chat/open",
|
||||
json={
|
||||
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
|
||||
"model_id": metadata.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'),
|
||||
"dataset_id_list": dataset_external_id_list,
|
||||
"multiple_rounds_dialogue": False,
|
||||
"dataset_setting": {
|
||||
"top_n": 10, "similarity": "0.3",
|
||||
"max_paragraph_char_number": 10000,
|
||||
"search_mode": "blend",
|
||||
"no_references_setting": {
|
||||
"value": "{question}",
|
||||
"status": "ai_questioning"
|
||||
}
|
||||
},
|
||||
"model_setting": {
|
||||
"prompt": "**相关文档内容**:{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**:{question}"
|
||||
},
|
||||
"problem_optimization": False
|
||||
}
|
||||
) as chat_response:
|
||||
|
||||
if chat_response.status != 200:
|
||||
error_msg = f"外部API调用失败: {await chat_response.text()}"
|
||||
logger.error(error_msg)
|
||||
await self.send_error(error_msg)
|
||||
return
|
||||
|
||||
chat_data = await chat_response.json()
|
||||
if chat_data.get('code') != 200 or not chat_data.get('data'):
|
||||
error_msg = f"外部API返回错误: {chat_data}"
|
||||
logger.error(error_msg)
|
||||
await self.send_error(error_msg)
|
||||
return
|
||||
|
||||
chat_id = chat_data['data']
|
||||
logger.info(f"成功创建聊天会话, chat_id: {chat_id}")
|
||||
|
||||
# 第二步: 建立流式连接
|
||||
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
|
||||
logger.info(f"开始流式请求: {message_url}")
|
||||
|
||||
# 创建流式请求
|
||||
async with session.post(
|
||||
url=message_url,
|
||||
json={"message": question, "re_chat": False, "stream": True},
|
||||
headers={"Content-Type": "application/json"}
|
||||
) as message_request:
|
||||
|
||||
if message_request.status != 200:
|
||||
error_msg = f"外部API聊天消息调用失败: {message_request.status}, {await message_request.text()}"
|
||||
logger.error(error_msg)
|
||||
await self.send_error(error_msg)
|
||||
return
|
||||
|
||||
# 创建一个缓冲区以处理分段的数据
|
||||
buffer = ""
|
||||
|
||||
# 读取并处理每个响应块
|
||||
logger.info("开始处理流式响应")
|
||||
async for chunk in message_request.content.iter_any():
|
||||
chunk_str = chunk.decode('utf-8')
|
||||
buffer += chunk_str
|
||||
|
||||
# 检查是否有完整的数据行
|
||||
while '\n\n' in buffer:
|
||||
parts = buffer.split('\n\n', 1)
|
||||
line = parts[0]
|
||||
buffer = parts[1]
|
||||
|
||||
if line.startswith('data: '):
|
||||
try:
|
||||
# 提取JSON数据
|
||||
json_str = line[6:] # 去掉 "data: " 前缀
|
||||
data = json.loads(json_str)
|
||||
|
||||
# 记录并处理部分响应
|
||||
if 'content' in data:
|
||||
content_part = data['content']
|
||||
full_content += content_part
|
||||
|
||||
# 发送部分内容
|
||||
await self.send_json({
|
||||
'code': 200,
|
||||
'message': 'partial',
|
||||
'data': {
|
||||
'id': str(answer_record.id),
|
||||
'conversation_id': str(conversation_id),
|
||||
'content': content_part,
|
||||
'is_end': data.get('is_end', False)
|
||||
}
|
||||
})
|
||||
|
||||
# 处理结束标记
|
||||
if data.get('is_end', False):
|
||||
logger.info("收到流式响应结束标记")
|
||||
# 保存完整内容
|
||||
await self.update_answer_content(answer_record.id, full_content.strip())
|
||||
|
||||
# 处理标题
|
||||
title = await self.get_or_generate_title(
|
||||
conversation_id,
|
||||
question,
|
||||
full_content.strip()
|
||||
)
|
||||
|
||||
# 发送最终响应
|
||||
await self.send_json({
|
||||
'code': 200,
|
||||
'message': '完成',
|
||||
'data': {
|
||||
'id': str(answer_record.id),
|
||||
'conversation_id': str(conversation_id),
|
||||
'title': title,
|
||||
'role': 'assistant',
|
||||
'content': full_content.strip(),
|
||||
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'is_end': True
|
||||
}
|
||||
})
|
||||
return
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {e}, 数据: {line}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"流式处理出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
await self.send_error(str(e))
|
||||
|
||||
# 保存已收集的内容
|
||||
if 'full_content' in locals() and full_content:
|
||||
try:
|
||||
await self.update_answer_content(answer_record.id, full_content.strip())
|
||||
except Exception as save_error:
|
||||
logger.error(f"保存部分内容失败: {str(save_error)}")
|
||||
|
||||
@database_sync_to_async
|
||||
def update_answer_content(self, answer_id, content):
|
||||
"""更新回答内容"""
|
||||
try:
|
||||
answer_record = ChatHistory.objects.get(id=answer_id)
|
||||
answer_record.content = content
|
||||
answer_record.save()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"更新回答内容失败: {str(e)}")
|
||||
return False
|
||||
|
||||
@database_sync_to_async
|
||||
def get_or_generate_title(self, conversation_id, question, answer):
|
||||
"""获取或生成对话标题"""
|
||||
try:
|
||||
# 先检查是否已有标题
|
||||
current_title = ChatHistory.objects.filter(
|
||||
conversation_id=str(conversation_id)
|
||||
).exclude(
|
||||
title__in=["New chat", "新对话", ""]
|
||||
).values_list('title', flat=True).first()
|
||||
|
||||
if current_title:
|
||||
return current_title
|
||||
|
||||
# 简单的标题生成逻辑 (可替换为调用DeepSeek API生成标题)
|
||||
generated_title = question[:20] + "..." if len(question) > 20 else question
|
||||
|
||||
# 更新所有相关记录的标题
|
||||
ChatHistory.objects.filter(
|
||||
conversation_id=str(conversation_id)
|
||||
).update(title=generated_title)
|
||||
|
||||
return generated_title
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取或生成标题失败: {str(e)}")
|
||||
return "新对话"
|
||||
|
||||
async def send_json(self, content):
|
||||
"""发送JSON格式的消息"""
|
||||
await self.send(text_data=json.dumps(content))
|
||||
|
||||
async def send_error(self, message):
|
||||
"""发送错误消息"""
|
||||
await self.send_json({
|
||||
'code': 500,
|
||||
'message': message,
|
||||
'data': {'is_end': True}
|
||||
})
|
||||
|
||||
|
38
apps/chat/migrations/0001_initial.py
Normal file
38
apps/chat/migrations/0001_initial.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.1 on 2025-05-23 10:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChatHistory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('knowledge_base_id', models.CharField(default='b680a4fa-37be-11f0-a7cb-0242ac120002', help_text='知识库ID,用于兼容原有结构', max_length=100)),
|
||||
('conversation_id', models.CharField(db_index=True, max_length=100)),
|
||||
('title', models.CharField(blank=True, default='New chat', help_text='对话标题', max_length=100, null=True)),
|
||||
('parent_id', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('role', models.CharField(choices=[('user', '用户'), ('assistant', 'AI助手'), ('system', '系统')], max_length=20)),
|
||||
('content', models.TextField()),
|
||||
('tokens', models.IntegerField(default=0, help_text='消息token数')),
|
||||
('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'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')],
|
||||
},
|
||||
),
|
||||
]
|
0
apps/chat/migrations/__init__.py
Normal file
0
apps/chat/migrations/__init__.py
Normal file
73
apps/chat/models.py
Normal file
73
apps/chat/models.py
Normal file
@ -0,0 +1,73 @@
|
||||
# apps/chat/models.py
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
import uuid
|
||||
from itertools import count
|
||||
from apps.user.models import User
|
||||
|
||||
class ChatHistory(models.Model):
|
||||
"""聊天历史记录"""
|
||||
ROLE_CHOICES = [
|
||||
('user', '用户'),
|
||||
('assistant', 'AI助手'),
|
||||
('system', '系统')
|
||||
]
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
# 改为使用字符串字段而非外键关联
|
||||
knowledge_base_id = models.CharField(max_length=100, default="b680a4fa-37be-11f0-a7cb-0242ac120002", help_text="知识库ID,用于兼容原有结构")
|
||||
# 用于标识知识库组合的对话
|
||||
conversation_id = models.CharField(max_length=100, db_index=True)
|
||||
# 对话标题
|
||||
title = models.CharField(max_length=100, null=True, blank=True, default='New chat', help_text="对话标题")
|
||||
parent_id = models.CharField(max_length=100, null=True, blank=True)
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
||||
content = models.TextField()
|
||||
tokens = models.IntegerField(default=0, help_text="消息token数")
|
||||
# 扩展metadata字段
|
||||
metadata = models.JSONField(default=dict, blank=True, help_text="存储额外信息")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['conversation_id', 'created_at']),
|
||||
models.Index(fields=['user', 'created_at']),
|
||||
models.Index(fields=['conversation_id', 'is_deleted']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email} - {self.title} - {self.created_at}"
|
||||
|
||||
@classmethod
|
||||
def get_conversation(cls, conversation_id):
|
||||
"""获取完整对话历史"""
|
||||
return cls.objects.filter(
|
||||
conversation_id=conversation_id,
|
||||
is_deleted=False
|
||||
).order_by('created_at')
|
||||
|
||||
@classmethod
|
||||
def get_conversations_by_user(cls, user):
|
||||
"""根据用户获取对话历史"""
|
||||
return cls.objects.filter(
|
||||
user=user,
|
||||
is_deleted=False
|
||||
).order_by('created_at')
|
||||
|
||||
def soft_delete(self):
|
||||
"""软删除消息"""
|
||||
self.is_deleted = True
|
||||
self.save()
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典格式"""
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'conversation_id': self.conversation_id,
|
||||
'role': self.role,
|
||||
'content': self.content,
|
||||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'metadata': self.metadata
|
||||
}
|
7
apps/chat/routing.py
Normal file
7
apps/chat/routing.py
Normal file
@ -0,0 +1,7 @@
|
||||
# apps/chat/routing.py
|
||||
from django.urls import re_path
|
||||
from apps.chat.consumers import ChatStreamConsumer
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/stream/$', ChatStreamConsumer.as_asgi()),
|
||||
]
|
18
apps/chat/serializers.py
Normal file
18
apps/chat/serializers.py
Normal file
@ -0,0 +1,18 @@
|
||||
# apps/chat/serializers.py
|
||||
from rest_framework import serializers
|
||||
from apps.chat.models import ChatHistory
|
||||
|
||||
class ChatHistorySerializer(serializers.ModelSerializer):
|
||||
knowledge_base_id = serializers.UUIDField(source='knowledge_base.id', read_only=True)
|
||||
dataset_name = serializers.CharField(source='knowledge_base.name', read_only=True)
|
||||
user_id = serializers.UUIDField(source='user.id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ChatHistory
|
||||
fields = [
|
||||
'id', 'user_id', 'knowledge_base_id', 'dataset_name', 'conversation_id',
|
||||
'title', 'role', 'content', 'parent_id', 'metadata', 'created_at', 'is_deleted'
|
||||
]
|
||||
read_only_fields = ['id', 'user_id', 'knowledge_base_id', 'dataset_name', 'created_at', 'is_deleted']
|
||||
|
||||
|
0
apps/chat/services/__init__.py
Normal file
0
apps/chat/services/__init__.py
Normal file
258
apps/chat/services/chat_api.py
Normal file
258
apps/chat/services/chat_api.py
Normal file
@ -0,0 +1,258 @@
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from rest_framework.exceptions import APIException
|
||||
from daren import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ExternalAPIError(APIException):
|
||||
status_code = 500
|
||||
default_detail = '外部API调用失败'
|
||||
default_code = 'external_api_error'
|
||||
|
||||
def stream_chat_answer(conversation_id, question, dataset_external_ids, metadata):
|
||||
"""流式调用外部聊天API,返回生成器以实时传输回答"""
|
||||
try:
|
||||
# 构造聊天请求数据
|
||||
chat_request_data = {
|
||||
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
|
||||
"model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006",
|
||||
"dataset_id_list": [str(id) for id in dataset_external_ids],
|
||||
"multiple_rounds_dialogue": False,
|
||||
"dataset_setting": {
|
||||
"top_n": 10, "similarity": "0.3",
|
||||
"max_paragraph_char_number": 10000,
|
||||
"search_mode": "blend",
|
||||
"no_references_setting": {
|
||||
"value": "{question}",
|
||||
"status": "ai_questioning"
|
||||
}
|
||||
},
|
||||
"model_setting": {
|
||||
"prompt": "**相关文档内容**:{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**:{question}"
|
||||
},
|
||||
"problem_optimization": False
|
||||
}
|
||||
|
||||
# 发起聊天请求
|
||||
logger.info(f"调用流式聊天API: {settings.API_BASE_URL}/api/application/chat/open")
|
||||
chat_response = requests.post(
|
||||
url=f"{settings.API_BASE_URL}/api/application/chat/open",
|
||||
json=chat_request_data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
logger.info(f"聊天API响应状态码: {chat_response.status_code}")
|
||||
if chat_response.status_code != 200:
|
||||
error_msg = f"聊天API调用失败: {chat_response.text}"
|
||||
logger.error(error_msg)
|
||||
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
|
||||
return
|
||||
|
||||
chat_data = chat_response.json()
|
||||
if chat_data.get('code') != 200 or not chat_data.get('data'):
|
||||
error_msg = f"聊天API返回错误: {chat_data}"
|
||||
logger.error(error_msg)
|
||||
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
|
||||
return
|
||||
|
||||
chat_id = chat_data['data']
|
||||
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
|
||||
logger.info(f"调用消息API: {message_url}")
|
||||
|
||||
# 发起消息请求(流式)
|
||||
message_request = requests.post(
|
||||
url=message_url,
|
||||
json={"message": question, "re_chat": False, "stream": True},
|
||||
headers={"Content-Type": "application/json"},
|
||||
stream=True
|
||||
)
|
||||
|
||||
if message_request.status_code != 200:
|
||||
error_msg = f"消息API调用失败: {message_request.status_code}, {message_request.text}"
|
||||
logger.error(error_msg)
|
||||
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
|
||||
return
|
||||
|
||||
buffer = ""
|
||||
for chunk in message_request.iter_content(chunk_size=1):
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
chunk_str = chunk.decode('utf-8')
|
||||
buffer += chunk_str
|
||||
|
||||
if '\n\n' in buffer:
|
||||
lines = buffer.split('\n\n')
|
||||
for line in lines[:-1]:
|
||||
if line.startswith('data: '):
|
||||
try:
|
||||
json_str = line[6:]
|
||||
data = json.loads(json_str)
|
||||
if 'content' in data:
|
||||
content_part = data['content']
|
||||
response_data = {
|
||||
'code': 200,
|
||||
'message': 'partial',
|
||||
'data': {
|
||||
'content': content_part,
|
||||
'is_end': data.get('is_end', False)
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(response_data)}\n\n"
|
||||
|
||||
if data.get('is_end', False):
|
||||
return
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {e}, 数据: {line}")
|
||||
|
||||
buffer = lines[-1]
|
||||
|
||||
if buffer and buffer.startswith('data: '):
|
||||
try:
|
||||
json_str = buffer[6:]
|
||||
data = json.loads(json_str)
|
||||
if 'content' in data:
|
||||
content_part = data['content']
|
||||
response_data = {
|
||||
'code': 200,
|
||||
'message': 'partial',
|
||||
'data': {
|
||||
'content': content_part,
|
||||
'is_end': data.get('is_end', False)
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(response_data)}\n\n"
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"处理剩余数据时JSON解析错误: {buffer}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"流式聊天API处理出错: {str(e)}")
|
||||
yield f"data: {json.dumps({'code': 500, 'message': f'流式处理出错: {str(e)}', 'data': {'is_end': True}})}\n\n"
|
||||
|
||||
def get_chat_answer(dataset_external_ids, question):
|
||||
"""非流式调用外部聊天API获取回答"""
|
||||
try:
|
||||
chat_request_data = {
|
||||
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
|
||||
"model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006",
|
||||
"dataset_id_list": [str(id) for id in dataset_external_ids],
|
||||
"multiple_rounds_dialogue": False,
|
||||
"dataset_setting": {
|
||||
"top_n": 10,
|
||||
"similarity": "0.3",
|
||||
"max_paragraph_char_number": 10000,
|
||||
"search_mode": "blend",
|
||||
"no_references_setting": {
|
||||
"value": "{question}",
|
||||
"status": "ai_questioning"
|
||||
}
|
||||
},
|
||||
"model_setting": {
|
||||
"prompt": "**相关文档内容**:{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**:{question}"
|
||||
},
|
||||
"problem_optimization": False
|
||||
}
|
||||
|
||||
logger.info(f"调用非流式聊天API: {settings.API_BASE_URL}/api/application/chat/open")
|
||||
chat_response = requests.post(
|
||||
url=f"{settings.API_BASE_URL}/api/application/chat/open",
|
||||
json=chat_request_data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
logger.info(f"聊天API响应状态码: {chat_response.status_code}")
|
||||
if chat_response.status_code != 200:
|
||||
logger.error(f"聊天API调用失败: {chat_response.text}")
|
||||
raise ExternalAPIError(f"聊天API调用失败: {chat_response.text}")
|
||||
|
||||
chat_data = chat_response.json()
|
||||
if chat_data.get('code') != 200 or not chat_data.get('data'):
|
||||
logger.error(f"聊天API返回错误: {chat_data}")
|
||||
raise ExternalAPIError(f"聊天API返回错误: {chat_data}")
|
||||
|
||||
chat_id = chat_data['data']
|
||||
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
|
||||
logger.info(f"调用消息API: {message_url}")
|
||||
|
||||
message_response = requests.post(
|
||||
url=message_url,
|
||||
json={"message": question, "re_chat": False, "stream": False},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if message_response.status_code != 200:
|
||||
logger.error(f"消息API调用失败: {message_response.status_code}, {message_response.text}")
|
||||
raise ExternalAPIError(f"消息API调用失败: {message_response.status_code}, {message_response.text}")
|
||||
|
||||
response_data = message_response.json()
|
||||
if response_data.get('code') != 200 or 'data' not in response_data:
|
||||
logger.error(f"消息API返回错误: {response_data}")
|
||||
raise ExternalAPIError(f"消息API返回错误: {response_data}")
|
||||
|
||||
answer_content = response_data.get('data', {}).get('content', '')
|
||||
return answer_content if answer_content else "无法获取回答内容"
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"聊天API网络错误: {str(e)}")
|
||||
raise ExternalAPIError(f"聊天API网络错误: {str(e)}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析聊天API响应JSON失败: {str(e)}")
|
||||
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"调用聊天API失败: {str(e)}")
|
||||
raise ExternalAPIError(f"调用聊天API失败: {str(e)}")
|
||||
|
||||
|
||||
def generate_conversation_title_from_deepseek(user_question, assistant_answer):
|
||||
"""调用SiliconCloud API生成会话标题,直接基于当前问题和回答内容"""
|
||||
try:
|
||||
# 从Django设置中获取API密钥
|
||||
api_key = settings.SILICON_CLOUD_API_KEY
|
||||
if not api_key:
|
||||
return "新对话"
|
||||
|
||||
# 构建提示信息
|
||||
prompt = f"请根据用户的问题和助手的回答,生成一个简短的对话标题(不超过20个字)。\n\n用户问题: {user_question}\n\n助手回答: {assistant_answer}"
|
||||
|
||||
import requests
|
||||
|
||||
url = "https://api.siliconflow.cn/v1/chat/completions"
|
||||
|
||||
payload = {
|
||||
"model": "deepseek-ai/DeepSeek-V3",
|
||||
"stream": False,
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.7,
|
||||
"top_k": 50,
|
||||
"frequency_penalty": 0.5,
|
||||
"n": 1,
|
||||
"stop": [],
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
response_data = response.json()
|
||||
|
||||
if response.status_code == 200 and 'choices' in response_data and response_data['choices']:
|
||||
title = response_data['choices'][0]['message']['content'].strip()
|
||||
return title[:50] # 截断过长的标题
|
||||
else:
|
||||
logger.error(f"生成标题时出错: {response.text}")
|
||||
return "新对话"
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"生成对话标题时发生错误: {str(e)}")
|
||||
return "新对话"
|
3
apps/chat/tests.py
Normal file
3
apps/chat/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
11
apps/chat/urls.py
Normal file
11
apps/chat/urls.py
Normal file
@ -0,0 +1,11 @@
|
||||
# apps/chat/urls.py
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.chat.views import ChatHistoryViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', ChatHistoryViewSet, basename='chat-history')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
601
apps/chat/views.py
Normal file
601
apps/chat/views.py
Normal file
@ -0,0 +1,601 @@
|
||||
import logging
|
||||
import json
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from django.db.models import Q, Max, Count
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from apps.user.models import User
|
||||
from apps.chat.models import ChatHistory
|
||||
from apps.chat.serializers import ChatHistorySerializer
|
||||
from apps.common.services.chat_service import ChatService
|
||||
from apps.chat.services.chat_api import (
|
||||
ExternalAPIError, stream_chat_answer, get_chat_answer,
|
||||
generate_conversation_title_from_deepseek
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ChatHistoryViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = ChatHistorySerializer
|
||||
queryset = ChatHistory.objects.all()
|
||||
|
||||
# 固定知识库ID
|
||||
DEFAULT_KNOWLEDGE_BASE_ID = "b680a4fa-37be-11f0-a7cb-0242ac120002"
|
||||
|
||||
def get_queryset(self):
|
||||
"""只过滤用户自己的未删除的聊天记录"""
|
||||
user = self.request.user
|
||||
return ChatHistory.objects.filter(
|
||||
user=user,
|
||||
is_deleted=False
|
||||
)
|
||||
|
||||
def list(self, request):
|
||||
"""获取对话列表概览"""
|
||||
try:
|
||||
page = int(request.query_params.get('page', 1))
|
||||
page_size = int(request.query_params.get('page_size', 10))
|
||||
|
||||
latest_chats = self.get_queryset().values(
|
||||
'conversation_id'
|
||||
).annotate(
|
||||
latest_id=Max('id'),
|
||||
message_count=Count('id'),
|
||||
last_message=Max('created_at')
|
||||
).order_by('-last_message')
|
||||
|
||||
total = latest_chats.count()
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
chats = latest_chats[start:end]
|
||||
|
||||
results = []
|
||||
for chat in chats:
|
||||
latest_record = ChatHistory.objects.get(id=chat['latest_id'])
|
||||
|
||||
results.append({
|
||||
'conversation_id': chat['conversation_id'],
|
||||
'message_count': chat['message_count'],
|
||||
'last_message': latest_record.content,
|
||||
'last_time': chat['last_message'].strftime('%Y-%m-%d %H:%M:%S'),
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '获取成功',
|
||||
'data': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'results': results
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取聊天记录失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'获取聊天记录失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def conversation_detail(self, request):
|
||||
"""获取特定对话的详细信息"""
|
||||
try:
|
||||
conversation_id = request.query_params.get('conversation_id')
|
||||
if not conversation_id:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少conversation_id参数',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
chat_service = ChatService()
|
||||
result = chat_service.get_conversation_detail(request.user, conversation_id)
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '获取成功',
|
||||
'data': result
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': str(e),
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
logger.error(f"获取对话详情失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'获取对话详情失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def available_datasets(self, request):
|
||||
"""获取默认知识库"""
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '获取成功',
|
||||
'data': []
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def create_conversation(self, request):
|
||||
"""创建会话 - 使用默认知识库ID"""
|
||||
try:
|
||||
# 创建一个新的会话ID
|
||||
conversation_id = str(uuid.uuid4())
|
||||
logger.info(f"创建新的会话ID: {conversation_id}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '会话创建成功',
|
||||
'data': {
|
||||
'conversation_id': conversation_id
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建会话失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'创建会话失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def create(self, request):
|
||||
"""创建聊天记录 - 使用默认知识库"""
|
||||
try:
|
||||
# 验证请求数据
|
||||
if 'question' not in request.data:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少必填字段: question',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if 'conversation_id' not in request.data:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少必填字段: conversation_id',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
conversation_id = request.data['conversation_id']
|
||||
question = request.data['question']
|
||||
title = request.data.get('title', 'New chat')
|
||||
|
||||
# 准备metadata
|
||||
metadata = {}
|
||||
|
||||
# 创建用户问题记录
|
||||
question_record = ChatHistory.objects.create(
|
||||
user=request.user,
|
||||
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
|
||||
conversation_id=conversation_id,
|
||||
title=title,
|
||||
role='user',
|
||||
content=question,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
# 设置外部API需要的ID列表
|
||||
external_id_list = []
|
||||
|
||||
use_stream = request.data.get('stream', True)
|
||||
|
||||
if use_stream:
|
||||
def stream_response():
|
||||
answer_record = ChatHistory.objects.create(
|
||||
user=question_record.user,
|
||||
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
|
||||
conversation_id=conversation_id,
|
||||
title=title,
|
||||
parent_id=str(question_record.id),
|
||||
role='assistant',
|
||||
content="",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
yield f"data: {json.dumps({'code': 200, 'message': '开始流式传输', 'data': {'id': str(answer_record.id), 'conversation_id': conversation_id, 'content': '', 'is_end': False}})}\n\n"
|
||||
|
||||
full_content = ""
|
||||
for data in stream_chat_answer(conversation_id, request.data['question'], external_id_list, metadata):
|
||||
parsed_data = json.loads(data[5:-2]) # 移除"data: "和"\n\n"
|
||||
if parsed_data['code'] == 200 and 'content' in parsed_data['data']:
|
||||
content_part = parsed_data['data']['content']
|
||||
full_content += content_part
|
||||
response_data = {
|
||||
'code': 200,
|
||||
'message': 'partial',
|
||||
'data': {
|
||||
'id': str(answer_record.id),
|
||||
'conversation_id': conversation_id,
|
||||
'title': title,
|
||||
'content': content_part,
|
||||
'is_end': parsed_data['data']['is_end']
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(response_data)}\n\n"
|
||||
|
||||
if parsed_data['data']['is_end']:
|
||||
answer_record.content = full_content.strip()
|
||||
answer_record.save()
|
||||
|
||||
current_title = ChatHistory.objects.filter(
|
||||
conversation_id=conversation_id
|
||||
).exclude(
|
||||
title__in=["New chat", "新对话", ""]
|
||||
).values_list('title', flat=True).first()
|
||||
|
||||
if current_title:
|
||||
title_updated = current_title
|
||||
else:
|
||||
try:
|
||||
generated_title = generate_conversation_title_from_deepseek(
|
||||
request.data['question'], full_content.strip()
|
||||
)
|
||||
if generated_title:
|
||||
ChatHistory.objects.filter(
|
||||
conversation_id=conversation_id
|
||||
).update(title=generated_title)
|
||||
title_updated = generated_title
|
||||
else:
|
||||
title_updated = "新对话"
|
||||
except Exception as e:
|
||||
logger.error(f"自动生成标题失败: {str(e)}")
|
||||
title_updated = "新对话"
|
||||
|
||||
final_response = {
|
||||
'code': 200,
|
||||
'message': '完成',
|
||||
'data': {
|
||||
'id': str(answer_record.id),
|
||||
'conversation_id': conversation_id,
|
||||
'title': title_updated,
|
||||
'role': 'assistant',
|
||||
'content': full_content.strip(),
|
||||
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'is_end': True
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(final_response)}\n\n"
|
||||
break
|
||||
elif parsed_data['code'] != 200:
|
||||
yield data
|
||||
break
|
||||
|
||||
if full_content:
|
||||
try:
|
||||
answer_record.content = full_content.strip()
|
||||
answer_record.save()
|
||||
except Exception as save_error:
|
||||
logger.error(f"保存部分内容失败: {str(save_error)}")
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_response(),
|
||||
content_type='text/event-stream',
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
response['Cache-Control'] = 'no-cache, no-store'
|
||||
response['Connection'] = 'keep-alive'
|
||||
return response
|
||||
else:
|
||||
logger.info("使用非流式输出模式")
|
||||
try:
|
||||
answer = get_chat_answer(external_id_list, request.data['question'])
|
||||
except ExternalAPIError as e:
|
||||
logger.error(f"获取回答失败: {str(e)}")
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'获取回答失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
if answer is None:
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': '获取回答失败',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
answer_record = ChatHistory.objects.create(
|
||||
user=request.user,
|
||||
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
|
||||
conversation_id=conversation_id,
|
||||
title=title,
|
||||
parent_id=str(question_record.id),
|
||||
role='assistant',
|
||||
content=answer,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
existing_records = ChatHistory.objects.filter(conversation_id=conversation_id)
|
||||
should_generate_title = not existing_records.exclude(id=question_record.id).exists() and (not title or title == 'New chat')
|
||||
if should_generate_title:
|
||||
try:
|
||||
generated_title = generate_conversation_title_from_deepseek(
|
||||
request.data['question'], answer
|
||||
)
|
||||
if generated_title:
|
||||
ChatHistory.objects.filter(conversation_id=conversation_id).update(title=generated_title)
|
||||
title = generated_title
|
||||
except Exception as e:
|
||||
logger.error(f"自动生成标题失败: {str(e)}")
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '成功',
|
||||
'data': {
|
||||
'id': str(answer_record.id),
|
||||
'conversation_id': conversation_id,
|
||||
'title': title,
|
||||
'role': 'assistant',
|
||||
'content': answer,
|
||||
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建聊天记录失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'创建聊天记录失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def _highlight_keyword(self, text, keyword):
|
||||
"""高亮关键词"""
|
||||
if not keyword or not text:
|
||||
return text
|
||||
return text.replace(keyword, f'<em class="highlight">{keyword}</em>')
|
||||
|
||||
def update(self, request, pk=None):
|
||||
"""更新聊天记录"""
|
||||
try:
|
||||
record = self.get_queryset().filter(id=pk).first()
|
||||
if not record:
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': '记录不存在或无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
data = request.data
|
||||
updateable_fields = ['content', 'metadata']
|
||||
if 'content' in data:
|
||||
record.content = data['content']
|
||||
if 'metadata' in data:
|
||||
current_metadata = record.metadata or {}
|
||||
current_metadata.update(data['metadata'])
|
||||
record.metadata = current_metadata
|
||||
|
||||
record.save()
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '更新成功',
|
||||
'data': {
|
||||
'id': str(record.id),
|
||||
'conversation_id': record.conversation_id,
|
||||
'role': record.role,
|
||||
'content': record.content,
|
||||
'metadata': record.metadata,
|
||||
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新聊天记录失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'更新聊天记录失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def destroy(self, request, pk=None):
|
||||
"""删除聊天记录(软删除)"""
|
||||
try:
|
||||
record = self.get_queryset().filter(id=pk).first()
|
||||
if not record:
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': '记录不存在或无权限',
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
record.soft_delete()
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '删除成功',
|
||||
'data': None
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除聊天记录失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'删除聊天记录失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def search(self, request):
|
||||
"""搜索聊天记录"""
|
||||
try:
|
||||
keyword = request.query_params.get('keyword', '').strip()
|
||||
start_date = request.query_params.get('start_date')
|
||||
end_date = request.query_params.get('end_date')
|
||||
page = int(request.query_params.get('page', 1))
|
||||
page_size = int(request.query_params.get('page_size', 10))
|
||||
|
||||
query = self.get_queryset()
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
Q(content__icontains=keyword)
|
||||
)
|
||||
if start_date:
|
||||
query = query.filter(created_at__gte=start_date)
|
||||
if end_date:
|
||||
query = query.filter(created_at__lte=end_date)
|
||||
|
||||
total = query.count()
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
records = query.order_by('-created_at')[start:end]
|
||||
|
||||
results = [
|
||||
{
|
||||
'id': str(record.id),
|
||||
'conversation_id': record.conversation_id,
|
||||
'role': record.role,
|
||||
'content': record.content,
|
||||
'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'metadata': record.metadata,
|
||||
'highlights': {'content': self._highlight_keyword(record.content, keyword)} if keyword else {}
|
||||
}
|
||||
for record in records
|
||||
]
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '搜索成功',
|
||||
'data': {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'results': results
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索聊天记录失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'搜索失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@action(detail=False, methods=['delete'])
|
||||
def delete_conversation(self, request):
|
||||
"""通过conversation_id删除一组会话"""
|
||||
try:
|
||||
conversation_id = request.query_params.get('conversation_id')
|
||||
if not conversation_id:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少必要参数: conversation_id',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
records = self.get_queryset().filter(conversation_id=conversation_id)
|
||||
if not records.exists():
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': '未找到该会话或无权限访问',
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
records_count = records.count()
|
||||
for record in records:
|
||||
record.soft_delete()
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '删除成功',
|
||||
'data': {
|
||||
'conversation_id': conversation_id,
|
||||
'deleted_count': records_count
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除会话失败: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f'删除会话失败: {str(e)}',
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='generate-conversation-title')
|
||||
def generate_conversation_title(self, request):
|
||||
"""更新会话标题"""
|
||||
try:
|
||||
conversation_id = request.query_params.get('conversation_id')
|
||||
if not conversation_id:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少conversation_id参数',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 检查对话是否存在
|
||||
messages = self.get_queryset().filter(
|
||||
conversation_id=conversation_id,
|
||||
is_deleted=False,
|
||||
user=request.user
|
||||
).order_by('created_at')
|
||||
|
||||
if not messages.exists():
|
||||
return Response({
|
||||
'code': 404,
|
||||
'message': '对话不存在或无权访问',
|
||||
'data': None
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# 检查是否有自定义标题参数
|
||||
custom_title = request.query_params.get('title')
|
||||
if not custom_title:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '缺少title参数',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 更新所有相关记录的标题
|
||||
ChatHistory.objects.filter(
|
||||
conversation_id=conversation_id,
|
||||
user=request.user
|
||||
).update(title=custom_title)
|
||||
|
||||
return Response({
|
||||
'code': 200,
|
||||
'message': '更新会话标题成功',
|
||||
'data': {
|
||||
'conversation_id': conversation_id,
|
||||
'title': custom_title
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新会话标题失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return Response({
|
||||
'code': 500,
|
||||
'message': f"更新会话标题失败: {str(e)}",
|
||||
'data': None
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
0
apps/common/__init__.py
Normal file
0
apps/common/__init__.py
Normal file
3
apps/common/admin.py
Normal file
3
apps/common/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
apps/common/apps.py
Normal file
6
apps/common/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.common'
|
46
apps/common/middlewares.py
Normal file
46
apps/common/middlewares.py
Normal file
@ -0,0 +1,46 @@
|
||||
from django.db import close_old_connections
|
||||
from rest_framework.authtoken.models import Token
|
||||
from channels.middleware import BaseMiddleware
|
||||
from channels.db import database_sync_to_async
|
||||
from urllib.parse import parse_qs
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user_from_token(token_key):
|
||||
try:
|
||||
token = Token.objects.select_related('user').get(key=token_key)
|
||||
return token.user
|
||||
except Token.DoesNotExist:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户Token失败: {str(e)}")
|
||||
return None
|
||||
|
||||
class TokenAuthMiddleware(BaseMiddleware):
|
||||
async def __call__(self, scope, receive, send):
|
||||
# 关闭之前的数据库连接
|
||||
close_old_connections()
|
||||
|
||||
# 从查询字符串中提取token
|
||||
query_string = scope.get('query_string', b'').decode()
|
||||
query_params = parse_qs(query_string)
|
||||
token_key = query_params.get('token', [''])[0]
|
||||
|
||||
if token_key:
|
||||
user = await get_user_from_token(token_key)
|
||||
if user:
|
||||
scope['user'] = user
|
||||
logger.info(f"WebSocket认证成功: 用户 {user.id}")
|
||||
else:
|
||||
logger.warning(f"WebSocket认证失败: 无效的Token {token_key}")
|
||||
scope['user'] = None
|
||||
else:
|
||||
logger.warning("WebSocket连接未提供Token")
|
||||
scope['user'] = None
|
||||
|
||||
return await super().__call__(scope, receive, send)
|
||||
|
||||
def TokenAuthMiddlewareStack(inner):
|
||||
return TokenAuthMiddleware(inner)
|
0
apps/common/migrations/__init__.py
Normal file
0
apps/common/migrations/__init__.py
Normal file
3
apps/common/models.py
Normal file
3
apps/common/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
0
apps/common/services/__init__.py
Normal file
0
apps/common/services/__init__.py
Normal file
202
apps/common/services/ai_service.py
Normal file
202
apps/common/services/ai_service.py
Normal file
@ -0,0 +1,202 @@
|
||||
import requests
|
||||
import logging
|
||||
import json
|
||||
from django.conf import settings
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AIService:
|
||||
"""
|
||||
通用AI服务类,提供对不同AI模型的统一调用接口
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def call_silicon_cloud_api(messages, model="deepseek-ai/DeepSeek-V3", max_tokens=512, temperature=0.7):
|
||||
"""
|
||||
调用SiliconCloud API
|
||||
|
||||
Args:
|
||||
messages: 消息列表,格式为[{"role": "user", "content": "..."}, ...]
|
||||
model: 使用的模型名称,默认为DeepSeek-V3
|
||||
max_tokens: 最大生成token数
|
||||
temperature: 温度参数,控制创造性
|
||||
|
||||
Returns:
|
||||
tuple: (生成内容, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 获取API密钥
|
||||
api_key = getattr(settings, 'SILICON_CLOUD_API_KEY', '')
|
||||
if not api_key:
|
||||
return None, "未配置Silicon Cloud API密钥"
|
||||
|
||||
url = "https://api.siliconflow.cn/v1/chat/completions"
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"stream": False,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"top_p": 0.7,
|
||||
"top_k": 50,
|
||||
"frequency_penalty": 0.5,
|
||||
"n": 1,
|
||||
"stop": [],
|
||||
"messages": messages
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 设置代理(如果配置了)
|
||||
proxies = None
|
||||
proxy_url = getattr(settings, 'PROXY_URL', None)
|
||||
if proxy_url:
|
||||
proxies = {
|
||||
"http": proxy_url,
|
||||
"https": proxy_url
|
||||
}
|
||||
|
||||
# 发送请求
|
||||
logger.info(f"正在调用SiliconCloud API,模型: {model}")
|
||||
response = requests.post(url, json=payload, headers=headers, proxies=proxies)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"API请求失败,状态码: {response.status_code},响应: {response.text}")
|
||||
return None, f"API请求失败,状态码: {response.status_code}"
|
||||
|
||||
result = response.json()
|
||||
|
||||
# 从API响应中提取内容
|
||||
if 'choices' in result and result['choices']:
|
||||
content = result['choices'][0]['message']['content'].strip()
|
||||
return content, None
|
||||
else:
|
||||
logger.error(f"API响应格式错误: {result}")
|
||||
return None, "API响应格式错误,无法提取内容"
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"API请求异常: {str(e)}")
|
||||
return None, f"API请求异常: {str(e)}"
|
||||
except Exception as e:
|
||||
logger.error(f"调用AI服务失败: {str(e)}")
|
||||
return None, f"调用AI服务失败: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def generate_email_reply(goal_description, conversation_summary, last_message):
|
||||
"""
|
||||
生成邮件回复话术
|
||||
|
||||
Args:
|
||||
goal_description: 用户目标描述
|
||||
conversation_summary: 对话摘要
|
||||
last_message: 达人最后发送的消息内容
|
||||
|
||||
Returns:
|
||||
tuple: (推荐话术内容, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 验证必要参数
|
||||
if not goal_description or not last_message:
|
||||
return None, "缺少必要参数:目标描述或最后消息"
|
||||
|
||||
# 准备提示信息
|
||||
prompt = f"""
|
||||
请为用户提供一条回复达人邮件的推荐话术,帮助用户实现销售目标。
|
||||
|
||||
用户目标:{goal_description}
|
||||
|
||||
对话摘要:{conversation_summary or '无对话摘要'}
|
||||
|
||||
达人最后发送的消息:
|
||||
{last_message}
|
||||
|
||||
要求:
|
||||
1. 回复应当专业礼貌,并围绕用户目标展开
|
||||
2. 提供有说服力的话术,推动促成合作
|
||||
3. 不超过200字
|
||||
4. 直接给出回复内容,不要包含任何额外解释
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的邮件回复助手,你的任务是生成有效的销售话术,帮助用户实现销售目标。"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
]
|
||||
|
||||
# 调用通用API方法
|
||||
return AIService.call_silicon_cloud_api(
|
||||
messages,
|
||||
model="Pro/deepseek-ai/DeepSeek-R1",
|
||||
max_tokens=512,
|
||||
temperature=0.7
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成回复话术失败: {str(e)}")
|
||||
return None, f"生成回复话术失败: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
def generate_conversation_summary(conversation_history):
|
||||
"""
|
||||
生成对话摘要
|
||||
|
||||
Args:
|
||||
conversation_history: 对话历史记录列表
|
||||
|
||||
Returns:
|
||||
tuple: (摘要内容, 错误信息)
|
||||
"""
|
||||
try:
|
||||
if not conversation_history:
|
||||
return None, "无对话历史,无法生成摘要"
|
||||
|
||||
# 构造对话历史文本
|
||||
conversation_text = "\n\n".join([
|
||||
f"**{'用户' if msg.get('is_from_user', False) else '达人'}**: {msg.get('content', '')}"
|
||||
for msg in conversation_history
|
||||
])
|
||||
|
||||
# 准备提示信息
|
||||
prompt = f"""
|
||||
请对以下用户与达人之间的对话内容进行总结:
|
||||
|
||||
{conversation_text}
|
||||
|
||||
要求:
|
||||
1. 总结应包含双方主要讨论的话题和关键点
|
||||
2. 特别关注产品详情、价格谈判、合作意向等商务要点
|
||||
3. 简明扼要,不超过200字
|
||||
4. 直接给出总结内容,不要包含任何额外解释
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的对话总结助手,擅长提取商务沟通中的关键信息。"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
]
|
||||
|
||||
# 调用通用API方法
|
||||
return AIService.call_silicon_cloud_api(
|
||||
messages,
|
||||
model="Pro/deepseek-ai/DeepSeek-R1",
|
||||
max_tokens=512,
|
||||
temperature=0.5
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成对话摘要失败: {str(e)}")
|
||||
return None, f"生成对话摘要失败: {str(e)}"
|
118
apps/common/services/chat_service.py
Normal file
118
apps/common/services/chat_service.py
Normal file
@ -0,0 +1,118 @@
|
||||
# apps/common/services/chat_service.py
|
||||
import logging
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from django.db import transaction
|
||||
from apps.user.models import User
|
||||
from apps.chat.models import ChatHistory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ChatService:
|
||||
@transaction.atomic
|
||||
def create_chat_record(self, user, data, conversation_id=None):
|
||||
"""创建聊天记录,供chat、gmail、feishu模块复用"""
|
||||
try:
|
||||
# 验证必填字段
|
||||
if 'question' not in data:
|
||||
raise ValueError("缺少必填字段: question")
|
||||
|
||||
# 如果未提供conversation_id,生成新的
|
||||
if not conversation_id:
|
||||
conversation_id = str(uuid4())
|
||||
logger.info(f"生成新的会话ID: {conversation_id}")
|
||||
|
||||
# 创建metadata
|
||||
metadata = {
|
||||
'model_id': data.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'),
|
||||
}
|
||||
|
||||
# 设置标题
|
||||
title = data.get('title', 'New chat')
|
||||
|
||||
# 创建用户问题记录
|
||||
question_record = ChatHistory.objects.create(
|
||||
user=user,
|
||||
knowledge_base_id="b680a4fa-37be-11f0-a7cb-0242ac120002", # 使用默认知识库ID
|
||||
conversation_id=conversation_id,
|
||||
title=title,
|
||||
role='user',
|
||||
content=data['question'],
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
return question_record, conversation_id, metadata, [], []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建聊天记录失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_conversation_detail(self, user, conversation_id):
|
||||
"""获取会话详情,供chat、gmail、feishu模块复用"""
|
||||
try:
|
||||
# 查询会话记录
|
||||
messages = ChatHistory.objects.filter(
|
||||
conversation_id=conversation_id,
|
||||
user=user,
|
||||
is_deleted=False
|
||||
).order_by('created_at')
|
||||
|
||||
if not messages.exists():
|
||||
raise ValueError("对话不存在或无权限")
|
||||
|
||||
# 构建消息列表
|
||||
message_list = [
|
||||
{
|
||||
'id': str(msg.id),
|
||||
'parent_id': msg.parent_id,
|
||||
'role': msg.role,
|
||||
'content': msg.content,
|
||||
'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'metadata': msg.metadata
|
||||
}
|
||||
for msg in messages
|
||||
]
|
||||
|
||||
return {
|
||||
'conversation_id': conversation_id,
|
||||
'messages': message_list
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取会话详情失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def can_user_access_conversation(self, user, conversation_id):
|
||||
"""检查用户是否有权限访问指定的对话"""
|
||||
return ChatHistory.objects.filter(
|
||||
conversation_id=conversation_id,
|
||||
user=user,
|
||||
is_deleted=False
|
||||
).exists()
|
||||
|
||||
def format_chat_response(self, chat_history_list):
|
||||
"""格式化聊天历史记录为前端需要的格式"""
|
||||
try:
|
||||
result = []
|
||||
|
||||
for item in chat_history_list:
|
||||
formatted_item = {
|
||||
"id": str(item.id),
|
||||
"conversation_id": str(item.conversation_id),
|
||||
"role": item.role,
|
||||
"content": item.content,
|
||||
"created_at": item.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"title": item.title
|
||||
}
|
||||
|
||||
# 添加parent_id如果存在
|
||||
if item.parent_id:
|
||||
formatted_item["parent_id"] = str(item.parent_id)
|
||||
|
||||
result.append(formatted_item)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"格式化聊天响应失败: {e}")
|
||||
return []
|
||||
|
320
apps/common/services/external_api_service.py
Normal file
320
apps/common/services/external_api_service.py
Normal file
@ -0,0 +1,320 @@
|
||||
# apps/common/services/external_api_service.py
|
||||
import traceback
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ExternalAPIError(APIException):
|
||||
status_code = 500
|
||||
default_detail = '外部API调用失败'
|
||||
default_code = 'external_api_error'
|
||||
|
||||
def create_external_dataset(knowledge_base):
|
||||
"""创建外部知识库"""
|
||||
try:
|
||||
api_data = {
|
||||
"name": knowledge_base.name,
|
||||
"desc": knowledge_base.desc,
|
||||
"type": "0",
|
||||
"meta": {},
|
||||
"documents": []
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f'{settings.API_BASE_URL}/api/dataset',
|
||||
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', '未知错误')}")
|
||||
|
||||
dataset_id = api_response.get('data', {}).get('id')
|
||||
if not dataset_id:
|
||||
raise ExternalAPIError("响应数据中缺少dataset id")
|
||||
|
||||
return dataset_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)}")
|
||||
|
||||
def delete_external_dataset(external_id):
|
||||
"""删除外部知识库"""
|
||||
try:
|
||||
if not external_id:
|
||||
logger.warning("外部知识库ID为空,跳过删除")
|
||||
return True
|
||||
|
||||
response = requests.delete(
|
||||
f'{settings.API_BASE_URL}/api/dataset/{external_id}',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
|
||||
logger.info(f"删除外部知识库响应: status_code={response.status_code}, response={response.text}")
|
||||
|
||||
if response.status_code == 404:
|
||||
logger.warning(f"外部知识库不存在: {external_id}")
|
||||
return True
|
||||
elif response.status_code not in [200, 204]:
|
||||
return True # 允许本地删除继续
|
||||
|
||||
if response.status_code == 204:
|
||||
logger.info(f"外部知识库删除成功: {external_id}")
|
||||
return True
|
||||
|
||||
try:
|
||||
api_response = response.json()
|
||||
if api_response.get('code') != 200:
|
||||
if "不存在" in api_response.get('message', ''):
|
||||
logger.warning(f"外部知识库ID不存在,视为删除成功: {external_id}")
|
||||
return True
|
||||
logger.warning(f"业务处理返回非200状态码: {api_response.get('code')}, {api_response.get('message')}")
|
||||
return True
|
||||
logger.info(f"外部知识库删除成功: {external_id}")
|
||||
return True
|
||||
except ValueError:
|
||||
logger.warning(f"外部知识库删除响应无法解析JSON,但状态码为200,视为成功: {external_id}")
|
||||
return True
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"删除外部知识库超时: {external_id}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"删除外部知识库请求异常: {external_id}, error={str(e)}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"删除外部知识库其他错误: {external_id}, error={str(e)}")
|
||||
return False
|
||||
|
||||
def call_split_api_multiple(files):
|
||||
"""调用文档分割API,支持多文件批量处理"""
|
||||
try:
|
||||
url = f'{settings.API_BASE_URL}/api/dataset/document/split'
|
||||
|
||||
# 准备请求数据 - 将所有文件作为 'file' 字段
|
||||
files_data = [('file', (file.name, file, file.content_type)) for file in files]
|
||||
|
||||
# 记录上传的文件信息
|
||||
for file in files:
|
||||
logger.info(f"准备上传文件: {file.name}, 大小: {file.size}字节, 类型: {file.content_type}")
|
||||
# 读取文件内容前100个字符进行记录
|
||||
if hasattr(file, 'read') and hasattr(file, 'seek'):
|
||||
file.seek(0)
|
||||
content_preview = file.read(100).decode('utf-8', errors='ignore')
|
||||
logger.info(f"文件内容预览: {content_preview}")
|
||||
file.seek(0) # 重置文件指针
|
||||
|
||||
logger.info(f"调用分割API URL: {url}")
|
||||
logger.info(f"上传文件数量: {len(files_data)}")
|
||||
|
||||
# 发送请求
|
||||
response = requests.post(
|
||||
url,
|
||||
files=files_data
|
||||
)
|
||||
|
||||
# 记录请求头和响应信息
|
||||
logger.info(f"请求头: {response.request.headers}")
|
||||
logger.info(f"响应状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"分割API返回错误状态码: {response.status_code}, 响应: {response.text}")
|
||||
return None
|
||||
|
||||
# 解析响应
|
||||
result = response.json()
|
||||
logger.info(f"分割API响应详情: {result}")
|
||||
|
||||
# 如果数据为空,可能是API处理失败,尝试后备方案
|
||||
if len(result.get('data', [])) == 0:
|
||||
logger.warning("分割API返回的数据为空,尝试使用后备方案")
|
||||
fallback_data = {
|
||||
'code': 200,
|
||||
'message': '成功',
|
||||
'data': [
|
||||
{
|
||||
'name': file.name,
|
||||
'content': [
|
||||
{
|
||||
'title': '文档内容',
|
||||
'content': '文件内容无法自动分割,请检查外部API。这是一个后备内容。'
|
||||
}
|
||||
]
|
||||
} for file in files
|
||||
]
|
||||
}
|
||||
logger.info("使用后备数据结构")
|
||||
return fallback_data
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"调用分割API失败: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 创建后备响应
|
||||
fallback_response = {
|
||||
'code': 200,
|
||||
'message': '成功',
|
||||
'data': [
|
||||
{
|
||||
'name': file.name,
|
||||
'content': [
|
||||
{
|
||||
'title': '文档内容',
|
||||
'content': '文件内容无法自动分割,请检查API连接。'
|
||||
}
|
||||
]
|
||||
} for file in files
|
||||
]
|
||||
}
|
||||
logger.info("由于异常,返回后备响应")
|
||||
return fallback_response
|
||||
|
||||
def call_upload_api(external_id, doc_data):
|
||||
"""调用文档上传API"""
|
||||
try:
|
||||
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document'
|
||||
logger.info(f"调用文档上传API: {url}")
|
||||
logger.info(f"上传文档数据: 文档名={doc_data.get('name')}, 段落数={len(doc_data.get('paragraphs', []))}")
|
||||
|
||||
response = requests.post(url, json=doc_data)
|
||||
|
||||
logger.info(f"上传API响应状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"上传API HTTP错误: {response.status_code}, 响应: {response.text}")
|
||||
return {
|
||||
'code': response.status_code,
|
||||
'message': f"上传失败,HTTP状态码: {response.status_code}",
|
||||
'data': None
|
||||
}
|
||||
|
||||
result = response.json()
|
||||
logger.info(f"上传API响应内容: {result}")
|
||||
|
||||
if result.get('code') != 200:
|
||||
error_msg = result.get('message', '未知错误')
|
||||
logger.error(f"上传API业务错误: {error_msg}")
|
||||
return {
|
||||
'code': result.get('code', 500),
|
||||
'message': error_msg,
|
||||
'data': None
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"调用上传API网络错误: {str(e)}")
|
||||
return {
|
||||
'code': 500,
|
||||
'message': f"网络请求错误: {str(e)}",
|
||||
'data': None
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析API响应JSON失败: {str(e)}")
|
||||
return {
|
||||
'code': 500,
|
||||
'message': f"解析响应数据失败: {str(e)}",
|
||||
'data': None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"调用上传API其他错误: {str(e)}")
|
||||
return {
|
||||
'code': 500,
|
||||
'message': f"上传API调用失败: {str(e)}",
|
||||
'data': None
|
||||
}
|
||||
|
||||
def call_delete_document_api(external_id, document_id):
|
||||
"""调用文档删除API"""
|
||||
try:
|
||||
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document/{document_id}'
|
||||
response = requests.delete(url)
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"调用删除API失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_external_document_list(external_id):
|
||||
"""获取外部知识库的文档列表"""
|
||||
try:
|
||||
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document'
|
||||
logger.info(f"调用获取文档列表API: {url}")
|
||||
|
||||
response = requests.get(
|
||||
url,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
|
||||
logger.info(f"文档列表API响应状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"获取文档列表失败: {response.status_code}, 响应: {response.text}")
|
||||
raise ExternalAPIError(f"获取文档列表失败,状态码: {response.status_code}")
|
||||
|
||||
result = response.json()
|
||||
logger.info(f"文档列表API响应内容: {result}")
|
||||
|
||||
if result.get('code') != 200:
|
||||
logger.error(f"获取文档列表业务错误: {result.get('message', '未知错误')}")
|
||||
raise ExternalAPIError(f"获取文档列表失败: {result.get('message', '未知错误')}")
|
||||
|
||||
return result.get('data', [])
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取文档列表网络错误: {str(e)}")
|
||||
raise ExternalAPIError(f"获取文档列表失败: {str(e)}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析文档列表响应JSON失败: {str(e)}")
|
||||
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取文档列表其他错误: {str(e)}")
|
||||
raise ExternalAPIError(f"获取文档列表失败: {str(e)}")
|
||||
|
||||
def get_external_document_paragraphs(external_id, document_external_id):
|
||||
"""获取外部文档的段落内容"""
|
||||
try:
|
||||
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document/{document_external_id}/paragraph'
|
||||
logger.info(f"调用获取文档段落API: {url}")
|
||||
|
||||
response = requests.get(url)
|
||||
|
||||
logger.info(f"文档段落API响应状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"获取文档段落内容失败: {response.status_code}, 响应: {response.text}")
|
||||
raise ExternalAPIError(f"获取文档段落内容失败,状态码: {response.status_code}")
|
||||
|
||||
result = response.json()
|
||||
logger.info(f"文档段落API响应内容: {result}")
|
||||
|
||||
if result.get('code') != 200:
|
||||
logger.error(f"获取文档段落内容业务错误: {result.get('message', '未知错误')}")
|
||||
raise ExternalAPIError(f"获取文档段落内容失败: {result.get('message', '未知错误')}")
|
||||
|
||||
return result.get('data', [])
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取文档段落内容网络错误: {str(e)}")
|
||||
raise ExternalAPIError(f"获取文档段落内容失败: {str(e)}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析文档段落响应JSON失败: {str(e)}")
|
||||
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取文档段落内容其他错误: {str(e)}")
|
||||
raise ExternalAPIError(f"获取文档段落内容失败: {str(e)}")
|
||||
|
||||
|
3
apps/common/tests.py
Normal file
3
apps/common/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
0
apps/common/urls.py
Normal file
0
apps/common/urls.py
Normal file
3
apps/common/views.py
Normal file
3
apps/common/views.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
@ -501,6 +501,7 @@ class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
import logging
|
||||
import json
|
||||
from apps.daren_detail.models import CreatorProfile
|
||||
from apps.user.models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -562,38 +563,46 @@ class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
matched_creator_ids = []
|
||||
|
||||
for creator_data in api_results:
|
||||
# 根据API结果获取或创建CreatorProfile
|
||||
try:
|
||||
# 尝试查找CreatorProfile,使用filter和first而非get避免多个结果的问题
|
||||
profile_obj = CreatorProfile.objects.filter(
|
||||
name=creator_data.get('name', 'Unknown')
|
||||
).first()
|
||||
# 安全获取创作者名称,确保有一个默认值
|
||||
creator_name = creator_data.get('name', 'Unknown Creator')
|
||||
|
||||
# 按名称找到第一个匹配的CreatorProfile或创建新的
|
||||
profile_obj = None
|
||||
try:
|
||||
profile_obj = CreatorProfile.objects.filter(name=creator_name).first()
|
||||
except Exception as e:
|
||||
logger.error(f"查询CreatorProfile时出错: {str(e)}")
|
||||
|
||||
# 如果没有找到,则创建一个新的
|
||||
if not profile_obj:
|
||||
profile_obj = CreatorProfile.objects.create(
|
||||
name=creator_data.get('name', 'Unknown'),
|
||||
avatar_url=creator_data.get('avatar_url'),
|
||||
email=creator_data.get('email'),
|
||||
instagram=creator_data.get('instagram'),
|
||||
tiktok_link=creator_data.get('tiktok_link'),
|
||||
location=creator_data.get('location'),
|
||||
live_schedule=creator_data.get('live_schedule'),
|
||||
category=creator_data.get('category'),
|
||||
e_commerce_level=creator_data.get('e_commerce_level'),
|
||||
exposure_level=creator_data.get('exposure_level'),
|
||||
followers=creator_data.get('followers', 0),
|
||||
gmv=creator_data.get('gmv', 0),
|
||||
items_sold=creator_data.get('items_sold', 0),
|
||||
avg_video_views=creator_data.get('avg_video_views', 0),
|
||||
pricing=creator_data.get('pricing'),
|
||||
pricing_package=creator_data.get('pricing_package'),
|
||||
collab_count=creator_data.get('collab_count'),
|
||||
latest_collab=creator_data.get('latest_collab'),
|
||||
profile=creator_data.get('profile'),
|
||||
hashtags=creator_data.get('hashtags', ''),
|
||||
trends=creator_data.get('trends', '')
|
||||
)
|
||||
# 如果没有找到,创建一个新的CreatorProfile
|
||||
try:
|
||||
profile_obj = CreatorProfile.objects.create(
|
||||
name=creator_name,
|
||||
avatar_url=creator_data.get('avatar_url'),
|
||||
email=creator_data.get('email'),
|
||||
instagram=creator_data.get('instagram'),
|
||||
tiktok_link=creator_data.get('tiktok_link'),
|
||||
location=creator_data.get('location'),
|
||||
live_schedule=creator_data.get('live_schedule'),
|
||||
category=creator_data.get('category'),
|
||||
e_commerce_level=creator_data.get('e_commerce_level'),
|
||||
exposure_level=creator_data.get('exposure_level'),
|
||||
followers=creator_data.get('followers', 0),
|
||||
gmv=creator_data.get('gmv', 0),
|
||||
items_sold=creator_data.get('items_sold', 0),
|
||||
avg_video_views=creator_data.get('avg_video_views', 0),
|
||||
pricing=creator_data.get('pricing'),
|
||||
pricing_package=creator_data.get('pricing_package'),
|
||||
collab_count=creator_data.get('collab_count'),
|
||||
latest_collab=creator_data.get('latest_collab'),
|
||||
profile=creator_data.get('profile'),
|
||||
hashtags=creator_data.get('hashtags', ''),
|
||||
trends=creator_data.get('trends', '')
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"创建CreatorProfile时出错: {str(e)}")
|
||||
continue
|
||||
|
||||
# 检查是否已经在当前session中存在相同creator
|
||||
existing_creator = Creator.objects.filter(
|
||||
|
@ -23,12 +23,14 @@ from channels.auth import AuthMiddlewareStack
|
||||
|
||||
# 确保在django.setup()之后再导入
|
||||
import apps.brands.routing
|
||||
import apps.chat.routing
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
'http': get_asgi_application(),
|
||||
'websocket': AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
apps.brands.routing.websocket_urlpatterns
|
||||
apps.brands.routing.websocket_urlpatterns +
|
||||
apps.chat.routing.websocket_urlpatterns
|
||||
)
|
||||
),
|
||||
})
|
||||
|
@ -49,6 +49,8 @@ INSTALLED_APPS = [
|
||||
"apps.brands.apps.BrandsConfig",
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'apps.chat.apps.ChatConfig',
|
||||
'apps.common.apps.CommonConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -100,7 +102,7 @@ DATABASES = {
|
||||
'NAME': 'daren_detail',
|
||||
'USER': 'root',
|
||||
'PASSWORD': '123456',
|
||||
'HOST': 'localhost',
|
||||
'HOST': '192.168.31.138',
|
||||
'PORT': '3306',
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4',
|
||||
@ -209,9 +211,13 @@ AUTH_USER_MODEL = 'user.User'
|
||||
|
||||
# REST Framework 设置
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [], # 默认不需要认证
|
||||
'DEFAULT_PERMISSION_CLASSES': [], # 默认不需要权限
|
||||
'UNAUTHENTICATED_USER': None
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'apps.user.authentication.CustomTokenAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
],
|
||||
}
|
||||
|
||||
# JWT 设置
|
||||
@ -230,3 +236,6 @@ SIMPLE_JWT = {
|
||||
'JTI_CLAIM': None, # 不在 token 中包含 JWT ID
|
||||
}
|
||||
|
||||
API_BASE_URL = 'http://81.69.223.133:48329'
|
||||
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'
|
||||
|
||||
|
@ -15,4 +15,5 @@ urlpatterns = [
|
||||
path('api/discovery/', include('apps.discovery.urls')),
|
||||
path('api/template/', include('apps.template.urls')),
|
||||
path('api/', include('apps.brands.urls')),
|
||||
path('api/chat-history/', include('apps.chat.urls')),
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user