快捷问题问答的部分

main
zc 3 months ago
parent 91c18d91f0
commit 43fbda1a50
  1. 62
      app/api/chat/ai/chat_router.py
  2. 42
      app/api/chat/ai/chat_service.py
  3. 3
      app/api/v1/upload/upload.py
  4. 4
      app/core/init_app.py
  5. 2
      web/src/api/index.js
  6. 44
      web/src/views/system/question/index.vue

@ -2,14 +2,15 @@ from fastapi import APIRouter, HTTPException
from starlette.requests import Request from starlette.requests import Request
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import AsyncGenerator from typing import AsyncGenerator, List
from .chat_service import classify, extract_spot, query_flow, gen_markdown_stream, ai_chat_stream from .chat_service import classify, extract_spot, query_flow, gen_markdown_stream, ai_chat_stream, handle_quick_question
from app.models.ChatIn import ChatIn from app.models.ChatIn import ChatIn
from app.models.quick_question import QuickQuestion
from pydantic import BaseModel
import hmac import hmac
import hashlib import hashlib
import time import time
import json import json
from pydantic import BaseModel
from app.settings.config import settings from app.settings.config import settings
router = APIRouter() router = APIRouter()
@ -43,23 +44,42 @@ async def h5_chat_stream(request: Request, inp: ChatIn):
conversation_history_str = await redis_client.get(conversation_history_key) conversation_history_str = await redis_client.get(conversation_history_key)
conversation_history = json.loads(conversation_history_str) if conversation_history_str else [] conversation_history = json.loads(conversation_history_str) if conversation_history_str else []
# 分类阶段(保留同步调用) # 获取开启的前4个问题(包含标题和内容)
cat = await classify(inp.message) questions = await QuickQuestion.filter(status="0").order_by("order_num").limit(4).values("title", "content")
question_titles = [q["title"] for q in questions]
# 检查消息是否在问题列表中
is_quick_question = inp.message in question_titles
# 分类阶段(如果不是快捷问题才执行)
cat = None
if not is_quick_question:
cat = await classify(inp.message)
async def content_stream() -> AsyncGenerator[str, None]: async def content_stream() -> AsyncGenerator[str, None]:
nonlocal conversation_history nonlocal conversation_history
try: try:
if cat == "游玩判断": if is_quick_question:
spot = await extract_spot(inp.message) # 找到对应的问题内容
data = await query_flow(request, spot) question_content = next(q["content"] for q in questions if q["title"] == inp.message)
async for chunk in gen_markdown_stream(inp.message, data, inp.language, conversation_history): # 处理快捷问题,传递content
async for chunk in handle_quick_question(inp, question_content):
yield chunk yield chunk
else: else:
async for chunk in ai_chat_stream(inp, conversation_history): # 原来的逻辑
yield chunk if cat == "游玩判断":
spot = await extract_spot(inp.message)
data = await query_flow(request, spot)
async for chunk in gen_markdown_stream(inp.message, data, inp.language, conversation_history):
yield chunk
else:
async for chunk in ai_chat_stream(inp, conversation_history):
yield chunk
# 将更新后的对话历史存回 Redis,并设置过期时间 # 将更新后的对话历史存回 Redis,并设置过期时间
await redis_client.setex(conversation_history_key, CONVERSATION_EXPIRE_TIME, json.dumps(conversation_history)) # 只有非快捷问题才保存对话历史
if not is_quick_question:
await redis_client.setex(conversation_history_key, CONVERSATION_EXPIRE_TIME, json.dumps(conversation_history))
except Exception as e: except Exception as e:
print(f"Error in content_stream: {e}") print(f"Error in content_stream: {e}")
raise raise
@ -102,4 +122,20 @@ def verify_signature(data: dict, sign: str) -> bool:
def verify_timestamp(timestamp: int) -> bool: def verify_timestamp(timestamp: int) -> bool:
current_timestamp = int(time.time() * 1000) current_timestamp = int(time.time() * 1000)
return abs(current_timestamp - timestamp) <= TIMESTAMP_TOLERANCE * 1000 return abs(current_timestamp - timestamp) <= TIMESTAMP_TOLERANCE * 1000
# 定义获取问题的响应模型
class QuestionResponse(BaseModel):
id: int
title: str
class Config:
orm_mode = True
@router.get("/getQuestion", summary="获取开启的前4个问题")
async def get_question():
# 查询状态为正常(0)的问题,按order_num正序排序,取前4条
questions = await QuickQuestion.filter(status="0").order_by("order_num").limit(4).values("id", "title")
return questions

@ -320,4 +320,44 @@ async def query_flow(request: Request, spot: str) -> str:
except Exception as e: except Exception as e:
print(f"[Redis] 写缓存失败: {e}") print(f"[Redis] 写缓存失败: {e}")
return result return result
async def handle_quick_question(inp: ChatIn, question_content: str) -> AsyncGenerator[str, None]:
chat_prompt = f"""
你是一个专门格式化内容的AI助手
负责将接收到的包含html标签内容进行格式化要求是将能够转换成markdown语法的内容中的html标签转换成markdown语法
不能转换的保留html标签不能修改内容仅格式化标签
"""
# 只包含系统提示和问题内容,不包含历史记录
messages = [
{"role": "system", "content": chat_prompt},
{"role": "user", "content": question_content}
]
full_response = ""
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True
)
# 使用异步方式处理同步流
import asyncio
for chunk in response:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
full_response += delta.content
yield delta.content
import sys
sys.stdout.flush()
# 短暂让出控制权,避免阻塞
await asyncio.sleep(0)
# 添加结束标记
yield " "
except Exception as e:
error_msg = f"当前访问人数过多,请稍后重试: {str(e)}"
print(error_msg)
yield error_msg
# 不保存快捷问题的对话历史
print("Quick question handling finished.")

