Browse Source

feat:代码优化

zhangwl 1 month ago
parent
commit
cffbd86ed7
8 changed files with 412 additions and 667 deletions
  1. 5 0
      app/core/ark_client.py
  2. 30 0
      app/dependencies/auth.py
  3. 271 663
      app/routers/chat.py
  4. 18 0
      app/routers/chat_tools.py
  5. 5 3
      app/routers/users.py
  6. 35 0
      app/schemas/chat.py
  7. 46 0
      app/utils/chat_utils.py
  8. 2 1
      main.py

+ 5 - 0
app/core/ark_client.py

@@ -0,0 +1,5 @@
+from volcenginesdkarkruntime import Ark
+from ..config.config import Config
+
+config = Config()
+client = Ark(api_key=config.API_KEY, base_url=config.BASE_URL)

+ 30 - 0
app/dependencies/auth.py

@@ -0,0 +1,30 @@
+from fastapi import HTTPException, Query, Depends
+from fastapi.security import OAuth2PasswordBearer
+from typing import Optional, Annotated
+import jwt
+from ..routers.users import SECRET_KEY, ALGORITHM
+from ..db.redis_client import get_app_user
+
+oauth2_scheme_optional = OAuth2PasswordBearer(tokenUrl="/users/token", auto_error=False)
+
+
+async def resolve_username(
+    jwt_token: Annotated[Optional[str], Depends(oauth2_scheme_optional)] = None,
+    source: Optional[str] = Query(default=None),
+    token: Optional[str] = Query(default=None),
+) -> str:
+    if source == "app" and token:
+        app_user = get_app_user(token)
+        if not app_user:
+            raise HTTPException(status_code=401, detail="无效的 App token")
+        return f"app_{app_user['userId']}"
+    if not jwt_token:
+        raise HTTPException(status_code=401, detail="未提供认证令牌")
+    try:
+        payload = jwt.decode(jwt_token, SECRET_KEY, algorithms=[ALGORITHM])
+        sub = payload.get("sub")
+        if not sub:
+            raise HTTPException(status_code=401, detail="无效的令牌")
+        return sub
+    except jwt.PyJWTError:
+        raise HTTPException(status_code=401, detail="无效的令牌")

+ 271 - 663
app/routers/chat.py

