From 43fbda1a50f28545859cebd519fac74a627c5523 Mon Sep 17 00:00:00 2001 From: zc Date: Mon, 4 Aug 2025 17:09:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E9=97=AE=E9=A2=98=E9=97=AE?= =?UTF-8?q?=E7=AD=94=E7=9A=84=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/chat/ai/chat_router.py | 62 +++++++++++++++++++------ app/api/chat/ai/chat_service.py | 42 ++++++++++++++++- app/api/v1/upload/upload.py | 3 +- app/core/init_app.py | 4 +- web/src/api/index.js | 2 + web/src/views/system/question/index.vue | 44 +++++++++--------- 6 files changed, 119 insertions(+), 38 deletions(-) diff --git a/app/api/chat/ai/chat_router.py b/app/api/chat/ai/chat_router.py index fe8bc6c..4438527 100644 --- a/app/api/chat/ai/chat_router.py +++ b/app/api/chat/ai/chat_router.py @@ -2,14 +2,15 @@ from fastapi import APIRouter, HTTPException from starlette.requests import Request from fastapi.responses import StreamingResponse -from typing import AsyncGenerator -from .chat_service import classify, extract_spot, query_flow, gen_markdown_stream, ai_chat_stream +from typing import AsyncGenerator, List +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.quick_question import QuickQuestion +from pydantic import BaseModel import hmac import hashlib import time import json -from pydantic import BaseModel from app.settings.config import settings 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 = json.loads(conversation_history_str) if conversation_history_str else [] - # 分类阶段(保留同步调用) - cat = await classify(inp.message) + # 获取开启的前4个问题(包含标题和内容) + 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]: nonlocal conversation_history try: - 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): + if is_quick_question: + # 找到对应的问题内容 + question_content = next(q["content"] for q in questions if q["title"] == inp.message) + # 处理快捷问题,传递content + async for chunk in handle_quick_question(inp, question_content): yield chunk 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,并设置过期时间 - 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: print(f"Error in content_stream: {e}") raise @@ -102,4 +122,20 @@ def verify_signature(data: dict, sign: str) -> bool: def verify_timestamp(timestamp: int) -> bool: current_timestamp = int(time.time() * 1000) - return abs(current_timestamp - timestamp) <= TIMESTAMP_TOLERANCE * 1000 \ No newline at end of file + 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 \ No newline at end of file diff --git a/app/api/chat/ai/chat_service.py b/app/api/chat/ai/chat_service.py index cfb95d5..0c982c1 100644 --- a/app/api/chat/ai/chat_service.py +++ b/app/api/chat/ai/chat_service.py @@ -320,4 +320,44 @@ async def query_flow(request: Request, spot: str) -> str: except Exception as e: print(f"[Redis] 写缓存失败: {e}") - return result \ No newline at end of file + 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.") \ No newline at end of file diff --git a/app/api/v1/upload/upload.py b/app/api/v1/upload/upload.py index d9a0bbb..e290611 100644 --- a/app/api/v1/upload/upload.py +++ b/app/api/v1/upload/upload.py @@ -1,4 +1,4 @@ -# app/api/v1/upload/upload.py + import os import uuid 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='上传图片') async def upload_image(file: UploadFile = File(...)): + print(file.filename) try: # 确保目录存在 os.makedirs(os.path.join(UPLOAD_DIR, 'images'), exist_ok=True) diff --git a/app/core/init_app.py b/app/core/init_app.py index 8f10a29..a412f9d 100644 --- a/app/core/init_app.py +++ b/app/core/init_app.py @@ -46,7 +46,9 @@ def make_middlewares(): "/api/v1/base/access_token", "/docs", "/openapi.json", - "/api/model/" + "/api/model/", + "/api/v1/upload/image", + "/api/v1/upload/video" ], ), ] diff --git a/web/src/api/index.js b/web/src/api/index.js index 94649d1..5aaaaad 100644 --- a/web/src/api/index.js +++ b/web/src/api/index.js @@ -45,4 +45,6 @@ export default { createQuestion: (data = {}) => request.post('/question/create', data), updateQuestion: (data = {}) => request.post('/question/update', data), deleteQuestion: (params = {}) => request.delete('/question/delete', { params }), + // 上传图片 + uploadImages: (data = {}) => request.post('/upload/image', data) } diff --git a/web/src/views/system/question/index.vue b/web/src/views/system/question/index.vue index 6d8f3e8..8744200 100644 --- a/web/src/views/system/question/index.vue +++ b/web/src/views/system/question/index.vue @@ -91,6 +91,7 @@ import { h, onMounted, ref, resolveDirective, withDirectives, onUnmounted, watch import { NButton, NForm, NFormItem, NInput, NPopconfirm, NTag, NSwitch } from 'naive-ui' + import { Editor, Toolbar } from '@wangeditor/editor-for-vue' import '@wangeditor/editor/dist/css/style.css' @@ -103,9 +104,8 @@ import { formatDate, renderIcon } from '@/utils' import { useCRUD } from '@/composables' import api from '@/api' import TheIcon from '@/components/icon/TheIcon.vue' -import { getToken } from '@/utils/auth/token' -import { request } from '@/utils/http'; // 导入项目中已有的request工具 +// 将 defineOptions 移到 script setup 内部 defineOptions({ name: '快捷问题管理' }) // 富文本编辑器相关 @@ -116,34 +116,34 @@ const editorConfig = ref({ placeholder: '请输入问题内容...', MENU_CONF: { uploadImage: { - server: '/api/v1/upload/image', fieldName: 'file', maxFileSize: 2 * 1024 * 1024, - headers: { - 'token': getToken() - }, - // 自定义处理后端返回的数据 - customInsert(res, insertFn) { - if (res.code === 200 && res.data && res.data.url) { - insertFn(res.data.url); - } else { - ElMessage.error(`上传失败: ${res.msg || '未知错误'}`); + customUpload: async (file, insertFn) => { + try { + const formData = new FormData(); + formData.append('file', file); + + console.log('FormData内容:', formData.get('file')); + console.log('准备发送请求...'); + const res = await api.uploadImages(formData); + 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) { if (res.code === 200 && res.data && res.data.url) { insertFn(res.data.url); } else { - ElMessage.error(`上传失败: ${res.msg || '未知错误'}`); + // 修复 NMessage 未定义的问题 + window.$message.error(`上传失败: ${res.msg || '未知错误'}`); } } }