@ -1,4 +1,4 @@
# app/api/v1/upload/upload.py
import os import os
import uuid import uuid
from fastapi import APIRouter, File, UploadFile from fastapi import APIRouter, File, UploadFile
@ -13,6 +13,7 @@ UPLOAD_DIR = os.path.join(settings.BASE_DIR, 'web', 'public', 'resource')
@router.post('/image', summary='上传图片') @router.post('/image', summary='上传图片')
async def upload_image(file: UploadFile = File(...)): async def upload_image(file: UploadFile = File(...)):
print(file.filename)
try: try:
# 确保目录存在 # 确保目录存在
os.makedirs(os.path.join(UPLOAD_DIR, 'images'), exist_ok=True) os.makedirs(os.path.join(UPLOAD_DIR, 'images'), exist_ok=True)

@ -46,7 +46,9 @@ def make_middlewares():
"/api/v1/base/access_token", "/api/v1/base/access_token",
"/docs", "/docs",
"/openapi.json", "/openapi.json",
"/api/model/" "/api/model/",
"/api/v1/upload/image",
"/api/v1/upload/video"
], ],
), ),
] ]

@ -45,4 +45,6 @@ export default {
createQuestion: (data = {}) => request.post('/question/create', data), createQuestion: (data = {}) => request.post('/question/create', data),
updateQuestion: (data = {}) => request.post('/question/update', data), updateQuestion: (data = {}) => request.post('/question/update', data),
deleteQuestion: (params = {}) => request.delete('/question/delete', { params }), deleteQuestion: (params = {}) => request.delete('/question/delete', { params }),
// 上传图片
uploadImages: (data = {}) => request.post('/upload/image', data)
} }

@ -91,6 +91,7 @@ import { h, onMounted, ref, resolveDirective, withDirectives, onUnmounted, watch
import { import {
NButton, NForm, NFormItem, NInput, NPopconfirm, NTag, NSwitch NButton, NForm, NFormItem, NInput, NPopconfirm, NTag, NSwitch
} from 'naive-ui' } from 'naive-ui'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue' import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css' import '@wangeditor/editor/dist/css/style.css'
@ -103,9 +104,8 @@ import { formatDate, renderIcon } from '@/utils'
import { useCRUD } from '@/composables' import { useCRUD } from '@/composables'
import api from '@/api' import api from '@/api'
import TheIcon from '@/components/icon/TheIcon.vue' import TheIcon from '@/components/icon/TheIcon.vue'
import { getToken } from '@/utils/auth/token'
import { request } from '@/utils/http'; // request
// defineOptions script setup
defineOptions({ name: '快捷问题管理' }) defineOptions({ name: '快捷问题管理' })
// //
@ -116,34 +116,34 @@ const editorConfig = ref({
placeholder: '请输入问题内容...', placeholder: '请输入问题内容...',
MENU_CONF: { MENU_CONF: {
uploadImage: { uploadImage: {
server: '/api/v1/upload/image',
fieldName: 'file', fieldName: 'file',
maxFileSize: 2 * 1024 * 1024, maxFileSize: 2 * 1024 * 1024,
headers: { customUpload: async (file, insertFn) => {
'token': getToken() try {
}, const formData = new FormData();
// formData.append('file', file);
customInsert(res, insertFn) {
if (res.code === 200 && res.data && res.data.url) { console.log('FormData内容:', formData.get('file'));
insertFn(res.data.url); console.log('准备发送请求...');
} else { const res = await api.uploadImages(formData);
ElMessage.error(`上传失败: ${res.msg || '未知错误'}`); console.log('请求响应:', res);
if (res.code === 200 && res.data && res.data.url) {
insertFn(res.data.url);
} else {
window.$message.error('图片上传失败: ' + (res.msg || '未知错误'));
}
} catch (error) {
console.error('上传失败:', error);
window.$message.error('图片上传失败: ' + error.message);
} }
}
},
uploadVideo: {
server: '/api/v1/upload/video',
fieldName: 'file',
maxFileSize: 100 * 1024 * 1024,
headers: {
'token': getToken()
}, },
//
customInsert(res, insertFn) { customInsert(res, insertFn) {
if (res.code === 200 && res.data && res.data.url) { if (res.code === 200 && res.data && res.data.url) {
insertFn(res.data.url); insertFn(res.data.url);
} else { } else {
ElMessage.error(`上传失败: ${res.msg || '未知错误'}`); // NMessage
window.$message.error(`上传失败: ${res.msg || '未知错误'}`);
} }
} }
} }

Loading…
Cancel
Save