@@ -1,663 +1,271 @@
-from fastapi import APIRouter, HTTPException, Depends
-from fastapi.responses import StreamingResponse
-from fastapi.security import OAuth2PasswordBearer
-from volcenginesdkarkruntime import Ark
-from pydantic import BaseModel
-from typing import List, Optional, Dict, Any, Annotated
-from datetime import datetime
-from fastapi import Query
-import json
-import asyncio
-import jwt
-from ..config.config import Config
-from ..routers.users import get_current_active_user, User, SECRET_KEY, ALGORITHM
-from ..db.mongo import save_chat_log
-from ..db.redis_client import get_app_user
-
-oauth2_scheme_optional = OAuth2PasswordBearer(tokenUrl="/users/token", auto_error=False)
-
-# =====================================================
-# 全局变量和配置
-# =====================================================
-
-# 内存存储用户聊天历史
-# 结构: {username: [ChatMessage, ChatMessage, ...]}
-# 注意: 生产环境中应该使用数据库存储,如Redis或PostgreSQL
-chatHistory = {}
-
-# 创建配置实例
-# 从配置文件中读取API密钥、模型名称等设置
-config = Config()
-
-# 创建OpenAI客户端实例
-# 使用配置中的API密钥和基础URL
-client = Ark(api_key=config.API_KEY, base_url=config.BASE_URL)
-
-# 创建FastAPI路由器实例
-# 所有的聊天相关路由都会注册到这个路由器上
-router = APIRouter()
-
-
-# =====================================================
-# Pydantic数据模型定义
-# =====================================================
-
-class ChatMessage(BaseModel):
-    """
-    聊天消息数据模型
-
-    用于表示对话中的单条消息,包含角色、内容和时间戳
-    """
-    role: str  # 消息角色: "user"(用户), "assistant"(AI助手), "system"(系统)
-    content: str  # 消息的文本内容
-    timestamp: Optional[datetime] = None  # 消息创建时间戳(可选)
-    response_id: Optional[str] = None  # API响应ID,用于多轮对话的上下文关联
-
-
-class ChatRequest(BaseModel):
-    """
-    客户端聊天请求数据模型
-
-    定义了客户端发送聊天请求时需要包含的所有参数
-    注意: 移除了userId字段,改为从JWT认证中获取用户身份
-    """
-    messages: List[ChatMessage]  # 对话历史消息列表,包含用户和助手的所有消息
-    model: Optional[str] = config.MODEL_NAME  # 要使用的AI模型名称,默认使用配置中的模型
-    temperature: Optional[float] = config.TEMPERATURE  # 创造性温度值(0-2),控制回答的随机性
-    max_tokens: Optional[int] = config.MAX_TOKENS  # 最大生成token数,限制回答长度
-    stream: Optional[bool] = False  # 是否启用流式输出,True时会实时返回生成的内容
-    source: Optional[str] = None  # 来源标识,source=app 时走第三方token认证
-    token: Optional[str] = None  # App端传入的第三方token,source=app时必填
-
-
-class ChatResponse(BaseModel):
-    """
-    服务端聊天响应数据模型(非流式)
-
-    用于非流式请求的响应,包含完整的AI回答和使用统计
-    """
-    message: ChatMessage  # AI助手的回复消息
-    model: str  # 实际使用的模型名称
-    usage: Optional[Dict[str, Any]] = None  # token使用情况统计
-    response_id: Optional[str] = None  # API响应ID,用于后续多轮对话
-
-
-class StreamResponse(BaseModel):
-    """
-    流式响应数据模型
-
-    用于流式输出时的每个数据块,支持Server-Sent Events (SSE)
-    """
-    content: str  # 本次返回的内容片段
-    finished: bool  # 是否为最后一个片段,True表示流式响应结束
-    model: str  # 使用的模型名称
-    timestamp: datetime  # 当前片段的时间戳
-
-
-# =====================================================
-# 消息处理工具函数
-# =====================================================
-
-def convert_messages_for_api(messages: List[ChatMessage]) -> List[Dict[str, str]]:
-    """
-    将自定义ChatMessage转换为OpenAI API需要的格式
-
-    OpenAI API需要的消息格式是字典列表,每个字典包含role和content字段
-
-    Args:
-        messages (List[ChatMessage]): 自定义的消息对象列表
-
-    Returns:
-        List[Dict[str, str]]: OpenAI API格式的消息列表
-
-    Example:
-        输入: [ChatMessage(role="user", content="你好")]
-        输出: [{"role": "user", "content": "你好"}]
-    """
-    return [{"role": msg.role, "content": msg.content} for msg in messages]
-
-
-def get_latest_user_message(messages: List[ChatMessage]) -> Optional[ChatMessage]:
-    """
-    获取消息列表中最后一条user角色的消息
-
-    在多轮对话中,消息列表可能包含user和assistant的消息,
-    流式场景下客户端会预先添加空的assistant消息作为占位符,
-    此函数确保获取到最后一条用户发送的消息
-
-    Args:
-        messages (List[ChatMessage]): 消息列表
-
-    Returns:
-        Optional[ChatMessage]: 最后一条user角色的消息,如果不存在则返回None
-    """
-    for message in reversed(messages):
-        if message.role == "user":
-            return message
-    return None
-
-
-async def generate_stream_response(request: ChatRequest, username: str):
-    """
-    生成流式响应的异步生成器
-
-    这个函数处理流式AI响应,将Ark API的流式输出转换为SSE格式
-
-    Args:
-        request (ChatRequest): 聊天请求对象
-        username (str): 当前用户名
-
-    Yields:
-        str: 格式化的SSE数据,每行以"data: "开头
-
-    Note:
-        使用Server-Sent Events (SSE) 协议进行实时数据传输
-        客户端需要使用EventSource或类似技术接收流式数据
-    """
-    try:
-        # 获取最后一条user角色的消息
-        latest_user_msg = get_latest_user_message(request.messages)
-        if not latest_user_msg:
-            raise ValueError("请求中没有找到user角色的消息")
-
-        # 将用户消息添加到历史记录
-        user_message = ChatMessage(
-            role=latest_user_msg.role,
-            content=latest_user_msg.content,
-            timestamp=datetime.now()
-        )
-        chatHistory[username].append(user_message)
-
-        # 转换消息格式为API需要的格式
-        api_messages = [{"role": latest_user_msg.role, "content": latest_user_msg.content}]
-
-        tools = [{
-            "type": "doubao_app",
-            "feature": {
-                "ai_search": {
-                    "type": "enabled",
-                    "role_description": config.ROLE_DESCRIPTION
-                }
-            },
-            "user_location": {
-                "type": "approximate",
-                "country": "中国",
-                "region": "浙江",
-                "city": "杭州"
-            }
-        }]
-
-        # 获取上一轮对话的response_id,用于多轮对话的上下文关联
-        previous_response_id = None
-        if username in chatHistory and len(chatHistory[username]) > 0:
-            # 从后往前查找最后一条assistant消息的response_id
-            for message in reversed(chatHistory[username]):
-                if message.role == "assistant" and message.response_id:
-                    previous_response_id = message.response_id
-                    break
-
-        # stream=True 启用流式输出,API会返回一个迭代器
-        stream = client.responses.create(
-            model=config.MODEL_NAME,
-            input=api_messages,
-            tools=tools,
-            stream=True,
-            store=True,  # 存储当前对话上下文。此字段不存储tools,每次调用仍需给tools赋值。
-            previous_response_id=previous_response_id,
-        )
-
-
-        # 用于累积完整的回答内容和response_id
-        accumulated_content = ""
-        response_id = None
-
-        # 遍历流式响应的每个数据块
-        for chunk in stream:
-            # 处理不同类型的流式事件
-            chunk_dict = chunk.__dict__ if hasattr(chunk, '__dict__') else {}
-            event_type = chunk_dict.get('type', '')
-
-            # 处理文本内容增量事件(普通文本)
-            if event_type == 'response.output_text.delta':
-                delta_text = chunk_dict.get('delta', '')
-                if delta_text:
-                    # 累积到完整内容中
-                    accumulated_content += delta_text
-
-                    # 构建流式响应数据对象
-                    response_data = StreamResponse(
-                        content=delta_text,  # 本次片段内容
-                        finished=False,  # 标记为未完成
-                        model= config.MODEL_NAME,  # 使用的模型
-                        timestamp=datetime.now()  # 当前时间戳
-                    )
-
-                    # 格式化为SSE格式并发送
-                    # SSE格式: "data: {json_data}\n\n"
-                    yield f"data: {response_data.model_dump_json()}\n\n"
-
-                    # 异步让出控制权,避免阻塞事件循环
-                    await asyncio.sleep(0.01)
-
-            # 处理DoubaoApp调用的文本输出增量事件
-            elif event_type == 'response.doubao_app_call_output_text.delta':
-                delta_text = chunk_dict.get('delta', '')
-                if delta_text:
-                    # 累积到完整内容中
-                    accumulated_content += delta_text
-
-                    # 构建流式响应数据对象
-                    response_data = StreamResponse(
-                        content=delta_text,  # 本次片段内容
-                        finished=False,  # 标记为未完成
-                        model= config.MODEL_NAME,  # 使用的模型
-                        timestamp=datetime.now()  # 当前时间戳
-                    )
-
-                    # 格式化为SSE格式并发送
-                    yield f"data: {response_data.model_dump_json()}\n\n"
-
-                    # 异步让出控制权,避免阻塞事件循环
-                    await asyncio.sleep(0.01)
-
-            # 处理响应完成事件,获取response_id并记录原始响应日志
-            elif event_type == 'response.completed':
-                if 'response' in chunk_dict and hasattr(chunk_dict['response'], 'id'):
-                    response_id = chunk_dict['response'].id
-                # 记录原始响应日志
-                save_chat_log(
-                    username=username,
-                    question=latest_user_msg.content,
-                    stream_mode=True,
-                    raw_response=repr(chunk_dict.get('response')),
-                    status="success",
-                )
-
-        # 流式响应结束后的处理
-        if accumulated_content:
-            # 构建结束信号响应
-            final_response = StreamResponse(
-                content='',  # 结束信号不包含内容
-                finished=True,  # 标记为已完成
-                model= config.MODEL_NAME,
-                timestamp=datetime.now()
-            )
-
-            # 将完整的AI回复保存到用户的聊天历史中,包含response_id
-            chatHistory[username].append(
-                ChatMessage(
-                    role="assistant",
-                    content=accumulated_content,
-                    timestamp=datetime.now(),
-                    response_id=response_id  # 保存response_id用于后续多轮对话
-                )
-            )
-
-
-            # 发送结束信号
-            yield f"data: {final_response.model_dump_json()}\n\n"
-
-            # 在控制台输出提示
-            print("流式内容已全部输出")
-
-    except Exception as e:
-        # 流式响应过程中的错误处理
-        # 构建错误响应并发送给客户端
-        error_response = {
-            "error": str(e),  # 错误信息
-            "finished": True,  # 标记为结束
-            "timestamp": datetime.now().isoformat()  # 错误发生时间
-        }
-
-        # 记录错误日志到 MongoDB
-        save_chat_log(
-            username=username,
-            question=latest_user_msg.content if latest_user_msg else "",
-            stream_mode=True,
-            status="error",
-            error=str(e),
-        )
-
-        # 发送错误信息
-        yield f"data: {json.dumps(error_response)}\n\n"
-
-
-# =====================================================
-# API路由端点定义
-# =====================================================
-
-@router.post("/chat", response_model=ChatResponse)
-async def chat(
-        request: ChatRequest,
-        jwt_token: Annotated[Optional[str], Depends(oauth2_scheme_optional)] = None,
-        source: Optional[str] = Query(default=None),      # 从 URL 获取
-        token: Optional[str] = Query(default=None)        # 从 URL 获取
-):
-    try:
-        # ===== 认证分支 =====
-        if source == "app" and token:
-            # 第三方 App token 认证
-            app_user = get_app_user(token)
-            if not app_user:
-                raise HTTPException(status_code=401, detail="无效的 App token")
-            username = f"app_{app_user['userId']}"
-        else:
-            # 原有本地 JWT 认证
-            if not jwt_token:
-                raise HTTPException(status_code=401, detail="未提供认证令牌")
-            try:
-                payload = jwt.decode(jwt_token, SECRET_KEY, algorithms=[ALGORITHM])
-                sub = payload.get("sub")
-                if not sub:
-                    raise HTTPException(status_code=401, detail="无效的令牌")
-            except jwt.PyJWTError:
-                raise HTTPException(status_code=401, detail="无效的令牌")
-            username = sub
-
-        # 初始化用户的聊天历史记录(如果不存在)
-        if username not in chatHistory:
-            chatHistory[username] = []
-
-        # 根据请求类型处理:流式 vs 非流式
-        if request.stream:
-            # ===== 流式输出处理 =====
-
-            # 返回流式响应
-            # StreamingResponse 用于处理SSE协议
-            return StreamingResponse(
-                generate_stream_response(request, username),  # 异步生成器
-                media_type="text/plain",  # 媒体类型
-                headers={
-                    "Cache-Control": "no-cache",  # 禁用缓存
-                    "Connection": "keep-alive",  # 保持连接
-                    "Content-Type": "text/event-stream",  # SSE内容类型
-                }
-            )
-
-        else:
-            # ===== 非流式输出处理 =====
-
-            # 获取最后一条user角色的消息
-            latest_user_msg = get_latest_user_message(request.messages)
-            if not latest_user_msg:
-                raise ValueError("请求中没有找到user角色的消息")
-
-            # 将用户消息添加到历史记录
-            user_message = ChatMessage(
-                role=latest_user_msg.role,
-                content=latest_user_msg.content,
-                timestamp=datetime.now()
-            )
-            chatHistory[username].append(user_message)
-
-            # 转换消息格式为API需要的格式
-            api_messages = [{"role": latest_user_msg.role, "content": latest_user_msg.content}]
-
-            tools = [{
-                "type": "doubao_app",
-                "feature": {
-                    "ai_search": {
-                        "type": "enabled",
-                        "role_description": config.ROLE_DESCRIPTION
-                    }
-                },
-                "user_location": {
-                    "type": "approximate",
-                    "country": "中国",
-                    "region": "浙江",
-                    "city": "杭州"
-                }
-            }]
-
-            # 获取上一轮对话的response_id,用于多轮对话的上下文关联
-            previous_response_id = None
-            if username in chatHistory and len(chatHistory[username]) > 0:
-                # 从后往前查找最后一条assistant消息的response_id
-                for message in reversed(chatHistory[username]):
-                    if message.role == "assistant" and message.response_id:
-                        previous_response_id = message.response_id
-                        break
-
-            response = client.responses.create(
-                model=config.MODEL_NAME,
-                input=api_messages,
-                tools=tools,
-                stream=False,
-                store=True,  # 存储当前对话上下文。此字段不存储tools,每次调用仍需给tools赋值。
-                previous_response_id=previous_response_id,
-                thinking={"type": "disabled"}, #  Manually disable deep thinking
-            )
-
-            # 记录原始响应日志到 MongoDB(解析前)
-            save_chat_log(
-                username=username,
-                question=latest_user_msg.content,
-                stream_mode=False,
-                raw_response=repr(response),
-                status="success",
-            )
-
-            # 检查API响应是否有效
-            if response.output and len(response.output) > 0:
-                # 从output中提取文本内容
-                message_content = ""
-
-                for item in response.output:
-                    # 处理 ItemDoubaoAppCall 类型(包含搜索结果和文本输出)
-                    if hasattr(item, 'type') and item.type == 'doubao_app_call':
-                        if hasattr(item, 'blocks') and item.blocks:
-                            # 从blocks中找到output_text类型的块
-                            for block in item.blocks:
-                                if hasattr(block, 'type') and block.type == 'output_text':
-                                    if hasattr(block, 'text'):
-                                        message_content += block.text
-
-                    # 处理其他类型的消息项
-                    elif hasattr(item, 'type') and item.type == 'message':
-                        if hasattr(item, 'content'):
-                            if isinstance(item.content, list):
-                                for content_item in item.content:
-                                    if hasattr(content_item, 'text'):
-                                        message_content += content_item.text
-                            else:
-                                message_content += str(item.content)
-
-                if message_content:
-                    # 构建AI助手的回复消息,包含response_id用于多轮对话
-                    assistant_message = ChatMessage(
-                        role="assistant",
-                        content=message_content,
-                        timestamp=datetime.now(),
-                        response_id=response.id  # 保存response_id用于后续多轮对话
-                    )
-
-                    # 将AI回复添加到用户的聊天历史
-                    chatHistory[username].append(assistant_message)
-
-                    # 构建完整的响应对象
-                    chat_response = ChatResponse(
-                        message=assistant_message,  # AI回复消息
-                        model=response.model,  # 实际使用的模型
-                        usage=response.usage.model_dump() if response.usage else None,  # token使用统计
-                        response_id=response.id  # 返回response_id供客户端保存
-                    )
-
-                    return chat_response
-                else:
-                    # 没有提取到文本内容的错误处理
-                    raise HTTPException(
-                        status_code=500,
-                        detail="无法从AI响应中提取文本内容"
-                    )
-            else:
-                # API返回空响应的错误处理
-                raise HTTPException(
-                    status_code=500,
-                    detail="AI模型返回了空响应"
-                )
-
-    except HTTPException:
-        # 重新抛出HTTP异常(如配额限制)
-        raise
-    except Exception as e:
-        # 捕获所有其他异常并转换为HTTP异常
-        error_message = f"处理聊天请求时发生错误: {str(e)}"
-        save_chat_log(
-            username=username,
-            question=request.messages[-1].content if request.messages else "",
-            stream_mode=request.stream,
-            status="error",
-            error=error_message,
-        )
-        raise HTTPException(status_code=500, detail=error_message)
-
-
-@router.get("/models")
-async def get_models(
-        current_user: Annotated[User, Depends(get_current_active_user)]
-):
-    """
-    获取可用的AI模型列表 - 需要登录
-
-    返回当前可用的AI模型列表,用户可以在聊天时选择使用
-
-    Args:
-        current_user (User): 通过JWT认证获取的当前用户信息
-
-    Returns:
-        dict: 包含模型列表和默认模型的字典
-
-    Note:
-        如果无法获取模型列表,会返回默认配置
-    """
-    try:
-        # 尝试从OpenAI API获取可用模型列表
-        models = client.models.list()
-
-        return {
-            "models": [model.id for model in models.data],  # 模型ID列表
-            "default_model": config.MODEL_NAME,  # 默认模型
-            "user": current_user.username  # 请求用户
-        }
-    except Exception as e:
-        # 如果获取模型列表失败,返回默认配置
-        return {
-            "models": [config.MODEL_NAME],  # 只返回默认模型
-            "default_model": config.MODEL_NAME,
-            "note": "使用默认模型配置",  # 提示信息
-            "user": current_user.username
-        }
-
-
-@router.get("/history")
-async def get_user_history(
-        current_user: Annotated[User, Depends(get_current_active_user)]
-) -> List[ChatMessage]:
-    """
-    获取当前用户的聊天历史 - 安全版本
-
-    只返回当前认证用户的聊天历史,确保数据隐私
-
-    Args:
-        current_user (User): 通过JWT认证获取的当前用户信息
-
-    Returns:
-        List[ChatMessage]: 用户的历史消息列表
-
-    Security:
-        用户只能访问自己的聊天历史,无法访问他人数据
-    """
-    username = current_user.username
-
-    # 如果用户没有聊天历史,返回空列表
-    if username not in chatHistory:
-        return []
-
-    # 返回用户的完整聊天历史
-    # 创建新的ChatMessage对象确保数据一致性
-    return [
-        ChatMessage(
-            role=msg.role,
-            content=msg.content,
-            timestamp=msg.timestamp
-        )
-        for msg in chatHistory[username]
-    ]
-
-
-@router.delete("/history")
-async def clear_user_history(
-        current_user: Annotated[User, Depends(get_current_active_user)]
-):
-    """
-    清空当前用户的聊天历史
-
-    删除用户的所有聊天记录,此操作不可逆
-
-    Args:
-        current_user (User): 通过JWT认证获取的当前用户信息
-
-    Returns:
-        dict: 操作确认信息
-
-    Warning:
-        此操作会永久删除用户的聊天历史,无法恢复
-    """
-    username = current_user.username
-
-    # 如果用户有聊天历史,则删除
-    if username in chatHistory:
-        # 获取删除前的消息数量用于统计
-        message_count = len(chatHistory[username])
-
-        # 删除用户的聊天历史
-        del chatHistory[username]
-
-        return {
-            "message": "聊天历史已清空",
-            "user": username,
-            "deleted_messages": message_count,  # 删除的消息数量
-            "timestamp": datetime.now()
-        }
-    else:
-        # 用户没有聊天历史
-        return {
-            "message": "用户没有聊天历史",
-            "user": username,
-            "deleted_messages": 0,
-            "timestamp": datetime.now()
-        }
-
-
-@router.get("/health")
-async def health_check():
-    """
-    健康检查接口 - 无需认证
-
-    用于监控服务状态,通常被负载均衡器或监控系统调用
-
-    Returns:
-        dict: 服务状态信息
-
-    Note:
-        此接口不需要认证,可被任何人访问
-    """
-    return {
-        "status": "healthy",  # 服务状态
-        "timestamp": datetime.now(),  # 当前时间
-        "version": "1.0.0",  # 服务版本
-        "model": config.MODEL_NAME,  # 默认AI模型
-    }
-
-
-# =====================================================
-# 路由器配置和元数据
-# =====================================================
-
-# 为路由器添加标签和元数据,用于API文档生成
-router.tags = ["聊天服务"]
-router.responses = {
-    401: {"description": "未授权 - 需要有效的JWT令牌"},
-    429: {"description": "请求过多 - 配额已用完"},
-    500: {"description": "服务器内部错误"}
-}
+from fastapi import APIRouter, HTTPException, Depends
+from fastapi.responses import StreamingResponse
+from typing import List, Annotated
+from datetime import datetime
+import json
+import asyncio
+
+from ..core.ark_client import config, client
+from ..schemas.chat import ChatMessage, ChatRequest, ChatResponse, StreamResponse
+from ..utils.chat_utils import get_latest_user_message, get_previous_response_id, get_doubao_tools
+from ..routers.users import get_current_active_user, User
+from ..db.mongo import save_chat_log
+from ..dependencies.auth import resolve_username
+
+# 内存存储用户聊天历史 {username: [ChatMessage, ...]}
+chatHistory = {}
+
+router = APIRouter()
+
+
+async def generate_stream_response(request: ChatRequest, username: str):
+    latest_user_msg = None
+    try:
+        latest_user_msg = get_latest_user_message(request.messages)
+        if not latest_user_msg:
+            raise ValueError("请求中没有找到user角色的消息")
+
+        chatHistory[username].append(ChatMessage(
+            role=latest_user_msg.role,
+            content=latest_user_msg.content,
+            timestamp=datetime.now()
+        ))
+
+        api_messages = [{"role": latest_user_msg.role, "content": latest_user_msg.content}]
+        tools = get_doubao_tools()
+        previous_response_id = get_previous_response_id(username, chatHistory)
+
+        stream = client.responses.create(
+            model=config.MODEL_NAME,
+            input=api_messages,
+            tools=tools,
+            stream=True,
+            store=True,
+            previous_response_id=previous_response_id,
+            # thinking={"type": "auto"},
+        )
+
+        accumulated_content = ""
+        response_id = None
+
+        for chunk in stream:
+            chunk_dict = chunk.__dict__ if hasattr(chunk, '__dict__') else {}
+            event_type = chunk_dict.get('type', '')
+
+            if event_type in ('response.output_text.delta', 'response.doubao_app_call_output_text.delta'):
+                delta_text = chunk_dict.get('delta', '')
+                if delta_text:
+                    accumulated_content += delta_text
+                    response_data = StreamResponse(
+                        content=delta_text,
+                        finished=False,
+                        model=config.MODEL_NAME,
+                        timestamp=datetime.now()
+                    )
+                    yield f"data: {response_data.model_dump_json()}\n\n"
+                    await asyncio.sleep(0.01)
+
+            elif event_type == 'response.completed':
+                if 'response' in chunk_dict and hasattr(chunk_dict['response'], 'id'):
+                    response_id = chunk_dict['response'].id
+                save_chat_log(
+                    username=username,
+                    question=latest_user_msg.content,
+                    stream_mode=True,
+                    raw_response=repr(chunk_dict.get('response')),
+                    status="success",
+                )
+
+        if accumulated_content:
+            chatHistory[username].append(ChatMessage(
+                role="assistant",
+                content=accumulated_content,
+                timestamp=datetime.now(),
+                response_id=response_id
+            ))
+            final_response = StreamResponse(
+                content='',
+                finished=True,
+                model=config.MODEL_NAME,
+                timestamp=datetime.now()
+            )
+            yield f"data: {final_response.model_dump_json()}\n\n"
+            print("流式内容已全部输出")
+
+    except Exception as e:
+        error_response = {
+            "error": str(e),
+            "finished": True,
+            "timestamp": datetime.now().isoformat()
+        }
+        save_chat_log(
+            username=username,
+            question=latest_user_msg.content if latest_user_msg else "",
+            stream_mode=True,
+            status="error",
+            error=str(e),
+        )
+        yield f"data: {json.dumps(error_response)}\n\n"
+
+
+@router.post("/chat", response_model=ChatResponse)
+async def chat(
+        request: ChatRequest,
+        username: Annotated[str, Depends(resolve_username)],
+):
+    try:
+        if username not in chatHistory:
+            chatHistory[username] = []
+
+        if request.stream:
+            # ===== 流式输出处理 =====
+
+            # 返回流式响应
+            # StreamingResponse 用于处理SSE协议
+            return StreamingResponse(
+                generate_stream_response(request, username),
+                media_type="text/plain",
+                headers={
+                    "Cache-Control": "no-cache",
+                    "Connection": "keep-alive",
+                    "Content-Type": "text/event-stream",
+                }
+            )
+
+        # 以下是非流式输出处理
+
+        latest_user_msg = get_latest_user_message(request.messages)
+        if not latest_user_msg:
+            raise ValueError("请求中没有找到user角色的消息")
+
+        chatHistory[username].append(ChatMessage(
+            role=latest_user_msg.role,
+            content=latest_user_msg.content,
+            timestamp=datetime.now()
+        ))
+
+        api_messages = [{"role": latest_user_msg.role, "content": latest_user_msg.content}]
+        tools = get_doubao_tools()
+        previous_response_id = get_previous_response_id(username, chatHistory)
+
+        response = client.responses.create(
+            model=config.MODEL_NAME,
+            input=api_messages,
+            tools=tools,
+            stream=False,
+            store=True,
+            previous_response_id=previous_response_id,
+            #  The parameter `thinking` specified in the request are not valid: `thinking` can not be set when enable doubao_app built-in tool.
+            # thinking={"type": "auto"},
+        )
+
+        save_chat_log(
+            username=username,
+            question=latest_user_msg.content,
+            stream_mode=False,
+            raw_response=repr(response),
+            status="success",
+        )
+
+        if not (response.output and len(response.output) > 0):
+            raise HTTPException(status_code=500, detail="AI模型返回了空响应")
+
+        message_content = ""
+        for item in response.output:
+            if hasattr(item, 'type') and item.type == 'doubao_app_call':
+                if hasattr(item, 'blocks') and item.blocks:
+                    for block in item.blocks:
+                        if hasattr(block, 'type') and block.type == 'output_text' and hasattr(block, 'text'):
+                            message_content += block.text
+            elif hasattr(item, 'type') and item.type == 'message':
+                if hasattr(item, 'content'):
+                    if isinstance(item.content, list):
+                        for content_item in item.content:
+                            if hasattr(content_item, 'text'):
+                                message_content += content_item.text
+                    else:
+                        message_content += str(item.content)
+
+        if not message_content:
+            raise HTTPException(status_code=500, detail="无法从AI响应中提取文本内容")
+
+        assistant_message = ChatMessage(
+            role="assistant",
+            content=message_content,
+            timestamp=datetime.now(),
+            response_id=response.id
+        )
+        chatHistory[username].append(assistant_message)
+        return ChatResponse(
+            message=assistant_message,
+            model=response.model,
+            usage=response.usage.model_dump() if response.usage else None,
+            response_id=response.id
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        error_message = f"处理聊天请求时发生错误: {str(e)}"
+        save_chat_log(
+            username=username,
+            question=request.messages[-1].content if request.messages else "",
+            stream_mode=request.stream,
+            status="error",
+            error=error_message,
+        )
+        raise HTTPException(status_code=500, detail=error_message)
+
+
+@router.get("/models")
+async def get_models(current_user: Annotated[User, Depends(get_current_active_user)]):
+    try:
+        models = client.models.list()
+        return {
+            "models": [model.id for model in models.data],
+            "default_model": config.MODEL_NAME,
+            "user": current_user.username
+        }
+    except Exception:
+        return {
+            "models": [config.MODEL_NAME],
+            "default_model": config.MODEL_NAME,
+            "note": "使用默认模型配置",
+            "user": current_user.username
+        }
+
+
+@router.get("/history")
+async def get_user_history(
+        current_user: Annotated[User, Depends(get_current_active_user)]
+) -> List[ChatMessage]:
+    username = current_user.username
+    if username not in chatHistory:
+        return []
+    return [
+        ChatMessage(role=msg.role, content=msg.content, timestamp=msg.timestamp)
+        for msg in chatHistory[username]
+    ]
+
+
+@router.delete("/history")
+async def clear_user_history(current_user: Annotated[User, Depends(get_current_active_user)]):
+    username = current_user.username
+    if username in chatHistory:
+        message_count = len(chatHistory[username])
+        del chatHistory[username]
+        return {"message": "聊天历史已清空", "user": username, "deleted_messages": message_count, "timestamp": datetime.now()}
+    return {"message": "用户没有聊天历史", "user": username, "deleted_messages": 0, "timestamp": datetime.now()}
+
+
+@router.get("/health")
+async def health_check():
+    return {"status": "healthy", "timestamp": datetime.now(), "version": "1.0.0", "model": config.MODEL_NAME}
+
+
+router.tags = ["聊天服务"]
+router.responses = {
+    401: {"description": "未授权 - 需要有效的JWT令牌"},
+    429: {"description": "请求过多 - 配额已用完"},
+    500: {"description": "服务器内部错误"}
+}

+ 18 - 0
app/routers/chat_tools.py

@@ -0,0 +1,18 @@
+from fastapi import APIRouter, HTTPException, Depends
+from fastapi.responses import StreamingResponse
+from typing import Annotated
+from fastapi import Query
+
+from ..core.ark_client import config, client
+from ..schemas.chat import ChatRequest, ChatResponse
+from ..dependencies.auth import resolve_username
+
+router = APIRouter()
+
+
+@router.post("/chat", response_model=ChatResponse)
+async def chat(
+        request: ChatRequest,
+        username: Annotated[str, Depends(resolve_username)],
+):
+    pass

+ 5 - 3
app/routers/users.py

@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
 from fastapi.security import OAuth2PasswordBearer
 from passlib.context import CryptContext
 from pydantic import BaseModel
+from pwdlib import PasswordHash
 
 # =====================================================
 # JWT和安全配置
@@ -18,7 +19,8 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30  # Token过期时间(分钟)
 # 密码加密上下文配置
 # schemes: 支持的密码哈希方案,bcrypt是目前推荐的安全哈希算法
 # deprecated: 标记为已弃用的方案(用于向后兼容)
-pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+# pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+pwd_context = PasswordHash.recommended()
 
 # OAuth2密码Bearer令牌方案
 # tokenUrl: 获取token的端点URL,必须与实际的token端点路径匹配
@@ -111,8 +113,8 @@ fake_users_db = {
         "username": "root",
         "full_name": "Administrator",
         "email": "admin@example.com",
-        # 这是"admin123"的bcrypt哈希值
-        "hashed_password": "$2b$12$p2v617r0nPHKa4LVd6j7puYqR0lD8xivcwvtCp9UBziF5c2dRhFe.",
+        # 这是"admin"的bcrypt哈希值
+        "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$AnPKKWadE68rd3n9CSM7vQ$p0LA3GqVSYxGRopB0B1yzzlO7W1LRCWagShF+Sbre9I",
         "disabled": False,
     }
 }

+ 35 - 0
app/schemas/chat.py

@@ -0,0 +1,35 @@
+from pydantic import BaseModel
+from typing import List, Optional, Dict, Any
+from datetime import datetime
+from ..core.ark_client import config
+
+
+class ChatMessage(BaseModel):
+    role: str  # "user" | "assistant" | "system"
+    content: str
+    timestamp: Optional[datetime] = None
+    response_id: Optional[str] = None
+
+
+class ChatRequest(BaseModel):
+    messages: List[ChatMessage]
+    model: Optional[str] = config.MODEL_NAME
+    temperature: Optional[float] = config.TEMPERATURE
+    max_tokens: Optional[int] = config.MAX_TOKENS
+    stream: Optional[bool] = False
+    source: Optional[str] = None  # source=app 时走第三方 token 认证
+    token: Optional[str] = None   # App 端传入的第三方 token
+
+
+class ChatResponse(BaseModel):
+    message: ChatMessage
+    model: str
+    usage: Optional[Dict[str, Any]] = None
+    response_id: Optional[str] = None
+
+
+class StreamResponse(BaseModel):
+    content: str
+    finished: bool
+    model: str
+    timestamp: datetime

+ 46 - 0
app/utils/chat_utils.py

@@ -0,0 +1,46 @@
+from typing import List, Optional, Dict
+from ..schemas.chat import ChatMessage
+from ..core.ark_client import config
+
+
+def convert_messages_for_api(messages: List[ChatMessage]) -> List[Dict[str, str]]:
+    return [{"role": msg.role, "content": msg.content} for msg in messages]
+
+
+def get_latest_user_message(messages: List[ChatMessage]) -> Optional[ChatMessage]:
+    for message in reversed(messages):
+        if message.role == "user":
+            return message
+    return None
+
+
+def get_previous_response_id(username: str, chat_history: dict) -> Optional[str]:
+    if username in chat_history:
+        for message in reversed(chat_history[username]):
+            if message.role == "assistant" and message.response_id:
+                return message.response_id
+    return None
+
+
+def get_doubao_tools() -> list:
+    return [{
+        "type": "doubao_app",
+        "feature": {
+            # 联网搜索功能
+            "ai_search": {
+                "type": "disabled",
+                "role_description": config.ROLE_DESCRIPTION
+            },
+            # 边想边搜功能
+            "reasoning_search":{
+                "type": "enabled",
+                "role_description": config.ROLE_DESCRIPTION
+            }
+        },
+        "user_location": {
+            "type": "approximate",
+            "country": "中国",
+            "region": "浙江",
+            "city": "杭州"
+        }
+    }]

+ 2 - 1
main.py

@@ -1,7 +1,7 @@
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 
-from app.routers import users, chat
+from app.routers import users, chat,chat_tools
 
 # 创建FastAPI应用实例
 app = FastAPI(title="聊天机器人", version="1.0.0", description="基于fastapi+VUE的聊天机器人")
@@ -20,6 +20,7 @@ app.add_middleware(
 # tags: 在API文档中用于分组显示,便于组织和查看
 app.include_router(users.router, prefix="/users", tags=["用户管理"])
 app.include_router(chat.router, prefix="/chat", tags=["聊天管理"])
+app.include_router(chat_tools.router, prefix="/chatTools", tags=["AI工具管理"])